diff --git a/.github/workflows/jms-build-test.yml b/.github/workflows/jms-build-test.yml new file mode 100644 index 000000000..0f5309cac --- /dev/null +++ b/.github/workflows/jms-build-test.yml @@ -0,0 +1,32 @@ +name: "Run Build Test" +on: + push: + branches: + - pr@* + - repr@* + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: docker/setup-qemu-action@v2 + + - uses: docker/setup-buildx-action@v2 + + - uses: docker/build-push-action@v3 + with: + context: . + push: false + tags: jumpserver/core:test + file: Dockerfile + cache-from: type=gha + cache-to: type=gha,mode=max + + - uses: LouisBrunner/checks-action@v1.5.0 + if: always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Check Build + conclusion: ${{ job.status }} diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 000000000..e59d309dc --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +line_length=120 +known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets diff --git a/Dockerfile b/Dockerfile index 57252eb14..f236c86eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,7 +99,8 @@ VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 +ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules -EXPOSE 8070 8080 +EXPOSE 8080 ENTRYPOINT ["./entrypoint.sh"] diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 index f98cd385f..c2fa521b6 100644 --- a/Dockerfile.loong64 +++ b/Dockerfile.loong64 @@ -91,6 +91,6 @@ VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 -EXPOSE 8070 8080 +EXPOSE 8080 ENTRYPOINT ["./entrypoint.sh"] diff --git a/apps/acls/api/__init__.py b/apps/acls/api/__init__.py index ff52a1ce9..2f720effe 100644 --- a/apps/acls/api/__init__.py +++ b/apps/acls/api/__init__.py @@ -1,3 +1,4 @@ +from .command_acl import * from .login_acl import * from .login_asset_acl import * from .login_asset_check import * diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py new file mode 100644 index 000000000..717e36930 --- /dev/null +++ b/apps/acls/api/command_acl.py @@ -0,0 +1,18 @@ +from orgs.mixins.api import OrgBulkModelViewSet +from .. import models, serializers + +__all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet'] + + +class CommandGroupViewSet(OrgBulkModelViewSet): + model = models.CommandGroup + filterset_fields = ('name',) + search_fields = filterset_fields + serializer_class = serializers.CommandGroupSerializer + + +class CommandFilterACLViewSet(OrgBulkModelViewSet): + model = models.CommandFilterACL + filterset_fields = ('name',) + search_fields = filterset_fields + serializer_class = serializers.CommandFilterACLSerializer diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index a7e8990c7..62babfafb 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -1,10 +1,10 @@ -from rest_framework.response import Response from rest_framework.generics import CreateAPIView +from rest_framework.response import Response from common.utils import reverse, lazyproperty from orgs.utils import tmp_to_org -from ..models import LoginAssetACL from .. import serializers +from ..models import LoginAssetACL __all__ = ['LoginAssetCheckAPI'] @@ -20,34 +20,40 @@ class LoginAssetCheckAPI(CreateAPIView): return LoginAssetACL.objects.all() def create(self, request, *args, **kwargs): - is_need_confirm, response_data = self.check_if_need_confirm() - return Response(data=response_data, status=200) + data = self.check_confirm() + return Response(data=data, status=200) - def check_if_need_confirm(self): - queries = { - 'user': self.serializer.user, 'asset': self.serializer.asset, - 'system_user': self.serializer.system_user, - 'action': LoginAssetACL.ActionChoices.login_confirm - } - with tmp_to_org(self.serializer.org): - acl = LoginAssetACL.filter(**queries).valid().first() + @lazyproperty + def serializer(self): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + return serializer - if not acl: - is_need_confirm = False - response_data = {} - else: - is_need_confirm = True + def check_confirm(self): + with tmp_to_org(self.serializer.asset.org): + kwargs = { + 'user': self.serializer.user, + 'asset': self.serializer.asset, + 'account_username': self.serializer.validated_data.get('account_username'), + 'action': LoginAssetACL.ActionChoices.review + } + acl = LoginAssetACL.filter_queryset(**kwargs).valid().first() + if acl: + need_confirm = True response_data = self._get_response_data_of_need_confirm(acl) - response_data['need_confirm'] = is_need_confirm - return is_need_confirm, response_data + else: + need_confirm = False + response_data = {} + response_data['need_confirm'] = need_confirm + return response_data - def _get_response_data_of_need_confirm(self, acl): + def _get_response_data_of_need_confirm(self, acl) -> dict: ticket = LoginAssetACL.create_login_asset_confirm_ticket( user=self.serializer.user, asset=self.serializer.asset, - system_user=self.serializer.system_user, + account_username=self.serializer.validated_data.get('account_username'), assignees=acl.reviewers.all(), - org_id=self.serializer.org.id, + org_id=self.serializer.asset.org.id, ) confirm_status_url = reverse( view_name='api-tickets:super-ticket-status', @@ -68,10 +74,3 @@ class LoginAssetCheckAPI(CreateAPIView): 'ticket_id': str(ticket.id) } return data - - @lazyproperty - def serializer(self): - serializer = self.get_serializer(data=self.request.data) - serializer.is_valid(raise_exception=True) - return serializer - diff --git a/apps/acls/migrations/0002_auto_20210926_1047.py b/apps/acls/migrations/0002_auto_20210926_1047.py index 53ab54711..2429b7cea 100644 --- a/apps/acls/migrations/0002_auto_20210926_1047.py +++ b/apps/acls/migrations/0002_auto_20210926_1047.py @@ -30,10 +30,11 @@ def migrate_login_confirm(apps, schema_editor): if reviewers.count() == 0: continue data = { + 'user': user, 'name': f'{user.name}-{login_confirm} ({date_created})', 'created_by': instance.created_by, - 'action': LoginACL.ActionChoices.confirm, + 'action': 'confirm', 'rules': {'ip_group': ['*'], 'time_period': DEFAULT_TIME_PERIODS} } instance = login_acl_model.objects.create(**data) @@ -44,7 +45,7 @@ def migrate_ip_group(apps, schema_editor): login_acl_model = apps.get_model("acls", "LoginACL") updates = list() with transaction.atomic(): - for instance in login_acl_model.objects.exclude(action=LoginACL.ActionChoices.confirm): + for instance in login_acl_model.objects.exclude(action='confirm'): instance.rules = {'ip_group': instance.ip_group, 'time_period': DEFAULT_TIME_PERIODS} updates.append(instance) login_acl_model.objects.bulk_update(updates, ['rules', ]) diff --git a/apps/acls/migrations/0004_auto_20220831_1658.py b/apps/acls/migrations/0004_auto_20220831_1658.py new file mode 100644 index 000000000..6fd1ef86b --- /dev/null +++ b/apps/acls/migrations/0004_auto_20220831_1658.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.13 on 2022-08-31 08:58 + +from django.db import migrations, models + + +def migrate_system_users_to_accounts(apps, schema_editor): + login_asset_acl_model = apps.get_model('acls', 'LoginAssetACL') + qs = login_asset_acl_model.objects.all() + login_asset_acls = [] + for instance in qs: + instance.accounts = instance.system_users + login_asset_acls.append(instance) + login_asset_acl_model.objects.bulk_update(login_asset_acls, ['accounts']) + + +class Migration(migrations.Migration): + dependencies = [ + ('acls', '0003_auto_20211130_1037'), + ] + + operations = [ + migrations.AddField( + model_name='loginassetacl', + name='accounts', + field=models.JSONField(verbose_name='Account'), + ), + migrations.RunPython(migrate_system_users_to_accounts), + migrations.RemoveField( + model_name='loginassetacl', + name='system_users', + ), + + ] diff --git a/apps/acls/migrations/0005_auto_20221201_1846.py b/apps/acls/migrations/0005_auto_20221201_1846.py new file mode 100644 index 000000000..4885ea97e --- /dev/null +++ b/apps/acls/migrations/0005_auto_20221201_1846.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.14 on 2022-12-01 10:46 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('acls', '0004_auto_20220831_1658'), + ] + + operations = [ + migrations.AlterField( + model_name='loginacl', + name='action', + field=models.CharField(default='reject', max_length=64, verbose_name='Action'), + ), + migrations.AlterField( + model_name='loginacl', + name='reviewers', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + migrations.AlterField( + model_name='loginassetacl', + name='action', + field=models.CharField(default='reject', max_length=64, verbose_name='Action'), + ), + migrations.AlterField( + model_name='loginassetacl', + name='reviewers', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + ] diff --git a/apps/acls/migrations/0006_commandfilteracl_commandgroup.py b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py new file mode 100644 index 000000000..95b12e9f0 --- /dev/null +++ b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.14 on 2022-12-01 11:39 + +import uuid + +import django.core.validators +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('acls', '0005_auto_20221201_1846'), + ] + + operations = [ + migrations.CreateModel( + name='CommandGroup', + 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(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')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('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')), + ('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')), + ], + options={ + 'verbose_name': 'Command filter rule', + 'unique_together': {('org_id', 'name')}, + }, + ), + migrations.CreateModel( + name='CommandFilterACL', + fields=[ + ('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)), + ('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')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', + validators=[django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(100)], + verbose_name='Priority')), + ('action', models.CharField(default='reject', max_length=64, verbose_name='Action')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('users', models.JSONField(verbose_name='User')), + ('accounts', models.JSONField(verbose_name='Account')), + ('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')), + ], + options={ + 'verbose_name': 'Command acl', + 'ordering': ('priority', '-date_updated', 'name'), + 'unique_together': {('name', 'org_id')}, + }, + ), + ] diff --git a/apps/acls/migrations/0007_auto_20221202_1048.py b/apps/acls/migrations/0007_auto_20221202_1048.py new file mode 100644 index 000000000..1a61a4ff4 --- /dev/null +++ b/apps/acls/migrations/0007_auto_20221202_1048.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.14 on 2022-12-02 02:48 + +from django.db import migrations + + +def migrate_login_type(apps, schema_editor): + login_asset_model = apps.get_model('acls', 'LoginAssetACL') + login_asset_model.objects.filter(action='login_confirm').update(action='review') + + login_system_model = apps.get_model('acls', 'LoginACL') + login_system_model.objects.filter(action='confirm').update(action='review') + + +class Migration(migrations.Migration): + dependencies = [ + ('acls', '0006_commandfilteracl_commandgroup'), + ] + + operations = [ + migrations.RunPython(migrate_login_type), + ] diff --git a/apps/acls/migrations/0008_commandgroup_comment.py b/apps/acls/migrations/0008_commandgroup_comment.py new file mode 100644 index 000000000..631ff8eb7 --- /dev/null +++ b/apps/acls/migrations/0008_commandgroup_comment.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-12-02 04:25 + +from django.db import migrations, models + + +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'), + ), + ] diff --git a/apps/acls/migrations/0009_auto_20221204_0001.py b/apps/acls/migrations/0009_auto_20221204_0001.py new file mode 100644 index 000000000..b5286160f --- /dev/null +++ b/apps/acls/migrations/0009_auto_20221204_0001.py @@ -0,0 +1,22 @@ +# 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/0010_auto_20221205_1122.py b/apps/acls/migrations/0010_auto_20221205_1122.py new file mode 100644 index 000000000..78adde93b --- /dev/null +++ b/apps/acls/migrations/0010_auto_20221205_1122.py @@ -0,0 +1,25 @@ +# 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/__init__.py b/apps/acls/models/__init__.py index 45d49c378..3c5416992 100644 --- a/apps/acls/models/__init__.py +++ b/apps/acls/models/__init__.py @@ -1,2 +1,3 @@ from .login_acl import * from .login_asset_acl import * +from .command_acl import * diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 73ab5c59c..704e3d743 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -1,10 +1,25 @@ -from django.db import models -from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +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.utils import contains_ip +from orgs.mixins.models import OrgModelMixin + +__all__ = [ + 'ACLManager', + 'BaseACL', + 'BaseACLQuerySet', + 'UserAssetAccountBaseACL', + 'UserAssetAccountACLQuerySet' +] -__all__ = ['BaseACL', 'BaseACLQuerySet'] +class ActionChoices(models.TextChoices): + reject = 'reject', _('Reject') + accept = 'accept', _('Accept') + review = 'review', _('Review') class BaseACLQuerySet(models.QuerySet): @@ -21,6 +36,37 @@ class BaseACLQuerySet(models.QuerySet): return self.inactive() +class UserAssetAccountACLQuerySet(BaseACLQuerySet): + def filter_user(self, username): + q = Q(users__username_group__contains=username) | \ + Q(users__username_group__contains='*') + return self.filter(q) + + def filter_asset(self, name=None, address=None): + queryset = self.filter() + if name: + q = Q(assets__name_group__contains=name) | \ + Q(assets__name_group__contains='*') + queryset = queryset.filter(q) + if address: + ids = [ + q.id for q in queryset + if contains_ip(address, q.assets.get('address_group', [])) + ] + queryset = queryset.filter(id__in=ids) + return queryset + + def filter_account(self, username): + q = Q(accounts__username_group__contains=username) | \ + Q(accounts__username_group__contains='*') + return self.filter(q) + + +class ACLManager(models.Manager): + def valid(self): + return self.get_queryset().valid() + + class BaseACL(CommonModelMixin): name = models.CharField(max_length=128, verbose_name=_('Name')) priority = models.IntegerField( @@ -28,8 +74,49 @@ class BaseACL(CommonModelMixin): help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)] ) + 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)() + class Meta: + ordering = ('priority', 'name') abstract = True + + +class UserAssetAccountBaseACL(BaseACL, OrgModelMixin): + # username_group + users = models.JSONField(verbose_name=_('User')) + # name_group, address_group + assets = models.JSONField(verbose_name=_('Asset')) + # username_group + accounts = models.JSONField(verbose_name=_('Account')) + + objects = ACLManager.from_queryset(UserAssetAccountACLQuerySet)() + + class Meta(BaseACL.Meta): + unique_together = ('name', 'org_id') + abstract = True + + @classmethod + def filter_queryset(cls, user=None, asset=None, account=None, account_username=None, **kwargs): + queryset = cls.objects.all() + org_id = None + if user: + queryset = queryset.filter_user(user.username) + if asset: + org_id = asset.org_id + queryset = queryset.filter_asset(asset.name, asset.address) + if account: + org_id = account.org_id + queryset = queryset.filter_account(account.username) + if account_username: + queryset = queryset.filter_account(username=account_username) + if org_id: + kwargs['org_id'] = org_id + if kwargs: + queryset = queryset.filter(**kwargs) + return queryset diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py new file mode 100644 index 000000000..f10373a02 --- /dev/null +++ b/apps/acls/models/command_acl.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +import re + +from django.db import models +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__) + + +class TypeChoices(models.TextChoices): + command = 'command', _('Command') + regex = 'regex', _('Regex') + + +class CommandGroup(JMSOrgBaseModel): + name = models.CharField(max_length=128, verbose_name=_("Name")) + type = models.CharField( + max_length=16, default=TypeChoices.command, choices=TypeChoices.choices, + verbose_name=_("Type") + ) + 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 + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _("Command group") + + @lazyproperty + def pattern(self): + if self.type == 'command': + s = self.construct_command_regex(self.content) + else: + s = r'{0}'.format(self.content) + return s + + @classmethod + def construct_command_regex(cls, content): + regex = [] + content = content.replace('\r\n', '\n') + for _cmd in content.split('\n'): + cmd = re.sub(r'\s+', ' ', _cmd) + cmd = re.escape(cmd) + cmd = cmd.replace('\\ ', '\s+') + + # 有空格就不能 铆钉单词了 + if ' ' in _cmd: + regex.append(cmd) + continue + if not cmd: + continue + + # 如果是单个字符 + if cmd[-1].isalpha(): + regex.append(r'\b{0}\b'.format(cmd)) + else: + regex.append(r'\b{0}'.format(cmd)) + s = r'{}'.format('|'.join(regex)) + return s + + def match(self, data): + succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case) + if not succeed: + return False, '' + + found = pattern.search(data) + if not found: + return False, '' + else: + return True, found.group() + + @staticmethod + def compile_regex(regex, ignore_case): + args = [] + if ignore_case: + args.append(re.IGNORECASE) + try: + pattern = re.compile(regex, *args) + except Exception as e: + error = _('The generated regular expression is incorrect: {}').format(str(e)) + logger.error(error) + return False, error, None + return True, '', pattern + + def __str__(self): + return '{} % {}'.format(self.name, self.type) + + +class CommandFilterACL(UserAssetAccountBaseACL): + command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Commands')) + + class Meta(UserAssetAccountBaseACL.Meta): + abstract = False + verbose_name = _('Command acl') + + def __str__(self): + return self.name + + def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): + from tickets.const import TicketType + from tickets.models import ApplyCommandTicket + data = { + 'title': _('Command confirm') + ' ({})'.format(session.user), + 'type': TicketType.command_confirm, + 'applicant': session.user_obj, + 'apply_run_user_id': session.user_id, + 'apply_run_asset': str(session.asset), + 'apply_run_account': str(session.account), + 'apply_run_command': run_command[:4090], + 'apply_from_session_id': str(session.id), + 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), + 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id), + 'org_id': org_id, + } + ticket = ApplyCommandTicket.objects.create(**data) + assignees = self.reviewers.all() + ticket.open_by_system(assignees) + return ticket diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 6fcff0231..1178993c8 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -1,45 +1,23 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from .base import BaseACL, BaseACLQuerySet + from common.utils import get_request_ip, get_ip_city from common.utils.ip import contains_ip from common.utils.time_period import contains_time_period from common.utils.timezone import local_now_display - - -class ACLManager(models.Manager): - - def valid(self): - return self.get_queryset().valid() +from .base import BaseACL class LoginACL(BaseACL): - class ActionChoices(models.TextChoices): - reject = 'reject', _('Reject') - allow = 'allow', _('Allow') - confirm = 'confirm', _('Login confirm') - - # 用户 user = models.ForeignKey( - 'users.User', on_delete=models.CASCADE, verbose_name=_('User'), - related_name='login_acls' + 'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User') ) - # 规则 + # 规则, ip_group, time_period rules = models.JSONField(default=dict, verbose_name=_('Rule')) - # 动作 - action = models.CharField( - max_length=64, verbose_name=_('Action'), - choices=ActionChoices.choices, default=ActionChoices.reject - ) - reviewers = models.ManyToManyField( - 'users.User', verbose_name=_("Reviewers"), - related_name="login_confirm_acls", blank=True - ) - objects = ACLManager.from_queryset(BaseACLQuerySet)() - class Meta: - ordering = ('priority', '-date_updated', 'name') + class Meta(BaseACL.Meta): verbose_name = _('Login acl') + abstract = False def __str__(self): return self.name @@ -53,12 +31,13 @@ class LoginACL(BaseACL): @staticmethod def match(user, ip): - acls = LoginACL.filter_acl(user) - if not acls: + acl_qs = LoginACL.filter_acl(user) + if not acl_qs: return - for acl in acls: - if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists(): + for acl in acl_qs: + if acl.is_action(LoginACL.ActionChoices.review) and \ + not acl.reviewers.exists(): continue ip_group = acl.rules.get('ip_group') time_periods = acl.rules.get('time_period') @@ -79,12 +58,12 @@ class LoginACL(BaseACL): login_datetime = local_now_display() data = { 'title': title, - 'type': const.TicketType.login_confirm, 'applicant': self.user, - 'apply_login_city': login_city, 'apply_login_ip': login_ip, - 'apply_login_datetime': login_datetime, 'org_id': Organization.ROOT_ID, + 'apply_login_city': login_city, + 'apply_login_datetime': login_datetime, + 'type': const.TicketType.login_confirm, } ticket = ApplyLoginTicket.objects.create(**data) assignees = self.reviewers.all() diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 9cf7989f9..bdaf9b60c 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -1,102 +1,32 @@ -from django.db import models -from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.models import OrgModelMixin, OrgManager -from .base import BaseACL, BaseACLQuerySet -from common.utils.ip import contains_ip -class ACLManager(OrgManager): - - def valid(self): - return self.get_queryset().valid() +from .base import UserAssetAccountBaseACL -class LoginAssetACL(BaseACL, OrgModelMixin): - class ActionChoices(models.TextChoices): - login_confirm = 'login_confirm', _('Login confirm') +class LoginAssetACL(UserAssetAccountBaseACL): - # 条件 - users = models.JSONField(verbose_name=_('User')) - system_users = models.JSONField(verbose_name=_('System User')) - assets = models.JSONField(verbose_name=_('Asset')) - # 动作 - action = models.CharField( - max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm, - verbose_name=_('Action') - ) - # 动作: 附加字段 - # - login_confirm - reviewers = models.ManyToManyField( - 'users.User', related_name='review_login_asset_acls', blank=True, - verbose_name=_("Reviewers") - ) - - objects = ACLManager.from_queryset(BaseACLQuerySet)() - - class Meta: - unique_together = ('name', 'org_id') - ordering = ('priority', '-date_updated', 'name') + class Meta(UserAssetAccountBaseACL.Meta): verbose_name = _('Login asset acl') + abstract = False def __str__(self): return self.name @classmethod - def filter(cls, user, asset, system_user, action): - queryset = cls.objects.filter(action=action) - queryset = cls.filter_user(user, queryset) - queryset = cls.filter_asset(asset, queryset) - queryset = cls.filter_system_user(system_user, queryset) - return queryset - - @classmethod - def filter_user(cls, user, queryset): - queryset = queryset.filter( - Q(users__username_group__contains=user.username) | - Q(users__username_group__contains='*') - ) - return queryset - - @classmethod - def filter_asset(cls, asset, queryset): - queryset = queryset.filter( - Q(assets__hostname_group__contains=asset.hostname) | - Q(assets__hostname_group__contains='*') - ) - ids = [q.id for q in queryset if contains_ip(asset.ip, q.assets.get('ip_group', []))] - queryset = cls.objects.filter(id__in=ids) - return queryset - - @classmethod - def filter_system_user(cls, system_user, queryset): - queryset = queryset.filter( - Q(system_users__name_group__contains=system_user.name) | - Q(system_users__name_group__contains='*') - ).filter( - Q(system_users__username_group__contains=system_user.username) | - Q(system_users__username_group__contains='*') - ).filter( - Q(system_users__protocol_group__contains=system_user.protocol) | - Q(system_users__protocol_group__contains='*') - ) - return queryset - - @classmethod - def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id): + def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id): from tickets.const import TicketType from tickets.models import ApplyLoginAssetTicket title = _('Login asset confirm') + ' ({})'.format(user) data = { 'title': title, - 'type': TicketType.login_asset_confirm, + 'org_id': org_id, 'applicant': user, 'apply_login_user': user, 'apply_login_asset': asset, - 'apply_login_system_user': system_user, - 'org_id': org_id, + 'apply_login_account': account_username, + 'type': TicketType.login_asset_confirm, } ticket = ApplyLoginAssetTicket.objects.create(**data) ticket.open_by_system(assignees) return ticket - diff --git a/apps/acls/serializers/__init__.py b/apps/acls/serializers/__init__.py index ff52a1ce9..88814b6c4 100644 --- a/apps/acls/serializers/__init__.py +++ b/apps/acls/serializers/__init__.py @@ -1,3 +1,4 @@ from .login_acl import * from .login_asset_acl import * from .login_asset_check import * +from .command_acl import * diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py new file mode 100644 index 000000000..fdf6f8a23 --- /dev/null +++ b/apps/acls/serializers/base.py @@ -0,0 +1,99 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from acls.models.base import ActionChoices +from common.drf.fields import LabeledChoiceField, ObjectRelatedField +from orgs.models import Organization +from users.models import User + +common_help_text = _( + "Format for comma-delimited string, with * indicating a match all. " +) + + +class ACLUsersSerializer(serializers.Serializer): + username_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, + ) + + +class ACLAssestsSerializer(serializers.Serializer): + address_group_help_text = _( + "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)" + ) + + name_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Name"), + help_text=common_help_text, + ) + address_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=1024), + label=_("IP/Host"), + help_text=address_group_help_text, + ) + + +class ACLAccountsSerializer(serializers.Serializer): + username_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, + ) + + +class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): + users = ACLUsersSerializer(label=_('User')) + assets = ACLAssestsSerializer(label=_('Asset')) + accounts = ACLAccountsSerializer(label=_('Account')) + reviewers = ObjectRelatedField( + queryset=User.objects, many=True, required=False, label=_('Reviewers') + ) + reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") + action = LabeledChoiceField( + choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action") + ) + + class Meta: + fields_mini = ["id", "name"] + fields_small = fields_mini + [ + "users", "accounts", "assets", "is_active", + "date_created", "date_updated", "priority", + "action", "comment", "created_by", "org_id", + ] + fields_m2m = ["reviewers", "reviewers_amount"] + fields = fields_small + fields_m2m + extra_kwargs = { + "reviewers": {"allow_null": False, "required": True}, + "priority": {"default": 50}, + "is_active": {"default": True}, + } + + def validate_reviewers(self, reviewers): + action = self.initial_data.get('action') + if not action and self.instance: + action = self.instance.action + if action != ActionChoices.review: + return reviewers + org_id = self.fields["org_id"].default() + org = Organization.get_instance(org_id) + if not org: + error = _("The organization `{}` does not exist".format(org_id)) + raise serializers.ValidationError(error) + users = org.get_members() + valid_reviewers = list(set(reviewers) & set(users)) + if not valid_reviewers: + error = _( + "None of the reviewers belong to Organization `{}`".format(org.name) + ) + raise serializers.ValidationError(error) + return valid_reviewers diff --git a/apps/acls/serializers/command_acl.py b/apps/acls/serializers/command_acl.py new file mode 100644 index 000000000..0accb5f56 --- /dev/null +++ b/apps/acls/serializers/command_acl.py @@ -0,0 +1,29 @@ +from django.utils.translation import ugettext_lazy as _ + +from acls.models import CommandGroup, CommandFilterACL +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer + +__all__ = ["CommandFilterACLSerializer", "CommandGroupSerializer"] + + +class CommandGroupSerializer(BulkOrgResourceModelSerializer): + type = LabeledChoiceField( + choices=CommandGroup.TypeChoices.choices, default=CommandGroup.TypeChoices.command, + label=_('Type') + ) + + class Meta: + model = CommandGroup + fields = ['id', 'name', 'type', 'content', 'ignore_case', 'comment'] + + +class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer): + command_groups = ObjectRelatedField( + queryset=CommandGroup.objects, many=True, required=False, label=_('Command group') + ) + + class Meta(BaseSerializer.Meta): + model = CommandFilterACL + fields = BaseSerializer.Meta.fields + ['command_groups'] diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index a699ae1ea..db89445c8 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -1,39 +1,48 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.drf.serializers import BulkModelSerializer -from common.drf.serializers import MethodSerializer + +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.drf.serializers import BulkModelSerializer, MethodSerializer from jumpserver.utils import has_valid_xpack_license -from ..models import LoginACL +from users.models import User from .rules import RuleSerializer +from ..models import LoginACL -__all__ = ['LoginACLSerializer', ] +__all__ = [ + "LoginACLSerializer", +] -common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') +common_help_text = _( + "Format for comma-delimited string, with * indicating a match all. " +) class LoginACLSerializer(BulkModelSerializer): - user_display = serializers.ReadOnlyField(source='user.username', label=_('Username')) - reviewers_display = serializers.SerializerMethodField(label=_('Reviewers')) - action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) - reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') + user = ObjectRelatedField(queryset=User.objects, label=_("User")) + reviewers = ObjectRelatedField( + queryset=User.objects, label=_("Reviewers"), many=True, required=False + ) + action = LabeledChoiceField(choices=LoginACL.ActionChoices.choices) + reviewers_amount = serializers.IntegerField( + read_only=True, source="reviewers.count" + ) rules = MethodSerializer() class Meta: model = LoginACL - fields_mini = ['id', 'name'] + fields_mini = ["id", "name"] fields_small = fields_mini + [ - 'priority', 'rules', 'action', 'action_display', - 'is_active', 'user', 'user_display', - 'date_created', 'date_updated', 'reviewers_amount', - 'comment', 'created_by' + "priority", "user", "rules", "action", + "is_active", "date_created", "date_updated", + "reviewers_amount", "comment", "created_by", ] - fields_fk = ['user', 'user_display'] - fields_m2m = ['reviewers', 'reviewers_display'] + fields_fk = ["user"] + fields_m2m = ["reviewers"] fields = fields_small + fields_fk + fields_m2m extra_kwargs = { - 'priority': {'default': 50}, - 'is_active': {'default': True}, - "reviewers": {'allow_null': False, 'required': True}, + "priority": {"default": 50}, + "is_active": {"default": True}, + "reviewers": {"allow_null": False, "required": True}, } def __init__(self, *args, **kwargs): @@ -41,16 +50,13 @@ class LoginACLSerializer(BulkModelSerializer): self.set_action_choices() def set_action_choices(self): - action = self.fields.get('action') + action = self.fields.get("action") if not action: return choices = action._choices if not has_valid_xpack_license(): - choices.pop(LoginACL.ActionChoices.confirm, None) + choices.pop(LoginACL.ActionChoices.review, None) action._choices = choices def get_rules_serializer(self): return RuleSerializer() - - def get_reviewers_display(self, obj): - return ','.join([str(user) for user in obj.reviewers.all()]) diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index df62a04f8..de160d124 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -1,105 +1,11 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from assets.models import SystemUser -from acls import models -from orgs.models import Organization + +from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer +from ..models import LoginAssetACL + +__all__ = ["LoginAssetACLSerializer"] -__all__ = ['LoginAssetACLSerializer'] - - -common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') - - -class LoginAssetACLUsersSerializer(serializers.Serializer): - username_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), - help_text=common_help_text - ) - - -class LoginAssetACLAssestsSerializer(serializers.Serializer): - ip_group_help_text = _( - '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)' - ) - - ip_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'), - help_text=ip_group_help_text - ) - hostname_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'), - help_text=common_help_text - ) - - -class LoginAssetACLSystemUsersSerializer(serializers.Serializer): - protocol_group_help_text = _( - 'Format for comma-delimited string, with * indicating a match all. ' - 'Protocol options: {}' - ) - - name_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Name'), - help_text=common_help_text - ) - username_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), - help_text=common_help_text - ) - protocol_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'), - help_text=protocol_group_help_text.format( - ', '.join([SystemUser.Protocol.ssh, SystemUser.Protocol.telnet]) - ) - ) - - @staticmethod - def validate_protocol_group(protocol_group): - unsupported_protocols = set(protocol_group) - set(SystemUser.ASSET_CATEGORY_PROTOCOLS + ['*']) - if unsupported_protocols: - error = _('Unsupported protocols: {}').format(unsupported_protocols) - raise serializers.ValidationError(error) - return protocol_group - - -class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): - users = LoginAssetACLUsersSerializer() - assets = LoginAssetACLAssestsSerializer() - system_users = LoginAssetACLSystemUsersSerializer() - reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') - action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) - - class Meta: - model = models.LoginAssetACL - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'users', 'system_users', 'assets', - 'is_active', - 'date_created', 'date_updated', - 'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id' - ] - fields_m2m = ['reviewers', 'reviewers_amount'] - fields = fields_small + fields_m2m - extra_kwargs = { - "reviewers": {'allow_null': False, 'required': True}, - 'priority': {'default': 50}, - 'is_active': {'default': True}, - } - - def validate_reviewers(self, reviewers): - org_id = self.fields['org_id'].default() - org = Organization.get_instance(org_id) - if not org: - error = _('The organization `{}` does not exist'.format(org_id)) - raise serializers.ValidationError(error) - users = org.get_members() - valid_reviewers = list(set(reviewers) & set(users)) - if not valid_reviewers: - error = _('None of the reviewers belong to Organization `{}`'.format(org.name)) - raise serializers.ValidationError(error) - return valid_reviewers +class LoginAssetACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer): + class Meta(BaseSerializer.Meta): + model = LoginAssetACL diff --git a/apps/acls/serializers/login_asset_check.py b/apps/acls/serializers/login_asset_check.py index ec7a7c35c..eac7481d5 100644 --- a/apps/acls/serializers/login_asset_check.py +++ b/apps/acls/serializers/login_asset_check.py @@ -1,10 +1,8 @@ -from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from orgs.utils import tmp_to_root_org from common.utils import get_object_or_none, lazyproperty from users.models import User -from assets.models import Asset, SystemUser - +from assets.models import Asset, Account __all__ = ['LoginAssetCheckSerializer'] @@ -12,60 +10,26 @@ __all__ = ['LoginAssetCheckSerializer'] class LoginAssetCheckSerializer(serializers.Serializer): user_id = serializers.UUIDField(required=True, allow_null=False) asset_id = serializers.UUIDField(required=True, allow_null=False) - system_user_id = serializers.UUIDField(required=True, allow_null=False) - system_user_username = serializers.CharField(max_length=128, default='') + account_username = serializers.CharField(max_length=128, default='') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.user = None self.asset = None - self._system_user = None - self._system_user_username = None def validate_user_id(self, user_id): - self.user = self.validate_object_exist(User, user_id) + self.user = self.get_object(User, user_id) return user_id def validate_asset_id(self, asset_id): - self.asset = self.validate_object_exist(Asset, asset_id) + self.asset = self.get_object(Asset, asset_id) return asset_id - def validate_system_user_id(self, system_user_id): - self._system_user = self.validate_object_exist(SystemUser, system_user_id) - return system_user_id - - def validate_system_user_username(self, system_user_username): - system_user_id = self.initial_data.get('system_user_id') - system_user = self.validate_object_exist(SystemUser, system_user_id) - if self._system_user.login_mode == SystemUser.LOGIN_MANUAL \ - and not system_user.username \ - and not system_user.username_same_with_user \ - and not system_user_username: - error = 'Missing parameter: system_user_username' - raise serializers.ValidationError(error) - self._system_user_username = system_user_username - return system_user_username - @staticmethod - def validate_object_exist(model, field_id): + def get_object(model, pk): with tmp_to_root_org(): - obj = get_object_or_none(model, pk=field_id) - if not obj: - error = '{} Model object does not exist'.format(model.__name__) - raise serializers.ValidationError(error) - return obj - - @lazyproperty - def system_user(self): - if self._system_user.username_same_with_user: - username = self.user.username - elif self._system_user.login_mode == SystemUser.LOGIN_MANUAL: - username = self._system_user_username - else: - username = self._system_user.username - self._system_user.username = username - return self._system_user - - @lazyproperty - def org(self): - return self.asset.org + obj = get_object_or_none(model, pk=pk) + if obj: + return obj + error = '{} Model object does not exist'.format(model.__name__) + raise serializers.ValidationError(error) diff --git a/apps/acls/urls/api_urls.py b/apps/acls/urls/api_urls.py index c4040ff45..0185278d9 100644 --- a/apps/acls/urls/api_urls.py +++ b/apps/acls/urls/api_urls.py @@ -1,14 +1,15 @@ from django.urls import path from rest_framework_bulk.routes import BulkRouter + from .. import api - app_name = 'acls' - router = BulkRouter() router.register(r'login-acls', api.LoginACLViewSet, 'login-acl') router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl') +router.register(r'command-groups', api.CommandGroupViewSet, 'command-group') +router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl') urlpatterns = [ path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'), diff --git a/apps/applications/admin.py b/apps/applications/admin.py deleted file mode 100644 index 8c38f3f3d..000000000 --- a/apps/applications/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/applications/api/__init__.py b/apps/applications/api/__init__.py deleted file mode 100644 index 7a32ac0a0..000000000 --- a/apps/applications/api/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .application import * -from .account import * -from .remote_app import * diff --git a/apps/applications/api/account.py b/apps/applications/api/account.py deleted file mode 100644 index bad9a02bd..000000000 --- a/apps/applications/api/account.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding: utf-8 -# - -from django_filters import rest_framework as filters -from django.db.models import Q - -from common.drf.filters import BaseFilterSet -from common.drf.api import JMSBulkModelViewSet -from common.mixins import RecordViewLogMixin -from common.permissions import UserConfirmation -from authentication.const import ConfirmType -from rbac.permissions import RBACPermission -from assets.models import SystemUser -from ..models import Account -from .. import serializers - - -class AccountFilterSet(BaseFilterSet): - username = filters.CharFilter(method='do_nothing') - type = filters.CharFilter(field_name='type', lookup_expr='exact') - category = filters.CharFilter(field_name='category', lookup_expr='exact') - app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact') - - class Meta: - model = Account - fields = ['app', 'systemuser'] - - @property - def qs(self): - qs = super().qs - qs = self.filter_username(qs) - return qs - - def filter_username(self, qs): - username = self.get_query_param('username') - if not username: - return qs - q = Q(username=username) | Q(systemuser__username=username) - qs = qs.filter(q).distinct() - return qs - - -class ApplicationAccountViewSet(JMSBulkModelViewSet): - model = Account - search_fields = ['username', 'app_display'] - filterset_class = AccountFilterSet - filterset_fields = ['username', 'app_display', 'type', 'category', 'app'] - serializer_class = serializers.AppAccountSerializer - - def get_queryset(self): - queryset = Account.get_queryset() - return queryset - - -class SystemUserAppRelationViewSet(ApplicationAccountViewSet): - perm_model = SystemUser - - -class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet): - serializer_class = serializers.AppAccountSecretSerializer - permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] - http_method_names = ['get', 'options'] - rbac_perms = { - 'retrieve': 'applications.view_applicationaccountsecret', - 'list': 'applications.view_applicationaccountsecret', - } diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index 4d04e894f..e69de29bb 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -1,40 +0,0 @@ -# coding: utf-8 -# -from orgs.mixins.api import OrgBulkModelViewSet -from rest_framework.decorators import action -from rest_framework.response import Response - - -from common.tree import TreeNodeSerializer -from common.mixins.api import SuggestionMixin -from .. import serializers -from ..models import Application - -__all__ = ['ApplicationViewSet'] - - -class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet): - model = Application - filterset_fields = { - 'name': ['exact'], - 'category': ['exact', 'in'], - 'type': ['exact', 'in'], - } - search_fields = ('name', 'type', 'category') - serializer_classes = { - 'default': serializers.AppSerializer, - 'get_tree': TreeNodeSerializer, - 'suggestion': serializers.MiniAppSerializer - } - rbac_perms = { - 'get_tree': 'applications.view_application', - 'match': 'applications.match_application' - } - - @action(methods=['GET'], detail=False, url_path='tree') - def get_tree(self, request, *args, **kwargs): - show_count = request.query_params.get('show_count', '1') == '1' - queryset = self.filter_queryset(self.get_queryset()) - tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count) - serializer = self.get_serializer(tree_nodes, many=True) - return Response(serializer.data) diff --git a/apps/applications/api/remote_app.py b/apps/applications/api/remote_app.py deleted file mode 100644 index 4aea7d93b..000000000 --- a/apps/applications/api/remote_app.py +++ /dev/null @@ -1,16 +0,0 @@ -# coding: utf-8 -# - -from orgs.mixins import generics -from .. import models -from ..serializers import RemoteAppConnectionInfoSerializer - - -__all__ = [ - 'RemoteAppConnectionInfoApi', -] - - -class RemoteAppConnectionInfoApi(generics.RetrieveAPIView): - model = models.Application - serializer_class = RemoteAppConnectionInfoSerializer diff --git a/apps/applications/const.py b/apps/applications/const.py deleted file mode 100644 index 0bf130f6f..000000000 --- a/apps/applications/const.py +++ /dev/null @@ -1,86 +0,0 @@ -# coding: utf-8 -# -from django.db import models -from django.utils.translation import ugettext_lazy as _ - - -class AppCategory(models.TextChoices): - db = 'db', _('Database') - remote_app = 'remote_app', _('Remote app') - cloud = 'cloud', 'Cloud' - - @classmethod - def get_label(cls, category): - return dict(cls.choices).get(category, '') - - @classmethod - def is_xpack(cls, category): - return category in ['remote_app'] - - -class AppType(models.TextChoices): - # db category - mysql = 'mysql', 'MySQL' - mariadb = 'mariadb', 'MariaDB' - oracle = 'oracle', 'Oracle' - pgsql = 'postgresql', 'PostgreSQL' - sqlserver = 'sqlserver', 'SQLServer' - redis = 'redis', 'Redis' - mongodb = 'mongodb', 'MongoDB' - clickhouse = 'clickhouse', 'ClickHouse' - - # remote-app category - chrome = 'chrome', 'Chrome' - mysql_workbench = 'mysql_workbench', 'MySQL Workbench' - vmware_client = 'vmware_client', 'vSphere Client' - custom = 'custom', _('Custom') - - # cloud category - k8s = 'k8s', 'Kubernetes' - - @classmethod - def category_types_mapper(cls): - return { - AppCategory.db: [ - cls.mysql, cls.mariadb, cls.oracle, cls.pgsql, - cls.sqlserver, cls.redis, cls.mongodb, cls.clickhouse - ], - AppCategory.remote_app: [ - cls.chrome, cls.mysql_workbench, - cls.vmware_client, cls.custom - ], - AppCategory.cloud: [cls.k8s] - } - - @classmethod - def type_category_mapper(cls): - mapper = {} - for category, tps in cls.category_types_mapper().items(): - for tp in tps: - mapper[tp] = category - return mapper - - @classmethod - def get_label(cls, tp): - return dict(cls.choices).get(tp, '') - - @classmethod - def db_types(cls): - return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]] - - @classmethod - def remote_app_types(cls): - return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]] - - @classmethod - def cloud_types(cls): - return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]] - - @classmethod - def is_xpack(cls, tp): - tp_category_mapper = cls.type_category_mapper() - category = tp_category_mapper[tp] - - if AppCategory.is_xpack(category): - return True - return tp in ['oracle', 'postgresql', 'sqlserver', 'clickhouse'] diff --git a/apps/applications/hands.py b/apps/applications/hands.py deleted file mode 100644 index c2bf4fd20..000000000 --- a/apps/applications/hands.py +++ /dev/null @@ -1,14 +0,0 @@ -""" - jumpserver.__app__.hands.py - ~~~~~~~~~~~~~~~~~ - - This app depends other apps api, function .. should be import or write mack here. - - Other module of this app shouldn't connect with other app. - - :copyright: (c) 2014-2018 by JumpServer Team. - :license: GPL v2, see LICENSE for more details. -""" - - -from users.models import User, UserGroup diff --git a/apps/applications/migrations/0010_appaccount_historicalappaccount.py b/apps/applications/migrations/0010_appaccount_historicalappaccount.py index f79fd2475..cd0bf88d1 100644 --- a/apps/applications/migrations/0010_appaccount_historicalappaccount.py +++ b/apps/applications/migrations/0010_appaccount_historicalappaccount.py @@ -71,6 +71,6 @@ class Migration(migrations.Migration): 'verbose_name': 'Account', 'unique_together': {('username', 'app', 'systemuser')}, }, - bases=(models.Model, assets.models.base.AuthMixin), + bases=(models.Model,), ), ] diff --git a/apps/applications/migrations/0022_auto_20220714_1046.py b/apps/applications/migrations/0022_auto_20220714_1046.py deleted file mode 100644 index 8ecb1cbe6..000000000 --- a/apps/applications/migrations/0022_auto_20220714_1046.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.12 on 2022-07-14 02:46 - -from django.db import migrations - - -def migrate_db_oracle_version_to_attrs(apps, schema_editor): - db_alias = schema_editor.connection.alias - model = apps.get_model("applications", "Application") - oracles = list(model.objects.using(db_alias).filter(type='oracle')) - for o in oracles: - o.attrs['version'] = '12c' - model.objects.using(db_alias).bulk_update(oracles, ['attrs']) - - -class Migration(migrations.Migration): - - dependencies = [ - ('applications', '0021_auto_20220629_1826'), - ] - - operations = [ - migrations.RunPython(migrate_db_oracle_version_to_attrs) - ] diff --git a/apps/applications/migrations/0022_auto_20220817_1346.py b/apps/applications/migrations/0022_auto_20220817_1346.py new file mode 100644 index 000000000..0cf950848 --- /dev/null +++ b/apps/applications/migrations/0022_auto_20220817_1346.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.14 on 2022-08-17 05:46 + +from django.db import migrations + + +def migrate_db_oracle_version_to_attrs(apps, schema_editor): + db_alias = schema_editor.connection.alias + model = apps.get_model("applications", "Application") + oracles = list(model.objects.using(db_alias).filter(type='oracle')) + for o in oracles: + o.attrs['version'] = '12c' + model.objects.using(db_alias).bulk_update(oracles, ['attrs']) + + +class Migration(migrations.Migration): + dependencies = [ + ('applications', '0021_auto_20220629_1826'), + ] + + operations = [ + migrations.RunPython(migrate_db_oracle_version_to_attrs), + migrations.AlterUniqueTogether( + name='account', + unique_together=None, + ), + migrations.RemoveField( + model_name='account', + name='app', + ), + migrations.RemoveField( + model_name='account', + name='systemuser', + ), + migrations.RemoveField( + model_name='application', + name='domain', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='app', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='history_user', + ), + migrations.RemoveField( + model_name='historicalaccount', + name='systemuser', + ), + ] diff --git a/apps/applications/migrations/0023_auto_20220715_1556.py b/apps/applications/migrations/0023_auto_20220817_1716.py similarity index 69% rename from apps/applications/migrations/0023_auto_20220715_1556.py rename to apps/applications/migrations/0023_auto_20220817_1716.py index 03123efab..6f49aff69 100644 --- a/apps/applications/migrations/0023_auto_20220715_1556.py +++ b/apps/applications/migrations/0023_auto_20220817_1716.py @@ -1,6 +1,5 @@ -# Generated by Django 3.1.14 on 2022-07-15 07:56 +# Generated by Django 3.2.14 on 2022-08-17 09:16 import time -from collections import defaultdict from django.db import migrations @@ -40,9 +39,22 @@ def migrate_account_dirty_data(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('applications', '0022_auto_20220714_1046'), + ('applications', '0022_auto_20220817_1346'), + ('perms', '0031_auto_20220816_1600'), + ('ops', '0022_auto_20220817_1346'), + ('assets', '0105_auto_20220817_1544'), + ('tickets', '0020_auto_20220817_1346'), ] operations = [ migrations.RunPython(migrate_account_dirty_data), + migrations.DeleteModel( + name='Account', + ), + migrations.DeleteModel( + name='HistoricalAccount', + ), + migrations.DeleteModel( + name='ApplicationUser', + ), ] diff --git a/apps/applications/migrations/0024_auto_20220818_1057.py b/apps/applications/migrations/0024_auto_20220818_1057.py new file mode 100644 index 000000000..7a0b8919a --- /dev/null +++ b/apps/applications/migrations/0024_auto_20220818_1057.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-08-18 02:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0023_auto_20220817_1716'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='category', + field=models.CharField(max_length=16, verbose_name='Category'), + ), + migrations.AlterField( + model_name='application', + name='type', + field=models.CharField(max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/applications/models.py b/apps/applications/models.py new file mode 100644 index 000000000..f1b1bbc8a --- /dev/null +++ b/apps/applications/models.py @@ -0,0 +1,28 @@ + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins.models import OrgModelMixin +from common.mixins import CommonModelMixin + + +class Application(CommonModelMixin, OrgModelMixin): + name = models.CharField(max_length=128, verbose_name=_('Name')) + category = models.CharField( + max_length=16, verbose_name=_('Category') + ) + type = models.CharField( + 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') + unique_together = [('org_id', 'name')] + ordering = ('name',) + permissions = [ + ('match_application', _('Can match application')), + ] diff --git a/apps/applications/models/__init__.py b/apps/applications/models/__init__.py deleted file mode 100644 index 4bd20e32f..000000000 --- a/apps/applications/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .application import * -from .account import * diff --git a/apps/applications/models/account.py b/apps/applications/models/account.py deleted file mode 100644 index 627dec91a..000000000 --- a/apps/applications/models/account.py +++ /dev/null @@ -1,110 +0,0 @@ -from django.db import models -from simple_history.models import HistoricalRecords -from django.db.models import F -from django.utils.translation import ugettext_lazy as _ - -from common.utils import lazyproperty -from assets.models.base import BaseUser - - -class Account(BaseUser): - app = models.ForeignKey( - 'applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Application') - ) - systemuser = models.ForeignKey( - 'assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user") - ) - version = models.IntegerField(default=1, verbose_name=_('Version')) - history = HistoricalRecords() - - auth_attrs = ['username', 'password', 'private_key', 'public_key'] - - class Meta: - verbose_name = _('Application account') - unique_together = [('username', 'app', 'systemuser')] - permissions = [ - ('view_applicationaccountsecret', _('Can view application account secret')), - ('change_appplicationaccountsecret', _('Can change application account secret')), - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.auth_snapshot = {} - - def get_or_systemuser_attr(self, attr): - val = getattr(self, attr, None) - if val: - return val - if self.systemuser: - return getattr(self.systemuser, attr, '') - return '' - - def load_auth(self): - for attr in self.auth_attrs: - value = self.get_or_systemuser_attr(attr) - self.auth_snapshot[attr] = [getattr(self, attr), value] - setattr(self, attr, value) - - def unload_auth(self): - if not self.systemuser: - return - - for attr, values in self.auth_snapshot.items(): - origin_value, loaded_value = values - current_value = getattr(self, attr, '') - if current_value == loaded_value: - setattr(self, attr, origin_value) - - def save(self, *args, **kwargs): - self.unload_auth() - instance = super().save(*args, **kwargs) - self.load_auth() - return instance - - @lazyproperty - def category(self): - return self.app.category - - @lazyproperty - def type(self): - return self.app.type - - @lazyproperty - def attrs(self): - return self.app.attrs - - @lazyproperty - def app_display(self): - return self.systemuser.name - - @property - def username_display(self): - return self.get_or_systemuser_attr('username') or '' - - @lazyproperty - def systemuser_display(self): - if not self.systemuser: - return '' - return str(self.systemuser) - - @property - def smart_name(self): - username = self.username_display - - if self.app: - app = str(self.app) - else: - app = '*' - return '{}@{}'.format(username, app) - - @classmethod - def get_queryset(cls): - queryset = cls.objects.all() \ - .annotate(type=F('app__type')) \ - .annotate(app_display=F('app__name')) \ - .annotate(systemuser_display=F('systemuser__name')) \ - .annotate(category=F('app__category')) - return queryset - - def __str__(self): - return self.smart_name diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py deleted file mode 100644 index 3661188fe..000000000 --- a/apps/applications/models/application.py +++ /dev/null @@ -1,310 +0,0 @@ -from collections import defaultdict -from urllib.parse import urlencode, parse_qsl - -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.conf import settings - -from orgs.mixins.models import OrgModelMixin -from common.mixins import CommonModelMixin -from common.tree import TreeNode -from common.utils import is_uuid -from assets.models import Asset, SystemUser - -from .. import const - - -class ApplicationTreeNodeMixin: - id: str - name: str - type: str - category: str - attrs: dict - - @staticmethod - def create_tree_id(pid, type, v): - i = dict(parse_qsl(pid)) - i[type] = v - tree_id = urlencode(i) - return tree_id - - @classmethod - def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None, - show_empty=True, show_count=True): - count = counts.get(c.value, 0) - if count == 0 and not show_empty: - return None - label = c.label - if count is not None and show_count: - label = '{} ({})'.format(label, count) - data = { - 'id': id_, - 'name': label, - 'title': label, - 'pId': pid, - 'isParent': bool(count), - 'open': opened, - 'iconSkin': '', - 'meta': { - 'type': tp, - 'data': { - 'name': c.name, - 'value': c.value - } - } - } - return TreeNode(**data) - - @classmethod - def create_root_tree_node(cls, queryset, show_count=True): - count = queryset.count() if show_count else None - root_id = 'applications' - root_name = _('Applications') - if count is not None and show_count: - root_name = '{} ({})'.format(root_name, count) - node = TreeNode(**{ - 'id': root_id, - 'name': root_name, - 'title': root_name, - 'pId': '', - 'isParent': True, - 'open': True, - 'iconSkin': '', - 'meta': { - 'type': 'applications_root', - } - }) - return node - - @classmethod - def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True): - nodes = [] - categories = const.AppType.category_types_mapper().keys() - for category in categories: - if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category): - continue - i = cls.create_tree_id(pid, 'category', category.value) - node = cls.create_choice_node( - category, i, pid=pid, tp='category', - counts=counts, opened=False, show_empty=show_empty, - show_count=show_count - ) - if not node: - continue - nodes.append(node) - return nodes - - @classmethod - def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True): - nodes = [] - temp_pid = pid - type_category_mapper = const.AppType.type_category_mapper() - types = const.AppType.type_category_mapper().keys() - - for tp in types: - if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp): - continue - category = type_category_mapper.get(tp) - pid = cls.create_tree_id(pid, 'category', category.value) - i = cls.create_tree_id(pid, 'type', tp.value) - node = cls.create_choice_node( - tp, i, pid, tp='type', counts=counts, opened=False, - show_empty=show_empty, show_count=show_count - ) - pid = temp_pid - if not node: - continue - nodes.append(node) - return nodes - - @staticmethod - def get_tree_node_counts(queryset): - counts = defaultdict(int) - values = queryset.values_list('type', 'category') - for i in values: - tp = i[0] - category = i[1] - counts[tp] += 1 - counts[category] += 1 - return counts - - @classmethod - def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True): - counts = cls.get_tree_node_counts(queryset) - tree_nodes = [] - - # 类别的节点 - tree_nodes += cls.create_category_tree_nodes( - pid, counts, show_empty=show_empty, - show_count=show_count - ) - - # 类型的节点 - tree_nodes += cls.create_types_tree_nodes( - pid, counts, show_empty=show_empty, - show_count=show_count - ) - return tree_nodes - - @classmethod - def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True): - tree_nodes = [] - - # 根节点有可能是组织名称 - if root_node is None: - root_node = cls.create_root_tree_node(queryset, show_count=show_count) - tree_nodes.append(root_node) - - tree_nodes += cls.create_category_type_tree_nodes( - queryset, root_node.id, show_empty=show_empty, show_count=show_count - ) - - # 应用的节点 - for app in queryset: - if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type): - continue - node = app.as_tree_node(root_node.id) - tree_nodes.append(node) - return tree_nodes - - def create_app_tree_pid(self, root_id): - pid = self.create_tree_id(root_id, 'category', self.category) - pid = self.create_tree_id(pid, 'type', self.type) - return pid - - def as_tree_node(self, pid, k8s_as_tree=False): - from ..utils import KubernetesTree - if self.type == const.AppType.k8s and k8s_as_tree: - node = KubernetesTree(pid).as_tree_node(self) - else: - node = self._as_tree_node(pid) - return node - - def _attrs_to_tree(self): - if self.category == const.AppCategory.db: - return self.attrs - return {} - - def _as_tree_node(self, pid): - icon_skin_category_mapper = { - 'remote_app': 'chrome', - 'db': 'database', - 'cloud': 'cloud' - } - icon_skin = icon_skin_category_mapper.get(self.category, 'file') - pid = self.create_app_tree_pid(pid) - node = TreeNode(**{ - 'id': str(self.id), - 'name': self.name, - 'title': self.name, - 'pId': pid, - 'isParent': False, - 'open': False, - 'iconSkin': icon_skin, - 'meta': { - 'type': 'application', - 'data': { - 'category': self.category, - 'type': self.type, - 'attrs': self._attrs_to_tree() - } - } - }) - return node - - -class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin): - APP_TYPE = const.AppType - - name = models.CharField(max_length=128, verbose_name=_('Name')) - category = models.CharField( - max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category') - ) - type = models.CharField( - max_length=16, choices=const.AppType.choices, verbose_name=_('Type') - ) - domain = models.ForeignKey( - 'assets.Domain', null=True, blank=True, related_name='applications', - on_delete=models.SET_NULL, verbose_name=_("Domain"), - ) - 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') - unique_together = [('org_id', 'name')] - ordering = ('name',) - permissions = [ - ('match_application', _('Can match application')), - ] - - def __str__(self): - category_display = self.get_category_display() - type_display = self.get_type_display() - return f'{self.name}({type_display})[{category_display}]' - - @property - def category_remote_app(self): - return self.category == const.AppCategory.remote_app.value - - @property - def category_cloud(self): - return self.category == const.AppCategory.cloud.value - - @property - def category_db(self): - return self.category == const.AppCategory.db.value - - def is_type(self, tp): - return self.type == tp - - def get_rdp_remote_app_setting(self): - from applications.serializers.attrs import get_serializer_class_by_application_type - if not self.category_remote_app: - raise ValueError(f"Not a remote app application: {self.name}") - serializer_class = get_serializer_class_by_application_type(self.type) - fields = serializer_class().get_fields() - - parameters = [self.type] - for field_name in list(fields.keys()): - if field_name in ['asset']: - continue - value = self.attrs.get(field_name) - if not value: - continue - if field_name == 'path': - value = '\"%s\"' % value - parameters.append(str(value)) - - parameters = ' '.join(parameters) - return { - 'program': '||jmservisor', - 'working_directory': '', - 'parameters': parameters - } - - def get_remote_app_asset(self, raise_exception=True): - asset_id = self.attrs.get('asset') - if is_uuid(asset_id): - return Asset.objects.filter(id=asset_id).first() - if raise_exception: - raise ValueError("Remote App not has asset attr") - - def get_target_ip(self): - target_ip = '' - if self.category_remote_app: - asset = self.get_remote_app_asset() - target_ip = asset.ip if asset else target_ip - elif self.category_cloud: - target_ip = self.attrs.get('cluster') - elif self.category_db: - target_ip = self.attrs.get('host') - return target_ip - - -class ApplicationUser(SystemUser): - class Meta: - proxy = True - verbose_name = _('Application user') diff --git a/apps/applications/permissions.py b/apps/applications/permissions.py deleted file mode 100644 index e56fd3652..000000000 --- a/apps/applications/permissions.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import permissions - - -__all__ = ['IsRemoteApp'] - - -class IsRemoteApp(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return obj.category_remote_app diff --git a/apps/applications/serializers/__init__.py b/apps/applications/serializers/__init__.py deleted file mode 100644 index 3785f035d..000000000 --- a/apps/applications/serializers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .application import * -from .remote_app import * diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py deleted file mode 100644 index ac8f38f2a..000000000 --- a/apps/applications/serializers/application.py +++ /dev/null @@ -1,184 +0,0 @@ -# coding: utf-8 -# -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from assets.serializers.base import AuthSerializerMixin -from common.drf.serializers import MethodSerializer, SecretReadableMixin -from .attrs import ( - category_serializer_classes_mapping, - type_serializer_classes_mapping, - type_secret_serializer_classes_mapping -) -from .. import models -from .. import const - -__all__ = [ - 'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin', - 'AppAccountSerializer', 'AppAccountSecretSerializer', 'AppAccountBackUpSerializer' -] - - -class AppSerializerMixin(serializers.Serializer): - attrs = MethodSerializer() - - @property - def app(self): - if isinstance(self.instance, models.Application): - instance = self.instance - else: - instance = None - return instance - - def get_attrs_serializer(self): - instance = self.app - tp = getattr(self, 'tp', None) - default_serializer = serializers.Serializer(read_only=True) - if not tp: - if instance: - tp = instance.type - category = instance.category - else: - tp = self.context['request'].query_params.get('type') - category = self.context['request'].query_params.get('category') - if tp: - if isinstance(self, AppAccountBackUpSerializer): - serializer_class = type_secret_serializer_classes_mapping.get(tp) - else: - serializer_class = type_serializer_classes_mapping.get(tp) - elif category: - serializer_class = category_serializer_classes_mapping.get(category) - else: - serializer_class = default_serializer - - if not serializer_class: - serializer_class = default_serializer - - if isinstance(serializer_class, type): - serializer = serializer_class() - else: - serializer = serializer_class - return serializer - - def create(self, validated_data): - return super().create(validated_data) - - def update(self, instance, validated_data): - return super().update(instance, validated_data) - - -class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer): - category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - - class Meta: - model = models.Application - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'category', 'category_display', 'type', 'type_display', - 'attrs', 'date_created', 'date_updated', 'created_by', 'comment' - ] - fields_fk = ['domain'] - fields = fields_small + fields_fk - read_only_fields = [ - 'created_by', 'date_created', 'date_updated', 'get_type_display', - ] - - def validate_attrs(self, attrs): - _attrs = self.instance.attrs if self.instance else {} - _attrs.update(attrs) - return _attrs - - -class MiniAppSerializer(serializers.ModelSerializer): - class Meta: - model = models.Application - fields = AppSerializer.Meta.fields_mini - - -class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResourceModelSerializer): - category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True) - category_display = serializers.SerializerMethodField(label=_('Category display')) - type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True) - type_display = serializers.SerializerMethodField(label=_('Type display')) - date_created = serializers.DateTimeField(label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True) - date_updated = serializers.DateTimeField(label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True) - - category_mapper = dict(const.AppCategory.choices) - type_mapper = dict(const.AppType.choices) - - class Meta: - model = models.Account - fields_mini = ['id', 'username', 'version'] - fields_write_only = ['password', 'private_key', 'public_key', 'passphrase'] - fields_other = ['date_created', 'date_updated'] - fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display'] - fields = fields_mini + fields_fk + fields_write_only + fields_other + [ - 'type', 'type_display', 'category', 'category_display', 'attrs' - ] - extra_kwargs = { - 'username': {'default': '', 'required': False}, - 'password': {'write_only': True}, - 'app_display': {'label': _('Application display')}, - 'systemuser_display': {'label': _('System User')}, - 'account': {'label': _('account')} - } - use_model_bulk_create = True - model_bulk_create_kwargs = { - 'ignore_conflicts': True - } - - @property - def app(self): - if isinstance(self.instance, models.Account): - instance = self.instance.app - else: - instance = None - return instance - - def get_category_display(self, obj): - return self.category_mapper.get(obj.category) - - def get_type_display(self, obj): - return self.type_mapper.get(obj.type) - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('systemuser', 'app') - return queryset - - def to_representation(self, instance): - instance.load_auth() - return super().to_representation(instance) - - -class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer): - class Meta(AppAccountSerializer.Meta): - extra_kwargs = { - 'password': {'write_only': False}, - 'private_key': {'write_only': False}, - 'public_key': {'write_only': False}, - 'app_display': {'label': _('Application display')}, - 'systemuser_display': {'label': _('System User')} - } - - -class AppAccountBackUpSerializer(AppAccountSecretSerializer): - class Meta(AppAccountSecretSerializer.Meta): - fields = [ - 'id', 'app_display', 'attrs', 'username', 'password', 'private_key', - 'public_key', 'date_created', 'date_updated', 'version' - ] - - def __init__(self, *args, **kwargs): - self.tp = kwargs.pop('tp', None) - super().__init__(*args, **kwargs) - - @classmethod - def setup_eager_loading(cls, queryset): - return queryset - - def to_representation(self, instance): - return super(AppAccountSerializer, self).to_representation(instance) diff --git a/apps/applications/serializers/attrs/__init__.py b/apps/applications/serializers/attrs/__init__.py deleted file mode 100644 index bbbffd064..000000000 --- a/apps/applications/serializers/attrs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .attrs import * diff --git a/apps/applications/serializers/attrs/application_category/__init__.py b/apps/applications/serializers/attrs/application_category/__init__.py deleted file mode 100644 index 1fd0beb3d..000000000 --- a/apps/applications/serializers/attrs/application_category/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .remote_app import * -from .db import * -from .cloud import * diff --git a/apps/applications/serializers/attrs/application_category/cloud.py b/apps/applications/serializers/attrs/application_category/cloud.py deleted file mode 100644 index 64306b24e..000000000 --- a/apps/applications/serializers/attrs/application_category/cloud.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -__all__ = ['CloudSerializer'] - - -class CloudSerializer(serializers.Serializer): - cluster = serializers.CharField(max_length=1024, label=_('Cluster'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_category/db.py b/apps/applications/serializers/attrs/application_category/db.py deleted file mode 100644 index 183397e8e..000000000 --- a/apps/applications/serializers/attrs/application_category/db.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 -# -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - - -__all__ = ['DBSerializer'] - - -class DBSerializer(serializers.Serializer): - host = serializers.CharField(max_length=128, label=_('Host'), allow_null=True) - port = serializers.IntegerField(label=_('Port'), allow_null=True) - database = serializers.CharField( - max_length=128, required=True, allow_null=True, label=_('Database') - ) - use_ssl = serializers.BooleanField(default=False, label=_('Use SSL')) - ca_cert = serializers.CharField( - required=False, allow_null=True, label=_('CA certificate') - ) - client_cert = serializers.CharField( - required=False, allow_null=True, label=_('Client certificate file') - ) - cert_key = serializers.CharField( - required=False, allow_null=True, label=_('Certificate key file') - ) - allow_invalid_cert = serializers.BooleanField(default=False, label=_('Allow invalid cert')) diff --git a/apps/applications/serializers/attrs/application_category/remote_app.py b/apps/applications/serializers/attrs/application_category/remote_app.py deleted file mode 100644 index 063af6daa..000000000 --- a/apps/applications/serializers/attrs/application_category/remote_app.py +++ /dev/null @@ -1,60 +0,0 @@ -# coding: utf-8 -# - -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ObjectDoesNotExist - -from common.utils import get_logger, is_uuid, get_object_or_none -from assets.models import Asset - -logger = get_logger(__file__) - -__all__ = ['RemoteAppSerializer'] - - -class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): - - def to_internal_value(self, data): - instance = super().to_internal_value(data) - return str(instance.id) - - def to_representation(self, _id): - # _id 是 instance.id - if self.pk_field is not None: - return self.pk_field.to_representation(_id) - # 解决删除资产后,远程应用更新页面会显示资产ID的问题 - asset = get_object_or_none(Asset, id=_id) - if not asset: - return None - return _id - - -class RemoteAppSerializer(serializers.Serializer): - asset_info = serializers.SerializerMethodField(label=_('Asset Info')) - asset = ExistAssetPrimaryKeyRelatedField( - queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True - ) - path = serializers.CharField( - max_length=128, label=_('Application path'), allow_null=True - ) - - def validate_asset(self, asset): - if not asset: - raise serializers.ValidationError(_('This field is required.')) - return asset - - @staticmethod - def get_asset_info(obj): - asset_id = obj.get('asset') - if not asset_id or not is_uuid(asset_id): - return {} - try: - asset = Asset.objects.get(id=str(asset_id)) - except ObjectDoesNotExist as e: - logger.error(e) - return {} - if not asset: - return {} - asset_info = {'id': str(asset.id), 'hostname': asset.hostname} - return asset_info diff --git a/apps/applications/serializers/attrs/application_type/__init__.py b/apps/applications/serializers/attrs/application_type/__init__.py index cebf0027c..e69de29bb 100644 --- a/apps/applications/serializers/attrs/application_type/__init__.py +++ b/apps/applications/serializers/attrs/application_type/__init__.py @@ -1,16 +0,0 @@ - -from .mysql import * -from .mariadb import * -from .oracle import * -from .pgsql import * -from .sqlserver import * -from .redis import * -from .mongodb import * -from .clickhouse import * - -from .chrome import * -from .mysql_workbench import * -from .vmware_client import * -from .custom import * - -from .k8s import * diff --git a/apps/applications/serializers/attrs/application_type/chrome.py b/apps/applications/serializers/attrs/application_type/chrome.py deleted file mode 100644 index ba491e2fe..000000000 --- a/apps/applications/serializers/attrs/application_type/chrome.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from common.drf.fields import EncryptedField -from ..application_category import RemoteAppSerializer - -__all__ = ['ChromeSerializer', 'ChromeSecretSerializer'] - - -class ChromeSerializer(RemoteAppSerializer): - CHROME_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' - - path = serializers.CharField( - max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True, - ) - chrome_target = serializers.CharField( - max_length=128, allow_blank=True, required=False, - label=_('Target URL'), allow_null=True, - ) - chrome_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, - label=_('Chrome username'), allow_null=True, - ) - chrome_password = EncryptedField( - max_length=128, allow_blank=True, required=False, - label=_('Chrome password'), allow_null=True, encrypted_key='chrome_password' - ) - - -class ChromeSecretSerializer(ChromeSerializer): - chrome_password = EncryptedField( - max_length=128, allow_blank=True, required=False, - label=_('Chrome password'), allow_null=True, write_only=False - ) diff --git a/apps/applications/serializers/attrs/application_type/custom.py b/apps/applications/serializers/attrs/application_type/custom.py deleted file mode 100644 index cfef59d5f..000000000 --- a/apps/applications/serializers/attrs/application_type/custom.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from common.drf.fields import EncryptedField -from ..application_category import RemoteAppSerializer - -__all__ = ['CustomSerializer', 'CustomSecretSerializer'] - - -class CustomSerializer(RemoteAppSerializer): - custom_cmdline = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Operating parameter'), - allow_null=True, - ) - custom_target = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Target url'), - allow_null=True, - ) - custom_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Custom Username'), - allow_null=True, - ) - custom_password = EncryptedField( - max_length=128, allow_blank=True, required=False, - label=_('Custom password'), allow_null=True, - ) - - -class CustomSecretSerializer(RemoteAppSerializer): - custom_password = EncryptedField( - max_length=128, allow_blank=True, required=False, write_only=False, - label=_('Custom password'), allow_null=True, - ) diff --git a/apps/applications/serializers/attrs/application_type/k8s.py b/apps/applications/serializers/attrs/application_type/k8s.py deleted file mode 100644 index cccfd67a0..000000000 --- a/apps/applications/serializers/attrs/application_type/k8s.py +++ /dev/null @@ -1,7 +0,0 @@ -from ..application_category import CloudSerializer - -__all__ = ['K8SSerializer'] - - -class K8SSerializer(CloudSerializer): - pass diff --git a/apps/applications/serializers/attrs/application_type/mariadb.py b/apps/applications/serializers/attrs/application_type/mariadb.py deleted file mode 100644 index e5693e429..000000000 --- a/apps/applications/serializers/attrs/application_type/mariadb.py +++ /dev/null @@ -1,7 +0,0 @@ -from .mysql import MySQLSerializer - -__all__ = ['MariaDBSerializer'] - - -class MariaDBSerializer(MySQLSerializer): - pass diff --git a/apps/applications/serializers/attrs/application_type/mongodb.py b/apps/applications/serializers/attrs/application_type/mongodb.py deleted file mode 100644 index 3824e9c8a..000000000 --- a/apps/applications/serializers/attrs/application_type/mongodb.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from ..application_category import DBSerializer - -__all__ = ['MongoDBSerializer'] - - -class MongoDBSerializer(DBSerializer): - port = serializers.IntegerField(default=27017, label=_('Port'), allow_null=True) - diff --git a/apps/applications/serializers/attrs/application_type/mysql.py b/apps/applications/serializers/attrs/application_type/mysql.py deleted file mode 100644 index 78f312ebe..000000000 --- a/apps/applications/serializers/attrs/application_type/mysql.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from ..application_category import DBSerializer - -__all__ = ['MySQLSerializer'] - - -class MySQLSerializer(DBSerializer): - port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True) - diff --git a/apps/applications/serializers/attrs/application_type/mysql_workbench.py b/apps/applications/serializers/attrs/application_type/mysql_workbench.py deleted file mode 100644 index 6092b2ed1..000000000 --- a/apps/applications/serializers/attrs/application_type/mysql_workbench.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from common.drf.fields import EncryptedField -from ..application_category import RemoteAppSerializer - -__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer'] - - -class MySQLWorkbenchSerializer(RemoteAppSerializer): - MYSQL_WORKBENCH_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe' - - path = serializers.CharField( - max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH, - allow_null=True, - ) - mysql_workbench_ip = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('IP'), - allow_null=True, - ) - mysql_workbench_port = serializers.IntegerField( - required=False, label=_('Port'), - allow_null=True, - ) - mysql_workbench_name = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Database'), - allow_null=True, - ) - mysql_workbench_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'), - allow_null=True, - ) - mysql_workbench_password = EncryptedField( - max_length=128, allow_blank=True, required=False, - label=_('Mysql workbench password'), allow_null=True, - ) - - -class MySQLWorkbenchSecretSerializer(RemoteAppSerializer): - mysql_workbench_password = EncryptedField( - max_length=128, allow_blank=True, required=False, write_only=False, - label=_('Mysql workbench password'), allow_null=True, - ) diff --git a/apps/applications/serializers/attrs/application_type/oracle.py b/apps/applications/serializers/attrs/application_type/oracle.py deleted file mode 100644 index c87c4904d..000000000 --- a/apps/applications/serializers/attrs/application_type/oracle.py +++ /dev/null @@ -1,10 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from ..application_category import DBSerializer - -__all__ = ['OracleSerializer'] - - -class OracleSerializer(DBSerializer): - port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_type/pgsql.py b/apps/applications/serializers/attrs/application_type/pgsql.py deleted file mode 100644 index 0434a0423..000000000 --- a/apps/applications/serializers/attrs/application_type/pgsql.py +++ /dev/null @@ -1,10 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from ..application_category import DBSerializer - -__all__ = ['PostgreSerializer'] - - -class PostgreSerializer(DBSerializer): - port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True) diff --git a/apps/applications/serializers/attrs/application_type/redis.py b/apps/applications/serializers/attrs/application_type/redis.py deleted file mode 100644 index 06cd1ae3b..000000000 --- a/apps/applications/serializers/attrs/application_type/redis.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from ..application_category import DBSerializer - -__all__ = ['RedisSerializer'] - - -class RedisSerializer(DBSerializer): - port = serializers.IntegerField(default=6379, label=_('Port'), allow_null=True) - diff --git a/apps/applications/serializers/attrs/application_type/sqlserver.py b/apps/applications/serializers/attrs/application_type/sqlserver.py deleted file mode 100644 index 5f9b5d2bf..000000000 --- a/apps/applications/serializers/attrs/application_type/sqlserver.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from ..application_category import DBSerializer - -__all__ = ['SQLServerSerializer'] - - -class SQLServerSerializer(DBSerializer): - port = serializers.IntegerField(default=1433, label=_('Port'), allow_null=True) - diff --git a/apps/applications/serializers/attrs/application_type/vmware_client.py b/apps/applications/serializers/attrs/application_type/vmware_client.py deleted file mode 100644 index d6b9cef0b..000000000 --- a/apps/applications/serializers/attrs/application_type/vmware_client.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from common.drf.fields import EncryptedField -from ..application_category import RemoteAppSerializer - -__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer'] - - -class VMwareClientSerializer(RemoteAppSerializer): - PATH = r''' - C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient - .exe - ''' - VMWARE_CLIENT_PATH = ''.join(PATH.split()) - - path = serializers.CharField( - max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH, - allow_null=True - ) - vmware_target = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Target URL'), - allow_null=True - ) - vmware_username = serializers.CharField( - max_length=128, allow_blank=True, required=False, label=_('Vmware username'), - allow_null=True - ) - vmware_password = EncryptedField( - max_length=128, allow_blank=True, required=False, - label=_('Vmware password'), allow_null=True - ) - - -class VMwareClientSecretSerializer(RemoteAppSerializer): - vmware_password = EncryptedField( - max_length=128, allow_blank=True, required=False, write_only=False, - label=_('Vmware password'), allow_null=True - ) diff --git a/apps/applications/serializers/attrs/attrs.py b/apps/applications/serializers/attrs/attrs.py deleted file mode 100644 index ab49cd7f2..000000000 --- a/apps/applications/serializers/attrs/attrs.py +++ /dev/null @@ -1,63 +0,0 @@ -import copy - -from applications import const -from . import application_category, application_type - -__all__ = [ - 'category_serializer_classes_mapping', - 'type_serializer_classes_mapping', - 'get_serializer_class_by_application_type', - 'type_secret_serializer_classes_mapping' -] - -# define `attrs` field `category serializers mapping` -# --------------------------------------------------- - -category_serializer_classes_mapping = { - const.AppCategory.db.value: application_category.DBSerializer, - const.AppCategory.remote_app.value: application_category.RemoteAppSerializer, - const.AppCategory.cloud.value: application_category.CloudSerializer, -} - -# define `attrs` field `type serializers mapping` -# ----------------------------------------------- - -type_serializer_classes_mapping = { - # db - const.AppType.mysql.value: application_type.MySQLSerializer, - const.AppType.mariadb.value: application_type.MariaDBSerializer, - const.AppType.oracle.value: application_type.OracleSerializer, - const.AppType.pgsql.value: application_type.PostgreSerializer, - const.AppType.sqlserver.value: application_type.SQLServerSerializer, - const.AppType.redis.value: application_type.RedisSerializer, - const.AppType.mongodb.value: application_type.MongoDBSerializer, - const.AppType.clickhouse.value: application_type.ClickHouseSerializer, - # cloud - const.AppType.k8s.value: application_type.K8SSerializer -} - -remote_app_serializer_classes_mapping = { - # remote-app - const.AppType.chrome.value: application_type.ChromeSerializer, - const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer, - const.AppType.vmware_client.value: application_type.VMwareClientSerializer, - const.AppType.custom.value: application_type.CustomSerializer -} - -type_serializer_classes_mapping.update(remote_app_serializer_classes_mapping) - -remote_app_secret_serializer_classes_mapping = { - # remote-app - const.AppType.chrome.value: application_type.ChromeSecretSerializer, - const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSecretSerializer, - const.AppType.vmware_client.value: application_type.VMwareClientSecretSerializer, - const.AppType.custom.value: application_type.CustomSecretSerializer -} - -type_secret_serializer_classes_mapping = copy.deepcopy(type_serializer_classes_mapping) - -type_secret_serializer_classes_mapping.update(remote_app_secret_serializer_classes_mapping) - - -def get_serializer_class_by_application_type(_application_type): - return type_serializer_classes_mapping.get(_application_type) diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py deleted file mode 100644 index d036a8c65..000000000 --- a/apps/applications/serializers/remote_app.py +++ /dev/null @@ -1,31 +0,0 @@ -# coding: utf-8 -# -from rest_framework import serializers -from common.utils import get_logger -from ..models import Application - - -logger = get_logger(__file__) - - -__all__ = ['RemoteAppConnectionInfoSerializer'] - - -class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): - parameter_remote_app = serializers.SerializerMethodField() - asset = serializers.SerializerMethodField() - - class Meta: - model = Application - fields = [ - 'id', 'name', 'asset', 'parameter_remote_app', - ] - read_only_fields = ['parameter_remote_app'] - - @staticmethod - def get_asset(obj): - return obj.attrs.get('asset') - - @staticmethod - def get_parameter_remote_app(obj): - return obj.get_rdp_remote_app_setting() diff --git a/apps/applications/tests.py b/apps/applications/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/apps/applications/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/applications/urls/__init__.py b/apps/applications/urls/__init__.py deleted file mode 100644 index 3aab4972f..000000000 --- a/apps/applications/urls/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# coding: utf-8 -# - - -__all__ = [ - -] diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py deleted file mode 100644 index 4fdf006b0..000000000 --- a/apps/applications/urls/api_urls.py +++ /dev/null @@ -1,25 +0,0 @@ -# coding:utf-8 -# -from django.urls import path -from rest_framework_bulk.routes import BulkRouter -from .. import api - - -app_name = 'applications' - - -router = BulkRouter() -router.register(r'applications', api.ApplicationViewSet, 'application') -router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account') -router.register(r'system-users-apps-relations', api.SystemUserAppRelationViewSet, 'system-users-apps-relation') -router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret') - - -urlpatterns = [ - path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), - # path('accounts/', api.ApplicationAccountViewSet.as_view(), name='application-account'), - # path('account-secrets/', api.ApplicationAccountSecretViewSet.as_view(), name='application-account-secret') -] - - -urlpatterns += router.urls diff --git a/apps/applications/utils/__init__.py b/apps/applications/utils/__init__.py deleted file mode 100644 index 5efec40b2..000000000 --- a/apps/applications/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from .kubernetes_util import * diff --git a/apps/applications/utils/kubernetes_util.py b/apps/applications/utils/kubernetes_util.py deleted file mode 100644 index e95922d48..000000000 --- a/apps/applications/utils/kubernetes_util.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -from urllib3.exceptions import MaxRetryError -from urllib.parse import urlencode - -from kubernetes.client import api_client -from kubernetes.client.api import core_v1_api -from kubernetes import client -from kubernetes.client.exceptions import ApiException - -from rest_framework.generics import get_object_or_404 - -from common.utils import get_logger -from common.tree import TreeNode -from assets.models import SystemUser - -from .. import const - -logger = get_logger(__file__) - - -class KubernetesClient: - def __init__(self, url, token): - self.url = url - self.token = token - - def get_api(self): - configuration = client.Configuration() - configuration.host = self.url - 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 - - @staticmethod - def get_kubernetes_data(app_id, system_user_id): - from ..models import Application - app = get_object_or_404(Application, id=app_id) - system_user = get_object_or_404(SystemUser, id=system_user_id) - k8s = KubernetesClient(app.attrs['cluster'], system_user.token) - return k8s.get_pods() - - -class KubernetesTree: - def __init__(self, tree_id): - self.tree_id = tree_id - - def as_tree_node(self, app): - pid = app.create_app_tree_pid(self.tree_id) - app_id = str(app.id) - parent_info = {'app_id': app_id} - node = self.create_tree_node( - app_id, pid, app.name, 'k8s', parent_info - ) - return node - - def as_system_user_tree_node(self, system_user, parent_info): - from ..models import ApplicationTreeNodeMixin - system_user_id = str(system_user.id) - username = system_user.username - username = username if username else '*' - name = f'{system_user.name}({username})' - pid = urlencode({'app_id': self.tree_id}) - i = ApplicationTreeNodeMixin.create_tree_id(pid, 'system_user_id', system_user_id) - parent_info.update({'system_user_id': system_user_id}) - node = self.create_tree_node( - i, pid, name, 'system_user', parent_info, icon='user-tie' - ) - return node - - def as_namespace_pod_tree_node(self, name, meta, type, counts=0, is_container=False): - from ..models import ApplicationTreeNodeMixin - i = ApplicationTreeNodeMixin.create_tree_id(self.tree_id, type, name) - meta.update({type: name}) - name = name if is_container else f'{name}({counts})' - node = self.create_tree_node( - i, self.tree_id, name, type, meta, icon='cloud', is_container=is_container - ) - return node - - @staticmethod - def create_tree_node(id_, pid, name, identity, parent_info, icon='', is_container=False): - node = TreeNode(**{ - 'id': id_, - 'name': name, - 'title': name, - 'pId': pid, - 'isParent': not is_container, - 'open': False, - 'iconSkin': icon, - 'parentInfo': urlencode(parent_info), - 'meta': { - 'type': 'application', - 'data': { - 'category': const.AppCategory.cloud, - 'type': const.AppType.k8s, - 'identity': identity - } - } - }) - return node - - def async_tree_node(self, parent_info): - pod_name = parent_info.get('pod') - app_id = parent_info.get('app_id') - namespace = parent_info.get('namespace') - system_user_id = parent_info.get('system_user_id') - - tree_nodes = [] - data = KubernetesClient.get_kubernetes_data(app_id, system_user_id) - if not data: - return tree_nodes - - if pod_name: - for container in next( - filter( - lambda x: x['pod_name'] == pod_name, data[namespace] - ) - )['containers']: - container_node = self.as_namespace_pod_tree_node( - container, parent_info, 'container', is_container=True - ) - tree_nodes.append(container_node) - elif namespace: - for pod in data[namespace]: - pod_nodes = self.as_namespace_pod_tree_node( - pod['pod_name'], parent_info, 'pod', len(pod['containers']) - ) - tree_nodes.append(pod_nodes) - elif system_user_id: - for namespace, pods in data.items(): - namespace_node = self.as_namespace_pod_tree_node( - namespace, parent_info, 'namespace', len(pods) - ) - tree_nodes.append(namespace_node) - return tree_nodes diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 3e95f59ab..36f734030 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,14 +1,11 @@ from .mixin import * -from .admin_user import * +from .category import * +from .platform import * from .asset import * from .label import * -from .system_user import * -from .system_user_relation import * -from .accounts import * +from .account import * from .node import * from .domain import * -from .cmd_filter import * +from .automations import * from .gathered_user import * from .favorite_asset import * -from .account_backup import * -from .account_history import * diff --git a/apps/assets/api/account/__init__.py b/apps/assets/api/account/__init__.py new file mode 100644 index 000000000..19f1af93d --- /dev/null +++ b/apps/assets/api/account/__init__.py @@ -0,0 +1,3 @@ +from .account import * +from .backup import * +from .template import * diff --git a/apps/assets/api/account/account.py b/apps/assets/api/account/account.py new file mode 100644 index 000000000..e22b13c16 --- /dev/null +++ b/apps/assets/api/account/account.py @@ -0,0 +1,97 @@ +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.generics import CreateAPIView, ListAPIView + +from orgs.mixins.api import OrgBulkModelViewSet +from rbac.permissions import RBACPermission + +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 + +__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI', 'AccountHistoriesSecretAPI'] + + +class AccountViewSet(OrgBulkModelViewSet): + model = Account + search_fields = ('username', 'asset__address', 'name') + filterset_class = AccountFilterSet + serializer_classes = { + 'default': serializers.AccountSerializer, + 'verify': serializers.AssetTaskSerializer + } + rbac_perms = { + 'verify': 'assets.test_account', + 'partial_update': 'assets.change_accountsecret', + } + + @action(methods=['post'], detail=True, url_path='verify') + def verify_account(self, request, *args, **kwargs): + account = super().get_object() + account_ids = [account.id] + asset_ids = [account.asset_id] + task = verify_accounts_connectivity.delay(account_ids, asset_ids) + return Response(data={'task': task.id}) + + +class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): + """ + 因为可能要导出所有账号,所以单独建立了一个 viewset + """ + serializer_classes = { + 'default': serializers.AccountSecretSerializer, + } + http_method_names = ['get', 'options'] + # Todo: 记得打开 + # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + rbac_perms = { + 'list': 'assets.view_accountsecret', + 'retrieve': 'assets.view_accountsecret', + } + + +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)] + rbac_perms = { + 'list': 'assets.view_accountsecret', + } + + def get_queryset(self): + return self.model.objects.filter(id=self.kwargs.get('pk')) + + +class AccountTaskCreateAPI(CreateAPIView): + serializer_class = serializers.AccountTaskSerializer + search_fields = AccountViewSet.search_fields + filterset_class = AccountViewSet.filterset_class + + def check_permissions(self, request): + return request.user.has_perm('assets.test_assetconnectivity') + + def get_accounts(self): + queryset = Account.objects.all() + queryset = self.filter_queryset(queryset) + return queryset + + def perform_create(self, serializer): + accounts = self.get_accounts() + account_ids = accounts.values_list('id', flat=True) + asset_ids = [account.asset_id for account in accounts] + task = verify_accounts_connectivity.delay(account_ids, asset_ids) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) + return task + + 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_backup.py b/apps/assets/api/account/backup.py similarity index 85% rename from apps/assets/api/account_backup.py rename to apps/assets/api/account/backup.py index bce4ce55b..ff46c3caf 100644 --- a/apps/assets/api/account_backup.py +++ b/apps/assets/api/account/backup.py @@ -4,9 +4,10 @@ from rest_framework import status, viewsets from rest_framework.response import Response from orgs.mixins.api import OrgBulkModelViewSet -from .. import serializers -from ..tasks import execute_account_backup_plan -from ..models import ( +from common.const.choices import Trigger +from assets import serializers +from assets.tasks import execute_account_backup_plan +from assets.models import ( AccountBackupPlan, AccountBackupPlanExecution ) @@ -38,9 +39,7 @@ class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) pid = serializer.data.get('plan') - task = execute_account_backup_plan.delay( - pid=pid, trigger=AccountBackupPlanExecution.Trigger.manual - ) + task = execute_account_backup_plan.delay(pid=pid, trigger=Trigger.manual) return Response({'task': task.id}, status=status.HTTP_201_CREATED) def filter_queryset(self, queryset): diff --git a/apps/assets/api/account/template.py b/apps/assets/api/account/template.py new file mode 100644 index 000000000..dd9ee1d00 --- /dev/null +++ b/apps/assets/api/account/template.py @@ -0,0 +1,29 @@ +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 + + +class AccountTemplateViewSet(OrgBulkModelViewSet): + model = AccountTemplate + filterset_fields = ("username", 'name') + search_fields = ('username', 'name') + serializer_classes = { + 'default': serializers.AccountTemplateSerializer + } + + +class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): + serializer_classes = { + 'default': serializers.AccountTemplateSecretSerializer, + } + http_method_names = ['get', 'options'] + # Todo: 记得打开 + # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + rbac_perms = { + 'list': 'assets.view_accounttemplatesecret', + 'retrieve': 'assets.view_accounttemplatesecret', + } diff --git a/apps/assets/api/account_history.py b/apps/assets/api/account_history.py deleted file mode 100644 index 3feddce91..000000000 --- a/apps/assets/api/account_history.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db.models import F - -from assets.api.accounts import ( - AccountFilterSet, AccountViewSet, AccountSecretsViewSet -) -from common.mixins import RecordViewLogMixin -from .. import serializers -from ..models import AuthBook - -__all__ = ['AccountHistoryViewSet', 'AccountHistorySecretsViewSet'] - - -class AccountHistoryFilterSet(AccountFilterSet): - class Meta: - model = AuthBook.history.model - fields = AccountFilterSet.Meta.fields - - -class AccountHistoryViewSet(AccountViewSet): - model = AuthBook.history.model - filterset_class = AccountHistoryFilterSet - serializer_classes = { - 'default': serializers.AccountHistorySerializer, - } - rbac_perms = { - 'list': 'assets.view_assethistoryaccount', - 'retrieve': 'assets.view_assethistoryaccount', - } - - http_method_names = ['get', 'options'] - - def get_queryset(self): - queryset = self.model.objects.all() \ - .annotate(ip=F('asset__ip')) \ - .annotate(hostname=F('asset__hostname')) \ - .annotate(platform=F('asset__platform__name')) \ - .annotate(protocols=F('asset__protocols')) - return queryset - - -class AccountHistorySecretsViewSet(RecordViewLogMixin, AccountHistoryViewSet): - serializer_classes = { - 'default': serializers.AccountHistorySecretSerializer - } - http_method_names = ['get'] - permission_classes = AccountSecretsViewSet.permission_classes - rbac_perms = { - 'list': 'assets.view_assethistoryaccountsecret', - 'retrieve': 'assets.view_assethistoryaccountsecret', - } diff --git a/apps/assets/api/accounts.py b/apps/assets/api/accounts.py deleted file mode 100644 index 6ad59ed88..000000000 --- a/apps/assets/api/accounts.py +++ /dev/null @@ -1,124 +0,0 @@ -from django.db.models import Q -from django.shortcuts import get_object_or_404 -from django_filters import rest_framework as filters -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.generics import CreateAPIView - -from orgs.mixins.api import OrgBulkModelViewSet -from rbac.permissions import RBACPermission -from common.drf.filters import BaseFilterSet -from common.mixins import RecordViewLogMixin -from common.permissions import UserConfirmation -from authentication.const import ConfirmType -from ..tasks.account_connectivity import test_accounts_connectivity_manual -from ..models import AuthBook, Node -from .. import serializers - -__all__ = ['AccountFilterSet', 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI'] - - -class AccountFilterSet(BaseFilterSet): - username = filters.CharFilter(method='do_nothing') - ip = filters.CharFilter(field_name='ip', lookup_expr='exact') - hostname = filters.CharFilter(field_name='hostname', lookup_expr='exact') - node = filters.CharFilter(method='do_nothing') - - @property - def qs(self): - qs = super().qs - qs = self.filter_username(qs) - qs = self.filter_node(qs) - qs = qs.distinct() - return qs - - def filter_username(self, qs): - username = self.get_query_param('username') - if not username: - return qs - qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct() - return qs - - def filter_node(self, qs): - node_id = self.get_query_param('node') - if not node_id: - return qs - node = get_object_or_404(Node, pk=node_id) - node_ids = node.get_all_children(with_self=True).values_list('id', flat=True) - node_ids = list(node_ids) - qs = qs.filter(asset__nodes__in=node_ids) - return qs - - class Meta: - model = AuthBook - fields = [ - 'asset', 'systemuser', 'id', - ] - - -class AccountViewSet(OrgBulkModelViewSet): - model = AuthBook - filterset_fields = ("username", "asset", "systemuser", 'ip', 'hostname') - search_fields = ('username', 'ip', 'hostname', 'systemuser__username') - filterset_class = AccountFilterSet - serializer_classes = { - 'default': serializers.AccountSerializer, - 'verify_account': serializers.AssetTaskSerializer - } - rbac_perms = { - 'verify_account': 'assets.test_authbook', - 'partial_update': 'assets.change_assetaccountsecret', - } - - def get_queryset(self): - queryset = AuthBook.get_queryset() - return queryset - - @action(methods=['post'], detail=True, url_path='verify') - def verify_account(self, request, *args, **kwargs): - account = super().get_object() - task = test_accounts_connectivity_manual.delay([account]) - return Response(data={'task': task.id}) - - -class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet): - """ - 因为可能要导出所有账号,所以单独建立了一个 viewset - """ - serializer_classes = { - 'default': serializers.AccountSecretSerializer - } - http_method_names = ['get'] - permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] - rbac_perms = { - 'list': 'assets.view_assetaccountsecret', - 'retrieve': 'assets.view_assetaccountsecret', - } - - -class AccountTaskCreateAPI(CreateAPIView): - serializer_class = serializers.AccountTaskSerializer - filterset_fields = AccountViewSet.filterset_fields - search_fields = AccountViewSet.search_fields - filterset_class = AccountViewSet.filterset_class - - def check_permissions(self, request): - return request.user.has_perm('assets.test_assetconnectivity') - - def get_accounts(self): - queryset = AuthBook.objects.all() - queryset = self.filter_queryset(queryset) - return queryset - - def perform_create(self, serializer): - accounts = self.get_accounts() - task = test_accounts_connectivity_manual.delay(accounts) - data = getattr(serializer, '_data', {}) - data["task"] = task.id - setattr(serializer, '_data', data) - return task - - def get_exception_handler(self): - def handler(e, context): - return Response({"error": str(e)}, status=400) - return handler diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py deleted file mode 100644 index 1192599b9..000000000 --- a/apps/assets/api/admin_user.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db.models import Count - -from orgs.mixins.api import OrgBulkModelViewSet -from common.utils import get_logger -from ..models import SystemUser -from .. import serializers -from rbac.permissions import RBACPermission - - -logger = get_logger(__file__) -__all__ = ['AdminUserViewSet'] - - -# 兼容一下老的 api -class AdminUserViewSet(OrgBulkModelViewSet): - """ - Admin user api set, for add,delete,update,list,retrieve resource - """ - model = SystemUser - filterset_fields = ("name", "username") - search_fields = filterset_fields - serializer_class = serializers.AdminUserSerializer - permission_classes = (RBACPermission,) - ordering_fields = ('name',) - ordering = ('name', ) - - def get_queryset(self): - queryset = super().get_queryset().filter(type=SystemUser.Type.admin) - queryset = queryset.annotate(assets_amount=Count('assets')) - return queryset diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py deleted file mode 100644 index f8c2ab179..000000000 --- a/apps/assets/api/asset.py +++ /dev/null @@ -1,310 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework.viewsets import ModelViewSet -from rest_framework.generics import RetrieveAPIView, ListAPIView -from django.shortcuts import get_object_or_404 -from django.db.models import Q - -from common.utils import get_logger, get_object_or_none -from common.mixins.api import SuggestionMixin, RenderToJsonMixin -from users.models import User, UserGroup -from users.serializers import UserSerializer, UserGroupSerializer -from users.filters import UserFilter -from perms.models import AssetPermission -from perms.serializers import AssetPermissionSerializer -from perms.filters import AssetPermissionFilter -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.mixins import generics -from assets.api import FilterAssetByNodeMixin -from ..models import Asset, Node, Platform, Gateway -from .. import serializers -from ..tasks import ( - update_assets_hardware_info_manual, test_assets_connectivity_manual, - test_system_users_connectivity_a_asset, push_system_users_a_asset -) -from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend - -logger = get_logger(__file__) -__all__ = [ - 'AssetViewSet', 'AssetPlatformRetrieveApi', - 'AssetGatewayListApi', 'AssetPlatformViewSet', - 'AssetTaskCreateApi', 'AssetsTaskCreateApi', - 'AssetPermUserListApi', 'AssetPermUserPermissionsListApi', - 'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi', -] - - -class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet): - """ - API endpoint that allows Asset to be viewed or edited. - """ - model = Asset - filterset_fields = { - 'hostname': ['exact'], - 'ip': ['exact'], - 'system_users__id': ['exact'], - 'platform__base': ['exact'], - 'is_active': ['exact'], - 'protocols': ['exact', 'icontains'] - } - search_fields = ("hostname", "ip") - ordering_fields = ("hostname", "ip", "port", "cpu_cores") - ordering = ('hostname', ) - serializer_classes = { - 'default': serializers.AssetSerializer, - 'suggestion': serializers.MiniAssetSerializer - } - rbac_perms = { - 'match': 'assets.match_asset' - } - extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend] - - def set_assets_node(self, assets): - if not isinstance(assets, list): - assets = [assets] - node_id = self.request.query_params.get('node_id') - if not node_id: - return - node = get_object_or_none(Node, pk=node_id) - if not node: - return - node.assets.add(*assets) - - def perform_create(self, serializer): - assets = serializer.save() - self.set_assets_node(assets) - - -class AssetPlatformRetrieveApi(RetrieveAPIView): - queryset = Platform.objects.all() - serializer_class = serializers.PlatformSerializer - rbac_perms = { - 'retrieve': 'assets.view_gateway' - } - - def get_object(self): - asset_pk = self.kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_pk) - return asset.platform - - -class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin): - queryset = Platform.objects.all() - serializer_class = serializers.PlatformSerializer - filterset_fields = ['name', 'base'] - search_fields = ['name'] - - def check_object_permissions(self, request, obj): - if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal: - self.permission_denied( - request, message={"detail": "Internal platform"} - ) - return super().check_object_permissions(request, obj) - - -class AssetsTaskMixin: - - def perform_assets_task(self, serializer): - data = serializer.validated_data - action = data['action'] - assets = data.get('assets', []) - if action == "refresh": - task = update_assets_hardware_info_manual.delay(assets) - else: - # action == 'test': - task = test_assets_connectivity_manual.delay(assets) - return task - - def perform_create(self, serializer): - task = self.perform_assets_task(serializer) - self.set_task_to_serializer_data(serializer, task) - - def set_task_to_serializer_data(self, serializer, task): - data = getattr(serializer, '_data', {}) - data["task"] = task.id - setattr(serializer, '_data', data) - - -class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): - model = Asset - serializer_class = serializers.AssetTaskSerializer - - def create(self, request, *args, **kwargs): - pk = self.kwargs.get('pk') - request.data['asset'] = pk - request.data['assets'] = [pk] - return super().create(request, *args, **kwargs) - - def check_permissions(self, request): - action = request.data.get('action') - action_perm_require = { - 'refresh': 'assets.refresh_assethardwareinfo', - 'push_system_user': 'assets.push_assetsystemuser', - 'test': 'assets.test_assetconnectivity', - 'test_system_user': 'assets.test_assetconnectivity' - } - perm_required = action_perm_require.get(action) - has = self.request.user.has_perm(perm_required) - - if not has: - self.permission_denied(request) - - def perform_asset_task(self, serializer): - data = serializer.validated_data - action = data['action'] - if action not in ['push_system_user', 'test_system_user']: - return - - asset = data['asset'] - system_users = data.get('system_users') - if not system_users: - system_users = asset.get_all_system_users() - if action == 'push_system_user': - task = push_system_users_a_asset.delay(system_users, asset=asset) - elif action == 'test_system_user': - task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset) - else: - task = None - return task - - def perform_create(self, serializer): - task = self.perform_asset_task(serializer) - if not task: - task = self.perform_assets_task(serializer) - self.set_task_to_serializer_data(serializer, task) - - -class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): - model = Asset - serializer_class = serializers.AssetsTaskSerializer - - def check_permissions(self, request): - action = request.data.get('action') - action_perm_require = { - 'refresh': 'assets.refresh_assethardwareinfo', - } - perm_required = action_perm_require.get(action) - has = self.request.user.has_perm(perm_required) - if not has: - self.permission_denied(request) - - -class AssetGatewayListApi(generics.ListAPIView): - serializer_class = serializers.GatewayWithAuthSerializer - rbac_perms = { - 'list': 'assets.view_gateway' - } - - def get_queryset(self): - asset_id = self.kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - if not asset.domain: - return Gateway.objects.none() - queryset = asset.domain.gateways.filter(protocol='ssh') - return queryset - - -class BaseAssetPermUserOrUserGroupListApi(ListAPIView): - rbac_perms = { - 'GET': 'perms.view_assetpermission' - } - - def get_object(self): - asset_id = self.kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - return asset - - def get_asset_related_perms(self): - asset = self.get_object() - nodes = asset.get_all_nodes(flat=True) - perms = AssetPermission.objects.filter(Q(assets=asset) | Q(nodes__in=nodes)) - return perms - - -class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi): - filterset_class = UserFilter - search_fields = ('username', 'email', 'name', 'id', 'source', 'role') - serializer_class = UserSerializer - rbac_perms = { - 'GET': 'perms.view_assetpermission' - } - - def get_queryset(self): - perms = self.get_asset_related_perms() - users = User.objects.filter( - Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms) - ).distinct() - return users - - -class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi): - serializer_class = UserGroupSerializer - - def get_queryset(self): - perms = self.get_asset_related_perms() - user_groups = UserGroup.objects.filter(assetpermissions__in=perms).distinct() - return user_groups - - -class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView): - model = AssetPermission - serializer_class = AssetPermissionSerializer - filterset_class = AssetPermissionFilter - search_fields = ('name',) - rbac_perms = { - 'list': 'perms.view_assetpermission' - } - - def get_object(self): - asset_id = self.kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - return asset - - def filter_asset_related(self, queryset): - asset = self.get_object() - nodes = asset.get_all_nodes(flat=True) - perms = queryset.filter(Q(assets=asset) | Q(nodes__in=nodes)) - return perms - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.filter_asset_related(queryset) - return queryset - - -class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin): - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.filter_user_related(queryset) - queryset = queryset.distinct() - return queryset - - def filter_user_related(self, queryset): - user = self.get_perm_user() - user_groups = user.groups.all() - perms = queryset.filter(Q(users=user) | Q(user_groups__in=user_groups)) - return perms - - def get_perm_user(self): - user_id = self.kwargs.get('perm_user_id') - user = get_object_or_404(User, pk=user_id) - return user - - -class AssetPermUserGroupPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin): - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.filter_user_group_related(queryset) - queryset = queryset.distinct() - return queryset - - def filter_user_group_related(self, queryset): - user_group = self.get_perm_user_group() - perms = queryset.filter(user_groups=user_group) - return perms - - def get_perm_user_group(self): - user_group_id = self.kwargs.get('perm_user_group_id') - user_group = get_object_or_404(UserGroup, pk=user_group_id) - return user_group - diff --git a/apps/assets/api/asset/__init__.py b/apps/assets/api/asset/__init__.py new file mode 100644 index 000000000..c20e44573 --- /dev/null +++ b/apps/assets/api/asset/__init__.py @@ -0,0 +1,7 @@ +from .asset import * +from .host import * +from .database import * +from .web import * +from .cloud import * +from .device import * +from .permission import * diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py new file mode 100644 index 000000000..df139a8c9 --- /dev/null +++ b/apps/assets/api/asset/asset.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# + +import django_filters +from rest_framework.decorators import action +from rest_framework.response import Response + +from assets import serializers +from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend +from assets.models import Asset, Gateway +from assets.tasks import ( + push_accounts_to_assets, test_assets_connectivity_manual, + update_assets_hardware_info_manual, verify_accounts_connectivity, +) +from common.drf.filters import BaseFilterSet +from common.mixins.api import SuggestionMixin +from common.utils import get_logger +from orgs.mixins import generics +from orgs.mixins.api import OrgBulkModelViewSet +from ..mixin import NodeFilterMixin + +logger = get_logger(__file__) +__all__ = [ + "AssetViewSet", + "AssetTaskCreateApi", + "AssetsTaskCreateApi", + 'AssetFilterSet' +] + + +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") + + class Meta: + model = Asset + fields = [ + "id", "name", "address", "is_active", + "type", "category", "hostname" + ] + + +class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): + """ + API endpoint that allows Asset to be viewed or edited. + """ + + model = Asset + filterset_class = AssetFilterSet + search_fields = ("name", "address") + ordering_fields = ("name", "address") + ordering = ("name",) + serializer_classes = ( + ("default", serializers.AssetSerializer), + ("suggestion", serializers.MiniAssetSerializer), + ("platform", serializers.PlatformSerializer), + ("gateways", serializers.GatewaySerializer), + ) + rbac_perms = ( + ("match", "assets.match_asset"), + ("platform", "assets.view_platform"), + ("gateways", "assets.view_gateway"), + ) + extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] + + @action(methods=["GET"], detail=True, url_path="platform") + def platform(self, *args, **kwargs): + asset = self.get_object() + serializer = self.get_serializer(asset.platform) + return Response(serializer.data) + + @action(methods=["GET"], detail=True, url_path="gateways") + def gateways(self, *args, **kwargs): + asset = self.get_object() + if not asset.domain: + gateways = Gateway.objects.none() + else: + gateways = asset.domain.gateways + return self.get_paginated_response_from_queryset(gateways) + + +class AssetsTaskMixin: + def perform_assets_task(self, serializer): + data = serializer.validated_data + assets = data.get("assets", []) + asset_ids = [asset.id for asset in assets] + if data["action"] == "refresh": + task = update_assets_hardware_info_manual.delay(asset_ids) + else: + task = test_assets_connectivity_manual.delay(asset_ids) + return task + + def perform_create(self, serializer): + task = self.perform_assets_task(serializer) + self.set_task_to_serializer_data(serializer, task) + + def set_task_to_serializer_data(self, serializer, task): + data = getattr(serializer, "_data", {}) + data["task"] = task.id + setattr(serializer, "_data", data) + + +class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): + model = Asset + serializer_class = serializers.AssetTaskSerializer + + def create(self, request, *args, **kwargs): + pk = self.kwargs.get("pk") + request.data["asset"] = pk + request.data["assets"] = [pk] + return super().create(request, *args, **kwargs) + + def check_permissions(self, request): + action = request.data.get("action") + action_perm_require = { + "refresh": "assets.refresh_assethardwareinfo", + "push_account": "assets.push_assetsystemuser", + "test": "assets.test_assetconnectivity", + "test_account": "assets.test_assetconnectivity", + } + perm_required = action_perm_require.get(action) + has = self.request.user.has_perm(perm_required) + + if not has: + self.permission_denied(request) + + @staticmethod + def perform_asset_task(serializer): + data = serializer.validated_data + if data["action"] not in ["push_system_user", "test_system_user"]: + return + + asset = data["asset"] + accounts = data.get("accounts") + if not accounts: + accounts = asset.accounts.all() + + asset_ids = [asset.id] + account_ids = accounts.values_list("id", flat=True) + if action == "push_account": + task = push_accounts_to_assets.delay(account_ids, asset_ids) + elif action == "test_account": + task = verify_accounts_connectivity.delay(account_ids, asset_ids) + else: + task = None + return task + + def perform_create(self, serializer): + task = self.perform_asset_task(serializer) + if not task: + task = self.perform_assets_task(serializer) + self.set_task_to_serializer_data(serializer, task) + + +class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): + model = Asset + serializer_class = serializers.AssetsTaskSerializer + + def check_permissions(self, request): + action = request.data.get("action") + action_perm_require = { + "refresh": "assets.refresh_assethardwareinfo", + } + perm_required = action_perm_require.get(action) + has = self.request.user.has_perm(perm_required) + if not has: + self.permission_denied(request) diff --git a/apps/assets/api/asset/cloud.py b/apps/assets/api/asset/cloud.py new file mode 100644 index 000000000..64ab6b738 --- /dev/null +++ b/apps/assets/api/asset/cloud.py @@ -0,0 +1,15 @@ +from assets.models import Cloud +from assets.serializers import CloudSerializer + +from .asset import AssetViewSet + +__all__ = ['CloudViewSet'] + + +class CloudViewSet(AssetViewSet): + model = Cloud + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = CloudSerializer + return serializer_classes diff --git a/apps/assets/api/asset/database.py b/apps/assets/api/asset/database.py new file mode 100644 index 000000000..d4f135cbf --- /dev/null +++ b/apps/assets/api/asset/database.py @@ -0,0 +1,15 @@ +from assets.models import Database +from assets.serializers import DatabaseSerializer + +from .asset import AssetViewSet + +__all__ = ['DatabaseViewSet'] + + +class DatabaseViewSet(AssetViewSet): + model = Database + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = DatabaseSerializer + return serializer_classes diff --git a/apps/assets/api/asset/device.py b/apps/assets/api/asset/device.py new file mode 100644 index 000000000..f6a457fe4 --- /dev/null +++ b/apps/assets/api/asset/device.py @@ -0,0 +1,15 @@ + +from assets.serializers import DeviceSerializer +from assets.models import Device +from .asset import AssetViewSet + +__all__ = ['DeviceViewSet'] + + +class DeviceViewSet(AssetViewSet): + model = Device + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = DeviceSerializer + return serializer_classes diff --git a/apps/assets/api/asset/host.py b/apps/assets/api/asset/host.py new file mode 100644 index 000000000..fbc2e997c --- /dev/null +++ b/apps/assets/api/asset/host.py @@ -0,0 +1,14 @@ +from assets.models import Host +from assets.serializers import HostSerializer +from .asset import AssetViewSet + +__all__ = ['HostViewSet'] + + +class HostViewSet(AssetViewSet): + model = Host + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = HostSerializer + return serializer_classes diff --git a/apps/assets/api/asset/permission.py b/apps/assets/api/asset/permission.py new file mode 100644 index 000000000..cab75bc17 --- /dev/null +++ b/apps/assets/api/asset/permission.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +from rest_framework.generics import ListAPIView +from django.shortcuts import get_object_or_404 +from django.db.models import Q + +from common.utils import get_logger +from users.models import User, UserGroup +from users.serializers import UserSerializer, UserGroupSerializer +from users.filters import UserFilter +from perms.models import AssetPermission +from perms.serializers import AssetPermissionSerializer +from perms.filters import AssetPermissionFilter +from orgs.mixins import generics +from assets.models import Asset + +logger = get_logger(__file__) +__all__ = [ + 'AssetPermUserListApi', 'AssetPermUserPermissionsListApi', + 'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi', +] + + +class BaseAssetPermUserOrUserGroupListApi(ListAPIView): + rbac_perms = { + 'GET': 'perms.view_assetpermission' + } + + def get_object(self): + asset_id = self.kwargs.get('pk') + asset = get_object_or_404(Asset, pk=asset_id) + return asset + + def get_asset_related_perms(self): + asset = self.get_object() + nodes = asset.get_all_nodes(flat=True) + perms = AssetPermission.objects.filter(Q(assets=asset) | Q(nodes__in=nodes)) + return perms + + +class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi): + filterset_class = UserFilter + search_fields = ('username', 'email', 'name', 'id', 'source', 'role') + serializer_class = UserSerializer + rbac_perms = { + 'GET': 'perms.view_assetpermission' + } + + def get_queryset(self): + perms = self.get_asset_related_perms() + users = User.objects.filter( + Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms) + ).distinct() + return users + + +class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi): + serializer_class = UserGroupSerializer + + def get_queryset(self): + perms = self.get_asset_related_perms() + user_groups = UserGroup.objects.filter(assetpermissions__in=perms).distinct() + return user_groups + + +class BaseAssetRelatedPermissionListApi(generics.ListAPIView): + model = AssetPermission + serializer_class = AssetPermissionSerializer + filterset_class = AssetPermissionFilter + search_fields = ('name',) + rbac_perms = { + 'list': 'perms.view_assetpermission' + } + + def get_object(self): + asset_id = self.kwargs.get('pk') + asset = get_object_or_404(Asset, pk=asset_id) + return asset + + def filter_asset_related(self, queryset): + asset = self.get_object() + nodes = asset.get_all_nodes(flat=True) + perms = queryset.filter(Q(assets=asset) | Q(nodes__in=nodes)) + return perms + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.filter_asset_related(queryset) + return queryset + + +class AssetPermUserPermissionsListApi(BaseAssetRelatedPermissionListApi): + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.filter_user_related(queryset) + queryset = queryset.distinct() + return queryset + + def filter_user_related(self, queryset): + user = self.get_perm_user() + user_groups = user.groups.all() + perms = queryset.filter(Q(users=user) | Q(user_groups__in=user_groups)) + return perms + + def get_perm_user(self): + user_id = self.kwargs.get('perm_user_id') + user = get_object_or_404(User, pk=user_id) + return user + + +class AssetPermUserGroupPermissionsListApi(BaseAssetRelatedPermissionListApi): + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.filter_user_group_related(queryset) + queryset = queryset.distinct() + return queryset + + def filter_user_group_related(self, queryset): + user_group = self.get_perm_user_group() + perms = queryset.filter(user_groups=user_group) + return perms + + def get_perm_user_group(self): + user_group_id = self.kwargs.get('perm_user_group_id') + user_group = get_object_or_404(UserGroup, pk=user_group_id) + return user_group + diff --git a/apps/assets/api/asset/web.py b/apps/assets/api/asset/web.py new file mode 100644 index 000000000..92aaeff9b --- /dev/null +++ b/apps/assets/api/asset/web.py @@ -0,0 +1,15 @@ +from assets.models import Web +from assets.serializers import WebSerializer + +from .asset import AssetViewSet + +__all__ = ['WebViewSet'] + + +class WebViewSet(AssetViewSet): + model = Web + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = WebSerializer + return serializer_classes diff --git a/apps/assets/api/automations/__init__.py b/apps/assets/api/automations/__init__.py new file mode 100644 index 000000000..e4daeda95 --- /dev/null +++ b/apps/assets/api/automations/__init__.py @@ -0,0 +1,3 @@ +from .base import * +from .change_secret import * +from .gather_accounts import * diff --git a/apps/assets/api/automations/base.py b/apps/assets/api/automations/base.py new file mode 100644 index 000000000..1b480cbdc --- /dev/null +++ b/apps/assets/api/automations/base.py @@ -0,0 +1,116 @@ +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 orgs.mixins import generics +from assets import serializers +from assets.tasks import execute_automation +from assets.models import BaseAutomation, AutomationExecution +from common.const.choices import Trigger + +__all__ = [ + 'AutomationAssetsListApi', 'AutomationRemoveAssetApi', + 'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationExecutionViewSet' +] + + +class AutomationAssetsListApi(generics.ListAPIView): + serializer_class = serializers.AutomationAssetsSerializer + filter_fields = ("name", "address") + search_fields = filter_fields + + def get_object(self): + pk = self.kwargs.get('pk') + return get_object_or_404(BaseAutomation, pk=pk) + + def get_queryset(self): + instance = self.get_object() + assets = instance.get_all_assets().only( + *self.serializer_class.Meta.only_fields + ) + return assets + + +class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView): + model = BaseAutomation + serializer_class = serializers.UpdateAssetSerializer + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.serializer_class(data=request.data) + + if not serializer.is_valid(): + return Response({'error': serializer.errors}) + + assets = serializer.validated_data.get('assets') + if assets: + instance.assets.remove(*tuple(assets)) + return Response({'msg': 'ok'}) + + +class AutomationAddAssetApi(generics.RetrieveUpdateAPIView): + model = BaseAutomation + serializer_class = serializers.UpdateAssetSerializer + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + assets = serializer.validated_data.get('assets') + if assets: + instance.assets.add(*tuple(assets)) + return Response({"msg": "ok"}) + else: + return Response({"error": serializer.errors}) + + +class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView): + model = BaseAutomation + serializer_class = serializers.UpdateAssetSerializer + + def update(self, request, *args, **kwargs): + action_params = ['add', 'remove'] + action = request.query_params.get('action') + if action not in action_params: + err_info = _("The parameter 'action' must be [{}]".format(','.join(action_params))) + return Response({"error": err_info}) + + instance = self.get_object() + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + nodes = serializer.validated_data.get('nodes') + if nodes: + # eg: plan.nodes.add(*tuple(assets)) + getattr(instance.nodes, action)(*tuple(nodes)) + return Response({"msg": "ok"}) + else: + return Response({"error": serializer.errors}) + + +class AutomationExecutionViewSet( + mixins.CreateModelMixin, mixins.ListModelMixin, + mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + search_fields = ('trigger',) + filterset_fields = ('trigger', 'automation_id') + serializer_class = serializers.AutomationExecutionSerializer + + def get_queryset(self): + queryset = AutomationExecution.objects.all() + return queryset + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = queryset.order_by('-date_start') + return queryset + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + automation = serializer.validated_data.get('automation') + tp = serializer.validated_data.get('type') + task = execute_automation.delay( + pid=automation.pk, trigger=Trigger.manual, tp=tp + ) + return Response({'task': task.id}, status=status.HTTP_201_CREATED) diff --git a/apps/assets/api/automations/change_secret.py b/apps/assets/api/automations/change_secret.py new file mode 100644 index 000000000..3cdea7e65 --- /dev/null +++ b/apps/assets/api/automations/change_secret.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import mixins + +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 + +__all__ = [ + 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet' +] + + +class ChangeSecretAutomationViewSet(OrgBulkModelViewSet): + model = ChangeSecretAutomation + filter_fields = ('name', 'secret_type', 'secret_strategy') + search_fields = filter_fields + ordering_fields = ('name',) + serializer_class = serializers.ChangeSecretAutomationSerializer + + +class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): + serializer_class = serializers.ChangeSecretRecordSerializer + filter_fields = ['asset', 'execution_id'] + search_fields = ['asset__hostname'] + + def get_queryset(self): + return ChangeSecretRecord.objects.all() + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + eid = self.request.GET.get('execution_id') + execution = get_object_or_none(AutomationExecution, pk=eid) + if execution: + queryset = queryset.filter(execution=execution) + queryset = queryset.order_by('-date_started') + return queryset diff --git a/apps/assets/api/automations/gather_accounts.py b/apps/assets/api/automations/gather_accounts.py new file mode 100644 index 000000000..e7a265f96 --- /dev/null +++ b/apps/assets/api/automations/gather_accounts.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +from orgs.mixins.api import OrgBulkModelViewSet + +from assets.models import GatherAccountsAutomation +from assets import serializers + +__all__ = [ + 'GatherAccountsAutomationViewSet', +] + + +class GatherAccountsAutomationViewSet(OrgBulkModelViewSet): + model = GatherAccountsAutomation + filter_fields = ('name',) + search_fields = filter_fields + ordering_fields = ('name',) + serializer_class = serializers.GatherAccountAutomationSerializer diff --git a/apps/assets/api/category.py b/apps/assets/api/category.py new file mode 100644 index 000000000..ae44ddc9b --- /dev/null +++ b/apps/assets/api/category.py @@ -0,0 +1,34 @@ +from rest_framework.mixins import ListModelMixin +from rest_framework.decorators import action +from rest_framework.response import Response + +from common.drf.api import JMSGenericViewSet +from assets.serializers import CategorySerializer, TypeSerializer +from assets.const import AllTypes + +__all__ = ['CategoryViewSet'] + + +class CategoryViewSet(ListModelMixin, JMSGenericViewSet): + serializer_classes = { + 'default': CategorySerializer, + 'types': TypeSerializer + } + permission_classes = () + + def get_queryset(self): + return AllTypes.categories() + + @action(methods=['get'], detail=False) + def types(self, request, *args, **kwargs): + queryset = AllTypes.types() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + @action(methods=['get'], detail=False) + def constraints(self, request, *args, **kwargs): + category = request.query_params.get('category') + tp = request.query_params.get('type') + constraints = AllTypes.get_constraints(category, tp) + return Response(constraints) + diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py deleted file mode 100644 index 0e09d5c73..000000000 --- a/apps/assets/api/cmd_filter.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework.response import Response -from rest_framework.generics import CreateAPIView -from django.shortcuts import get_object_or_404 - -from common.utils import reverse -from common.utils import lazyproperty -from orgs.mixins.api import OrgBulkModelViewSet -from ..models import CommandFilter, CommandFilterRule -from .. import serializers - -__all__ = [ - 'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI', -] - - -class CommandFilterViewSet(OrgBulkModelViewSet): - model = CommandFilter - filterset_fields = ("name",) - search_fields = filterset_fields - serializer_class = serializers.CommandFilterSerializer - - -class CommandFilterRuleViewSet(OrgBulkModelViewSet): - model = CommandFilterRule - filterset_fields = ('content',) - search_fields = filterset_fields - serializer_class = serializers.CommandFilterRuleSerializer - - def get_queryset(self): - fpk = self.kwargs.get('filter_pk') - if not fpk: - return CommandFilterRule.objects.none() - cmd_filter = get_object_or_404(CommandFilter, pk=fpk) - return cmd_filter.rules.all() - - -class CommandConfirmAPI(CreateAPIView): - serializer_class = serializers.CommandConfirmSerializer - rbac_perms = { - 'POST': 'tickets.add_superticket' - } - - def create(self, request, *args, **kwargs): - ticket = self.create_command_confirm_ticket() - response_data = self.get_response_data(ticket) - return Response(data=response_data, status=200) - - def create_command_confirm_ticket(self): - ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket( - run_command=self.serializer.data.get('run_command'), - session=self.serializer.session, - cmd_filter_rule=self.serializer.cmd_filter_rule, - org_id=self.serializer.org.id, - ) - return ticket - - @staticmethod - def get_response_data(ticket): - confirm_status_url = reverse( - view_name='api-tickets:super-ticket-status', - kwargs={'pk': str(ticket.id)} - ) - ticket_detail_url = reverse( - view_name='api-tickets:ticket-detail', - kwargs={'pk': str(ticket.id)}, - external=True, api_to_ui=True - ) - ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) - ticket_assignees = ticket.current_step.ticket_assignees.all() - return { - 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, - 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, - 'ticket_detail_url': ticket_detail_url, - 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees] - } - - @lazyproperty - def serializer(self): - serializer = self.get_serializer(data=self.request.data) - serializer.is_valid(raise_exception=True) - return serializer - diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 1e500c29a..954f4842c 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -1,5 +1,4 @@ # ~*~ coding: utf-8 ~*~ - from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext as _ from rest_framework.views import APIView, Response @@ -10,18 +9,17 @@ from orgs.mixins.api import OrgBulkModelViewSet from ..models import Domain, Gateway from .. import serializers - logger = get_logger(__file__) __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] class DomainViewSet(OrgBulkModelViewSet): model = Domain - filterset_fields = ("name", ) + filterset_fields = ("name",) search_fields = filterset_fields serializer_class = serializers.DomainSerializer ordering_fields = ('name',) - ordering = ('name', ) + ordering = ('name',) def get_serializer_class(self): if self.request.query_params.get('gateway'): @@ -30,27 +28,33 @@ class DomainViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet): - model = Gateway - filterset_fields = ("domain__name", "name", "username", "ip", "domain") - search_fields = ("domain__name", "name", "username", "ip") + perm_model = Gateway + filterset_fields = ("domain__name", "name", "domain") + search_fields = ("domain__name",) serializer_class = serializers.GatewaySerializer + def get_queryset(self): + queryset = Domain.get_gateway_queryset() + return queryset + class GatewayTestConnectionApi(SingleObjectMixin, APIView): - queryset = Gateway.objects.all() - object = None rbac_perms = { 'POST': 'assets.test_gateway' } + def get_queryset(self): + queryset = Domain.get_gateway_queryset() + return queryset + def post(self, request, *args, **kwargs): - self.object = self.get_object(Gateway.objects.all()) - local_port = self.request.data.get('port') or self.object.port + gateway = self.get_object() + local_port = self.request.data.get('port') or gateway.port try: local_port = int(local_port) except ValueError: raise ValidationError({'port': _('Number required')}) - ok, e = self.object.test_connective(local_port=local_port) + ok, e = gateway.test_connective(local_port=local_port) if ok: return Response("ok") else: diff --git a/apps/assets/api/gathered_user.py b/apps/assets/api/gathered_user.py index 22be7daf7..8fcf59456 100644 --- a/apps/assets/api/gathered_user.py +++ b/apps/assets/api/gathered_user.py @@ -16,5 +16,5 @@ class GatheredUserViewSet(OrgModelViewSet): serializer_class = GatheredUserSerializer extra_filter_backends = [AssetRelatedByNodeFilterBackend] - filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id'] - search_fields = ['username', 'asset__ip', 'asset__hostname'] + 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 43220a1a5..2abe967b0 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -1,10 +1,10 @@ from typing import List -from common.utils.common import timeit -from assets.models import Node, Asset -from assets.pagination import NodeAssetTreePagination -from common.utils import lazyproperty -from assets.utils import get_node, is_query_node_all_assets +from rest_framework.request import Request + +from assets.models import Node, PlatformProtocol +from assets.utils import get_node_from_request, is_query_node_all_assets +from common.utils import lazyproperty, timeit class SerializeToTreeNodeMixin: @@ -38,16 +38,11 @@ class SerializeToTreeNodeMixin: ] return data - def get_platform(self, asset: Asset): - default = 'file' - icon = {'windows', 'linux'} - platform = asset.platform_base.lower() - if platform in icon: - return platform - return default - @timeit def serialize_assets(self, assets, node_key=None): + sftp_enabled_platform = PlatformProtocol.objects \ + .filter(name='ssh', setting__sftp_enabled=True) \ + .values_list('platform', flat=True).distinct() if node_key is None: get_pid = lambda asset: getattr(asset, 'parent_key', '') else: @@ -56,22 +51,18 @@ class SerializeToTreeNodeMixin: data = [ { 'id': str(asset.id), - 'name': asset.hostname, - 'title': asset.ip, + 'name': asset.name, + 'title': asset.address, 'pId': get_pid(asset), 'isParent': False, 'open': False, - 'iconSkin': self.get_platform(asset), + 'iconSkin': asset.type, 'chkDisabled': not asset.is_active, 'meta': { 'type': 'asset', 'data': { - 'id': asset.id, - 'hostname': asset.hostname, - 'ip': asset.ip, - 'protocols': asset.protocols_as_list, - 'platform': asset.platform_base, - 'org_name': asset.org_name + 'org_name': asset.org_name, + 'sftp': asset.platform_id in sftp_enabled_platform, }, } } @@ -80,8 +71,8 @@ class SerializeToTreeNodeMixin: return data -class FilterAssetByNodeMixin: - pagination_class = NodeAssetTreePagination +class NodeFilterMixin: + request: Request @lazyproperty def is_query_node_all_assets(self): @@ -89,4 +80,4 @@ class FilterAssetByNodeMixin: @lazyproperty def node(self): - return get_node(self.request) + return get_node_from_request(self.request) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 0ef3aecc6..85935dec2 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,13 +1,14 @@ # ~*~ coding: utf-8 ~*~ from functools import partial from collections import namedtuple, defaultdict +from django.core.exceptions import PermissionDenied 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.shortcuts import get_object_or_404, Http404 from django.db.models.signals import m2m_changed from common.const.http import POST @@ -15,7 +16,7 @@ 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.utils import get_logger, get_object_or_none +from common.utils import get_logger from common.tree import TreeNodeSerializer from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics @@ -27,6 +28,7 @@ from ..tasks import ( check_node_assets_amount_task ) from .. import serializers +from ..const import AllTypes from .mixin import SerializeToTreeNodeMixin from assets.locks import NodeAddChildrenLock @@ -34,9 +36,8 @@ logger = get_logger(__file__) __all__ = [ 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi', - 'NodeAddChildrenApi', 'NodeListAsTreeApi', - 'NodeChildrenAsTreeApi', - 'NodeTaskCreateApi', + 'NodeAddChildrenApi', 'NodeListAsTreeApi', 'NodeChildrenAsTreeApi', + 'NodeTaskCreateApi', 'CategoryTreeApi', ] @@ -200,12 +201,26 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): if not self.instance or not include_assets: return [] assets = self.instance.get_assets().only( - "id", "hostname", "ip", "os", "platform_id", - "org_id", "protocols", "is_active", + "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 @@ -324,7 +339,7 @@ class NodeTaskCreateApi(generics.CreateAPIView): def get_object(self): node_id = self.kwargs.get('pk') - node = get_object_or_none(self.model, id=node_id) + node = get_object_or_404(self.model, id=node_id) return node @staticmethod @@ -346,8 +361,6 @@ class NodeTaskCreateApi(generics.CreateAPIView): task = self.refresh_nodes_cache() self.set_serializer_data(serializer, task) return - if node is None: - raise Http404() if action == "refresh": task = update_node_assets_hardware_info_manual.delay(node) else: diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py new file mode 100644 index 000000000..411bf8b86 --- /dev/null +++ b/apps/assets/api/platform.py @@ -0,0 +1,36 @@ + +from common.drf.api import JMSModelViewSet +from common.drf.serializers import GroupedChoiceSerializer +from assets.models import Platform +from assets.serializers import PlatformSerializer + + +__all__ = ['AssetPlatformViewSet'] + + +class AssetPlatformViewSet(JMSModelViewSet): + queryset = Platform.objects.all() + serializer_classes = { + 'default': PlatformSerializer, + 'categories': GroupedChoiceSerializer + } + filterset_fields = ['name', 'category', 'type'] + search_fields = ['name'] + rbac_perms = { + 'categories': 'assets.view_platform', + 'type_constraints': 'assets.view_platform', + 'ops_methods': 'assets.view_platform' + } + + def get_object(self): + pk = self.kwargs.get('pk', '') + if pk.isnumeric(): + return super().get_object() + return self.get_queryset().get(name=pk) + + def check_object_permissions(self, request, obj): + if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal: + self.permission_denied( + request, message={"detail": "Internal platform"} + ) + return super().check_object_permissions(request, obj) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py deleted file mode 100644 index 24cb347d9..000000000 --- a/apps/assets/api/system_user.py +++ /dev/null @@ -1,253 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -from django.shortcuts import get_object_or_404 -from rest_framework.response import Response -from rest_framework.decorators import action - -from common.utils import get_logger, get_object_or_none -from common.permissions import IsValidUser -from common.mixins.api import SuggestionMixin -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.mixins import generics -from orgs.utils import tmp_to_root_org -from ..models import SystemUser, CommandFilterRule -from .. import serializers -from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer -from ..tasks import ( - push_system_user_to_assets_manual, test_system_user_connectivity_manual, - push_system_user_to_assets -) - -logger = get_logger(__file__) -__all__ = [ - 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', - 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView', - 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', -] - - -class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): - """ - System user api set, for add,delete,update,list,retrieve resource - """ - model = SystemUser - filterset_fields = { - 'name': ['exact'], - 'username': ['exact'], - 'protocol': ['exact', 'in'], - 'type': ['exact', 'in'], - } - search_fields = filterset_fields - serializer_class = serializers.SystemUserSerializer - serializer_classes = { - 'default': serializers.SystemUserSerializer, - 'suggestion': serializers.MiniSystemUserSerializer - } - ordering_fields = ('name', 'protocol', 'login_mode') - ordering = ('name', ) - rbac_perms = { - 'su_from': 'assets.view_systemuser', - 'su_to': 'assets.view_systemuser', - 'match': 'assets.match_systemuser' - } - - @action(methods=['get'], detail=False, url_path='su-from') - def su_from(self, request, *args, **kwargs): - """ API 获取可选的 su_from 系统用户""" - queryset = self.filter_queryset(self.get_queryset()) - queryset = queryset.filter( - protocol=SystemUser.Protocol.ssh, login_mode=SystemUser.LOGIN_AUTO - ) - return self.get_paginate_response_if_need(queryset) - - @action(methods=['get'], detail=True, url_path='su-to') - def su_to(self, request, *args, **kwargs): - """ 获取系统用户的所有 su_to 系统用户 """ - pk = kwargs.get('pk') - system_user = get_object_or_404(SystemUser, pk=pk) - queryset = system_user.su_to.all() - queryset = self.filter_queryset(queryset) - return self.get_paginate_response_if_need(queryset) - - def get_paginate_response_if_need(self, queryset): - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - - -class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): - """ - Get system user auth info - """ - model = SystemUser - serializer_class = SystemUserWithAuthInfoSerializer - rbac_perms = { - 'retrieve': 'assets.view_systemusersecret', - 'list': 'assets.view_systemusersecret', - 'change': 'assets.change_systemuser', - 'destroy': 'assets.change_systemuser', - } - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - instance.clear_auth() - return Response(status=204) - - -class SystemUserTempAuthInfoApi(generics.CreateAPIView): - model = SystemUser - permission_classes = (IsValidUser,) - serializer_class = SystemUserTempAuthSerializer - - def create(self, request, *args, **kwargs): - serializer = super().get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - pk = kwargs.get('pk') - data = serializer.validated_data - asset_or_app_id = data.get('instance_id') - - with tmp_to_root_org(): - instance = get_object_or_404(SystemUser, pk=pk) - instance.set_temp_auth(asset_or_app_id, self.request.user.id, data) - return Response(serializer.data, status=201) - - -class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): - """ - Get system user with asset auth info - """ - model = SystemUser - serializer_class = SystemUserWithAuthInfoSerializer - - def get_object(self): - instance = super().get_object() - asset_id = self.kwargs.get('asset_id') - user_id = self.request.query_params.get("user_id") - username = self.request.query_params.get("username") - instance.load_asset_more_auth(asset_id, username, user_id) - return instance - - -class SystemUserAppAuthInfoApi(generics.RetrieveAPIView): - """ - Get system user with asset auth info - """ - model = SystemUser - serializer_class = SystemUserWithAuthInfoSerializer - rbac_perms = { - 'retrieve': 'assets.view_systemusersecret', - } - - def get_object(self): - instance = super().get_object() - app_id = self.kwargs.get('app_id') - user_id = self.request.query_params.get("user_id") - username = self.request.query_params.get("username") - instance.load_app_more_auth(app_id, username, user_id) - return instance - - -class SystemUserTaskApi(generics.CreateAPIView): - serializer_class = serializers.SystemUserTaskSerializer - - def do_push(self, system_user, asset_ids=None): - if asset_ids is None: - task = push_system_user_to_assets_manual.delay(system_user) - else: - username = self.request.query_params.get('username') - task = push_system_user_to_assets.delay( - system_user.id, asset_ids, username=username - ) - return task - - @staticmethod - def do_test(system_user, asset_ids): - task = test_system_user_connectivity_manual.delay(system_user, asset_ids) - return task - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(SystemUser, pk=pk) - - def check_permissions(self, request): - action = request.data.get('action') - action_perm_require = { - 'push': 'assets.push_assetsystemuser', - 'test': 'assets.test_assetconnectivity' - } - perm_required = action_perm_require.get(action) - has = self.request.user.has_perm(perm_required) - - if not has: - self.permission_denied(request) - - def perform_create(self, serializer): - action = serializer.validated_data["action"] - asset = serializer.validated_data.get('asset') - - if asset: - assets = [asset] - else: - assets = serializer.validated_data.get('assets') or [] - - asset_ids = [asset.id for asset in assets] - asset_ids = asset_ids if asset_ids else None - - system_user = self.get_object() - if action == 'push': - task = self.do_push(system_user, asset_ids) - else: - task = self.do_test(system_user, asset_ids) - data = getattr(serializer, '_data', {}) - data["task"] = task.id - setattr(serializer, '_data', data) - - -class SystemUserCommandFilterRuleListApi(generics.ListAPIView): - rbac_perms = { - 'list': 'assets.view_commandfilterule', - } - - def get_serializer_class(self): - from ..serializers import CommandFilterRuleSerializer - return CommandFilterRuleSerializer - - def get_queryset(self): - user_id = self.request.query_params.get('user_id') - user_group_id = self.request.query_params.get('user_group_id') - system_user_id = self.kwargs.get('pk', None) - system_user = get_object_or_none(SystemUser, pk=system_user_id) - if not system_user: - system_user_id = self.request.query_params.get('system_user_id') - asset_id = self.request.query_params.get('asset_id') - node_id = self.request.query_params.get('node_id') - application_id = self.request.query_params.get('application_id') - rules = CommandFilterRule.get_queryset( - user_id=user_id, - user_group_id=user_group_id, - system_user_id=system_user_id, - asset_id=asset_id, - node_id=node_id, - application_id=application_id - ) - return rules - - -class SystemUserAssetsListView(generics.ListAPIView): - serializer_class = serializers.AssetSimpleSerializer - filterset_fields = ("hostname", "ip") - search_fields = filterset_fields - rbac_perms = { - 'list': 'assets.view_asset' - } - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(SystemUser, pk=pk) - - def get_queryset(self): - system_user = self.get_object() - return system_user.get_all_assets() diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py deleted file mode 100644 index 2d8018e4d..000000000 --- a/apps/assets/api/system_user_relation.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -# -from collections import defaultdict -from django.db.models import F, Value, Model -from django.db.models.signals import m2m_changed -from django.db.models.functions import Concat - -from common.utils import get_logger -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.utils import current_org -from .. import models, serializers - -__all__ = [ - 'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet', - 'SystemUserUserRelationViewSet', 'BaseRelationViewSet', -] - -logger = get_logger(__name__) - - -class RelationMixin: - model: Model - - def get_queryset(self): - queryset = self.model.objects.all() - if not current_org.is_root(): - org_id = current_org.org_id() - queryset = queryset.filter(systemuser__org_id=org_id) - - queryset = queryset.annotate(systemuser_display=Concat( - F('systemuser__name'), Value('('), - F('systemuser__username'), Value(')') - )) - return queryset - - def send_post_add_signal(self, instance): - if not isinstance(instance, list): - instance = [instance] - - system_users_objects_map = defaultdict(list) - model, object_field = self.get_objects_attr() - - for i in instance: - _id = getattr(i, object_field).id - system_users_objects_map[i.systemuser].append(_id) - - sender = self.get_sender() - for system_user, object_ids in system_users_objects_map.items(): - logger.debug('System user relation changed, send m2m_changed signals') - m2m_changed.send( - sender=sender, instance=system_user, action='post_add', - reverse=False, model=model, pk_set=set(object_ids) - ) - - def get_sender(self): - return self.model - - def get_objects_attr(self): - return models.Asset, 'asset' - - def perform_create(self, serializer): - instance = serializer.save() - self.send_post_add_signal(instance) - - -class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet): - perm_model = models.SystemUser - - -class SystemUserAssetRelationViewSet(BaseRelationViewSet): - perm_model = models.AuthBook - serializer_class = serializers.SystemUserAssetRelationSerializer - model = models.SystemUser.assets.through - filterset_fields = [ - 'id', 'asset', 'systemuser', - ] - search_fields = [ - "id", "asset__hostname", "asset__ip", - "systemuser__name", "systemuser__username", - ] - - def get_objects_attr(self): - return models.Asset, 'asset' - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - asset_display=Concat( - F('asset__hostname'), Value('('), - F('asset__ip'), Value(')') - ) - ) - return queryset - - -class SystemUserNodeRelationViewSet(BaseRelationViewSet): - serializer_class = serializers.SystemUserNodeRelationSerializer - model = models.SystemUser.nodes.through - filterset_fields = [ - 'id', 'node', 'systemuser', - ] - search_fields = [ - "node__value", "systemuser__name", "systemuser__username" - ] - - def get_objects_attr(self): - return models.Node, 'node' - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(node_key=F('node__key')) - return queryset - - -class SystemUserUserRelationViewSet(BaseRelationViewSet): - serializer_class = serializers.SystemUserUserRelationSerializer - model = models.SystemUser.users.through - filterset_fields = [ - 'id', 'user', 'systemuser', - ] - search_fields = [ - "user__username", "user__name", - "systemuser__name", "systemuser__username", - ] - - def get_objects_attr(self): - from users.models import User - return User, 'user' - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - user_display=Concat( - F('user__name'), Value('('), - F('user__username'), Value(')') - ) - ) - return queryset diff --git a/apps/assets/automations/__init__.py b/apps/assets/automations/__init__.py new file mode 100644 index 000000000..30fb03cda --- /dev/null +++ b/apps/assets/automations/__init__.py @@ -0,0 +1,2 @@ +from .endpoint import ExecutionManager +from .methods import platform_automation_methods, filter_platform_methods diff --git a/apps/assets/task_handlers/backup/__init__.py b/apps/assets/automations/backup_account/__init__.py similarity index 100% rename from apps/assets/task_handlers/backup/__init__.py rename to apps/assets/automations/backup_account/__init__.py diff --git a/apps/assets/task_handlers/backup/handlers.py b/apps/assets/automations/backup_account/handlers.py similarity index 61% rename from apps/assets/task_handlers/backup/handlers.py rename to apps/assets/automations/backup_account/handlers.py index 43e6a80db..1575ce8fd 100644 --- a/apps/assets/task_handlers/backup/handlers.py +++ b/apps/assets/automations/backup_account/handlers.py @@ -5,15 +5,12 @@ from collections import defaultdict, OrderedDict from django.conf import settings from django.db.models import F -from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from assets.models import AuthBook, SystemUser, Asset -from assets.serializers import AccountBackUpSerializer +from assets.models import Account +from assets.const import AllTypes +from assets.serializers import AccountSecretSerializer from assets.notifications import AccountBackupExecutionTaskMsg -from applications.models import Account, Application -from applications.const import AppType -from applications.serializers import AppAccountBackUpSerializer from users.models import User from common.utils import get_logger from common.utils.timezone import local_now_display @@ -39,7 +36,7 @@ class BaseAccountHandler: @classmethod def get_header_fields(cls, serializer: serializers.Serializer): try: - backup_fields = getattr(serializer, 'Meta').fields + backup_fields = getattr(serializer, 'Meta').fields_backup except AttributeError: backup_fields = serializer.fields.keys() header_fields = {} @@ -92,84 +89,42 @@ class AssetAccountHandler(BaseAccountHandler): @staticmethod def get_filename(plan_name): filename = os.path.join( - PATH, f'{plan_name}-{_("Asset")}-{local_now_display()}-{time.time()}.xlsx' + PATH, f'{plan_name}-{local_now_display()}-{time.time()}.xlsx' ) return filename @classmethod - def replace_account_info(cls, account, asset_dict, system_user_dict): - asset = asset_dict.get(account.asset_id) - account.ip = asset.ip if asset else '' - account.hostname = asset.hostname if asset else '' - account = cls.replace_auth(account, system_user_dict) - return account + def create_data_map(cls, categories: list): + data_map = defaultdict(list) - @classmethod - def create_data_map(cls, system_user_dict): - sheet_name = AuthBook._meta.verbose_name - assets = Asset.objects.only('id', 'hostname', 'ip') - asset_dict = {asset.id: asset for asset in assets} - accounts = AuthBook.objects.all() - if not accounts.exists(): - return - - header_fields = cls.get_header_fields(AccountBackUpSerializer(accounts.first())) - for account in accounts: - cls.replace_account_info(account, asset_dict, system_user_dict) - data = AccountBackUpSerializer(accounts, many=True).data - data_map = cls.add_rows(data, header_fields, sheet_name) - logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count())) - return data_map - - -class AppAccountHandler(BaseAccountHandler): - @staticmethod - def get_filename(plan_name): - filename = os.path.join( - PATH, f'{plan_name}-{_("Application")}-{local_now_display()}-{time.time()}.xlsx' - ) - return filename - - @classmethod - def replace_account_info(cls, account, app_dict, system_user_dict): - app = app_dict.get(account.app_id) - account.type = app.type if app else '' - account.app_display = app.name if app else '' - account.category = app.category if app else '' - account = cls.replace_auth(account, system_user_dict) - return account - - @classmethod - def create_data_map(cls, system_user_dict): - apps = Application.objects.only('id', 'type', 'name', 'category') - app_dict = {app.id: app for app in apps} - qs = Account.objects.all().annotate(app_type=F('app__type')) + # TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作 + qs = Account.objects.filter( + asset__platform__type__in=categories + ).annotate(category=F('asset__platform__type')) + print(qs, categories) if not qs.exists(): - return + return data_map + + category_dict = {} + for i in AllTypes.grouped_choices_to_objs(): + for j in i['children']: + category_dict[j['value']] = j['display_name'] + + header_fields = cls.get_header_fields(AccountSecretSerializer(qs.first())) + account_category_map = defaultdict(list) + for account in qs: + account_category_map[account.category].append(account) - account_type_map = defaultdict(list) - for i in qs: - account_type_map[i.app_type].append(i) data_map = {} - for app_type, accounts in account_type_map.items(): - sheet_name = AppType.get_label(app_type) - header_fields = cls.get_header_fields(AppAccountBackUpSerializer(tp=app_type)) - if not accounts: - continue - for account in accounts: - cls.replace_account_info(account, app_dict, system_user_dict) - data = AppAccountBackUpSerializer(accounts, many=True, tp=app_type).data + for category, accounts in account_category_map.items(): + sheet_name = category_dict.get(category, category) + data = AccountSecretSerializer(accounts, many=True).data data_map.update(cls.add_rows(data, header_fields, sheet_name)) - logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(qs.count())) + + logger.info('\n\033[33m- 共收集 {} 条账号\033[0m'.format(qs.count())) return data_map -handler_map = { - 'asset': AssetAccountHandler, - 'application': AppAccountHandler -} - - class AccountBackupHandler: def __init__(self, execution): self.execution = execution @@ -185,28 +140,21 @@ class AccountBackupHandler: # Print task start date time_start = time.time() files = [] - system_user_qs = SystemUser.objects.only( - 'id', 'username', 'password', 'private_key', 'public_key' - ) - system_user_dict = {i.id: i for i in system_user_qs} - for account_type in self.execution.types: - handler = handler_map.get(account_type) - if not handler: - continue + categories = self.execution.categories - data_map = handler.create_data_map(system_user_dict) - if not data_map: - continue + data_map = AssetAccountHandler.create_data_map(categories) + if not data_map: + return files - filename = handler.get_filename(self.plan_name) + filename = AssetAccountHandler.get_filename(self.plan_name) - wb = Workbook(filename) - for sheet, data in data_map.items(): - ws = wb.create_sheet(str(sheet)) - for row in data: - ws.append(row) - wb.save(filename) - files.append(filename) + wb = Workbook(filename) + for sheet, data in data_map.items(): + ws = wb.create_sheet(str(sheet)) + for row in data: + ws.append(row) + wb.save(filename) + files.append(filename) timedelta = round((time.time() - time_start), 2) logger.info('步骤完成: 用时 {}s'.format(timedelta)) return files diff --git a/apps/assets/task_handlers/backup/manager.py b/apps/assets/automations/backup_account/manager.py similarity index 97% rename from apps/assets/task_handlers/backup/manager.py rename to apps/assets/automations/backup_account/manager.py index c9558fea0..311361c69 100644 --- a/apps/assets/task_handlers/backup/manager.py +++ b/apps/assets/automations/backup_account/manager.py @@ -12,7 +12,7 @@ from .handlers import AccountBackupHandler logger = get_logger(__name__) -class AccountBackupExecutionManager: +class AccountBackupManager: def __init__(self, execution): self.execution = execution self.date_start = timezone.now() diff --git a/apps/perms/tree/__init__.py b/apps/assets/automations/base/__init__.py similarity index 100% rename from apps/perms/tree/__init__.py rename to apps/assets/automations/base/__init__.py diff --git a/apps/assets/automations/base/base_inventory.txt b/apps/assets/automations/base/base_inventory.txt new file mode 100644 index 000000000..a2b73db16 --- /dev/null +++ b/apps/assets/automations/base/base_inventory.txt @@ -0,0 +1,14 @@ +## all connection vars +hostname asset_name=name asset_type=type asset_primary_protocol=ssh asset_primary_port=22 asset_protocols=[] + +## local connection +hostname ansible_connection=local + +## local connection with gateway +hostname ansible_connection=ssh ansible_user=gateway.username ansible_port=gateway.port ansible_host=gateway.host ansible_ssh_private_key_file=gateway.key + +## ssh connection for windows +hostname ansible_connection=ssh ansible_shell_type=powershell/cmd ansible_user=windows.username ansible_port=windows.port ansible_host=windows.host ansible_ssh_private_key_file=windows.key + +## ssh connection +hostname ansible_user=user ansible_password=pass ansible_host=host ansible_port=port ansible_ssh_private_key_file=key ssh_args="-o StrictHostKeyChecking=no" diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py new file mode 100644 index 000000000..6d1563258 --- /dev/null +++ b/apps/assets/automations/base/manager.py @@ -0,0 +1,237 @@ +import os +import yaml +import shutil +from hashlib import md5 +from copy import deepcopy +from socket import gethostname +from collections import defaultdict + +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 ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback + +logger = get_logger(__name__) + + +class PushOrVerifyHostCallbackMixin: + execution: callable + host_account_mapper: dict + ignore_account: bool + need_privilege_account: bool + generate_public_key: callable + generate_private_key_path: callable + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): + host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) + if host.get('error'): + return host + + accounts = asset.accounts.all() + if self.need_privilege_account and accounts.count() > 1 and account: + accounts = accounts.exclude(id=account.id) + + if '*' not in self.execution.snapshot['accounts']: + accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) + + inventory_hosts = [] + for account in accounts: + h = deepcopy(host) + h['name'] += '_' + account.username + self.host_account_mapper[h['name']] = account + secret = account.secret + + private_key_path = None + if account.secret_type == SecretType.SSH_KEY: + private_key_path = self.generate_private_key_path(secret, path_dir) + secret = self.generate_public_key(secret) + + h['secret_type'] = account.secret_type + h['account'] = { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': secret, + 'private_key_path': private_key_path + } + inventory_hosts.append(h) + return inventory_hosts + + +class PlaybookCallback(DefaultCallback): + def playbook_on_stats(self, event_data, **kwargs): + super().playbook_on_stats(event_data, **kwargs) + + +class BasePlaybookManager: + bulk_size = 100 + ansible_account_policy = 'privileged_first' + + def __init__(self, execution): + self.execution = execution + self.automation = execution.automation + self.method_id_meta_mapper = { + method['id']: method + for method in platform_automation_methods + if method['method'] == self.__class__.method_type() + } + # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 + # 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook + # 避免一个 playbook 中包含太多的主机 + self.method_hosts_mapper = defaultdict(list) + self.playbooks = [] + + @classmethod + def method_type(cls): + raise NotImplementedError + + def get_assets_group_by_platform(self): + return self.automation.all_assets_group_by_platform() + + @property + def runtime_dir(self): + ansible_dir = settings.ANSIBLE_DIR + dir_name = '{}_{}'.format(self.automation.name.replace(' ', '_'), self.execution.id) + path = os.path.join( + ansible_dir, 'automations', self.execution.snapshot['type'], + dir_name, timezone.now().strftime('%Y%m%d_%H%M%S') + ) + if not os.path.exists(path): + 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()) + + method_enabled = automation and \ + getattr(automation, enabled_attr) and \ + getattr(automation, method_attr) and \ + getattr(automation, method_attr) in self.method_id_meta_mapper + + if not method_enabled: + host['error'] = _('{} disabled'.format(self.__class__.method_type())) + return host + return host + + @staticmethod + def generate_public_key(private_key): + return ssh_pubkey_gen(private_key=private_key, hostname=gethostname()) + + @staticmethod + def generate_private_key_path(secret, path_dir): + key_name = '.' + md5(secret.encode('utf-8')).hexdigest() + key_path = os.path.join(path_dir, key_name) + + if not os.path.exists(key_path): + ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + + def generate_inventory(self, platformed_assets, inventory_path): + inventory = JMSInventory( + assets=platformed_assets, + account_policy=self.ansible_account_policy, + host_callback=self.host_callback, + ) + inventory.write_to_file(inventory_path) + + def generate_playbook(self, platformed_assets, platform, sub_playbook_dir): + method_id = getattr(platform.automation, '{}_method'.format(self.__class__.method_type())) + method = self.method_id_meta_mapper.get(method_id) + if not method: + logger.error("Method not found: {}".format(method_id)) + return method + method_playbook_dir_path = method['dir'] + sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml') + shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path)) + + with open(sub_playbook_path, 'r') as f: + plays = yaml.safe_load(f) + for play in plays: + play['hosts'] = 'all' + + with open(sub_playbook_path, 'w') as f: + yaml.safe_dump(plays, f) + return sub_playbook_path + + def get_runners(self): + runners = [] + for platform, assets in self.get_assets_group_by_platform().items(): + assets_bulked = [assets[i:i + self.bulk_size] for i in range(0, len(assets), self.bulk_size)] + + for i, _assets in enumerate(assets_bulked, start=1): + sub_dir = '{}_{}'.format(platform.name, i) + playbook_dir = os.path.join(self.runtime_dir, sub_dir) + inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json') + self.generate_inventory(_assets, inventory_path) + playbook_path = self.generate_playbook(_assets, platform, playbook_dir) + + runer = PlaybookRunner( + inventory_path, + playbook_path, + self.runtime_dir, + callback=PlaybookCallback(), + ) + runners.append(runer) + return runners + + def on_host_success(self, host, result): + pass + + def on_host_error(self, host, error, result): + pass + + def on_runner_success(self, runner, cb): + summary = cb.summary + for state, hosts in summary.items(): + for host in hosts: + result = cb.host_results.get(host) + if state == 'ok': + self.on_host_success(host, result) + elif state == 'skipped': + # TODO + print('skipped: ', hosts) + else: + error = hosts.get(host) + self.on_host_error(host, error, result) + + def on_runner_failed(self, runner, e): + print("Runner failed: {} {}".format(e, self)) + + def before_runner_start(self, runner): + print("Start run task: ") + print(" inventory: {}".format(runner.inventory)) + print(" playbook: {}".format(runner.playbook)) + + def run(self, *args, **kwargs): + runners = self.get_runners() + if len(runners) > 1: + print("### 分批次执行开始任务, 总共 {}\n".format(len(runners))) + else: + print(">>> 开始执行任务\n") + + self.execution.date_start = timezone.now() + for i, runner in enumerate(runners, start=1): + if len(runners) > 1: + print(">>> 开始执行第 {} 批任务".format(i)) + self.before_runner_start(runner) + try: + cb = runner.run(**kwargs) + self.on_runner_success(runner, cb) + except Exception as e: + self.on_runner_failed(runner, e) + print('\n') + self.execution.status = 'success' + self.execution.date_finished = timezone.now() + self.execution.save() diff --git a/apps/assets/signal_handlers/common.py b/apps/assets/automations/change_secret/__init__.py similarity index 100% rename from apps/assets/signal_handlers/common.py rename to apps/assets/automations/change_secret/__init__.py diff --git a/apps/assets/automations/change_secret/database/mongodb/main.yml b/apps/assets/automations/change_secret/database/mongodb/main.yml new file mode 100644 index 000000000..02a568e0b --- /dev/null +++ b/apps/assets/automations/change_secret/database/mongodb/main.yml @@ -0,0 +1,43 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MongoDB connection + mongodb_ping: + 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 }}" + register: db_info + + - name: Display MongoDB version + debug: + var: db_info.server_version + when: db_info is succeeded + + - name: Change MongoDB password + 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 }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + mongodb_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/mongodb/manifest.yml b/apps/assets/automations/change_secret/database/mongodb/manifest.yml new file mode 100644 index 000000000..a59c0033b --- /dev/null +++ b/apps/assets/automations/change_secret/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_mongodb +name: Change password for MongoDB +category: database +type: + - mongodb +method: change_secret diff --git a/apps/assets/automations/change_secret/database/mysql/main.yml b/apps/assets/automations/change_secret/database/mysql/main.yml new file mode 100644 index 000000000..c76b53b08 --- /dev/null +++ b/apps/assets/automations/change_secret/database/mysql/main.yml @@ -0,0 +1,41 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MySQL connection + community.mysql.mysql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version + register: db_info + + - name: MySQL version + debug: + var: db_info.version.full + + - name: Change MySQL password + 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: "%" + when: db_info is succeeded + register: change_info + + - name: Verify password + community.mysql.mysql_info: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version + when: + - db_info is succeeded + - change_info is succeeded \ No newline at end of file diff --git a/apps/assets/automations/change_secret/database/mysql/manifest.yml b/apps/assets/automations/change_secret/database/mysql/manifest.yml new file mode 100644 index 000000000..ca2aefc01 --- /dev/null +++ b/apps/assets/automations/change_secret/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_mysql +name: Change password for MySQL +category: database +type: + - mysql +method: change_secret diff --git a/apps/assets/automations/change_secret/database/oracle/main.yml b/apps/assets/automations/change_secret/database/oracle/main.yml new file mode 100644 index 000000000..c7b20a8db --- /dev/null +++ b/apps/assets/automations/change_secret/database/oracle/main.yml @@ -0,0 +1,45 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test Oracle connection + oracle_ping: + 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 }}" + register: db_info + + - name: Display Oracle version + debug: + var: db_info.server_version + when: db_info is succeeded + + - name: Change Oracle password + 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 }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + oracle_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ account.mode }}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/oracle/manifest.yml b/apps/assets/automations/change_secret/database/oracle/manifest.yml new file mode 100644 index 000000000..19f109ba6 --- /dev/null +++ b/apps/assets/automations/change_secret/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_oracle +name: Change password for Oracle +category: database +type: + - oracle +method: change_secret diff --git a/apps/assets/automations/change_secret/database/postgresql/main.yml b/apps/assets/automations/change_secret/database/postgresql/main.yml new file mode 100644 index 000000000..ada11bbd6 --- /dev/null +++ b/apps/assets/automations/change_secret/database/postgresql/main.yml @@ -0,0 +1,42 @@ +- hosts: postgre + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test PostgreSQL connection + community.postgresql.postgresql_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_db: "{{ jms_asset.specific.db_name }}" + register: db_info + + - name: Display PostgreSQL version + debug: + var: db_info.server_version.full + when: db_info is succeeded + + - name: Change PostgreSQL password + 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 }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + community.postgresql.postgresql_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + db: "{{ jms_asset.specific.db_name }}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/postgresql/manifest.yml b/apps/assets/automations/change_secret/database/postgresql/manifest.yml new file mode 100644 index 000000000..48238f5ec --- /dev/null +++ b/apps/assets/automations/change_secret/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_postgresql +name: Change password for PostgreSQL +category: database +type: + - postgresql +method: change_secret diff --git a/apps/assets/automations/change_secret/database/sqlserver/main.yml b/apps/assets/automations/change_secret/database/sqlserver/main.yml new file mode 100644 index 000000000..a617a1434 --- /dev/null +++ b/apps/assets/automations/change_secret/database/sqlserver/main.yml @@ -0,0 +1,47 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test SQLServer connection + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version + register: db_info + + - name: SQLServer version + set_fact: + info: + version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}" + - debug: + var: info + + - name: Change SQLServer password + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version" + when: db_info is succeeded + register: change_info + + - name: Verify password + community.general.mssql_script: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/sqlserver/manifest.yml b/apps/assets/automations/change_secret/database/sqlserver/manifest.yml new file mode 100644 index 000000000..799c9e623 --- /dev/null +++ b/apps/assets/automations/change_secret/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_sqlserver +name: Change password for SQLServer +category: database +type: + - sqlserver +method: change_secret diff --git a/apps/assets/automations/change_secret/demo_inventory.txt b/apps/assets/automations/change_secret/demo_inventory.txt new file mode 100644 index 000000000..dcc7d1b6d --- /dev/null +++ b/apps/assets/automations/change_secret/demo_inventory.txt @@ -0,0 +1,2 @@ +# all base inventory in base/base_inventory.txt +asset_name(ip)_account_username account={"username": "", "password": "xxx"} ...base_inventory_vars diff --git a/apps/assets/automations/change_secret/host/posix/main.yml b/apps/assets/automations/change_secret/host/posix/main.yml new file mode 100644 index 000000000..1a4e6a6a4 --- /dev/null +++ b/apps/assets/automations/change_secret/host/posix/main.yml @@ -0,0 +1,58 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Test privileged account + ansible.builtin.ping: + # + # - name: print variables + # debug: + # msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ secret_type }}" + + - name: Change password + ansible.builtin.user: + name: "{{ account.username }}" + password: "{{ account.secret | password_hash('sha512') }}" + update_password: always + when: secret_type == "password" + + - name: create user If it already exists, no operation will be performed + ansible.builtin.user: + name: "{{ account.username }}" + when: secret_type == "ssh_key" + + - name: remove jumpserver ssh key + ansible.builtin.lineinfile: + dest: "{{ kwargs.dest }}" + regexp: "{{ kwargs.regexp }}" + state: absent + when: + - secret_type == "ssh_key" + - kwargs.strategy == "set_jms" + + - name: Change SSH key + ansible.builtin.authorized_key: + user: "{{ account.username }}" + key: "{{ account.secret }}" + exclusive: "{{ kwargs.exclusive }}" + when: secret_type == "ssh_key" + + - name: Refresh connection + ansible.builtin.meta: reset_connection + + - name: Verify password + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + ansible_become: no + when: secret_type == "password" + + - name: Verify SSH key + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_ssh_private_key_file: "{{ account.private_key_path }}" + ansible_become: no + when: secret_type == "ssh_key" diff --git a/apps/assets/automations/change_secret/host/posix/manifest.yml b/apps/assets/automations/change_secret/host/posix/manifest.yml new file mode 100644 index 000000000..491fb14a2 --- /dev/null +++ b/apps/assets/automations/change_secret/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: change_secret_posix +name: Change secret for posix +category: host +type: + - unix + - linux +method: change_secret diff --git a/apps/assets/automations/change_secret/host/windows/main.yml b/apps/assets/automations/change_secret/host/windows/main.yml new file mode 100644 index 000000000..0c27301dc --- /dev/null +++ b/apps/assets/automations/change_secret/host/windows/main.yml @@ -0,0 +1,26 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Test privileged account + ansible.windows.win_ping: + +# - name: Print variables +# debug: +# msg: "Username: {{ account.username }}, Password: {{ account.secret }}" + + - name: Change password + ansible.windows.win_user: + name: "{{ account.username }}" + password: "{{ account.secret }}" + update_password: always + when: account.secret_type == "password" + + - name: Refresh connection + ansible.builtin.meta: reset_connection + + - name: Verify password + ansible.windows.win_ping: + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + when: account.secret_type == "password" diff --git a/apps/assets/automations/change_secret/host/windows/manifest.yml b/apps/assets/automations/change_secret/host/windows/manifest.yml new file mode 100644 index 000000000..80d0fb782 --- /dev/null +++ b/apps/assets/automations/change_secret/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: change_secret_local_windows +name: Change password local account for Windows +version: 1 +method: change_secret +category: host +type: + - windows diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py new file mode 100644 index 000000000..30606956d --- /dev/null +++ b/apps/assets/automations/change_secret/manager.py @@ -0,0 +1,233 @@ +import os +import time +import random +import string +from copy import deepcopy +from openpyxl import Workbook +from collections import defaultdict + +from django.utils import timezone +from django.conf import settings + +from common.utils.timezone import local_now_display +from common.utils.file import encrypt_and_compress_zip_file +from common.utils import get_logger, lazyproperty, gen_key_pair +from users.models import User +from assets.models import ChangeSecretRecord +from assets.notifications import ChangeSecretExecutionTaskMsg +from assets.serializers import ChangeSecretRecordBackUpSerializer +from assets.const import ( + AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES +) +from ..base.manager import BasePlaybookManager + +logger = get_logger(__name__) + + +class ChangeSecretManager(BasePlaybookManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.method_hosts_mapper = defaultdict(list) + self.secret_type = self.execution.snapshot['secret_type'] + self.secret_strategy = self.execution.snapshot['secret_strategy'] + self._password_generated = None + self._ssh_key_generated = None + self.name_recorder_mapper = {} # 做个映射,方便后面处理 + + @classmethod + def method_type(cls): + return AutomationTypes.change_secret + + @lazyproperty + def related_accounts(self): + pass + + @staticmethod + def generate_ssh_key(): + private_key, public_key = gen_key_pair() + return private_key + + def generate_password(self): + kwargs = self.execution.snapshot['password_rules'] or {} + length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) + symbol_set = kwargs.get('symbol_set') + if symbol_set is None: + symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] + + no_special_chars = string.ascii_letters + string.digits + chars = no_special_chars + symbol_set + + first_char = random.choice(no_special_chars) + password = ''.join([random.choice(chars) for _ in range(length - 1)]) + password = first_char + password + return password + + def get_ssh_key(self): + if self.secret_strategy == SecretStrategy.custom: + secret = self.execution.snapshot['secret'] + if not secret: + raise ValueError("Automation SSH key must be set") + return secret + elif self.secret_strategy == SecretStrategy.random_one: + if not self._ssh_key_generated: + self._ssh_key_generated = self.generate_ssh_key() + return self._ssh_key_generated + else: + return self.generate_ssh_key() + + def get_password(self): + if self.secret_strategy == SecretStrategy.custom: + password = self.execution.snapshot['secret'] + if not password: + raise ValueError("Automation Password must be set") + return password + elif self.secret_strategy == SecretStrategy.random_one: + if not self._password_generated: + self._password_generated = self.generate_password() + return self._password_generated + else: + return self.generate_password() + + def get_secret(self): + if self.secret_type == SecretType.SSH_KEY: + secret = self.get_ssh_key() + elif self.secret_type == SecretType.PASSWORD: + secret = self.get_password() + else: + raise ValueError("Secret must be set") + return secret + + def get_kwargs(self, account, secret): + kwargs = {} + if self.secret_type != SecretType.SSH_KEY: + return kwargs + kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy'] + kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' + + if kwargs['strategy'] == SSHKeyStrategy.set_jms: + kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username) + kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) + + return kwargs + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): + host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) + if host.get('error'): + return host + + accounts = asset.accounts.all() + if account: + accounts = accounts.exclude(id=account.id) + + if '*' not in self.execution.snapshot['accounts']: + accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) + + accounts = accounts.filter(secret_type=self.secret_type) + method_attr = getattr(automation, self.method_type() + '_method') + method_hosts = self.method_hosts_mapper[method_attr] + method_hosts = [h for h in method_hosts if h != host['name']] + inventory_hosts = [] + records = [] + + host['secret_type'] = self.secret_type + for account in accounts: + h = deepcopy(host) + h['name'] += '_' + account.username + new_secret = self.get_secret() + + recorder = ChangeSecretRecord( + asset=asset, account=account, execution=self.execution, + old_secret=account.secret, new_secret=new_secret, + ) + records.append(recorder) + self.name_recorder_mapper[h['name']] = recorder + + private_key_path = None + if self.secret_type == SecretType.SSH_KEY: + private_key_path = self.generate_private_key_path(new_secret, path_dir) + new_secret = self.generate_public_key(new_secret) + + h['kwargs'] = self.get_kwargs(account, new_secret) + h['account'] = { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': new_secret, + 'private_key_path': private_key_path + } + if asset.platform.type == 'oracle': + h['account']['mode'] = 'sysdba' if account.privileged else None + inventory_hosts.append(h) + method_hosts.append(h['name']) + self.method_hosts_mapper[method_attr] = method_hosts + ChangeSecretRecord.objects.bulk_create(records) + return inventory_hosts + + def on_host_success(self, host, result): + recorder = self.name_recorder_mapper.get(host) + if not recorder: + return + recorder.status = 'success' + recorder.date_finished = timezone.now() + recorder.save() + + account = recorder.account + account.secret = recorder.new_secret + account.save(update_fields=['secret']) + + def on_host_error(self, host, error, result): + recorder = self.name_recorder_mapper.get(host) + if not recorder: + return + recorder.status = 'failed' + recorder.date_finished = timezone.now() + recorder.error = error + recorder.save() + + def on_runner_failed(self, runner, e): + logger.error("Change secret error: ", e) + + def run(self, *args, **kwargs): + super().run(*args, **kwargs) + recorders = self.name_recorder_mapper.values() + recorders = list(recorders) + self.send_recorder_mail(recorders) + + def send_recorder_mail(self, recorders): + recipients = self.execution.recipients + if not recorders or not recipients: + return + recipients = User.objects.filter(id__in=list(recipients)) + + name = self.execution.snapshot['name'] + path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') + filename = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.xlsx') + if not self.create_file(recorders, filename): + return + + for user in recipients: + attachments = [] + if user.secret_key: + password = user.secret_key.encode('utf8') + attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip') + encrypt_and_compress_zip_file(attachment, password, [filename]) + attachments = [attachment] + ChangeSecretExecutionTaskMsg(name, user).publish(attachments) + os.remove(filename) + + @staticmethod + def create_file(recorders, filename): + serializer_cls = ChangeSecretRecordBackUpSerializer + serializer = serializer_cls(recorders, many=True) + header = [v.label for v in serializer.child.fields.values()] + rows = [list(row.values()) for row in serializer.data] + if not rows: + return False + + rows.insert(0, header) + wb = Workbook(filename) + ws = wb.create_sheet('Sheet1') + for row in rows: + ws.append(row) + wb.save(filename) + return True diff --git a/apps/assets/automations/endpoint.py b/apps/assets/automations/endpoint.py new file mode 100644 index 000000000..2548e9184 --- /dev/null +++ b/apps/assets/automations/endpoint.py @@ -0,0 +1,28 @@ +from .change_secret.manager import ChangeSecretManager +from .gather_facts.manager import GatherFactsManager +from .gather_accounts.manager import GatherAccountsManager +from .verify_account.manager import VerifyAccountManager +from .push_account.manager import PushAccountManager +from .backup_account.manager import AccountBackupManager +from .ping.manager import PingManager +from ..const import AutomationTypes + + +class ExecutionManager: + manager_type_mapper = { + AutomationTypes.ping: PingManager, + AutomationTypes.push_account: PushAccountManager, + AutomationTypes.gather_facts: GatherFactsManager, + AutomationTypes.change_secret: ChangeSecretManager, + AutomationTypes.verify_account: VerifyAccountManager, + AutomationTypes.gather_accounts: GatherAccountsManager, + # TODO 后期迁移到自动化策略中 + 'backup_account': AccountBackupManager, + } + + def __init__(self, execution): + self.execution = execution + self._runner = self.manager_type_mapper[execution.manager_type](execution) + + def run(self, *args, **kwargs): + return self._runner.run(*args, **kwargs) diff --git a/apps/tickets/api/assignee.py b/apps/assets/automations/gather_accounts/__init__.py similarity index 100% rename from apps/tickets/api/assignee.py rename to apps/assets/automations/gather_accounts/__init__.py diff --git a/apps/assets/automations/gather_accounts/database/mongodb/main.yml b/apps/assets/automations/gather_accounts/database/mongodb/main.yml new file mode 100644 index 000000000..fd7a296b7 --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/mongodb/main.yml @@ -0,0 +1,22 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + community.mongodb.mongodb_info: + 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 }}" + filter: users + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.users }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml b/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml new file mode 100644 index 000000000..a002848b4 --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: gather_accounts_mongodb +name: Gather account from MongoDB +category: database +type: + - mongodb +method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/database/mysql/main.yml b/apps/assets/automations/gather_accounts/database/mysql/main.yml new file mode 100644 index 000000000..cc934f20f --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/mysql/main.yml @@ -0,0 +1,21 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + community.mysql.mysql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: users + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.users }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_accounts/database/mysql/manifest.yml b/apps/assets/automations/gather_accounts/database/mysql/manifest.yml new file mode 100644 index 000000000..e69cca67b --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: gather_accounts_mysql +name: Gather account from MySQL +category: database +type: + - mysql +method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/database/oracle/main.yml b/apps/assets/automations/gather_accounts/database/oracle/main.yml new file mode 100644 index 000000000..a0e20ff7b --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/oracle/main.yml @@ -0,0 +1,23 @@ +- hosts: oralce + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + oracle_info: + 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 }}" + filter: users + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.users }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_accounts/database/oracle/manifest.yml b/apps/assets/automations/gather_accounts/database/oracle/manifest.yml new file mode 100644 index 000000000..4753f1495 --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: gather_accounts_oracle +name: Gather account from Oracle +category: database +type: + - oracle +method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/database/postgresql/main.yml b/apps/assets/automations/gather_accounts/database/postgresql/main.yml new file mode 100644 index 000000000..f282b390d --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/postgresql/main.yml @@ -0,0 +1,22 @@ +- hosts: postgresql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + community.postgresql.postgresql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_db: "{{ jms_asset.specific.db_name }}" + filter: "roles" + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.roles }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_accounts/database/postgresql/manifest.yml b/apps/assets/automations/gather_accounts/database/postgresql/manifest.yml new file mode 100644 index 000000000..3e563053a --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: gather_accounts_postgresql +name: Gather account for PostgreSQL +category: database +type: + - postgresql +method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/filter.py b/apps/assets/automations/gather_accounts/filter.py new file mode 100644 index 000000000..ebaf6d9b1 --- /dev/null +++ b/apps/assets/automations/gather_accounts/filter.py @@ -0,0 +1,59 @@ +from django.utils import timezone + +__all__ = ['GatherAccountsFilter'] + + +# TODO 后期会挪到playbook中 +class GatherAccountsFilter: + + def __init__(self, tp): + self.tp = tp + + @staticmethod + def mysql_filter(info): + result = {} + for _, user_dict in info.items(): + for username, data in user_dict.items(): + if data.get('account_locked') == 'N': + result[username] = {} + return result + + @staticmethod + def postgresql_filter(info): + result = {} + for username in info: + result[username] = {} + return result + + @staticmethod + def posix_filter(info): + result = {} + for line in info: + data = line.split('@') + if len(data) != 3: + continue + username, address, dt = data + date = timezone.datetime.strptime(f'{dt} +0800', '%b %d %H:%M:%S %Y %z') + result[username] = {'address': address, 'date': date} + return result + + @staticmethod + def windows_filter(info): + info = info[4:-2] + result = {} + for i in info: + for username in i.split(): + result[username] = {} + return result + + def run(self, method_id_meta_mapper, info): + run_method_name = None + for k, v in method_id_meta_mapper.items(): + if self.tp not in v['type']: + continue + run_method_name = k.replace(f'{v["method"]}_', '') + + if not run_method_name: + return info + + return getattr(self, f'{run_method_name}_filter')(info) diff --git a/apps/assets/automations/gather_accounts/host/posix/main.yml b/apps/assets/automations/gather_accounts/host/posix/main.yml new file mode 100644 index 000000000..a64323f9d --- /dev/null +++ b/apps/assets/automations/gather_accounts/host/posix/main.yml @@ -0,0 +1,16 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Gather posix account + ansible.builtin.shell: + cmd: > + users=$(getent passwd | grep -v nologin | grep -v shutdown | awk -F":" '{ print $1 }');for i in $users; + do last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $1"@"$3"@"$5,$6,$7,$8 }';done + register: result + + - name: Define info by set_fact + ansible.builtin.set_fact: + info: "{{ result.stdout_lines }}" + + - debug: + var: info \ No newline at end of file diff --git a/apps/assets/automations/gather_accounts/host/posix/manifest.yml b/apps/assets/automations/gather_accounts/host/posix/manifest.yml new file mode 100644 index 000000000..a761c9796 --- /dev/null +++ b/apps/assets/automations/gather_accounts/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: gather_accounts_posix +name: Gather posix account +category: host +type: + - linux + - unix +method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/host/windows/main.yml b/apps/assets/automations/gather_accounts/host/windows/main.yml new file mode 100644 index 000000000..97326431d --- /dev/null +++ b/apps/assets/automations/gather_accounts/host/windows/main.yml @@ -0,0 +1,14 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Gather posix account + ansible.builtin.win_shell: + cmd: net user + register: result + + - name: Define info by set_fact + ansible.builtin.set_fact: + info: "{{ result.stdout_lines }}" + + - debug: + var: info \ No newline at end of file diff --git a/apps/assets/automations/gather_accounts/host/windows/manifest.yml b/apps/assets/automations/gather_accounts/host/windows/manifest.yml new file mode 100644 index 000000000..ffc2ef7ee --- /dev/null +++ b/apps/assets/automations/gather_accounts/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: gather_accounts_windows +name: Gather account windows +version: 1 +method: gather_accounts +category: host +type: + - windows diff --git a/apps/assets/automations/gather_accounts/manager.py b/apps/assets/automations/gather_accounts/manager.py new file mode 100644 index 000000000..40253be82 --- /dev/null +++ b/apps/assets/automations/gather_accounts/manager.py @@ -0,0 +1,45 @@ +from common.utils import get_logger +from assets.const import AutomationTypes +from orgs.utils import tmp_to_org +from .filter import GatherAccountsFilter +from ...models import GatheredUser +from ..base.manager import BasePlaybookManager + +logger = get_logger(__name__) + + +class GatherAccountsManager(BasePlaybookManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_asset_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.gather_accounts + + def host_callback(self, host, asset=None, **kwargs): + super().host_callback(host, asset=asset, **kwargs) + self.host_asset_mapper[host['name']] = asset + return host + + def filter_success_result(self, host, result): + result = GatherAccountsFilter(host).run(self.method_id_meta_mapper, result) + return result + + 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) + else: + logger.error("Not found info".format(host)) diff --git a/apps/assets/automations/gather_facts/__init__.py b/apps/assets/automations/gather_facts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/gather_facts/database/mongodb/main.yml b/apps/assets/automations/gather_facts/database/mongodb/main.yml new file mode 100644 index 000000000..37ce8bbd3 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/mongodb/main.yml @@ -0,0 +1,22 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + mongodb_ping: + 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 }}" + register: db_info + + - name: Define info by set_fact + set_fact: + info: + version: "{{ db_info.server_version }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/database/mongodb/manifest.yml b/apps/assets/automations/gather_facts/database/mongodb/manifest.yml new file mode 100644 index 000000000..9b832e838 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: gather_facts_mongodb +name: Gather facts from MongoDB +category: database +type: + - mongodb +method: gather_facts diff --git a/apps/assets/automations/gather_facts/database/mysql/main.yml b/apps/assets/automations/gather_facts/database/mysql/main.yml new file mode 100644 index 000000000..8ba210283 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/mysql/main.yml @@ -0,0 +1,22 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + community.mysql.mysql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version + register: db_info + + - name: Define info by set_fact + set_fact: + info: + version: "{{ db_info.version.full }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/database/mysql/manifest.yml b/apps/assets/automations/gather_facts/database/mysql/manifest.yml new file mode 100644 index 000000000..33109b29b --- /dev/null +++ b/apps/assets/automations/gather_facts/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: gather_facts_mysql +name: Gather facts from MySQL +category: database +type: + - mysql +method: gather_facts diff --git a/apps/assets/automations/gather_facts/database/oracle/main.yml b/apps/assets/automations/gather_facts/database/oracle/main.yml new file mode 100644 index 000000000..21ab639a4 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/oracle/main.yml @@ -0,0 +1,23 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + oracle_ping: + 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 }}" + register: db_info + + - name: Define info by set_fact + set_fact: + info: + version: "{{ db_info.server_version }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/database/oracle/manifest.yml b/apps/assets/automations/gather_facts/database/oracle/manifest.yml new file mode 100644 index 000000000..d350579d6 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: gather_facts_oracle +name: Gather facts from Oracle +category: database +type: + - oracle +method: gather_facts diff --git a/apps/assets/automations/gather_facts/database/postgresql/main.yml b/apps/assets/automations/gather_facts/database/postgresql/main.yml new file mode 100644 index 000000000..a3c481b48 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/postgresql/main.yml @@ -0,0 +1,22 @@ +- hosts: postgresql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + community.postgresql.postgresql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_db: "{{ jms_asset.specific.db_name }}" + register: db_info + + - name: Define info by set_fact + set_fact: + info: + version: "{{ db_info.server_version.raw }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/database/postgresql/manifest.yml b/apps/assets/automations/gather_facts/database/postgresql/manifest.yml new file mode 100644 index 000000000..19bf255de --- /dev/null +++ b/apps/assets/automations/gather_facts/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: gather_facts_postgresql +name: Gather facts for PostgreSQL +category: database +type: + - postgresql +method: gather_facts diff --git a/apps/assets/automations/gather_facts/demo_inventory.txt b/apps/assets/automations/gather_facts/demo_inventory.txt new file mode 100644 index 000000000..529a74e67 --- /dev/null +++ b/apps/assets/automations/gather_facts/demo_inventory.txt @@ -0,0 +1,2 @@ +# all base inventory in base/base_inventory.txt +asset_name ...base_inventory_vars diff --git a/apps/assets/automations/gather_facts/host/posix/main.yml b/apps/assets/automations/gather_facts/host/posix/main.yml new file mode 100644 index 000000000..81aef9aac --- /dev/null +++ b/apps/assets/automations/gather_facts/host/posix/main.yml @@ -0,0 +1,19 @@ +- hosts: demo + gather_facts: yes + tasks: + - name: Get info + ansible.builtin.set_fact: + info: + arch: "{{ ansible_architecture }}" + distribution: "{{ ansible_distribution }}" + distribution_version: "{{ ansible_distribution_version }}" + kernel: "{{ ansible_kernel }}" + vendor: "{{ ansible_system_vendor }}" + model: "{{ ansible_product_name }}" + sn: "{{ ansible_product_serial }}" + cpu_vcpus: "{{ ansible_processor_vcpus }}" + memory: "{{ ansible_memtotal_mb }}" + disk_total: "{{ (ansible_mounts | map(attribute='size_total') | sum / 1024 / 1024 / 1024) | round(2) }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/host/posix/manifest.yml b/apps/assets/automations/gather_facts/host/posix/manifest.yml new file mode 100644 index 000000000..b59b701aa --- /dev/null +++ b/apps/assets/automations/gather_facts/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: gather_facts_posix +name: Gather posix facts +category: host +type: + - linux + - unix +method: gather_facts diff --git a/apps/assets/automations/gather_facts/host/windows/main.yml b/apps/assets/automations/gather_facts/host/windows/main.yml new file mode 100644 index 000000000..377ffd10a --- /dev/null +++ b/apps/assets/automations/gather_facts/host/windows/main.yml @@ -0,0 +1,18 @@ +- hosts: windows + gather_facts: yes + tasks: + - name: Get info + set_fact: + info: + arch: "{{ ansible_architecture2 }}" + distribution: "{{ ansible_distribution }}" + distribution_version: "{{ ansible_distribution_version }}" + kernel: "{{ ansible_kernel }}" + vendor: "{{ ansible_system_vendor }}" + model: "{{ ansible_product_name }}" + sn: "{{ ansible_product_serial }}" + cpu_vcpus: "{{ ansible_processor_vcpus }}" + memory: "{{ ansible_memtotal_mb }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/host/windows/manifest.yml b/apps/assets/automations/gather_facts/host/windows/manifest.yml new file mode 100644 index 000000000..929a6626f --- /dev/null +++ b/apps/assets/automations/gather_facts/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: gather_facts_windows +name: Gather facts windows +version: 1 +method: gather_facts +category: host +type: + - windows diff --git a/apps/assets/automations/gather_facts/manager.py b/apps/assets/automations/gather_facts/manager.py new file mode 100644 index 000000000..90fae1d75 --- /dev/null +++ b/apps/assets/automations/gather_facts/manager.py @@ -0,0 +1,29 @@ +from common.utils import get_logger +from assets.const import AutomationTypes +from ..base.manager import BasePlaybookManager + +logger = get_logger(__name__) + + +class GatherFactsManager(BasePlaybookManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_asset_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.gather_facts + + def host_callback(self, host, asset=None, **kwargs): + super().host_callback(host, asset=asset, **kwargs) + self.host_asset_mapper[host['name']] = asset + return host + + def on_host_success(self, host, result): + info = result.get('debug', {}).get('res', {}).get('info', {}) + asset = self.host_asset_mapper.get(host) + if asset and info: + asset.info = info + asset.save() + else: + logger.error("Not found info: {}".format(host)) diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py new file mode 100644 index 000000000..811d0c77d --- /dev/null +++ b/apps/assets/automations/methods.py @@ -0,0 +1,66 @@ +import os +import yaml +import json +from functools import partial + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def check_platform_method(manifest, manifest_path): + required_keys = ['category', 'method', 'name', 'id', 'type'] + less_key = set(required_keys) - set(manifest.keys()) + if less_key: + raise ValueError("Manifest missing keys: {}, {}".format(less_key, manifest_path)) + if not isinstance(manifest['type'], list): + raise ValueError("Manifest type must be a list: {}".format(manifest_path)) + return True + + +def check_platform_methods(methods): + ids = [m['id'] for m in methods] + for i, _id in enumerate(ids): + if _id in ids[i+1:]: + raise ValueError("Duplicate id: {}".format(_id)) + + +def get_platform_automation_methods(): + methods = [] + for root, dirs, files in os.walk(BASE_DIR, topdown=False): + for name in files: + path = os.path.join(root, name) + if not path.endswith('manifest.yml'): + continue + + with open(path, 'r') as f: + manifest = yaml.safe_load(f) + check_platform_method(manifest, path) + manifest['dir'] = os.path.dirname(path) + methods.append(manifest) + + check_platform_methods(methods) + return methods + + +def filter_key(manifest, attr, value): + manifest_value = manifest.get(attr, '') + if isinstance(manifest_value, str): + manifest_value = [manifest_value] + return value in manifest_value or 'all' in manifest_value + + +def filter_platform_methods(category, tp, method=None): + methods = platform_automation_methods + if category: + methods = filter(partial(filter_key, attr='category', value=category), methods) + if tp: + methods = filter(partial(filter_key, attr='type', value=tp), methods) + if method: + methods = filter(lambda x: x['method'] == method, methods) + return methods + + +platform_automation_methods = get_platform_automation_methods() + + +if __name__ == '__main__': + print(json.dumps(platform_automation_methods, indent=4)) diff --git a/apps/assets/automations/ping/__init__.py b/apps/assets/automations/ping/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/ping/database/mongodb/main.yml b/apps/assets/automations/ping/database/mongodb/main.yml new file mode 100644 index 000000000..867c51ace --- /dev/null +++ b/apps/assets/automations/ping/database/mongodb/main.yml @@ -0,0 +1,13 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MongoDB connection + mongodb_ping: + 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 }}" diff --git a/apps/assets/automations/ping/database/mongodb/manifest.yml b/apps/assets/automations/ping/database/mongodb/manifest.yml new file mode 100644 index 000000000..45b90eb72 --- /dev/null +++ b/apps/assets/automations/ping/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: mongodb_ping +name: Ping MongoDB +category: database +type: + - mongodb +method: ping diff --git a/apps/assets/automations/ping/database/mysql/main.yml b/apps/assets/automations/ping/database/mysql/main.yml new file mode 100644 index 000000000..ec7ca9432 --- /dev/null +++ b/apps/assets/automations/ping/database/mysql/main.yml @@ -0,0 +1,13 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MySQL connection + community.mysql.mysql_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version diff --git a/apps/assets/automations/ping/database/mysql/manifest.yml b/apps/assets/automations/ping/database/mysql/manifest.yml new file mode 100644 index 000000000..aded00b1f --- /dev/null +++ b/apps/assets/automations/ping/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: mysql_ping +name: Ping MySQL +category: database +type: + - mysql +method: ping diff --git a/apps/assets/automations/ping/database/oracle/main.yml b/apps/assets/automations/ping/database/oracle/main.yml new file mode 100644 index 000000000..fefad7148 --- /dev/null +++ b/apps/assets/automations/ping/database/oracle/main.yml @@ -0,0 +1,14 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test Oracle connection + oracle_ping: + 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 }}" diff --git a/apps/assets/automations/ping/database/oracle/manifest.yml b/apps/assets/automations/ping/database/oracle/manifest.yml new file mode 100644 index 000000000..3912941c3 --- /dev/null +++ b/apps/assets/automations/ping/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: oracle_ping +name: Ping Oracle +category: database +type: + - oracle +method: ping diff --git a/apps/assets/automations/ping/database/postgresql/main.yml b/apps/assets/automations/ping/database/postgresql/main.yml new file mode 100644 index 000000000..e97b3946d --- /dev/null +++ b/apps/assets/automations/ping/database/postgresql/main.yml @@ -0,0 +1,13 @@ +- hosts: postgre + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test PostgreSQL connection + community.postgresql.postgresql_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_db: "{{ jms_asset.specific.db_name }}" diff --git a/apps/assets/automations/ping/database/postgresql/manifest.yml b/apps/assets/automations/ping/database/postgresql/manifest.yml new file mode 100644 index 000000000..337b2b50d --- /dev/null +++ b/apps/assets/automations/ping/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: ping_postgresql +name: Ping PostgreSQL +category: database +type: + - postgresql +method: ping diff --git a/apps/assets/automations/ping/database/sqlserver/main.yml b/apps/assets/automations/ping/database/sqlserver/main.yml new file mode 100644 index 000000000..839a785a5 --- /dev/null +++ b/apps/assets/automations/ping/database/sqlserver/main.yml @@ -0,0 +1,15 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test SQLServer connection + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version diff --git a/apps/assets/automations/ping/database/sqlserver/manifest.yml b/apps/assets/automations/ping/database/sqlserver/manifest.yml new file mode 100644 index 000000000..b5cd6bf88 --- /dev/null +++ b/apps/assets/automations/ping/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: sqlserver_ping +name: Ping SQLServer +category: database +type: + - sqlserver +method: ping diff --git a/apps/assets/automations/ping/demo_inventory.txt b/apps/assets/automations/ping/demo_inventory.txt new file mode 100644 index 000000000..dcc7d1b6d --- /dev/null +++ b/apps/assets/automations/ping/demo_inventory.txt @@ -0,0 +1,2 @@ +# all base inventory in base/base_inventory.txt +asset_name(ip)_account_username account={"username": "", "password": "xxx"} ...base_inventory_vars diff --git a/apps/assets/automations/ping/host/posix/main.yml b/apps/assets/automations/ping/host/posix/main.yml new file mode 100644 index 000000000..8e5f375dd --- /dev/null +++ b/apps/assets/automations/ping/host/posix/main.yml @@ -0,0 +1,5 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Posix ping + ansible.builtin.ping: diff --git a/apps/assets/automations/ping/host/posix/manifest.yml b/apps/assets/automations/ping/host/posix/manifest.yml new file mode 100644 index 000000000..4b9afde37 --- /dev/null +++ b/apps/assets/automations/ping/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: posix_ping +name: Posix ping +category: host +type: + - linux + - unix +method: ping diff --git a/apps/assets/automations/ping/host/windows/main.yml b/apps/assets/automations/ping/host/windows/main.yml new file mode 100644 index 000000000..d5af857a4 --- /dev/null +++ b/apps/assets/automations/ping/host/windows/main.yml @@ -0,0 +1,5 @@ +- hosts: windows + gather_facts: no + tasks: + - name: Windows ping + ansible.builtin.win_ping: diff --git a/apps/assets/automations/ping/host/windows/manifest.yml b/apps/assets/automations/ping/host/windows/manifest.yml new file mode 100644 index 000000000..6218e5978 --- /dev/null +++ b/apps/assets/automations/ping/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: win_ping +name: Windows ping +version: 1 +method: ping +category: host +type: + - windows diff --git a/apps/assets/automations/ping/manager.py b/apps/assets/automations/ping/manager.py new file mode 100644 index 000000000..305771f0b --- /dev/null +++ b/apps/assets/automations/ping/manager.py @@ -0,0 +1,34 @@ +from common.utils import get_logger +from assets.const import AutomationTypes, Connectivity +from ..base.manager import BasePlaybookManager + +logger = get_logger(__name__) + + +class PingManager(BasePlaybookManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_asset_and_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.ping + + def host_callback(self, host, asset=None, account=None, **kwargs): + super().host_callback(host, asset=asset, account=account, **kwargs) + self.host_asset_and_account_mapper[host['name']] = (asset, account) + return host + + def on_host_success(self, host, result): + asset, account = self.host_asset_and_account_mapper.get(host) + asset.set_connectivity(Connectivity.OK) + if not account: + return + account.set_connectivity(Connectivity.OK) + + def on_host_error(self, host, error, result): + asset, account = self.host_asset_and_account_mapper.get(host) + asset.set_connectivity(Connectivity.FAILED) + if not account: + return + account.set_connectivity(Connectivity.FAILED) diff --git a/apps/assets/automations/push_account/__init__.py b/apps/assets/automations/push_account/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/push_account/database/mongodb/main.yml b/apps/assets/automations/push_account/database/mongodb/main.yml new file mode 100644 index 000000000..d516251db --- /dev/null +++ b/apps/assets/automations/push_account/database/mongodb/main.yml @@ -0,0 +1,16 @@ +- 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 new file mode 100644 index 000000000..9de93f5e7 --- /dev/null +++ b/apps/assets/automations/push_account/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000..bf10c95af --- /dev/null +++ b/apps/assets/automations/push_account/database/mysql/main.yml @@ -0,0 +1,15 @@ +- 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 new file mode 100644 index 000000000..d954cb1d8 --- /dev/null +++ b/apps/assets/automations/push_account/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000..5812dfee1 --- /dev/null +++ b/apps/assets/automations/push_account/database/oracle/main.yml @@ -0,0 +1,16 @@ +- 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 new file mode 100644 index 000000000..da1faed6f --- /dev/null +++ b/apps/assets/automations/push_account/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000..febb213c4 --- /dev/null +++ b/apps/assets/automations/push_account/database/postgresql/main.yml @@ -0,0 +1,16 @@ +- 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 new file mode 100644 index 000000000..6488ddd5a --- /dev/null +++ b/apps/assets/automations/push_account/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 000000000..e78c57152 --- /dev/null +++ b/apps/assets/automations/push_account/host/posix/main.yml @@ -0,0 +1,19 @@ +- 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 new file mode 100644 index 000000000..9a7cc5c8c --- /dev/null +++ b/apps/assets/automations/push_account/host/posix/manifest.yml @@ -0,0 +1,7 @@ +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 new file mode 100644 index 000000000..bbe8219d0 --- /dev/null +++ b/apps/assets/automations/push_account/host/windows/main.yml @@ -0,0 +1,13 @@ +- 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 new file mode 100644 index 000000000..7e0256f44 --- /dev/null +++ b/apps/assets/automations/push_account/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: push_account_windows +name: Push account windows +version: 1 +method: push_account +category: host +type: + - windows diff --git a/apps/assets/automations/push_account/manager.py b/apps/assets/automations/push_account/manager.py new file mode 100644 index 000000000..f849f3e6e --- /dev/null +++ b/apps/assets/automations/push_account/manager.py @@ -0,0 +1,17 @@ +from common.utils import get_logger +from assets.const import AutomationTypes +from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin + +logger = get_logger(__name__) + + +class PushAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): + need_privilege_account = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.push_account diff --git a/apps/assets/automations/verify_account/__init__.py b/apps/assets/automations/verify_account/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/verify_account/database/mongodb/main.yml b/apps/assets/automations/verify_account/database/mongodb/main.yml new file mode 100644 index 000000000..1cf79b694 --- /dev/null +++ b/apps/assets/automations/verify_account/database/mongodb/main.yml @@ -0,0 +1,13 @@ +- hosts: mongdb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + mongodb_ping: + 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 }}" diff --git a/apps/assets/automations/verify_account/database/mongodb/manifest.yml b/apps/assets/automations/verify_account/database/mongodb/manifest.yml new file mode 100644 index 000000000..24cc398c2 --- /dev/null +++ b/apps/assets/automations/verify_account/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_mongodb +name: Verify account from MongoDB +category: database +type: + - mongodb +method: verify_account diff --git a/apps/assets/automations/verify_account/database/mysql/main.yml b/apps/assets/automations/verify_account/database/mysql/main.yml new file mode 100644 index 000000000..59c13d98a --- /dev/null +++ b/apps/assets/automations/verify_account/database/mysql/main.yml @@ -0,0 +1,13 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + community.mysql.mysql_info: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + filter: version diff --git a/apps/assets/automations/verify_account/database/mysql/manifest.yml b/apps/assets/automations/verify_account/database/mysql/manifest.yml new file mode 100644 index 000000000..a20b5c7d7 --- /dev/null +++ b/apps/assets/automations/verify_account/database/mysql/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_mysql +name: Verify account from MySQL +category: database +type: + - mysql +method: verify_account diff --git a/apps/assets/automations/verify_account/database/oracle/main.yml b/apps/assets/automations/verify_account/database/oracle/main.yml new file mode 100644 index 000000000..ed4091401 --- /dev/null +++ b/apps/assets/automations/verify_account/database/oracle/main.yml @@ -0,0 +1,14 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + oracle_ping: + 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 }}" diff --git a/apps/assets/automations/verify_account/database/oracle/manifest.yml b/apps/assets/automations/verify_account/database/oracle/manifest.yml new file mode 100644 index 000000000..b58a7f888 --- /dev/null +++ b/apps/assets/automations/verify_account/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_oracle +name: Verify account from Oracle +category: database +type: + - oracle +method: verify_account diff --git a/apps/assets/automations/verify_account/database/postgresql/main.yml b/apps/assets/automations/verify_account/database/postgresql/main.yml new file mode 100644 index 000000000..08db4d869 --- /dev/null +++ b/apps/assets/automations/verify_account/database/postgresql/main.yml @@ -0,0 +1,13 @@ +- hosts: postgresql + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + community.postgresql.postgresql_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + db: "{{ jms_asset.specific.db_name }}" diff --git a/apps/assets/automations/verify_account/database/postgresql/manifest.yml b/apps/assets/automations/verify_account/database/postgresql/manifest.yml new file mode 100644 index 000000000..4c9e2cbec --- /dev/null +++ b/apps/assets/automations/verify_account/database/postgresql/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_postgresql +name: Verify account for PostgreSQL +category: database +type: + - postgresql +method: verify_account diff --git a/apps/assets/automations/verify_account/database/sqlserver/main.yml b/apps/assets/automations/verify_account/database/sqlserver/main.yml new file mode 100644 index 000000000..256803702 --- /dev/null +++ b/apps/assets/automations/verify_account/database/sqlserver/main.yml @@ -0,0 +1,15 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version diff --git a/apps/assets/automations/verify_account/database/sqlserver/manifest.yml b/apps/assets/automations/verify_account/database/sqlserver/manifest.yml new file mode 100644 index 000000000..8af52ab0b --- /dev/null +++ b/apps/assets/automations/verify_account/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_sqlserver +name: Verify account from SQLServer +category: database +type: + - sqlserver +method: verify_account diff --git a/apps/assets/automations/verify_account/host/posix/main.yml b/apps/assets/automations/verify_account/host/posix/main.yml new file mode 100644 index 000000000..41ae1768d --- /dev/null +++ b/apps/assets/automations/verify_account/host/posix/main.yml @@ -0,0 +1,11 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Verify account + ansible.builtin.ping: + become: no + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" + ansible_ssh_private_key_file: "{{ account.private_key_path }}" + ansible_become: no diff --git a/apps/assets/automations/verify_account/host/posix/manifest.yml b/apps/assets/automations/verify_account/host/posix/manifest.yml new file mode 100644 index 000000000..5b9a1e51b --- /dev/null +++ b/apps/assets/automations/verify_account/host/posix/manifest.yml @@ -0,0 +1,7 @@ +id: verify_account_posix +name: Verify posix account +category: host +type: + - linux + - unix +method: verify_account diff --git a/apps/assets/automations/verify_account/host/windows/main.yml b/apps/assets/automations/verify_account/host/windows/main.yml new file mode 100644 index 000000000..da9d40a74 --- /dev/null +++ b/apps/assets/automations/verify_account/host/windows/main.yml @@ -0,0 +1,8 @@ +- hosts: windows + gather_facts: yes + tasks: + - name: Verify account + ansible.windows.win_ping: + vars: + ansible_user: "{{ account.username }}" + ansible_password: "{{ account.secret }}" diff --git a/apps/assets/automations/verify_account/host/windows/manifest.yml b/apps/assets/automations/verify_account/host/windows/manifest.yml new file mode 100644 index 000000000..69faf4217 --- /dev/null +++ b/apps/assets/automations/verify_account/host/windows/manifest.yml @@ -0,0 +1,7 @@ +id: verify_account_windows +name: Verify account windows +version: 1 +method: verify_account +category: host +type: + - windows diff --git a/apps/assets/automations/verify_account/manager.py b/apps/assets/automations/verify_account/manager.py new file mode 100644 index 000000000..f261631e5 --- /dev/null +++ b/apps/assets/automations/verify_account/manager.py @@ -0,0 +1,25 @@ +from common.utils import get_logger +from assets.const import AutomationTypes, Connectivity +from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin + +logger = get_logger(__name__) + + +class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): + need_privilege_account = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.verify_account + + def on_host_success(self, host, result): + account = self.host_account_mapper.get(host) + account.set_connectivity(Connectivity.OK) + + def on_host_error(self, host, error, result): + account = self.host_account_mapper.get(host) + account.set_connectivity(Connectivity.FAILED) diff --git a/apps/assets/const.py b/apps/assets/const.py deleted file mode 100644 index ec51c5a2b..000000000 --- a/apps/assets/const.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -# diff --git a/apps/assets/const/__init__.py b/apps/assets/const/__init__.py new file mode 100644 index 000000000..bc30f388d --- /dev/null +++ b/apps/assets/const/__init__.py @@ -0,0 +1,7 @@ +from .base import * +from .host import * +from .types import * +from .account import * +from .protocol import * +from .category import * +from .automation import * diff --git a/apps/assets/const/account.py b/apps/assets/const/account.py new file mode 100644 index 000000000..ebeb855ed --- /dev/null +++ b/apps/assets/const/account.py @@ -0,0 +1,15 @@ +from django.db.models import TextChoices +from django.utils.translation import ugettext_lazy as _ + + +class Connectivity(TextChoices): + UNKNOWN = 'unknown', _('Unknown') + OK = 'ok', _('Ok') + FAILED = 'failed', _('Failed') + + +class SecretType(TextChoices): + PASSWORD = 'password', _('Password') + SSH_KEY = 'ssh_key', _('SSH key') + ACCESS_KEY = 'access_key', _('Access key') + TOKEN = 'token', _('Token') diff --git a/apps/assets/const/automation.py b/apps/assets/const/automation.py new file mode 100644 index 000000000..99acefa7a --- /dev/null +++ b/apps/assets/const/automation.py @@ -0,0 +1,46 @@ +from django.db.models import TextChoices +from django.utils.translation import ugettext_lazy as _ + +string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' +DEFAULT_PASSWORD_LENGTH = 30 +DEFAULT_PASSWORD_RULES = { + 'length': DEFAULT_PASSWORD_LENGTH, + 'symbol_set': string_punctuation +} + + +class AutomationTypes(TextChoices): + ping = 'ping', _('Ping') + gather_facts = 'gather_facts', _('Gather facts') + push_account = 'push_account', _('Create account') + change_secret = 'change_secret', _('Change secret') + verify_account = 'verify_account', _('Verify account') + gather_accounts = 'gather_accounts', _('Gather accounts') + + @classmethod + def get_type_model(cls, tp): + from assets.models import ( + PingAutomation, GatherFactsAutomation, PushAccountAutomation, + ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation, + ) + type_model_dict = { + cls.ping: PingAutomation, + cls.gather_facts: GatherFactsAutomation, + cls.push_account: PushAccountAutomation, + cls.change_secret: ChangeSecretAutomation, + cls.verify_account: VerifyAccountAutomation, + cls.gather_accounts: GatherAccountsAutomation, + } + return type_model_dict.get(tp) + + +class SecretStrategy(TextChoices): + custom = 'specific', _('Specific') + random_one = 'random_one', _('All assets use the same random password') + random_all = 'random_all', _('All assets use different random password') + + +class SSHKeyStrategy(TextChoices): + add = 'add', _('Append SSH KEY') + set = 'set', _('Empty and append SSH KEY') + set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ') diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py new file mode 100644 index 000000000..35ad76757 --- /dev/null +++ b/apps/assets/const/base.py @@ -0,0 +1,55 @@ +from django.db.models import TextChoices + +from .protocol import Protocol + + +class BaseType(TextChoices): + """ + 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持 + """ + @classmethod + def get_constrains(cls): + constrains = {} + + base = cls._get_base_constrains() + protocols = cls._get_protocol_constrains() + automation = cls._get_automation_constrains() + + base_default = base.pop('*', {}) + protocols_default = protocols.pop('*', {}) + automation_default = automation.pop('*', {}) + + for k, v in cls.choices: + tp_base = {**base_default, **base.get(k, {})} + tp_auto = {**automation_default, **automation.get(k, {})} + tp_protocols = {**protocols_default, **protocols.get(k, {})} + tp_protocols = cls._parse_protocols(tp_protocols, k) + tp_constrains = {**tp_base, 'protocols': tp_protocols, 'automation': tp_auto} + constrains[k] = tp_constrains + return constrains + + @classmethod + def _parse_protocols(cls, protocol, tp): + settings = Protocol.settings() + choices = protocol.get('choices', []) + if choices == '__self__': + choices = [tp] + protocols = [{'name': name, **settings.get(name, {})} for name in choices] + protocols[0]['primary'] = True + return protocols + + @classmethod + def _get_base_constrains(cls) -> dict: + raise NotImplementedError + + @classmethod + def _get_protocol_constrains(cls) -> dict: + raise NotImplementedError + + @classmethod + def _get_automation_constrains(cls) -> dict: + raise NotImplementedError + + @classmethod + def internal_platforms(cls): + raise NotImplementedError diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py new file mode 100644 index 000000000..9e76946f3 --- /dev/null +++ b/apps/assets/const/category.py @@ -0,0 +1,19 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.db.models import ChoicesMixin + + +__all__ = ['Category'] + + +class Category(ChoicesMixin, models.TextChoices): + HOST = 'host', _('Host') + DEVICE = 'device', _("Device") + DATABASE = 'database', _("Database") + CLOUD = 'cloud', _("Cloud service") + WEB = 'web', _("Web") + + + + diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py new file mode 100644 index 000000000..7bc1864f1 --- /dev/null +++ b/apps/assets/const/cloud.py @@ -0,0 +1,51 @@ +from .base import BaseType + + +class CloudTypes(BaseType): + PUBLIC = 'public', 'Public cloud' + PRIVATE = 'private', 'Private cloud' + K8S = 'k8s', 'Kubernetes' + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': False, + 'domain_enabled': False, + 'su_enabled': False, + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'ansible_enabled': False, + 'ansible_config': {}, + 'gather_facts_enabled': False, + 'verify_account_enabled': False, + 'change_secret_enabled': False, + 'push_account_enabled': False, + 'gather_accounts_enabled': False, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['http'], + }, + cls.K8S: { + 'choices': ['k8s'] + } + } + + @classmethod + def internal_platforms(cls): + return { + cls.PUBLIC: [], + cls.PRIVATE: [{'name': 'Vmware-vSphere'}], + cls.K8S: [{'name': 'Kubernetes'}], + } diff --git a/apps/assets/const/database.py b/apps/assets/const/database.py new file mode 100644 index 000000000..47e45dc71 --- /dev/null +++ b/apps/assets/const/database.py @@ -0,0 +1,60 @@ + +from .base import BaseType + + +class DatabaseTypes(BaseType): + MYSQL = 'mysql', 'MySQL' + MARIADB = 'mariadb', 'MariaDB' + POSTGRESQL = 'postgresql', 'PostgreSQL' + ORACLE = 'oracle', 'Oracle' + SQLSERVER = 'sqlserver', 'SQLServer' + MONGODB = 'mongodb', 'MongoDB' + REDIS = 'redis', 'Redis' + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': False, + 'domain_enabled': True, + 'su_enabled': False, + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'ansible_enabled': True, + 'ansible_config': { + 'ansible_connection': 'local', + }, + 'gather_facts_enabled': True, + 'gather_accounts_enabled': True, + 'verify_account_enabled': True, + 'change_secret_enabled': True, + 'push_account_enabled': True, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': '__self__', + } + } + + @classmethod + def internal_platforms(cls): + return { + cls.MYSQL: [{'name': 'MySQL'}], + cls.MARIADB: [{'name': 'MariaDB'}], + cls.POSTGRESQL: [{'name': 'PostgreSQL'}], + cls.ORACLE: [{'name': 'Oracle'}], + cls.SQLSERVER: [{'name': 'SQLServer'}], + cls.MONGODB: [{'name': 'MongoDB'}], + cls.REDIS: [{'name': 'Redis'}], + } + diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py new file mode 100644 index 000000000..1e2a5b717 --- /dev/null +++ b/apps/assets/const/device.py @@ -0,0 +1,54 @@ +from django.utils.translation import gettext_lazy as _ + +from .base import BaseType + + +class DeviceTypes(BaseType): + GENERAL = 'general', _("General") + SWITCH = 'switch', _("Switch") + ROUTER = 'router', _("Router") + FIREWALL = 'firewall', _("Firewall") + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': False, + 'domain_enabled': True, + 'su_enabled': False, + } + } + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['ssh', 'telnet'] + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + return { + '*': { + 'ansible_enabled': True, + 'ansible_config': { + 'ansible_connection': 'local', + }, + 'ping_enabled': True, + 'gather_facts_enabled': False, + 'gather_accounts_enabled': False, + 'verify_account_enabled': False, + 'change_secret_enabled': False, + 'push_account_enabled': False, + } + } + + @classmethod + def internal_platforms(cls): + return { + cls.GENERAL: [{'name': 'General'}, {'name': 'Cisco'}, {'name': 'Huawei'}, {'name': 'H3C'}], + cls.SWITCH: [], + cls.ROUTER: [], + cls.FIREWALL: [] + } diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py new file mode 100644 index 000000000..8be44db6f --- /dev/null +++ b/apps/assets/const/host.py @@ -0,0 +1,108 @@ +from .base import BaseType + +GATEWAY_NAME = 'Gateway' + + +class HostTypes(BaseType): + LINUX = 'linux', 'Linux' + WINDOWS = 'windows', 'Windows' + UNIX = 'unix', 'Unix' + OTHER_HOST = 'other', "Other" + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': True, + 'charset': 'utf-8', # default + 'domain_enabled': True, + 'su_enabled': True, + 'su_methods': [ + {'name': 'sudo su', 'id': 'sudo su'}, + {'name': 'su -', 'id': 'su -'} + ], + }, + cls.WINDOWS: { + 'su_enabled': False, + }, + cls.OTHER_HOST: { + 'su_enabled': False, + } + } + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['ssh', 'telnet', 'vnc', 'rdp'] + }, + cls.WINDOWS: { + 'choices': ['rdp', 'ssh', 'vnc'] + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + return { + '*': { + 'ansible_enabled': True, + 'ansible_config': { + 'ansible_connection': 'smart', + }, + 'ping_enabled': True, + 'gather_facts_enabled': True, + 'gather_accounts_enabled': True, + 'verify_account_enabled': True, + 'change_secret_enabled': True, + 'push_account_enabled': True, + }, + cls.WINDOWS: { + 'ansible_config': { + 'ansible_shell_type': 'cmd', + 'ansible_connection': 'ssh', + }, + }, + } + + @classmethod + def internal_platforms(cls): + return { + cls.LINUX: [ + {'name': 'Linux'}, + {'name': GATEWAY_NAME} + ], + cls.UNIX: [ + {'name': 'Unix'}, + {'name': 'macOS'}, + {'name': 'BSD'}, + {'name': 'AIX', 'automation': { + 'push_account_method': 'push_account_aix', + 'change_secret_method': 'push_secret_aix' + }} + ], + cls.WINDOWS: [ + {'name': 'Windows'}, + { + 'name': 'Windows-TLS', + 'protocols_setting': { + 'rdp': {'security': 'tls'}, + } + }, + { + 'name': 'Windows-RDP', + 'protocols_setting': { + 'rdp': {'security': 'rdp'}, + } + }, + { + 'name': 'RemoteAppHost', + '_protocols': ['rdp', 'ssh'], + 'protocols_setting': { + 'ssh': { + 'required': True + } + } + } + ], + cls.OTHER_HOST: [] + } diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py new file mode 100644 index 000000000..92ca5f3a1 --- /dev/null +++ b/apps/assets/const/protocol.py @@ -0,0 +1,113 @@ +from django.db import models +from common.db.models import ChoicesMixin + +__all__ = ['Protocol'] + + +class Protocol(ChoicesMixin, models.TextChoices): + ssh = 'ssh', 'SSH' + rdp = 'rdp', 'RDP' + telnet = 'telnet', 'Telnet' + vnc = 'vnc', 'VNC' + + mysql = 'mysql', 'MySQL' + mariadb = 'mariadb', 'MariaDB' + oracle = 'oracle', 'Oracle' + postgresql = 'postgresql', 'PostgreSQL' + sqlserver = 'sqlserver', 'SQLServer' + redis = 'redis', 'Redis' + mongodb = 'mongodb', 'MongoDB' + + k8s = 'k8s', 'K8S' + http = 'http', 'HTTP' + _settings = None + + @classmethod + def device_protocols(cls): + return { + cls.ssh: { + 'port': 22, + 'secret_types': ['password', 'ssh_key'], + 'setting': { + 'sftp_enabled': True, + 'sftp_home': '/tmp', + } + }, + cls.rdp: { + 'port': 3389, + 'secret_types': ['password'], + 'setting': { + 'console': True, + 'security': 'any', + } + }, + cls.vnc: { + 'port': 5900, + 'secret_types': ['password'], + }, + cls.telnet: { + 'port': 23, + 'secret_types': ['password'], + }, + } + + @classmethod + def database_protocols(cls): + return { + cls.mysql: { + 'port': 3306, + 'secret_types': ['password'], + 'setting': { + } + }, + cls.mariadb: { + 'port': 3306, + 'secret_types': ['password'], + }, + cls.postgresql: { + 'port': 5432, + 'secret_types': ['password'], + }, + cls.oracle: { + 'port': 1521, + 'secret_types': ['password'], + }, + cls.sqlserver: { + 'port': 1433, + 'secret_types': ['password'], + }, + cls.mongodb: { + 'port': 27017, + 'secret_types': ['password'], + }, + cls.redis: { + 'port': 6379, + 'secret_types': ['password'], + }, + } + + @classmethod + def cloud_protocols(cls): + return { + cls.k8s: { + 'port': 443, + 'secret_types': ['token'], + }, + cls.http: { + 'port': 80, + 'secret_types': ['password'], + 'setting': { + 'username_selector': 'input[type=text]', + 'password_selector': 'input[type=password]', + 'submit_selector': 'button[type=submit]', + } + }, + } + + @classmethod + def settings(cls): + return { + **cls.device_protocols(), + **cls.database_protocols(), + **cls.cloud_protocols() + } diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py new file mode 100644 index 000000000..f9cf83b85 --- /dev/null +++ b/apps/assets/const/types.py @@ -0,0 +1,260 @@ +from copy import deepcopy + +from common.db.models import ChoicesMixin +from common.tree import TreeNode + +from .category import Category +from .host import HostTypes +from .device import DeviceTypes +from .database import DatabaseTypes +from .web import WebTypes +from .cloud import CloudTypes + + +class AllTypes(ChoicesMixin): + choices: list + includes = [ + HostTypes, DeviceTypes, DatabaseTypes, + CloudTypes, WebTypes, + ] + _category_constrains = {} + + @classmethod + def choices(cls): + choices = [] + for tp in cls.includes: + choices.extend(tp.choices) + return choices + + @classmethod + def get_constraints(cls, category, tp): + types_cls = dict(cls.category_types()).get(category) + if not types_cls: + return {} + type_constraints = types_cls.get_constrains() + constraints = type_constraints.get(tp, {}) + cls.set_automation_methods(category, tp, constraints) + return constraints + + @classmethod + def get_primary_protocol_name(cls, category, tp): + constraints = cls.get_constraints(category, tp) + if not constraints: + return None + return constraints.get('protocols')[0]['name'] + + @classmethod + def set_automation_methods(cls, category, tp, constraints): + from assets.automations import filter_platform_methods + automation = constraints.get('automation', {}) + automation_methods = {} + for item, enabled in automation.items(): + if not enabled: + continue + 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.update(automation_methods) + constraints['automation'] = automation + return constraints + + @classmethod + def types(cls, with_constraints=True): + types = [] + for category, tps in cls.category_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(): + category_data = { + 'value': category.value, + 'label': category.label, + 'types': [cls.serialize_type(category, tp, with_constraints) for tp in tps] + } + categories.append(category_data) + return categories + + @classmethod + def serialize_type(cls, category, tp, with_constraints=True): + data = { + 'value': tp.value, + 'label': tp.label, + 'category': category, + } + + if with_constraints: + data['constraints'] = cls.get_constraints(category, tp) + else: + data['constraints'] = [] + return data + + @classmethod + def grouped_choices(cls): + grouped_types = [(str(ca), tp.choices) for ca, tp in cls.category_types()] + return grouped_types + + @classmethod + def grouped_choices_to_objs(cls): + choices = cls.serialize_to_objs(Category.choices) + mapper = dict(cls.grouped_choices()) + for choice in choices: + children = cls.serialize_to_objs(mapper[choice['value']]) + choice['children'] = children + return choices + + @staticmethod + def serialize_to_objs(choices): + title = ['value', 'display_name'] + return [dict(zip(title, choice)) for choice in choices] + + @classmethod + def category_types(cls): + return ( + (Category.HOST, HostTypes), + (Category.DEVICE, DeviceTypes), + (Category.DATABASE, DatabaseTypes), + (Category.WEB, WebTypes), + (Category.CLOUD, CloudTypes) + ) + + @staticmethod + def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None): + node = TreeNode(**{ + 'id': choice.name, + 'name': choice.label, + 'title': choice.label, + 'pId': pid, + 'open': opened, + 'isParent': is_parent, + }) + if meta: + node.meta = meta + return node + + @classmethod + def to_tree_nodes(cls): + root = TreeNode(id='ROOT', name='类型节点', title='类型节点') + nodes = [root] + for category, types in cls.category_types(): + category_node = cls.choice_to_node(category, 'ROOT', meta={'type': 'category'}) + nodes.append(category_node) + for tp in types: + tp_node = cls.choice_to_node(tp, category_node.id, meta={'type': 'type'}) + nodes.append(tp_node) + return nodes + + @classmethod + def get_type_default_platform(cls, category, tp): + constraints = cls.get_constraints(category, tp) + data = { + 'category': category, + 'type': tp, 'internal': True, + 'charset': constraints.get('charset', 'utf-8'), + 'domain_enabled': constraints.get('domain_enabled', False), + 'su_enabled': constraints.get('su_enabled', False), + } + if data['su_enabled'] and data.get('su_methods'): + data['su_method'] = data['su_methods'][0]['id'] + + protocols = constraints.get('protocols', []) + for p in protocols: + p.pop('secret_types', None) + data['protocols'] = protocols + + automation = constraints.get('automation', {}) + + enable_fields = {k: v for k, v in automation.items() if k.endswith('_enabled')} + for k, v in enable_fields.items(): + auto_item = k.replace('_enabled', '') + methods = automation.pop(auto_item + '_methods', []) + if methods: + automation[auto_item + '_method'] = methods[0]['id'] + data['automation'] = automation + return data + + @classmethod + def create_or_update_by_platform_data(cls, name, platform_data): + from assets.models import Platform, PlatformAutomation, PlatformProtocol + + automation_data = platform_data.pop('automation', {}) + protocols_data = platform_data.pop('protocols', []) + + platform, created = Platform.objects.update_or_create( + defaults=platform_data, name=name + ) + if not platform.automation: + automation = PlatformAutomation.objects.create() + platform.automation = automation + platform.save() + else: + automation = platform.automation + for k, v in automation_data.items(): + setattr(automation, k, v) + automation.save() + + platform.protocols.all().delete() + for p in protocols_data: + p.pop('primary', None) + PlatformProtocol.objects.create(**p, platform=platform) + + @classmethod + def create_or_update_internal_platforms(cls): + print("\n\tCreate internal platforms") + for category, type_cls in cls.category_types(): + print("\t## Category: {}".format(category.label)) + data = type_cls.internal_platforms() + + for tp, platform_datas in data.items(): + print("\t >> Type: {}".format(tp.label)) + default_platform_data = cls.get_type_default_platform(category, tp) + default_automation = default_platform_data.pop('automation', {}) + default_protocols = default_platform_data.pop('protocols', []) + + for d in platform_datas: + name = d['name'] + print("\t - Platform: {}".format(name)) + _automation = d.pop('automation', {}) + _protocols = d.pop('_protocols', []) + _protocols_setting = d.pop('protocols_setting', {}) + + protocols_data = deepcopy(default_protocols) + if _protocols: + protocols_data = [p for p in protocols_data if p['name'] in _protocols] + for p in protocols_data: + setting = _protocols_setting.get(p['name'], {}) + p['required'] = setting.pop('required', False) + p['default'] = setting.pop('default', False) + p['setting'] = {**setting, **p.get('setting', {})} + + platform_data = { + **default_platform_data, **d, + 'automation': {**default_automation, **_automation}, + 'protocols': protocols_data + } + cls.create_or_update_by_platform_data(name, platform_data) + + @classmethod + def update_user_create_platforms(cls, platform_cls): + internal_platforms = [] + for category, type_cls in cls.category_types(): + data = type_cls.internal_platforms() + for tp, platform_datas in data.items(): + for d in platform_datas: + internal_platforms.append(d['name']) + + user_platforms = platform_cls.objects.exclude(name__in=internal_platforms) + user_platforms.update(internal=False) + + for platform in user_platforms: + 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 new file mode 100644 index 000000000..20c35b3a1 --- /dev/null +++ b/apps/assets/const/web.py @@ -0,0 +1,46 @@ +from django.utils.translation import gettext_lazy as _ + +from .base import BaseType + + +class WebTypes(BaseType): + WEBSITE = 'website', _('Website') + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': False, + 'domain_enabled': False, + 'su_enabled': False, + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'gather_facts_enabled': False, + 'verify_account_enabled': False, + 'change_secret_enabled': False, + 'push_account_enabled': False, + 'gather_accounts_enabled': False, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['http'], + } + } + + @classmethod + def internal_platforms(cls): + return { + cls.WEBSITE: [ + {'name': 'Website'}, + ], + } diff --git a/apps/assets/filters.py b/apps/assets/filters.py index b807396e0..f1b869805 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- # - -from rest_framework.compat import coreapi, coreschema -from rest_framework import filters from django.db.models import Q +from django_filters import rest_framework as drf_filters +from rest_framework import filters +from rest_framework.compat import coreapi, coreschema -from .models import Label -from assets.utils import is_query_node_all_assets, get_node +from assets.utils import get_node_from_request, is_query_node_all_assets +from common.drf.filters import BaseFilterSet + +from .models import Account, Label, Node class AssetByNodeFilterBackend(filters.BaseFilterBackend): @@ -31,7 +33,7 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend): return queryset.filter(nodes__key=node.key).distinct() def filter_queryset(self, request, queryset, view): - node = get_node(request) + node = get_node_from_request(request) if node is None: return queryset @@ -42,9 +44,9 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend): return self.filter_node_related_direct(queryset, node) -class FilterAssetByNodeFilterBackend(filters.BaseFilterBackend): +class NodeFilterBackend(filters.BaseFilterBackend): """ - 需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用 + 需要与 `assets.api.mixin.NodeFilterMixin` 配合使用 """ fields = ['node', 'all'] @@ -58,16 +60,18 @@ class FilterAssetByNodeFilterBackend(filters.BaseFilterBackend): ] def filter_queryset(self, request, queryset, view): - node = view.node + node = get_node_from_request(request) if node is None: return queryset - query_all = view.is_query_node_all_assets + + query_all = is_query_node_all_assets(request) if query_all: return queryset.filter( Q(nodes__key__istartswith=f'{node.key}:') | Q(nodes__key=node.key) ).distinct() else: + print("Query query origin: ", queryset.count()) return queryset.filter(nodes__key=node.key).distinct() @@ -93,6 +97,9 @@ class LabelFilterBackend(filters.BaseFilterBackend): for kv in labels_query: if '#' in kv: self.sep = '#' + break + + for kv in labels_query: if self.sep not in kv: continue key, value = kv.strip().split(self.sep)[:2] @@ -104,7 +111,7 @@ class LabelFilterBackend(filters.BaseFilterBackend): q = Q(name=key, value=value) if not q: return [] - labels = Label.objects.filter(q, is_active=True)\ + labels = Label.objects.filter(q, is_active=True) \ .values_list('id', flat=True) return labels @@ -136,7 +143,7 @@ class IpInFilterBackend(filters.BaseFilterBackend): if not ips: return queryset ip_list = [i.strip() for i in ips.split(',')] - queryset = queryset.filter(ip__in=ip_list) + queryset = queryset.filter(address__in=ip_list) return queryset def get_schema_fields(self, view): @@ -145,7 +152,43 @@ class IpInFilterBackend(filters.BaseFilterBackend): name='ips', location='query', required=False, type='string', schema=coreschema.String( title='ips', - description='ip in filter' + description='address in filter' ) ) ] + + +class AccountFilterSet(BaseFilterSet): + ip = drf_filters.CharFilter(field_name='address', lookup_expr='exact') + hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact') + username = drf_filters.CharFilter(field_name="username", lookup_expr='exact') + address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact') + asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact') + assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') + nodes = drf_filters.CharFilter(method='filter_nodes') + has_secret = drf_filters.BooleanFilter(method='filter_has_secret') + + @staticmethod + def filter_has_secret(queryset, name, has_secret): + q = Q(secret__isnull=True) | Q(secret='') + if has_secret: + return queryset.exclude(q) + else: + return queryset.filter(q) + + @staticmethod + def filter_nodes(queryset, name, value): + nodes = Node.objects.filter(id=value) + if not nodes: + return queryset + + node_qs = Node.objects.none() + for node in nodes: + node_qs |= node.get_all_children(with_self=True) + node_ids = list(node_qs.values_list('id', flat=True)) + queryset = queryset.filter(asset__nodes__in=node_ids) + return queryset + + class Meta: + model = Account + fields = ['id'] diff --git a/apps/assets/migrations/0001_initial.py b/apps/assets/migrations/0001_initial.py index 7c0a9e95a..03c945c57 100644 --- a/apps/assets/migrations/0001_initial.py +++ b/apps/assets/migrations/0001_initial.py @@ -16,14 +16,6 @@ def add_default_group(apps, schema_editor): ) -def add_default_cluster(apps, schema_editor): - cluster_model = apps.get_model("assets", "Cluster") - db_alias = schema_editor.connection.alias - cluster_model.objects.using(db_alias).create( - name="Default" - ) - - class Migration(migrations.Migration): initial = True @@ -163,6 +155,5 @@ class Migration(migrations.Migration): unique_together=set([('ip', 'port')]), ), - migrations.RunPython(add_default_cluster), migrations.RunPython(add_default_group), ] diff --git a/apps/assets/migrations/0003_auto_20180109_2331.py b/apps/assets/migrations/0003_auto_20180109_2331.py index 254de6236..097bc607a 100644 --- a/apps/assets/migrations/0003_auto_20180109_2331.py +++ b/apps/assets/migrations/0003_auto_20180109_2331.py @@ -14,9 +14,4 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='asset', - name='cluster', - field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'), - ), ] diff --git a/apps/assets/migrations/0007_auto_20180225_1815.py b/apps/assets/migrations/0007_auto_20180225_1815.py index 009381bcb..4ce2b1e05 100644 --- a/apps/assets/migrations/0007_auto_20180225_1815.py +++ b/apps/assets/migrations/0007_auto_20180225_1815.py @@ -50,7 +50,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='asset', name='nodes', - field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), + field=models.ManyToManyField(default=assets.models.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), ), migrations.AddField( model_name='systemuser', diff --git a/apps/assets/migrations/0045_auto_20191206_1607.py b/apps/assets/migrations/0045_auto_20191206_1607.py index f51839289..a2a136c3b 100644 --- a/apps/assets/migrations/0045_auto_20191206_1607.py +++ b/apps/assets/migrations/0045_auto_20191206_1607.py @@ -34,7 +34,7 @@ class Migration(migrations.Migration): model_name='asset', name='platform', field=models.ForeignKey( - default=assets.models.asset.Platform.default, + default='', on_delete=django.db.models.deletion.PROTECT, related_name='assets', to='assets.Platform', verbose_name='Platform'), diff --git a/apps/assets/migrations/0060_node_full_value.py b/apps/assets/migrations/0060_node_full_value.py index bf3afbbc0..0f633deac 100644 --- a/apps/assets/migrations/0060_node_full_value.py +++ b/apps/assets/migrations/0060_node_full_value.py @@ -19,10 +19,10 @@ def migrate_nodes_value_with_slash(apps, schema_editor): db_alias = schema_editor.connection.alias nodes = model.objects.using(db_alias).filter(value__contains='/') print('') - print("- Start migrate node value if has /") + print("\t- Start migrate node value if has /") for i, node in enumerate(list(nodes)): new_value = node.value.replace('/', '|') - print("{} start migrate node value: {} => {}".format(i, node.value, new_value)) + print("\t - {} start migrate node value: {} => {}".format(i, node.value, new_value)) node.value = new_value node.save() @@ -31,9 +31,9 @@ def migrate_nodes_full_value(apps, schema_editor): model = apps.get_model("assets", "Node") db_alias = schema_editor.connection.alias nodes = model.objects.using(db_alias).all() - print("- Start migrate node full value") + print("\n\t- Start migrate node full value") for i, node in enumerate(list(nodes)): - print("{} start migrate {} node full value".format(i, node.value)) + print("\t - {} start migrate {} node full value".format(i, node.value)) ancestor_keys = get_node_ancestor_keys(node.key, True) values = model.objects.filter(key__in=ancestor_keys).values_list('key', 'value') values = [v for k, v in sorted(values, key=lambda x: len(x[0]))] diff --git a/apps/assets/migrations/0092_add_host.py b/apps/assets/migrations/0092_add_host.py new file mode 100644 index 000000000..83a63056f --- /dev/null +++ b/apps/assets/migrations/0092_add_host.py @@ -0,0 +1,106 @@ +# Generated by Django 3.1.14 on 2022-03-30 10:35 + +import uuid +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0091_auto_20220629_1826'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='info', + field=models.JSONField(blank=True, default=dict, verbose_name='Info'), + ), + migrations.RenameField( + model_name='asset', + old_name='hostname', + new_name='name', + ), + migrations.AlterField( + model_name='asset', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['name'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, + ), + migrations.RenameField( + model_name='asset', + old_name='ip', + new_name='address', + ), + migrations.AddField( + model_name='asset', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='asset', + name='updated_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='asset', + name='created_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), + ), + migrations.CreateModel( + name='Host', + fields=[ + ('asset_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), + ], + ), + migrations.CreateModel( + name='Database', + fields=[ + ('asset_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), + ('db_name', models.CharField(blank=True, max_length=1024, verbose_name='Database')), + ], + options={ + 'verbose_name': 'Database', + }, + bases=('assets.asset',), + ), + migrations.CreateModel( + name='Device', + fields=[ + ('asset_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), + ], + options={ + 'abstract': False, + }, + bases=('assets.asset',), + ), + migrations.CreateModel( + name='Cloud', + fields=[ + ('asset_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), + ], + options={ + 'abstract': False, + }, + bases=('assets.asset',), + ), + migrations.CreateModel( + name='Web', + fields=[ + ('asset_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), + ('autofill', models.CharField(choices=[('no', 'Disabled'), ('basic', 'Basic'), ('script', 'Script')], default='basic', max_length=16, verbose_name='Autofill')), + ('password_selector', models.CharField(blank=True, default='', max_length=128, verbose_name='Password selector')), + ('submit_selector', models.CharField(blank=True, default='', max_length=128, verbose_name='Submit selector')), + ('username_selector', models.CharField(blank=True, default='', max_length=128, verbose_name='Username selector')), + ('script', models.JSONField(blank=True, default=list, verbose_name='Script')), + ], + options={ + 'abstract': False, + }, + bases=('assets.asset',), + ), + ] diff --git a/apps/assets/migrations/0093_auto_20220403_1627.py b/apps/assets/migrations/0093_auto_20220403_1627.py new file mode 100644 index 000000000..0fe74ed50 --- /dev/null +++ b/apps/assets/migrations/0093_auto_20220403_1627.py @@ -0,0 +1,60 @@ +# Generated by Django 3.1.14 on 2022-04-02 08:27 + +from django.utils import timezone +from django.db import migrations, models + + +def migrate_to_host(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + host_model = apps.get_model("assets", 'Host') + db_alias = schema_editor.connection.alias + + count = 0 + batch_size = 1000 + + while True: + assets = asset_model.objects.using(db_alias).all()[count:count+batch_size] + if not assets: + break + count += len(assets) + hosts = [host_model(asset_ptr=asset) for asset in assets] + host_model.objects.using(db_alias).bulk_create(hosts, ignore_conflicts=True) + + +def migrate_hardware_info(apps, *args): + asset_model = apps.get_model("assets", "Asset") + + count = 0 + batch_size = 1000 + hardware_fields = [ + 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'cpu_cores', + 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_arch', + 'os_version', 'hostname_raw', 'number' + ] + + while True: + assets = asset_model.objects.all()[count:count+batch_size] + if not assets: + break + count += len(assets) + + updated = [] + for asset in assets: + info = {field: getattr(asset, field) for field in hardware_fields if getattr(asset, field)} + if not info: + continue + asset.info = info + updated.append(asset) + asset_model.objects.bulk_update(updated, ['info']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0092_add_host'), + ] + + operations = [ + migrations.RunPython(migrate_hardware_info), + migrations.RunPython(migrate_to_host), + ] diff --git a/apps/assets/migrations/0094_auto_20220402_1736.py b/apps/assets/migrations/0094_auto_20220402_1736.py new file mode 100644 index 000000000..daf53270b --- /dev/null +++ b/apps/assets/migrations/0094_auto_20220402_1736.py @@ -0,0 +1,69 @@ +# Generated by Django 3.1.14 on 2022-04-02 09:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0093_auto_20220403_1627'), + ] + + operations = [ + migrations.RemoveField( + model_name='asset', + name='cpu_cores', + ), + migrations.RemoveField( + model_name='asset', + name='cpu_count', + ), + migrations.RemoveField( + model_name='asset', + name='cpu_model', + ), + migrations.RemoveField( + model_name='asset', + name='cpu_vcpus', + ), + migrations.RemoveField( + model_name='asset', + name='disk_info', + ), + migrations.RemoveField( + model_name='asset', + name='disk_total', + ), + migrations.RemoveField( + model_name='asset', + name='hostname_raw', + ), + migrations.RemoveField( + model_name='asset', + name='memory', + ), + migrations.RemoveField( + model_name='asset', + name='model', + ), + migrations.RemoveField( + model_name='asset', + name='os', + ), + migrations.RemoveField( + model_name='asset', + name='os_arch', + ), + migrations.RemoveField( + model_name='asset', + name='os_version', + ), + migrations.RemoveField( + model_name='asset', + name='sn', + ), + migrations.RemoveField( + model_name='asset', + name='vendor', + ), + ] diff --git a/apps/assets/migrations/0095_auto_20220407_1726.py b/apps/assets/migrations/0095_auto_20220407_1726.py new file mode 100644 index 000000000..c11517a35 --- /dev/null +++ b/apps/assets/migrations/0095_auto_20220407_1726.py @@ -0,0 +1,57 @@ +# Generated by Django 3.1.14 on 2022-04-07 09:26 + +from django.db import migrations, models + + +def migrate_platform_type_to_lower(apps, *args): + platform_model = apps.get_model('assets', 'Platform') + platforms = platform_model.objects.all() + for p in platforms: + p.type = p.type.lower() + p.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0094_auto_20220402_1736'), + ] + + operations = [ + migrations.RenameField( + model_name='platform', + old_name='base', + new_name='type', + ), + migrations.AddField( + model_name='platform', + name='category', + field=models.CharField(default='host', max_length=32, verbose_name='Category'), + ), + migrations.AlterField( + model_name='platform', + name='type', + field=models.CharField(default='linux', max_length=32, verbose_name='Type'), + ), + migrations.AddField( + model_name='platform', + name='domain_enabled', + field=models.BooleanField(default=True, verbose_name='Domain enabled'), + ), + migrations.AddField( + model_name='platform', + name='protocols_enabled', + field=models.BooleanField(default=True, verbose_name='Protocols enabled'), + ), + migrations.AddField( + model_name='platform', + name='su_enabled', + field=models.BooleanField(default=False, verbose_name='Su enabled'), + ), + migrations.AddField( + model_name='platform', + 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 new file mode 100644 index 000000000..8d7e6be27 --- /dev/null +++ b/apps/assets/migrations/0096_auto_20220426_1550.py @@ -0,0 +1,51 @@ +# 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'), + ] + + operations = [ + migrations.CreateModel( + name='PlatformProtocol', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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'),), + ('default', models.BooleanField(default=True, verbose_name='Default')), + ('required', models.BooleanField(default=False, verbose_name='Required')), + ], + ), + migrations.CreateModel( + name='PlatformAutomation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ansible_enabled', models.BooleanField(default=False, verbose_name='Enabled')), + ('ansible_config', models.JSONField(default=dict, verbose_name='Ansible config')), + ('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')), + ('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')), + ('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')), + ('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')), + ], + ), + 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'), + ), + ] diff --git a/apps/assets/migrations/0097_auto_20220426_1558.py b/apps/assets/migrations/0097_auto_20220426_1558.py new file mode 100644 index 000000000..f563d50d7 --- /dev/null +++ b/apps/assets/migrations/0097_auto_20220426_1558.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.14 on 2022-04-26 07:58 + +from django.db import migrations +from assets.const import AllTypes + + +def create_internal_platforms(apps, *args): + AllTypes.create_or_update_internal_platforms() + + +def update_user_platforms(apps, *args): + platform_cls = apps.get_model('assets', 'Platform') + AllTypes.update_user_create_platforms(platform_cls) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0096_auto_20220426_1550'), + ] + + operations = [ + migrations.RunPython(create_internal_platforms), + migrations.RunPython(update_user_platforms), + ] diff --git a/apps/assets/migrations/0098_auto_20220430_2126.py b/apps/assets/migrations/0098_auto_20220430_2126.py new file mode 100644 index 000000000..d3eab6855 --- /dev/null +++ b/apps/assets/migrations/0098_auto_20220430_2126.py @@ -0,0 +1,138 @@ +# Generated by Django 3.1.14 on 2022-04-26 07:58 +import uuid + +from django.db import migrations + +failed_apps = [] + + +def get_prop_name_id(apps, app, category): + asset_model = apps.get_model('assets', 'Asset') + _id = app.id + id_exists = asset_model.objects.filter(id=_id).exists() + if id_exists: + _id = uuid.uuid4() + name = app.name + name_exists = asset_model.objects.filter(name=name).exists() + if name_exists: + name = category + '-' + app.name + return _id, name + + +def migrate_database_to_asset(apps, *args): + app_model = apps.get_model('applications', 'Application') + db_model = apps.get_model('assets', 'Database') + platform_model = apps.get_model('assets', 'Platform') + + applications = app_model.objects.filter(category='db') + platforms = platform_model.objects.all().filter(internal=True) + platforms_map = {p.type: p for p in platforms} + print() + + for app in applications: + attrs = {'host': '', 'port': 0, 'database': ''} + _attrs = app.attrs or {} + attrs.update(_attrs) + + db = db_model( + id=app.id, name=app.name, address=attrs['host'], + protocols='{}/{}'.format(app.type, attrs['port']), + db_name=attrs['database'] or '', + platform=platforms_map[app.type], + org_id=app.org_id + ) + try: + print("\t- Create database: ", app.name) + db.save() + except: + failed_apps.append(app) + pass + + +def migrate_cloud_to_asset(apps, *args): + app_model = apps.get_model('applications', 'Application') + cloud_model = apps.get_model('assets', 'Cloud') + platform_model = apps.get_model('assets', 'Platform') + + applications = app_model.objects.filter(category='cloud') + platform = platform_model.objects.filter(type='k8s').first() + print() + + for app in applications: + attrs = app.attrs + print("\t- Create cloud: {}".format(app.name)) + cloud = cloud_model( + id=app.id, name=app.name, + address=attrs.get('cluster', ''), + protocols='', platform=platform, + org_id=app.org_id, + ) + + try: + cloud.save() + except Exception as e: + failed_apps.append(cloud) + print("Error: ", e) + + +def create_app_nodes(apps, org_id): + node_model = apps.get_model('assets', 'Node') + + child_pattern = r'^[0-9]+:[0-9]+$' + node_keys = node_model.objects.filter(org_id=org_id) \ + .filter(key__regex=child_pattern) \ + .values_list('key', flat=True) + if not node_keys: + return + node_key_split = [key.split(':') for key in node_keys] + next_value = max([int(k[1]) for k in node_key_split]) + 1 + parent_key = node_key_split[0][0] + next_key = '{}:{}'.format(parent_key, next_value) + name = 'Apps' + parent = node_model.objects.get(key=parent_key) + full_value = parent.full_value + '/' + name + defaults = { + 'key': next_key, 'value': name, 'parent_key': parent_key, + 'full_value': full_value, 'org_id': org_id + } + node, created = node_model.objects.get_or_create( + defaults=defaults, value=name, org_id=org_id, + ) + node.parent = parent + return node + + +def migrate_to_nodes(apps, *args): + org_model = apps.get_model('orgs', 'Organization') + asset_model = apps.get_model('assets', 'Asset') + orgs = org_model.objects.all() + + # Todo: 优化一些 + for org in orgs: + node = create_app_nodes(apps, org.id) + assets = asset_model.objects.filter( + platform__category__in=['remote_app', 'database', 'cloud'], + org_id=org.id + ) + if not node: + continue + print("\t- Set node asset: ", node) + node.assets_amount = len(assets) + node.save() + node.assets.set(assets) + parent = node.parent + parent.assets_amount += len(assets) + parent.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0097_auto_20220426_1558'), + ('applications', '0020_auto_20220316_2028') + ] + + operations = [ + migrations.RunPython(migrate_database_to_asset), + migrations.RunPython(migrate_cloud_to_asset), + migrations.RunPython(migrate_to_nodes) + ] diff --git a/apps/assets/migrations/0099_auto_20220711_1409.py b/apps/assets/migrations/0099_auto_20220711_1409.py new file mode 100644 index 000000000..01ad1cf1f --- /dev/null +++ b/apps/assets/migrations/0099_auto_20220711_1409.py @@ -0,0 +1,90 @@ +# 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 django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0098_auto_20220430_2126'), + ] + + operations = [ + migrations.CreateModel( + 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', 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)), + ], + options={ + 'verbose_name': 'historical Account', + 'verbose_name_plural': 'historical Accounts', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='Account', + fields=[ + ('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', 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')), + ('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')), + ], + 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')], + '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'), + ), + migrations.CreateModel( + name='AccountTemplate', + fields=[ + ('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', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('comment', models.TextField(blank=True, 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')), + ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), + ], + options={ + 'verbose_name': 'Account template', + 'unique_together': {('name', 'org_id')}, + }, + ), + ] diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py new file mode 100644 index 000000000..f45ff27da --- /dev/null +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -0,0 +1,87 @@ +# Generated by Django 3.2.12 on 2022-07-11 06:13 + +import time +from django.db import migrations +from assets.models import Platform + + +def migrate_accounts(apps, schema_editor): + auth_book_model = apps.get_model('assets', 'AuthBook') + account_model = apps.get_model('assets', 'Account') + + count = 0 + bulk_size = 1000 + print("\n\tStart migrate accounts") + while True: + start = time.time() + auth_books = auth_book_model.objects \ + .prefetch_related('systemuser') \ + .all()[count:count+bulk_size] + if not auth_books: + break + + count += len(auth_books) + accounts = [] + # auth book 和 account 相同的属性 + same_attrs = [ + 'id', 'username', 'comment', 'date_created', 'date_updated', + 'created_by', 'asset_id', 'org_id', + ] + # 认证的属性,可能是 authbook 的,可能是 systemuser 的 + auth_attrs = ['password', 'private_key', 'token'] + all_attrs = same_attrs + auth_attrs + + for auth_book in auth_books: + values = {'version': 1} + + system_user = auth_book.systemuser + if system_user: + # 更新一次系统用户的认证属性 + values.update({attr: getattr(system_user, attr, '') for attr in all_attrs}) + values['created_by'] = str(system_user.id) + values['privileged'] = system_user.type == 'admin' + + auth_book_auth = {attr: getattr(auth_book, attr, '') for attr in all_attrs if getattr(auth_book, attr, '')} + # 最终使用 authbook 的认证属性 + values.update(auth_book_auth) + + auth_infos = [] + username = values['username'] + for attr in auth_attrs: + secret = values.pop(attr, None) + if not secret: + continue + + if attr == 'private_key': + secret_type = 'ssh_key' + name = f'{username}(ssh key)' + elif attr == 'token': + secret_type = 'token' + name = f'{username}(token)' + else: + secret_type = attr + name = username + auth_infos.append((name, secret_type, secret)) + + if not auth_infos: + auth_infos.append((username, 'password', '')) + + for name, secret_type, secret in auth_infos: + account = account_model(**values, name=name, secret=secret, secret_type=secret_type) + accounts.append(account) + + account_model.objects.bulk_create(accounts, ignore_conflicts=True) + print("\t - Create accounts: {}-{} using: {:.2f}s".format( + count - len(auth_books), count, time.time()-start + )) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0099_auto_20220711_1409'), + ] + + operations = [ + migrations.RunPython(migrate_accounts), + ] diff --git a/apps/assets/migrations/0101_auto_20220803_1448.py b/apps/assets/migrations/0101_auto_20220803_1448.py new file mode 100644 index 000000000..9a07c9593 --- /dev/null +++ b/apps/assets/migrations/0101_auto_20220803_1448.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.14 on 2022-08-03 10:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0100_auto_20220711_1413'), + ] + + operations = [ + migrations.RenameField( + model_name='asset', + old_name='protocols', + new_name='_protocols', + ), + migrations.CreateModel( + name='Protocol', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='Name')), + ('port', models.IntegerField(verbose_name='Port')), + ('asset', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.asset', verbose_name='Asset')), + ], + ), + ] diff --git a/apps/assets/migrations/0102_auto_20220803_1859.py b/apps/assets/migrations/0102_auto_20220803_1859.py new file mode 100644 index 000000000..afaa63d41 --- /dev/null +++ b/apps/assets/migrations/0102_auto_20220803_1859.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.14 on 2022-08-03 10:59 +import time +from django.db import migrations + + +def migrate_asset_protocols(apps, schema_editor): + asset_model = apps.get_model('assets', 'Asset') + protocol_model = apps.get_model('assets', 'Protocol') + + count = 0 + bulk_size = 1000 + print("\n\tStart migrate asset protocols") + while True: + start = time.time() + assets = asset_model.objects.all()[count:count+bulk_size] + if not assets: + break + count += len(assets) + assets_protocols = [] + + for asset in assets: + old_protocols = asset._protocols or '{}/{}'.format(asset.protocol, asset.port) or 'ssh/22' + + if ',' in old_protocols: + _protocols = old_protocols.split(',') + else: + _protocols = old_protocols.split() + + for name_port in _protocols: + name_port_list = name_port.split('/') + if len(name_port_list) != 2: + continue + + name, port = name_port_list + protocol = protocol_model(**{'name': name, 'port': port, 'asset': asset}) + assets_protocols.append(protocol) + + protocol_model.objects.bulk_create(assets_protocols, ignore_conflicts=True) + print("\t - Create asset protocols: {}-{} using: {:.2f}s".format( + count - len(assets), count, time.time()-start + )) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0101_auto_20220803_1448'), + ] + + operations = [ + migrations.RunPython(migrate_asset_protocols) + ] diff --git a/apps/assets/migrations/0103_auto_20220811_1511.py b/apps/assets/migrations/0103_auto_20220811_1511.py new file mode 100644 index 000000000..b10455d2f --- /dev/null +++ b/apps/assets/migrations/0103_auto_20220811_1511.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.14 on 2022-08-11 07:11 +import assets.models.platform +import django.db.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0102_auto_20220803_1859'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='platform', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assets', to='assets.platform', verbose_name='Platform'), + ), + migrations.RemoveField( + model_name='asset', + name='admin_user', + ), + migrations.RemoveField( + model_name='asset', + name='port', + ), + migrations.RemoveField( + model_name='asset', + name='protocol', + ), + migrations.RemoveField( + model_name='asset', + name='_protocols', + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(default='ssh', max_length=16, verbose_name='Protocol'), + ), + migrations.RemoveField( + model_name='asset', + name='number', + ), + migrations.RemoveField( + model_name='asset', + name='public_ip', + ), + ] diff --git a/apps/assets/migrations/0104_auto_20220816_1022.py b/apps/assets/migrations/0104_auto_20220816_1022.py new file mode 100644 index 000000000..111e4a479 --- /dev/null +++ b/apps/assets/migrations/0104_auto_20220816_1022.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.14 on 2022-08-16 02:22 +import time +from django.db import migrations, models +from django.db.models import Count + + +def migrate_command_filter_to_assets(apps, schema_editor): + command_filter_model = apps.get_model('assets', 'CommandFilter') + + count = 0 + bulk_size = 1000 + print("\n\tStart migrate command filters to assets") + while True: + start = time.time() + command_filters = command_filter_model.objects.all() \ + .prefetch_related('system_users')[count:count + bulk_size] + if not command_filters: + break + count += len(command_filters) + updated = [] + for command_filter in command_filters: + command_filter.accounts = [s.username for s in command_filter.system_users.all()] + updated.append(command_filter) + command_filter_model.objects.bulk_update(updated, ['accounts']) + + print("\tCreate assets: {}-{} using: {:.2f}s".format( + count - len(command_filters), count, time.time() - start + )) + + +def migrate_command_filter_apps(apps, schema_editor): + command_filter_model = apps.get_model('assets', 'CommandFilter') + command_filters = command_filter_model.objects \ + .annotate(app_count=Count('applications')) \ + .filter(app_count__gt=0) + + for command_filter in command_filters: + app_ids = command_filter.applications.all().values_list('id', flat=True) + + try: + command_filter.assets.add(*app_ids) + except: + print("Migrate command filter apps failed: {}, skip".format(command_filter.id)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0103_auto_20220811_1511'), + ] + + operations = [ + migrations.AddField( + model_name='commandfilter', + name='accounts', + field=models.JSONField(default=list, verbose_name='Accounts'), + ), + migrations.RunPython(migrate_command_filter_to_assets), + migrations.RemoveField( + model_name='commandfilter', + name='system_users', + ), + migrations.RunPython(migrate_command_filter_apps), + migrations.RemoveField( + model_name='commandfilter', + name='applications', + ), + ] diff --git a/apps/assets/migrations/0105_auto_20220817_1544.py b/apps/assets/migrations/0105_auto_20220817_1544.py new file mode 100644 index 000000000..40084ffdd --- /dev/null +++ b/apps/assets/migrations/0105_auto_20220817_1544.py @@ -0,0 +1,65 @@ +# Generated by Django 3.2.14 on 2022-08-17 07:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0104_auto_20220816_1022'), + ] + + operations = [ + migrations.RemoveField( + model_name='historicalauthbook', + name='asset', + ), + migrations.RemoveField( + model_name='historicalauthbook', + name='history_user', + ), + migrations.RemoveField( + model_name='historicalauthbook', + name='systemuser', + ), + migrations.RemoveField( + model_name='systemuser', + name='assets', + ), + migrations.RemoveField( + model_name='systemuser', + name='groups', + ), + migrations.RemoveField( + model_name='systemuser', + name='nodes', + ), + migrations.RemoveField( + model_name='systemuser', + name='users', + ), + migrations.AlterUniqueTogether( + name='authbook', + unique_together=None, + ), + migrations.RemoveField( + model_name='authbook', + name='asset', + ), + migrations.RemoveField( + model_name='authbook', + name='systemuser', + ), + migrations.DeleteModel( + name='Cluster', + ), + migrations.DeleteModel( + name='AdminUser', + ), + migrations.DeleteModel( + name='HistoricalAuthBook', + ), + migrations.DeleteModel( + name='AuthBook', + ), + ] diff --git a/apps/assets/migrations/0106_auto_20220916_1556.py b/apps/assets/migrations/0106_auto_20220916_1556.py new file mode 100644 index 000000000..70db1a2f8 --- /dev/null +++ b/apps/assets/migrations/0106_auto_20220916_1556.py @@ -0,0 +1,71 @@ +# Generated by Django 3.2.13 on 2022-09-16 07:56 +from functools import reduce +from django.db import migrations, models +from assets.const import AllTypes, HostTypes + + +def migrate_backup_types(apps, schema_editor): + all_types = list(reduce( + lambda x, y: x + y, + [ + [j['value'] for j in i['children']] + for i in AllTypes.grouped_choices_to_objs() + ] + )) + asset_types = [i[0] for i in HostTypes.choices] + app_types = list(set(all_types) - set(asset_types)) + + backup_model = apps.get_model("assets", "AccountBackupPlan") + backup_objs = [] + for instance in backup_model.objects.all(): + types = instance.types + if types == 1: + instance.categories = asset_types + elif types == 2: + instance.categories = app_types + elif types == 255: + instance.categories = all_types + else: + instance.categories = [] + backup_objs.append(instance) + backup_model.objects.bulk_update(backup_objs, ['categories']) + + backup_execution_model = apps.get_model("assets", "AccountBackupPlanExecution") + backup_execution_objs = [] + for instance in backup_execution_model.objects.all(): + types = instance.plan_snapshot.get('types', []) + if 'all' in types: + instance.plan_snapshot['categories'] = all_types + elif 'asset' in types: + instance.plan_snapshot['categories'] = asset_types + elif 'application' in types: + instance.plan_snapshot['categories'] = app_types + else: + instance.categories = [] + instance.plan_snapshot.pop('types', None) + backup_execution_objs.append(instance) + backup_execution_model.objects.bulk_update(backup_execution_objs, ['plan_snapshot']) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0105_auto_20220817_1544'), + ] + + operations = [ + migrations.AlterField( + model_name='accountbackupplan', + name='types', + field=models.BigIntegerField(), + ), + migrations.AddField( + model_name='accountbackupplan', + name='categories', + field=models.JSONField(default=list), + ), + migrations.RunPython(migrate_backup_types), + migrations.RemoveField( + model_name='accountbackupplan', + name='types', + ), + ] diff --git a/apps/assets/migrations/0107_auto_20221019_1115.py b/apps/assets/migrations/0107_auto_20221019_1115.py new file mode 100644 index 000000000..384178376 --- /dev/null +++ b/apps/assets/migrations/0107_auto_20221019_1115.py @@ -0,0 +1,170 @@ +# Generated by Django 3.2.14 on 2022-10-19 03:15 + +import common.db.fields +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'), + ] + + operations = [ + migrations.CreateModel( + name='AutomationExecution', + fields=[ + ('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)), + ('status', models.CharField(default='pending', max_length=16)), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, verbose_name='Automation snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')), + ], + options={ + 'verbose_name': 'Automation task execution', + }, + ), + migrations.CreateModel( + name='BaseAutomation', + fields=[ + ('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')), + ('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')), + ('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')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('accounts', models.JSONField(default=list, verbose_name='Accounts')), + ('type', models.CharField(max_length=16, verbose_name='Type')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')), + ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')), + ], + options={ + 'verbose_name': 'Automation task', + 'unique_together': {('org_id', 'name')}, + }, + ), + migrations.AddField( + model_name='label', + name='created_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), + ), + migrations.AddField( + model_name='label', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='label', + 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=[ + ('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': 'Gather asset facts', + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='PushAccountAutomation', + 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': 'Push asset account', + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='VerifyAccountAutomation', + 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': 'Verify asset account', + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='ChangeSecretRecord', + 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(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)), + ('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')), + ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), + ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), + ('status', models.CharField(default='pending', max_length=16)), + ('error', models.TextField(blank=True, null=True, verbose_name='Error')), + ('account', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.account')), + ('execution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.automationexecution')), + ], + options={ + 'verbose_name': 'Change secret record', + }, + ), + migrations.AddField( + model_name='automationexecution', + name='automation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation task'), + ), + migrations.CreateModel( + name='ChangeSecretAutomation', + 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')), + ('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_strategy', 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')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), + ('ssh_key_change_strategy', 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')), + ('recipients', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Change secret automation', + }, + bases=('assets.baseautomation',), + ), + ] diff --git a/apps/assets/migrations/0108_auto_20221027_1053.py b/apps/assets/migrations/0108_auto_20221027_1053.py new file mode 100644 index 000000000..0aa1b5c7a --- /dev/null +++ b/apps/assets/migrations/0108_auto_20221027_1053.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.14 on 2022-10-27 02:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0107_auto_20221019_1115'), + ] + + operations = [ + migrations.CreateModel( + name='GatherAccountsAutomation', + 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': 'Gather asset accounts', + }, + bases=('assets.baseautomation',), + ), + migrations.AlterField( + model_name='baseautomation', + name='type', + field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Create account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'), + ), + migrations.CreateModel( + name='PingAutomation', + 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': 'Ping asset', + }, + bases=('assets.baseautomation',), + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['name'], + 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), + ('test_assetconnectivity', 'Can test asset connectivity'), + ('push_assetaccount', 'Can push account to asset'), + ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), + ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, + ), + ] diff --git a/apps/assets/migrations/0109_auto_20221102_2017.py b/apps/assets/migrations/0109_auto_20221102_2017.py new file mode 100644 index 000000000..a9dcc4446 --- /dev/null +++ b/apps/assets/migrations/0109_auto_20221102_2017.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.14 on 2022-11-02 12:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0108_auto_20221027_1053'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Is active'), + ), + migrations.AddField( + model_name='account', + name='updated_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='accounttemplate', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Is active'), + ), + migrations.AddField( + model_name='accounttemplate', + name='updated_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), + ), + migrations.AddField( + model_name='gateway', + name='updated_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), + ), + migrations.AlterField( + model_name='account', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='accounttemplate', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='gateway', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), + ] diff --git a/apps/assets/migrations/0110_changesecretrecord_asset.py b/apps/assets/migrations/0110_changesecretrecord_asset.py new file mode 100644 index 000000000..76a3f9872 --- /dev/null +++ b/apps/assets/migrations/0110_changesecretrecord_asset.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.14 on 2022-11-03 13:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0109_auto_20221102_2017'), + ] + + operations = [ + migrations.RenameField( + model_name='accountbackupplan', + old_name='categories', + new_name='types', + ), + migrations.AddField( + model_name='changesecretrecord', + name='asset', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset'), + ), + ] diff --git a/apps/assets/migrations/0111_alter_automationexecution_status.py b/apps/assets/migrations/0111_alter_automationexecution_status.py new file mode 100644 index 000000000..5ccaf638f --- /dev/null +++ b/apps/assets/migrations/0111_alter_automationexecution_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-11 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0110_changesecretrecord_asset'), + ] + + operations = [ + migrations.AlterField( + model_name='automationexecution', + name='status', + field=models.CharField(default='pending', max_length=16, verbose_name='Status'), + ), + ] diff --git a/apps/assets/migrations/0112_gateway_to_asset.py b/apps/assets/migrations/0112_gateway_to_asset.py new file mode 100644 index 000000000..67c874761 --- /dev/null +++ b/apps/assets/migrations/0112_gateway_to_asset.py @@ -0,0 +1,88 @@ +# Generated by Django 3.2.13 on 2022-09-29 11:03 + +from django.db import migrations + +from assets.const.host import GATEWAY_NAME + + +def _create_account_obj(secret, secret_type, gateway, asset, account_model): + return account_model( + asset=asset, + secret=secret, + org_id=gateway.org_id, + secret_type=secret_type, + username=gateway.username, + name=f'{gateway.name}-{secret_type}-{GATEWAY_NAME.lower()}', + ) + + +def migrate_gateway_to_asset(apps, schema_editor): + db_alias = schema_editor.connection.alias + gateway_model = apps.get_model('assets', 'Gateway') + platform_model = apps.get_model('assets', 'Platform') + gateway_platform = platform_model.objects.using(db_alias).get(name=GATEWAY_NAME) + + print('>>> migrate gateway to asset') + asset_dict = {} + host_model = apps.get_model('assets', 'Host') + asset_model = apps.get_model('assets', 'Asset') + protocol_model = apps.get_model('assets', 'Protocol') + gateways = gateway_model.objects.all() + for gateway in gateways: + comment = gateway.comment if gateway.comment else '' + data = { + 'comment': comment, + 'name': f'{gateway.name}-{GATEWAY_NAME.lower()}', + 'address': gateway.ip, + 'domain': gateway.domain, + 'org_id': gateway.org_id, + 'is_active': gateway.is_active, + 'platform': gateway_platform, + } + asset = asset_model.objects.using(db_alias).create(**data) + asset_dict[gateway.id] = asset + protocol_model.objects.using(db_alias).create(name='ssh', port=gateway.port, asset=asset) + hosts = [host_model(asset_ptr=asset) for asset in asset_dict.values()] + host_model.objects.using(db_alias).bulk_create(hosts, ignore_conflicts=True) + + print('>>> migrate gateway to account') + accounts = [] + account_model = apps.get_model('assets', 'Account') + for gateway in gateways: + password = gateway.password + private_key = gateway.private_key + asset = asset_dict[gateway.id] + if password: + accounts.append(_create_account_obj( + password, 'password', gateway, asset, account_model + )) + + if private_key: + accounts.append(_create_account_obj( + private_key, 'ssh_key', gateway, asset, account_model + )) + account_model.objects.using(db_alias).bulk_create(accounts) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0111_alter_automationexecution_status'), + ] + + operations = [ + migrations.RunPython(migrate_gateway_to_asset), + migrations.DeleteModel( + name='Gateway', + ), + migrations.CreateModel( + name='Gateway', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.host',), + ), + ] diff --git a/apps/assets/migrations/0113_alter_accounttemplate_options.py b/apps/assets/migrations/0113_alter_accounttemplate_options.py new file mode 100644 index 000000000..9488b5499 --- /dev/null +++ b/apps/assets/migrations/0113_alter_accounttemplate_options.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.14 on 2022-11-28 10:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0112_gateway_to_asset'), + ] + + operations = [ + migrations.AlterModelOptions( + name='accounttemplate', + options={'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), + ('change_accounttemplatesecret', 'Can change asset account template secret')], + 'verbose_name': 'Account template'}, + ), + migrations.AddField( + model_name='database', + name='allow_invalid_cert', + field=models.BooleanField(default=False, verbose_name='Allow invalid cert'), + ), + migrations.AddField( + model_name='database', + name='ca_cert', + field=models.TextField(blank=True, verbose_name='CA cert'), + ), + migrations.AddField( + model_name='database', + name='client_cert', + field=models.TextField(blank=True, verbose_name='Client cert'), + ), + migrations.AddField( + model_name='database', + name='client_key', + field=models.TextField(blank=True, verbose_name='Client key'), + ), + migrations.AddField( + model_name='database', + name='use_ssl', + field=models.BooleanField(default=False, verbose_name='Use SSL'), + ), + ] diff --git a/apps/assets/migrations/0114_node_domain.py b/apps/assets/migrations/0114_node_domain.py new file mode 100644 index 000000000..1abcde400 --- /dev/null +++ b/apps/assets/migrations/0114_node_domain.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.14 on 2022-11-29 05:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0113_alter_accounttemplate_options'), + ] + + operations = [ + migrations.AddField( + model_name='database', + name='allow_invalid_cert', + field=models.BooleanField(default=False, verbose_name='Allow invalid cert'), + ), + migrations.AddField( + model_name='database', + name='ca_cert', + field=models.TextField(blank=True, verbose_name='CA cert'), + ), + migrations.AddField( + model_name='database', + name='client_cert', + field=models.TextField(blank=True, verbose_name='Client cert'), + ), + migrations.AddField( + model_name='database', + name='client_key', + field=models.TextField(blank=True, verbose_name='Client key'), + ), + migrations.AddField( + model_name='database', + name='use_ssl', + field=models.BooleanField(default=False, verbose_name='Use SSL'), + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index d2dd03885..376355657 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -1,15 +1,18 @@ from .base import * +from .platform import * from .asset import * from .label import Label -from .user import * -from .cluster import * from .group import * from .domain import * from .node import * -from .cmd_filter import * -from .authbook import * from .utils import * -from .authbook import * from .gathered_user import * from .favorite_asset import * +from .account import * from .backup import * +from .automations import * +from ._user import * +# 废弃以下 +# from ._authbook import * +from .cmd_filter import * + diff --git a/apps/assets/models/_user.py b/apps/assets/models/_user.py new file mode 100644 index 000000000..d7809a2b3 --- /dev/null +++ b/apps/assets/models/_user.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import logging +import uuid +from common.db import fields + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.core.validators import MinValueValidator, MaxValueValidator + +from orgs.mixins.models import OrgModelMixin + + +__all__ = ['SystemUser'] +logger = logging.getLogger(__name__) + + +class SystemUser(OrgModelMixin): + LOGIN_AUTO = 'auto' + LOGIN_MANUAL = 'manual' + LOGIN_MODE_CHOICES = ( + (LOGIN_AUTO, _('Automatic managed')), + (LOGIN_MANUAL, _('Manually input')) + ) + + class Type(models.TextChoices): + common = 'common', _('Common user') + admin = 'admin', _('Admin user') + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) + password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) + private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) + public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) + token = models.TextField(default='', verbose_name=_('Token')) + + comment = models.TextField(blank=True, 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')) + + username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user")) + type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type')) + priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) + protocol = models.CharField(max_length=16, default='ssh', verbose_name=_('Protocol')) + auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) + sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) + shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) + login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) + sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) + home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) + system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) + ad_domain = models.CharField(default='', max_length=256) + # linux su 命令 (switch user) + su_enabled = models.BooleanField(default=False, verbose_name=_('User switch')) + su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from")) + privileged = None + + class Meta: + ordering = ['name'] + unique_together = [('name', 'org_id')] + verbose_name = _("System user") + permissions = [ + ('match_systemuser', _('Can match system user')), + ] diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py new file mode 100644 index 000000000..930fd7882 --- /dev/null +++ b/apps/assets/models/account.py @@ -0,0 +1,103 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from simple_history.models import HistoricalRecords + +from common.utils import lazyproperty + +from .base import AbsConnectivity, BaseAccount + +__all__ = ['Account', 'AccountTemplate'] + + +class AccountHistoricalRecords(HistoricalRecords): + def __init__(self, *args, **kwargs): + self.included_fields = kwargs.pop('included_fields', None) + super().__init__(*args, **kwargs) + + def post_save(self, instance, created, using=None, **kwargs): + if not self.included_fields: + return super().post_save(instance, created, using=using, **kwargs) + + check_fields = set(self.included_fields) - {'version'} + history_attrs = instance.history.all().values(*check_fields).first() + if history_attrs is None: + return super().post_save(instance, created, using=using, **kwargs) + + attrs = {field: getattr(instance, field) for field in check_fields} + history_attrs = set(history_attrs.items()) + attrs = set(attrs.items()) + diff = attrs - history_attrs + if not diff: + return + super().post_save(instance, created, using=using, **kwargs) + + def create_history_model(self, model, inherited): + if self.included_fields and not self.excluded_fields: + self.excluded_fields = [ + field.name for field in model._meta.fields + if field.name not in self.included_fields + ] + return super().create_history_model(model, inherited) + + +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') + ) + su_from = models.ForeignKey( + 'assets.Account', related_name='su_to', null=True, + on_delete=models.SET_NULL, verbose_name=_("Su from") + ) + version = models.IntegerField(default=0, verbose_name=_('Version')) + history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version']) + + class Meta: + verbose_name = _('Account') + unique_together = [ + ('username', 'asset', 'secret_type'), + ('name', 'asset'), + ] + 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')), + ] + + @lazyproperty + def platform(self): + return self.asset.platform + + def __str__(self): + return '{}'.format(self.username) + + @classmethod + def get_manual_account(cls): + """ @INPUT 手动登录的账号(any) """ + return cls(name=cls.AliasAccount.INPUT.label, username=cls.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) + + +class AccountTemplate(BaseAccount): + class Meta: + verbose_name = _('Account template') + unique_together = ( + ('name', 'org_id'), + ) + permissions = [ + ('view_accounttemplatesecret', _('Can view asset account template secret')), + ('change_accounttemplatesecret', _('Can change asset account template secret')), + ] + + def __str__(self): + return self.username diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py deleted file mode 100644 index b02e6a8d6..000000000 --- a/apps/assets/models/asset.py +++ /dev/null @@ -1,395 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import uuid -import logging -from functools import reduce -from collections import OrderedDict - -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from rest_framework.exceptions import ValidationError - -from common.db.fields import JsonDictTextField -from common.utils import lazyproperty -from orgs.mixins.models import OrgModelMixin, OrgManager - -from .base import AbsConnectivity - -__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet'] -logger = logging.getLogger(__name__) - - -def default_cluster(): - from .cluster import Cluster - name = "Default" - defaults = {"name": name} - cluster, created = Cluster.objects.get_or_create( - defaults=defaults, name=name - ) - return cluster.id - - -def default_node(): - try: - from .node import Node - root = Node.org_root() - return Node.objects.filter(id=root.id) - except: - return None - - -class AssetManager(OrgManager): - pass - - -class AssetQuerySet(models.QuerySet): - def active(self): - return self.filter(is_active=True) - - def valid(self): - return self.active() - - def has_protocol(self, name): - return self.filter(protocols__contains=name) - - -class ProtocolsMixin: - protocols = '' - - class Protocol(models.TextChoices): - ssh = 'ssh', 'SSH' - rdp = 'rdp', 'RDP' - telnet = 'telnet', 'Telnet' - vnc = 'vnc', 'VNC' - - @property - def protocols_as_list(self): - if not self.protocols: - return [] - return self.protocols.split(' ') - - @property - def protocols_as_dict(self): - d = OrderedDict() - protocols = self.protocols_as_list - for i in protocols: - if '/' not in i: - continue - name, port = i.split('/')[:2] - if not all([name, port]): - continue - d[name] = int(port) - return d - - @property - def protocols_as_json(self): - return [ - {"name": name, "port": port} - for name, port in self.protocols_as_dict.items() - ] - - def has_protocol(self, name): - return name in self.protocols_as_dict - - @property - def ssh_port(self): - return self.protocols_as_dict.get("ssh", 22) - - -class NodesRelationMixin: - NODES_CACHE_KEY = 'ASSET_NODES_{}' - ALL_ASSET_NODES_CACHE_KEY = 'ALL_ASSETS_NODES' - CACHE_TIME = 3600 * 24 * 7 - id = "" - _all_nodes_keys = None - - def get_nodes(self): - from .node import Node - nodes = self.nodes.all() - if not nodes: - nodes = Node.objects.filter(id=Node.org_root().id) - return nodes - - def get_all_nodes(self, flat=False): - nodes = [] - for node in self.get_nodes(): - _nodes = node.get_ancestors(with_self=True) - nodes.extend(list(_nodes)) - if flat: - nodes = list(set([node.id for node in nodes])) - return nodes - - -class Platform(models.Model): - CHARSET_CHOICES = ( - ('utf8', 'UTF-8'), - ('gbk', 'GBK'), - ) - BASE_CHOICES = ( - ('Linux', 'Linux'), - ('Unix', 'Unix'), - ('MacOS', 'MacOS'), - ('BSD', 'BSD'), - ('Windows', 'Windows'), - ('Other', 'Other'), - ) - name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True) - base = models.CharField(choices=BASE_CHOICES, max_length=16, default='Linux', verbose_name=_("Base")) - charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset")) - meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta")) - internal = models.BooleanField(default=False, verbose_name=_("Internal")) - comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) - - @classmethod - def default(cls): - linux, created = cls.objects.get_or_create( - defaults={'name': 'Linux'}, name='Linux' - ) - return linux.id - - def is_windows(self): - return self.base.lower() in ('windows',) - - def is_unixlike(self): - return self.base.lower() in ("linux", "unix", "macos", "bsd") - - def __str__(self): - return self.name - - class Meta: - verbose_name = _("Platform") - # ordering = ('name',) - - -class AbsHardwareInfo(models.Model): - # Collect - vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor')) - model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model')) - sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number')) - - cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model')) - cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count')) - cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores')) - cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus')) - memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory')) - disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total')) - disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info')) - - os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS')) - os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version')) - os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch')) - hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw')) - - class Meta: - abstract = True - - @property - def cpu_info(self): - info = "" - if self.cpu_model: - info += self.cpu_model - if self.cpu_count and self.cpu_cores: - info += "{}*{}".format(self.cpu_count, self.cpu_cores) - return info - - @property - def hardware_info(self): - if self.cpu_count: - return '{} Core {} {}'.format( - self.cpu_vcpus or self.cpu_count * self.cpu_cores, - self.memory, self.disk_total - ) - else: - return '' - - -class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin, OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) - hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) - protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh, - choices=ProtocolsMixin.Protocol.choices, verbose_name=_('Protocol')) - port = models.IntegerField(default=22, verbose_name=_('Port')) - protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) - platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') - domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) - nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) - is_active = models.BooleanField(default=True, verbose_name=_('Is active')) - - # Auth - admin_user = models.ForeignKey('assets.SystemUser', on_delete=models.SET_NULL, null=True, verbose_name=_("Admin user"), related_name='admin_assets') - - # Some information - public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) - number = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Asset number')) - - labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) - created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by')) - 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')) - - objects = AssetManager.from_queryset(AssetQuerySet)() - - def __str__(self): - return '{0.hostname}({0.ip})'.format(self) - - def get_target_ip(self): - return self.ip - - def set_admin_user_relation(self): - from .authbook import AuthBook - if not self.admin_user: - return - if self.admin_user.type != 'admin': - raise ValidationError('System user should be type admin') - - defaults = {'asset': self, 'systemuser': self.admin_user, 'org_id': self.org_id} - AuthBook.objects.get_or_create(defaults=defaults, asset=self, systemuser=self.admin_user) - - @property - def admin_user_display(self): - if not self.admin_user: - return '' - return str(self.admin_user) - - @property - def is_valid(self): - warning = '' - if not self.is_active: - warning += ' inactive' - if warning: - return False, warning - return True, warning - - @lazyproperty - def platform_base(self): - return self.platform.base - - @lazyproperty - def admin_user_username(self): - """求可连接性时,直接用用户名去取,避免再查一次admin user - serializer 中直接通过annotate方式返回了这个 - """ - return self.admin_user.username - - def is_windows(self): - return self.platform.is_windows() - - def is_unixlike(self): - return self.platform.is_unixlike() - - def is_support_ansible(self): - return self.has_protocol('ssh') and self.platform_base not in ("Other",) - - def get_auth_info(self, with_become=False): - if not self.admin_user: - return {} - - if self.is_unixlike() and self.admin_user.su_enabled and self.admin_user.su_from: - auth_user = self.admin_user.su_from - become_user = self.admin_user - else: - auth_user = self.admin_user - become_user = None - - auth_user.load_asset_special_auth(self) - info = { - 'username': auth_user.username, - 'password': auth_user.password, - 'private_key': auth_user.private_key_file - } - - if not with_become or self.is_windows(): - return info - - if become_user: - become_user.load_asset_special_auth(self) - become_method = 'su' - become_username = become_user.username - become_pass = become_user.password - else: - become_method = 'sudo' - become_username = 'root' - become_pass = auth_user.password - become_info = { - 'become': { - 'method': become_method, - 'username': become_username, - 'pass': become_pass - } - } - info.update(become_info) - return info - - def nodes_display(self): - names = [] - for n in self.nodes.all(): - names.append(n.full_value) - return names - - def labels_display(self): - names = [] - for n in self.labels.all(): - names.append(n.name + ':' + n.value) - return names - - def as_node(self): - from .node import Node - fake_node = Node() - fake_node.id = self.id - fake_node.key = self.id - fake_node.value = self.hostname - fake_node.asset = self - fake_node.is_node = False - return fake_node - - def as_tree_node(self, parent_node): - from common.tree import TreeNode - icon_skin = 'file' - if self.platform_base.lower() == 'windows': - icon_skin = 'windows' - elif self.platform_base.lower() == 'linux': - icon_skin = 'linux' - data = { - 'id': str(self.id), - 'name': self.hostname, - 'title': self.ip, - 'pId': parent_node.key, - 'isParent': False, - 'open': False, - 'iconSkin': icon_skin, - 'meta': { - 'type': 'asset', - 'data': { - 'id': self.id, - 'hostname': self.hostname, - 'ip': self.ip, - 'protocols': self.protocols_as_list, - 'platform': self.platform_base, - } - } - } - tree_node = TreeNode(**data) - return tree_node - - def get_all_system_users(self): - from .user import SystemUser - system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\ - .values_list('systemuser_id', flat=True) - system_users = SystemUser.objects.filter(id__in=system_user_ids) - return system_users - - class Meta: - unique_together = [('org_id', 'hostname')] - verbose_name = _("Asset") - ordering = ["hostname", ] - permissions = [ - ('refresh_assethardwareinfo', _('Can refresh asset hardware info')), - ('test_assetconnectivity', _('Can test asset connectivity')), - ('push_assetsystemuser', _('Can push system user to asset')), - ('match_asset', _('Can match asset')), - ('add_assettonode', _('Add asset to node')), - ('move_assettonode', _('Move asset to node')), - ] diff --git a/apps/assets/models/asset/__init__.py b/apps/assets/models/asset/__init__.py new file mode 100644 index 000000000..793df7455 --- /dev/null +++ b/apps/assets/models/asset/__init__.py @@ -0,0 +1,6 @@ +from .common import * +from .host import * +from .database import * +from .device import * +from .web import * +from .cloud import * diff --git a/apps/assets/models/asset/cloud.py b/apps/assets/models/asset/cloud.py new file mode 100644 index 000000000..c45631331 --- /dev/null +++ b/apps/assets/models/asset/cloud.py @@ -0,0 +1,7 @@ + +from .common import Asset + + +class Cloud(Asset): + def __str__(self): + return self.name diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py new file mode 100644 index 000000000..81693252d --- /dev/null +++ b/apps/assets/models/asset/common.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# -*- 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 + +__all__ = ['Asset', 'AssetQuerySet', 'default_node', 'Protocol'] +logger = logging.getLogger(__name__) + + +def default_node(): + try: + from assets.models import Node + root = Node.org_root() + return Node.objects.filter(id=root.id) + except: + return None + + +class AssetManager(OrgManager): + pass + + +class AssetQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + def valid(self): + return self.active() + + def has_protocol(self, name): + return self.filter(protocols__contains=name) + + def group_by_platform(self) -> dict: + groups = defaultdict(list) + for asset in self.all(): + groups[asset.platform].append(asset) + return groups + + +class NodesRelationMixin: + NODES_CACHE_KEY = 'ASSET_NODES_{}' + ALL_ASSET_NODES_CACHE_KEY = 'ALL_ASSETS_NODES' + CACHE_TIME = 3600 * 24 * 7 + id = "" + _all_nodes_keys = None + + def get_nodes(self): + from assets.models import Node + nodes = self.nodes.all() + if not nodes: + nodes = Node.objects.filter(id=Node.org_root().id) + return nodes + + def get_all_nodes(self, flat=False): + from ..node import Node + 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 + + +class Protocol(models.Model): + name = models.CharField(max_length=32, verbose_name=_("Name")) + port = models.IntegerField(verbose_name=_("Port")) + asset = models.ForeignKey('Asset', on_delete=models.CASCADE, related_name='protocols', verbose_name=_("Asset")) + + def __str__(self): + return '{}/{}'.format(self.name, self.port) + + +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') + domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', + verbose_name=_("Domain"), on_delete=models.SET_NULL) + nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', + 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)() + + def __str__(self): + return '{0.name}({0.address})'.format(self) + + @property + def specific(self): + if not hasattr(self, self.category): + return {} + instance = getattr(self, self.category) + private_fields = [i.name for i in instance._meta.local_fields if i.name != 'asset_ptr'] + return {i: getattr(instance, i) for i in private_fields} + + def get_target_ip(self): + return self.address + + def get_target_ssh_port(self): + protocol = self.protocols.all().filter(name='ssh').first() + return protocol.port if protocol else 22 + + @property + def is_valid(self): + warning = '' + if not self.is_active: + warning += ' inactive' + if warning: + return False, warning + return True, warning + + def nodes_display(self): + names = [] + for n in self.nodes.all(): + names.append(n.full_value) + return names + + def labels_display(self): + names = [] + for n in self.labels.all(): + names.append(n.name + ':' + n.value) + return names + + @lazyproperty + def primary_protocol(self): + from assets.const.types import AllTypes + primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type) + protocol = self.protocols.get(name=primary_protocol_name) + return protocol + + @lazyproperty + def protocol(self): + if not self.primary_protocol: + return 'none' + return self.primary_protocol.name + + @lazyproperty + def port(self): + if not self.primary_protocol: + return 0 + return self.primary_protocol.port + + @lazyproperty + def type(self): + return self.platform.type + + @lazyproperty + def category(self): + return self.platform.category + + def as_node(self): + from assets.models import Node + fake_node = Node() + fake_node.id = self.id + fake_node.key = self.id + fake_node.value = self.name + fake_node.asset = self + fake_node.is_node = False + return fake_node + + def as_tree_node(self, parent_node): + from common.tree import TreeNode + icon_skin = 'file' + platform_type = self.platform.type.lower() + if platform_type == 'windows': + icon_skin = 'windows' + elif platform_type == 'linux': + icon_skin = 'linux' + data = { + 'id': str(self.id), + 'name': self.name, + 'title': self.address, + 'pId': parent_node.key, + 'isParent': False, + 'open': False, + 'iconSkin': icon_skin, + 'meta': { + 'type': 'asset', + 'data': { + 'id': self.id, + 'name': self.name, + 'address': self.address, + 'protocols': self.protocols, + } + } + } + tree_node = TreeNode(**data) + return tree_node + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _("Asset") + ordering = ["name", ] + permissions = [ + ('refresh_assethardwareinfo', _('Can refresh asset hardware info')), + ('test_assetconnectivity', _('Can test asset connectivity')), + ('push_assetaccount', _('Can push account to asset')), + ('match_asset', _('Can match asset')), + ('add_assettonode', _('Add asset to node')), + ('move_assettonode', _('Move asset to node')), + ] diff --git a/apps/assets/models/asset/database.py b/apps/assets/models/asset/database.py new file mode 100644 index 000000000..4772a6b08 --- /dev/null +++ b/apps/assets/models/asset/database.py @@ -0,0 +1,34 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .common import Asset + + +class Database(Asset): + db_name = models.CharField(max_length=1024, verbose_name=_("Database"), blank=True) + use_ssl = models.BooleanField(default=False, verbose_name=_("Use SSL")) + ca_cert = models.TextField(verbose_name=_("CA cert"), blank=True) + client_cert = models.TextField(verbose_name=_("Client cert"), blank=True) + client_key = models.TextField(verbose_name=_("Client key"), blank=True) + allow_invalid_cert = models.BooleanField(default=False, verbose_name=_('Allow invalid cert')) + + def __str__(self): + return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name) + + @property + def ip(self): + return self.address + + @property + def specific(self): + return { + 'db_name': self.db_name, + 'use_ssl': self.use_ssl, + 'ca_cert': self.ca_cert, + 'client_cert': self.client_cert, + 'client_key': self.client_key, + 'allow_invalid_cert': self.allow_invalid_cert, + } + + class Meta: + verbose_name = _("Database") diff --git a/apps/assets/models/asset/device.py b/apps/assets/models/asset/device.py new file mode 100644 index 000000000..c629a5fe4 --- /dev/null +++ b/apps/assets/models/asset/device.py @@ -0,0 +1,5 @@ +from .common import Asset + + +class Device(Asset): + pass diff --git a/apps/assets/models/asset/host.py b/apps/assets/models/asset/host.py new file mode 100644 index 000000000..6ca93b89f --- /dev/null +++ b/apps/assets/models/asset/host.py @@ -0,0 +1,6 @@ +from assets.const import GATEWAY_NAME +from .common import Asset + + +class Host(Asset): + pass diff --git a/apps/assets/models/asset/web.py b/apps/assets/models/asset/web.py new file mode 100644 index 000000000..15445b3d6 --- /dev/null +++ b/apps/assets/models/asset/web.py @@ -0,0 +1,17 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .common import Asset + + +class Web(Asset): + class FillType(models.TextChoices): + no = 'no', _('Disabled') + basic = 'basic', _('Basic') + script = 'script', _('Script') + + autofill = models.CharField(max_length=16, choices=FillType.choices, default='basic', verbose_name=_("Autofill")) + username_selector = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Username selector")) + password_selector = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Password selector")) + submit_selector = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Submit selector")) + script = models.JSONField(blank=True, default=list, verbose_name=_("Script")) diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py deleted file mode 100644 index a6b52927b..000000000 --- a/apps/assets/models/authbook.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.db import models -from django.db.models import F -from django.utils.translation import ugettext_lazy as _ -from simple_history.models import HistoricalRecords - -from common.utils import lazyproperty, get_logger -from .base import BaseUser, AbsConnectivity - -logger = get_logger(__name__) - - -__all__ = ['AuthBook'] - - -class AuthBook(BaseUser, AbsConnectivity): - asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) - systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user")) - version = models.IntegerField(default=1, verbose_name=_('Version')) - history = HistoricalRecords() - - auth_attrs = ['username', 'password', 'private_key', 'public_key'] - - class Meta: - verbose_name = _('AuthBook') - unique_together = [('username', 'asset', 'systemuser')] - permissions = [ - ('test_authbook', _('Can test asset account connectivity')), - ('view_assetaccountsecret', _('Can view asset account secret')), - ('change_assetaccountsecret', _('Can change asset account secret')), - ('view_assethistoryaccount', _('Can view asset history account')), - ('view_assethistoryaccountsecret', _('Can view asset history account secret')), - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.auth_snapshot = {} - - def get_or_systemuser_attr(self, attr): - val = getattr(self, attr, None) - if val: - return val - if self.systemuser: - return getattr(self.systemuser, attr, '') - return '' - - def load_auth(self): - for attr in self.auth_attrs: - value = self.get_or_systemuser_attr(attr) - self.auth_snapshot[attr] = [getattr(self, attr), value] - setattr(self, attr, value) - - def unload_auth(self): - if not self.systemuser: - return - - for attr, values in self.auth_snapshot.items(): - origin_value, loaded_value = values - current_value = getattr(self, attr, '') - if current_value == loaded_value: - setattr(self, attr, origin_value) - - def save(self, *args, **kwargs): - self.unload_auth() - instance = super().save(*args, **kwargs) - self.load_auth() - return instance - - @property - def username_display(self): - return self.get_or_systemuser_attr('username') or '*' - - @lazyproperty - def systemuser_display(self): - if not self.systemuser: - return '' - return str(self.systemuser) - - @property - def smart_name(self): - username = self.username_display - - if self.asset: - asset = str(self.asset) - else: - asset = '*' - return '{}@{}'.format(username, asset) - - def sync_to_system_user_account(self): - if self.systemuser: - return - matched = AuthBook.objects.filter( - asset=self.asset, systemuser__username=self.username - ) - if not matched: - return - - for i in matched: - i.password = self.password - i.private_key = self.private_key - i.public_key = self.public_key - i.comment = 'Update triggered by account {}'.format(self.id) - - # 不触发post_save信号 - self.__class__.objects.bulk_update(matched, fields=['password', 'private_key', 'public_key']) - - def remove_asset_admin_user_if_need(self): - if not self.asset or not self.systemuser: - return - if not self.systemuser.is_admin_user or self.asset.admin_user != self.systemuser: - return - self.asset.admin_user = None - self.asset.save() - logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser)) - - def update_asset_admin_user_if_need(self): - if not self.asset or not self.systemuser: - return - if not self.systemuser.is_admin_user or self.asset.admin_user == self.systemuser: - return - self.asset.admin_user = self.systemuser - self.asset.save() - logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser)) - - @classmethod - def get_queryset(cls): - queryset = cls.objects.all() \ - .annotate(ip=F('asset__ip')) \ - .annotate(hostname=F('asset__hostname')) \ - .annotate(platform=F('asset__platform__name')) \ - .annotate(protocols=F('asset__protocols')) - return queryset - - def __str__(self): - return self.smart_name - diff --git a/apps/assets/models/automations/__init__.py b/apps/assets/models/automations/__init__.py new file mode 100644 index 000000000..82fa19620 --- /dev/null +++ b/apps/assets/models/automations/__init__.py @@ -0,0 +1,8 @@ +from .ping import * +from .base import * +from .push_account import * +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 new file mode 100644 index 000000000..9977f6830 --- /dev/null +++ b/apps/assets/models/automations/base.py @@ -0,0 +1,120 @@ +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.models import Node, Asset +from assets.tasks import execute_automation +from assets.const import AutomationTypes + + +class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): + 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) + + @classmethod + def generate_unique_name(cls, name): + while True: + name = name + str(uuid.uuid4())[:8] + try: + cls.objects.get(name=name) + except cls.DoesNotExist: + return name + + def get_all_assets(self): + nodes = self.nodes.all() + node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) + direct_asset_ids = self.assets.all().values_list('id', flat=True) + asset_ids = set(list(direct_asset_ids) + list(node_asset_ids)) + return Asset.objects.filter(id__in=asset_ids) + + def all_assets_group_by_platform(self): + assets = self.get_all_assets().prefetch_related('platform') + return assets.group_by_platform() + + def get_register_task(self): + name = f"automation_{self.type}_strategy_period_{str(self.id)[:8]}" + task = execute_automation.name + args = (str(self.id), Trigger.timing, self.type) + kwargs = {} + return name, task, args, kwargs + + def get_many_to_many_ids(self, field: str): + return [str(i) for i in getattr(self, field).all().values_list('id', flat=True)] + + def to_attr_json(self): + return { + 'name': self.name, + 'type': self.type, + 'org_id': str(self.org_id), + 'comment': self.comment, + 'accounts': self.accounts, + 'nodes': self.get_many_to_many_ids('nodes'), + 'assets': self.get_many_to_many_ids('assets'), + } + + def execute(self, trigger=Trigger.manual): + try: + eid = current_task.request.id + except AttributeError: + eid = str(uuid.uuid4()) + + execution = self.executions.model.objects.create( + id=eid, trigger=trigger, automation=self, + snapshot=self.to_attr_json(), + ) + return execution.start() + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _("Automation task") + + +class AutomationExecution(OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + automation = models.ForeignKey( + 'BaseAutomation', related_name='executions', on_delete=models.CASCADE, + verbose_name=_('Automation task') + ) + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) + date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) + snapshot = EncryptJsonDictTextField( + default=dict, blank=True, null=True, verbose_name=_('Automation snapshot') + ) + trigger = models.CharField( + max_length=128, default=Trigger.manual, choices=Trigger.choices, + verbose_name=_('Trigger mode') + ) + + class Meta: + verbose_name = _('Automation task execution') + + @property + def manager_type(self): + return self.snapshot['type'] + + @property + def recipients(self): + recipients = self.snapshot.get('recipients') + if not recipients: + return [] + return recipients.values() + + def start(self): + from assets.automations.endpoint import ExecutionManager + manager = ExecutionManager(execution=self) + return manager.run() diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py new file mode 100644 index 000000000..ecc0e98d4 --- /dev/null +++ b/apps/assets/models/automations/change_secret.py @@ -0,0 +1,73 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +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'] + + +class ChangeSecretAutomation(BaseAutomation): + secret_type = models.CharField( + choices=SecretType.choices, max_length=16, + default=SecretType.PASSWORD, verbose_name=_('Secret type') + ) + secret_strategy = models.CharField( + choices=SecretStrategy.choices, max_length=16, + default=SecretStrategy.custom, verbose_name=_('Secret strategy') + ) + secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) + password_rules = models.JSONField(default=dict, verbose_name=_('Password rules')) + ssh_key_change_strategy = models.CharField( + choices=SSHKeyStrategy.choices, max_length=16, + default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy') + ) + recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True) + + def save(self, *args, **kwargs): + self.type = AutomationTypes.change_secret + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Change secret automation") + + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + 'secret': self.secret, + 'secret_type': self.secret_type, + 'secret_strategy': self.secret_strategy, + 'password_rules': self.password_rules, + 'ssh_key_change_strategy': self.ssh_key_change_strategy, + 'recipients': { + str(recipient.id): (str(recipient), bool(recipient.secret_key)) + for recipient in self.recipients.all() + } + }) + return attr_json + + +class ChangeSecretRecord(JMSBaseModel): + execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE) + asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True) + account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, null=True) + old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret')) + new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) + date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started')) + date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished')) + status = models.CharField(max_length=16, default='pending') + error = models.TextField(blank=True, null=True, verbose_name=_('Error')) + + class Meta: + verbose_name = _("Change secret record") + + def __str__(self): + return self.account.__str__() + + @property + def timedelta(self): + if self.date_started and self.date_finished: + return self.date_finished - self.date_started + return None diff --git a/apps/assets/models/automations/discovery_account.py b/apps/assets/models/automations/discovery_account.py new file mode 100644 index 000000000..9e2adf610 --- /dev/null +++ b/apps/assets/models/automations/discovery_account.py @@ -0,0 +1,15 @@ +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/gather_accounts.py b/apps/assets/models/automations/gather_accounts.py new file mode 100644 index 000000000..a3aa42383 --- /dev/null +++ b/apps/assets/models/automations/gather_accounts.py @@ -0,0 +1,19 @@ +from django.utils.translation import ugettext_lazy as _ + +from assets.const import AutomationTypes +from .base import BaseAutomation + +__all__ = ['GatherAccountsAutomation'] + + +class GatherAccountsAutomation(BaseAutomation): + def save(self, *args, **kwargs): + self.type = AutomationTypes.gather_accounts + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Gather asset accounts") + + @property + def executed_amount(self): + return self.executions.count() diff --git a/apps/assets/models/automations/gather_facts.py b/apps/assets/models/automations/gather_facts.py new file mode 100644 index 000000000..1641c9f81 --- /dev/null +++ b/apps/assets/models/automations/gather_facts.py @@ -0,0 +1,15 @@ +from django.utils.translation import ugettext_lazy as _ + +from assets.const import AutomationTypes +from .base import BaseAutomation + +__all__ = ['GatherFactsAutomation'] + + +class GatherFactsAutomation(BaseAutomation): + def save(self, *args, **kwargs): + self.type = AutomationTypes.gather_facts + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Gather asset facts") diff --git a/apps/assets/models/automations/ping.py b/apps/assets/models/automations/ping.py new file mode 100644 index 000000000..b327bc4ea --- /dev/null +++ b/apps/assets/models/automations/ping.py @@ -0,0 +1,15 @@ +from django.utils.translation import ugettext_lazy as _ + +from assets.const import AutomationTypes +from .base import BaseAutomation + +__all__ = ['PingAutomation'] + + +class PingAutomation(BaseAutomation): + def save(self, *args, **kwargs): + self.type = AutomationTypes.ping + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Ping asset") diff --git a/apps/assets/models/automations/push_account.py b/apps/assets/models/automations/push_account.py new file mode 100644 index 000000000..8439041cb --- /dev/null +++ b/apps/assets/models/automations/push_account.py @@ -0,0 +1,16 @@ +from django.utils.translation import ugettext_lazy as _ + +from assets.const import AutomationTypes +from .base import BaseAutomation + +__all__ = ['PushAccountAutomation'] + + +class PushAccountAutomation(BaseAutomation): + + def save(self, *args, **kwargs): + self.type = AutomationTypes.push_account + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Push asset account") diff --git a/apps/assets/models/automations/verify_account.py b/apps/assets/models/automations/verify_account.py new file mode 100644 index 000000000..cf7004820 --- /dev/null +++ b/apps/assets/models/automations/verify_account.py @@ -0,0 +1,15 @@ +from django.utils.translation import ugettext_lazy as _ + +from assets.const import AutomationTypes +from .base import BaseAutomation + +__all__ = ['VerifyAccountAutomation'] + + +class VerifyAccountAutomation(BaseAutomation): + def save(self, *args, **kwargs): + self.type = AutomationTypes.verify_account + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Verify asset account") diff --git a/apps/assets/models/backup.py b/apps/assets/models/backup.py index 437e91dbd..3cf49a94d 100644 --- a/apps/assets/models/backup.py +++ b/apps/assets/models/backup.py @@ -10,43 +10,18 @@ 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.db.models import BitOperationChoice from common.mixins.models import CommonModelMixin -__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution', 'Type'] +__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution'] logger = get_logger(__file__) -class Type(BitOperationChoice): - NONE = 0 - ALL = 0xff - - Asset = 0b1 - App = 0b1 << 1 - - DB_CHOICES = ( - (ALL, _('All')), - (Asset, _('Asset')), - (App, _('Application')) - ) - - NAME_MAP = { - ALL: "all", - Asset: "asset", - App: "application" - } - - NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} - CHOICES = [] - for i, j in DB_CHOICES: - CHOICES.append((NAME_MAP[i], j)) - - class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) - types = models.IntegerField(choices=Type.DB_CHOICES, default=Type.ALL, verbose_name=_('Type')) + types = models.JSONField(default=list) recipients = models.ManyToManyField( 'users.User', related_name='recipient_escape_route_plans', blank=True, verbose_name=_("Recipient") @@ -65,7 +40,7 @@ class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): from ..tasks import execute_account_backup_plan name = "account_backup_plan_period_{}".format(str(self.id)[:8]) task = execute_account_backup_plan.name - args = (str(self.id), AccountBackupPlanExecution.Trigger.timing) + args = (str(self.id), Trigger.timing) kwargs = {} return name, task, args, kwargs @@ -77,7 +52,7 @@ class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): 'crontab': self.crontab, 'org_id': self.org_id, 'created_by': self.created_by, - 'types': Type.value_to_choices(self.types), + 'types': self.types, 'recipients': { str(recipient.id): (str(recipient), bool(recipient.secret_key)) for recipient in self.recipients.all() @@ -96,10 +71,6 @@ class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): class AccountBackupPlanExecution(OrgModelMixin): - class Trigger(models.TextChoices): - manual = 'manual', _('Manual trigger') - timing = 'timing', _('Timing trigger') - id = models.UUIDField(default=uuid.uuid4, primary_key=True) date_start = models.DateTimeField( auto_now_add=True, verbose_name=_('Date start') @@ -139,7 +110,11 @@ class AccountBackupPlanExecution(OrgModelMixin): return [] return recipients.values() + @property + def manager_type(self): + return 'backup_account' + def start(self): - from ..task_handlers import ExecutionManager + from assets.automations.endpoint import ExecutionManager manager = ExecutionManager(execution=self) return manager.run() diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 493036efc..0885f5b88 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -1,40 +1,28 @@ # -*- coding: utf-8 -*- # -import io import os -import uuid from hashlib import md5 import sshpubkeys -from django.core.cache import cache +from django.conf import settings from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from django.db.models import QuerySet -from common.utils import random_string, signer -from common.utils import ( - ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty -) -from common.utils.encode import ssh_pubkey_gen -from common.validators import alphanumeric +from assets.const import Connectivity, SecretType from common.db import fields -from orgs.mixins.models import OrgModelMixin - +from common.utils import ( + ssh_key_string_to_obj, ssh_key_gen, get_logger, + random_string, lazyproperty, parse_ssh_public_key_str +) +from orgs.mixins.models import JMSOrgBaseModel logger = get_logger(__file__) -class Connectivity(models.TextChoices): - unknown = 'unknown', _('Unknown') - ok = 'ok', _('Ok') - failed = 'failed', _('Failed') - - class AbsConnectivity(models.Model): connectivity = models.CharField( - choices=Connectivity.choices, default=Connectivity.unknown, + choices=Connectivity.choices, default=Connectivity.UNKNOWN, max_length=16, verbose_name=_('Connectivity') ) date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified")) @@ -44,9 +32,15 @@ class AbsConnectivity(models.Model): self.date_verified = timezone.now() self.save(update_fields=['connectivity', 'date_verified']) + @property + def is_connective(self): + if self.connectivity == Connectivity.OK: + return True + return False + @classmethod def bulk_set_connectivity(cls, queryset_or_id, connectivity): - if not isinstance(queryset_or_id, QuerySet): + if not isinstance(queryset_or_id, models.QuerySet): queryset = cls.objects.filter(id__in=queryset_or_id) else: queryset = queryset_or_id @@ -56,11 +50,50 @@ class AbsConnectivity(models.Model): abstract = True -class AuthMixin: - private_key = '' - password = '' - public_key = '' - username = '' +class BaseAccount(JMSOrgBaseModel): + name = models.CharField(max_length=128, verbose_name=_("Name")) + username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) + secret_type = models.CharField( + max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type') + ) + 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')) + + @property + def has_secret(self): + return bool(self.secret) + + @property + def has_username(self): + return bool(self.username) + + @property + def specific(self): + data = {} + if self.secret_type != SecretType.SSH_KEY: + return data + data['ssh_key_fingerprint'] = self.ssh_key_fingerprint + return data + + @property + def private_key(self): + if self.secret_type == SecretType.SSH_KEY: + return self.secret + return None + + @private_key.setter + def private_key(self, value): + self.secret = value + self.secret_type = SecretType.SSH_KEY + + @lazyproperty + def public_key(self): + if self.secret_type == SecretType.SSH_KEY and self.private_key: + return parse_ssh_public_key_str(self.private_key) + return None @property def ssh_key_fingerprint(self): @@ -68,7 +101,7 @@ class AuthMixin: public_key = self.public_key elif self.private_key: try: - public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password) + public_key = parse_ssh_public_key_str(self.private_key) except IOError as e: return str(e) else: @@ -81,14 +114,14 @@ class AuthMixin: @property def private_key_obj(self): if self.private_key: - key_obj = ssh_key_string_to_obj(self.private_key, password=self.password) + key_obj = ssh_key_string_to_obj(self.private_key) return key_obj else: return None @property - def private_key_file(self): - if not self.private_key_obj: + def private_key_path(self): + if not self.secret_type != SecretType.SSH_KEY or not self.secret: return None project_dir = settings.PROJECT_DIR tmp_dir = os.path.join(project_dir, 'tmp') @@ -100,12 +133,9 @@ class AuthMixin: return key_path def get_private_key(self): - if not self.private_key_obj: + if not self.private_key: return None - string_io = io.StringIO() - self.private_key_obj.write_private_key(string_io) - private_key = string_io.getvalue() - return private_key + return self.private_key @property def public_key_obj(self): @@ -116,122 +146,22 @@ class AuthMixin: pass return None - def set_auth(self, password=None, private_key=None, public_key=None): - update_fields = [] - if password: - self.password = password - update_fields.append('password') - if private_key: - self.private_key = private_key - update_fields.append('private_key') - if public_key: - self.public_key = public_key - update_fields.append('public_key') - - if update_fields: - self.save(update_fields=update_fields) - - def _merge_auth(self, other): - if other.password: - self.password = other.password - if other.public_key or other.private_key: - self.private_key = other.private_key - self.public_key = other.public_key - - def clear_auth(self): - self.password = '' - self.private_key = '' - self.public_key = '' - self.save() - @staticmethod def gen_password(length=36): return random_string(length, special_char=True) @staticmethod def gen_key(username): - private_key, public_key = ssh_key_gen( - username=username - ) + private_key, public_key = ssh_key_gen(username=username) return private_key, public_key - def auto_gen_auth(self, password=True, key=True): - _password = None - _private_key = None - _public_key = None - - if password: - _password = self.gen_password() - if key: - _private_key, _public_key = self.gen_key(self.username) - self.set_auth( - password=_password, private_key=_private_key, - public_key=_public_key - ) - - -class BaseUser(OrgModelMixin, AuthMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) - password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) - public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) - comment = models.TextField(blank=True, 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')) - - ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT" - ASSET_USER_CACHE_TIME = 600 - - APPS_AMOUNT_CACHE_KEY = "APP_USER_{}_APPS_AMOUNT" - APP_USER_CACHE_TIME = 600 - - def get_related_assets(self): - assets = self.assets.filter(org_id=self.org_id) - return assets - - def get_related_apps(self): - from applications.models import Account - apps = Account.objects.filter(systemuser=self) - return apps - - def get_username(self): - return self.username - - @lazyproperty - def assets_amount(self): - cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) - cached = cache.get(cache_key) - if not cached: - cached = self.get_related_assets().count() - cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME) - return cached - - @property - def apps_amount(self): - cache_key = self.APPS_AMOUNT_CACHE_KEY.format(self.id) - cached = cache.get(cache_key) - if not cached: - cached = self.get_related_apps().count() - cache.set(cache_key, cached, self.APP_USER_CACHE_TIME) - return cached - - def expire_assets_amount(self): - cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) - cache.delete(cache_key) - def _to_secret_json(self): """Push system user use it""" return { 'name': self.name, 'username': self.username, - 'password': self.password, 'public_key': self.public_key, - 'private_key': self.private_key_file, } class Meta: abstract = True - diff --git a/apps/assets/models/cluster.py b/apps/assets/models/cluster.py deleted file mode 100644 index 6c0692ab9..000000000 --- a/apps/assets/models/cluster.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import logging -import uuid - -from django.db import models -from django.utils.translation import ugettext_lazy as _ - - -__all__ = ['Cluster'] -logger = logging.getLogger(__name__) - - -class Cluster(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=32, verbose_name=_('Name')) - admin_user = models.ForeignKey('assets.AdminUser', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) - bandwidth = models.CharField(max_length=32, blank=True, verbose_name=_('Bandwidth')) - contact = models.CharField(max_length=128, blank=True, verbose_name=_('Contact')) - phone = models.CharField(max_length=32, blank=True, verbose_name=_('Phone')) - address = models.CharField(max_length=128, blank=True, verbose_name=_("Address")) - intranet = models.TextField(blank=True, verbose_name=_('Intranet')) - extranet = models.TextField(blank=True, verbose_name=_('Extranet')) - date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) - operator = models.CharField(max_length=32, blank=True, verbose_name=_('Operator')) - created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - - def __str__(self): - return self.name - - @classmethod - def initial(cls): - return cls.objects.get_or_create(name=_('Default'), created_by=_('System'), comment=_('Default Cluster'))[0] - - class Meta: - ordering = ['name'] - verbose_name = _("Cluster") diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index cec03827e..9c70f2bb9 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- # import uuid -import re -from django.db import models -from django.db.models import Q from django.core.validators import MinValueValidator, MaxValueValidator +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 @@ -41,13 +40,7 @@ class CommandFilter(OrgModelMixin): 'assets.Asset', related_name='cmd_filters', blank=True, verbose_name=_("Asset") ) - system_users = models.ManyToManyField( - 'assets.SystemUser', related_name='cmd_filters', blank=True, - verbose_name=_("System user")) - applications = models.ManyToManyField( - 'applications.Application', related_name='cmd_filters', blank=True, - verbose_name=_("Application") - ) + accounts = models.JSONField(default=list, verbose_name=_("Accounts")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) comment = models.TextField(blank=True, default='', verbose_name=_("Comment")) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) @@ -105,139 +98,3 @@ class CommandFilterRule(OrgModelMixin): class Meta: ordering = ('priority', 'action') verbose_name = _("Command filter rule") - - @lazyproperty - def pattern(self): - if self.type == 'command': - s = self.construct_command_regex(content=self.content) - else: - s = r'{0}'.format(self.content) - - return s - - @classmethod - def construct_command_regex(cls, content): - regex = [] - content = content.replace('\r\n', '\n') - for _cmd in content.split('\n'): - cmd = re.sub(r'\s+', ' ', _cmd) - cmd = re.escape(cmd) - cmd = cmd.replace('\\ ', '\s+') - - # 有空格就不能 铆钉单词了 - if ' ' in _cmd: - regex.append(cmd) - continue - - if not cmd: - continue - - # 如果是单个字符 - if cmd[-1].isalpha(): - regex.append(r'\b{0}\b'.format(cmd)) - else: - regex.append(r'\b{0}'.format(cmd)) - s = r'{}'.format('|'.join(regex)) - return s - - @staticmethod - def compile_regex(regex, ignore_case): - try: - if ignore_case: - pattern = re.compile(regex, re.IGNORECASE) - else: - pattern = re.compile(regex) - except Exception as e: - error = _('The generated regular expression is incorrect: {}').format(str(e)) - logger.error(error) - return False, error, None - return True, '', pattern - - def match(self, data): - succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case) - if not succeed: - return self.ACTION_UNKNOWN, '' - - found = pattern.search(data) - if not found: - return self.ACTION_UNKNOWN, '' - - if self.action == self.ActionChoices.allow: - return self.ActionChoices.allow, found.group() - else: - return self.ActionChoices.deny, found.group() - - def __str__(self): - return '{} % {}'.format(self.type, self.content) - - def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): - from tickets.const import TicketType - from tickets.models import ApplyCommandTicket - data = { - 'title': _('Command confirm') + ' ({})'.format(session.user), - 'type': TicketType.command_confirm, - 'applicant': session.user_obj, - 'apply_run_user_id': session.user_id, - 'apply_run_asset': str(session.asset), - 'apply_run_system_user_id': session.system_user_id, - 'apply_run_command': run_command[:4090], - 'apply_from_session_id': str(session.id), - 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), - 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id), - 'org_id': org_id, - } - ticket = ApplyCommandTicket.objects.create(**data) - assignees = self.reviewers.all() - ticket.open_by_system(assignees) - return ticket - - @classmethod - def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, - asset_id=None, node_id=None, application_id=None, org_id=None): - # user & user_group - user_groups = [] - user = get_object_or_none(User, pk=user_id) - if user: - user_groups.extend(list(user.groups.all())) - user_group = get_object_or_none(UserGroup, pk=user_group_id) - if user_group: - org_id = user_group.org_id - user_groups.append(user_group) - - # asset & node - nodes = [] - asset = get_object_or_none(Asset, pk=asset_id) - if asset: - nodes.extend(asset.get_all_nodes()) - node = get_object_or_none(Node, pk=node_id) - if node: - org_id = node.org_id - nodes.extend(list(node.get_ancestors(with_self=True))) - - system_user = get_object_or_none(SystemUser, pk=system_user_id) - application = get_object_or_none(Application, pk=application_id) - q = Q() - if user: - q |= Q(users=user) - if user_groups: - q |= Q(user_groups__in=set(user_groups)) - if system_user: - org_id = system_user.org_id - q |= Q(system_users=system_user) - if asset: - org_id = asset.org_id - q |= Q(assets=asset) - if nodes: - q |= Q(nodes__in=set(nodes)) - if application: - org_id = application.org_id - q |= Q(applications=application) - if q: - cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) - if org_id: - cmd_filters = cmd_filters.filter(org_id=org_id) - rule_ids = cmd_filters.values_list('rules', flat=True) - rules = cls.objects.filter(id__in=rule_ids) - else: - rules = cls.objects.none() - return rules diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index a57cbdffc..e9894f72c 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- # -import socket import uuid import random - -from django.core.cache import cache +import socket import paramiko + 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 .base import BaseUser +from assets.models import Host, Platform +from assets.const import GATEWAY_NAME, SecretType, Connectivity +from orgs.mixins.models import OrgManager logger = get_logger(__file__) @@ -22,8 +23,7 @@ class Domain(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) 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')) + date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) class Meta: verbose_name = _("Domain") @@ -33,12 +33,16 @@ class Domain(OrgModelMixin): def __str__(self): return self.name - def has_gateway(self): - return self.gateway_set.filter(is_active=True).exists() + @classmethod + def get_gateway_queryset(cls): + return Gateway.objects.all() @lazyproperty def gateways(self): - return self.gateway_set.filter(is_active=True) + return self.get_gateway_queryset().filter(domain=self, is_active=True) + + def select_gateway(self): + return self.random_gateway() def random_gateway(self): gateways = [gw for gw in self.gateways if gw.is_connective] @@ -50,115 +54,138 @@ class Domain(OrgModelMixin): return random.choice(self.gateways) -class Gateway(BaseUser): - UNCONNECTIVE_KEY_TMPL = 'asset_unconnective_gateway_{}' - UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}' - UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5 +class GatewayManager(OrgManager): + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(platform__name=GATEWAY_NAME) + return queryset - class Protocol(models.TextChoices): - ssh = 'ssh', 'SSH' + def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): + platform = Gateway().default_platform + for obj in objs: + obj.platform_id = platform.id + return super().bulk_create(objs, batch_size, ignore_conflicts) - ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) - port = models.IntegerField(default=22, verbose_name=_('Port')) - protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")) - domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) - comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) - is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - def __str__(self): - return self.name +class Gateway(Host): + objects = GatewayManager() class Meta: - unique_together = [('name', 'org_id')] - verbose_name = _("Gateway") - permissions = [ - ('test_gateway', _('Test gateway')) - ] + proxy = True - def set_unconnective(self): - unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id) - unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id) + @lazyproperty + def default_platform(self): + return Platform.objects.get(name=GATEWAY_NAME, internal=True) - unconnective_silence_period = cache.get(unconnective_silence_period_key, - self.UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE) - cache.set(unconnective_silence_period_key, unconnective_silence_period * 2) - cache.set(unconnective_key, unconnective_silence_period, unconnective_silence_period) + def save(self, *args, **kwargs): + platform = self.default_platform + self.platform_id = platform.id + return super().save(*args, **kwargs) - def set_connective(self): - unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id) - unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id) + @lazyproperty + def select_accounts(self) -> dict: + account_dict = {} + accounts = self.accounts.filter(is_active=True).order_by('-privileged', '-date_updated') + password_account = accounts.filter(secret_type=SecretType.PASSWORD).first() + if password_account: + account_dict[SecretType.PASSWORD] = password_account - cache.delete(unconnective_key) - cache.delete(unconnective_silence_period_key) - - def get_is_unconnective(self): - unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id) - return cache.get(unconnective_key, False) + ssh_key_account = accounts.filter(secret_type=SecretType.SSH_KEY).first() + if ssh_key_account: + account_dict[SecretType.SSH_KEY] = ssh_key_account + return account_dict @property - def is_connective(self): - return not self.get_is_unconnective() + def password(self): + account = self.select_accounts.get(SecretType.PASSWORD) + return account.secret if account else None - @is_connective.setter - def is_connective(self, value): - if value: - self.set_connective() - else: - self.set_unconnective() + @property + def private_key(self): + account = self.select_accounts.get(SecretType.SSH_KEY) + return account.private_key if account else None + + @property + def private_key_obj(self): + account = self.select_accounts.get(SecretType.SSH_KEY) + return account.private_key_obj if account else None + + @property + def private_key_path(self): + account = self.select_accounts.get(SecretType.SSH_KEY) + return account.private_key_path if account else None + + @lazyproperty + def username(self): + accounts = self.select_accounts.values() + if len(accounts) == 0: + return None + accounts = sorted( + accounts, key=lambda x: x['privileged'], reverse=True + ) + return accounts[0].username def test_connective(self, local_port=None): - if local_port is None: - local_port = self.port - + local_port = self.port if local_port is None else local_port client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) proxy = paramiko.SSHClient() proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - proxy.connect(self.ip, port=self.port, - username=self.username, - password=self.password, - pkey=self.private_key_obj) - except(paramiko.AuthenticationException, - paramiko.BadAuthenticationType, - paramiko.SSHException, - paramiko.ChannelException, - paramiko.ssh_exception.NoValidConnectionsError, - socket.gaierror) as e: + proxy.connect( + self.address, + port=self.port, + username=self.username, + password=self.password, + pkey=self.private_key_obj + ) + except( + paramiko.AuthenticationException, + paramiko.BadAuthenticationType, + paramiko.SSHException, + paramiko.ChannelException, + paramiko.ssh_exception.NoValidConnectionsError, + socket.gaierror + ) as e: err = str(e) if err.startswith('[Errno None] Unable to connect to port'): - err = _('Unable to connect to port {port} on {ip}') - err = err.format(port=self.port, ip=self.ip) + err = _('Unable to connect to port {port} on {address}') + err = err.format(port=self.port, address=self.address) elif err == 'Authentication failed.': err = _('Authentication failed') elif err == 'Connect failed': err = _('Connect failed') - self.is_connective = False + self.set_connectivity(Connectivity.FAILED) return False, err try: sock = proxy.get_transport().open_channel( 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0) ) - client.connect("127.0.0.1", port=local_port, - username=self.username, - password=self.password, - key_filename=self.private_key_file, - sock=sock, - timeout=5) - except (paramiko.SSHException, + client.connect( + '127.0.0.1', + sock=sock, + timeout=5, + port=local_port, + username=self.username, + password=self.password, + key_filename=self.private_key_path, + ) + except ( + paramiko.SSHException, paramiko.ssh_exception.SSHException, paramiko.ChannelException, paramiko.AuthenticationException, - TimeoutError) as e: + TimeoutError + ) as e: err = getattr(e, 'text', str(e)) if err == 'Connect failed': err = _('Connect failed') - self.is_connective = False + self.set_connectivity(Connectivity.FAILED) return False, err finally: client.close() - self.is_connective = True + self.set_connectivity(Connectivity.OK) return True, None diff --git a/apps/assets/models/gathered_user.py b/apps/assets/models/gathered_user.py index d00021c56..3c0a743b9 100644 --- a/apps/assets/models/gathered_user.py +++ b/apps/assets/models/gathered_user.py @@ -20,19 +20,19 @@ class GatheredUser(OrgModelMixin): date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) @property - def hostname(self): - return self.asset.hostname + def name(self): + return self.asset.name @property def ip(self): - return self.asset.ip + return self.asset.address class Meta: verbose_name = _('GatherUser') ordering = ['asset'] def __str__(self): - return '{}: {}'.format(self.asset.hostname, self.username) + return '{}: {}'.format(self.asset.name, self.username) diff --git a/apps/assets/models/label.py b/apps/assets/models/label.py index f7820ccb1..afad1f069 100644 --- a/apps/assets/models/label.py +++ b/apps/assets/models/label.py @@ -1,29 +1,25 @@ # -*- coding: utf-8 -*- # -import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.models import OrgModelMixin + +from orgs.mixins.models import JMSOrgBaseModel -class Label(OrgModelMixin): +class Label(JMSOrgBaseModel): SYSTEM_CATEGORY = "S" USER_CATEGORY = "U" CATEGORY_CHOICES = ( ("S", _("System")), ("U", _("User")) ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_("Name")) value = models.CharField(max_length=128, verbose_name=_("Value")) 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")) - date_created = models.DateTimeField( - auto_now_add=True, null=True, blank=True, verbose_name=_('Date created') - ) @classmethod def get_queryset_group_by_name(cls): diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 0e98bce14..54bdecf61 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -554,8 +554,9 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): full_value = models.CharField(max_length=4096, verbose_name=_('Full value'), default='') child_mark = models.IntegerField(default=0) date_create = models.DateTimeField(auto_now_add=True) - parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"), - db_index=True, default='') + parent_key = models.CharField( + max_length=64, verbose_name=_("Parent key"), db_index=True, default='' + ) assets_amount = models.IntegerField(default=0) objects = OrgManager.from_queryset(NodeQuerySet)() diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py new file mode 100644 index 000000000..64aeb2da3 --- /dev/null +++ b/apps/assets/models/platform.py @@ -0,0 +1,111 @@ +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 + +__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] + + +class PlatformProtocol(models.Model): + SETTING_ATTRS = { + 'console': True, + 'security': 'any,tls,rdp', + 'sftp_enabled': True, + 'sftp_home': '/tmp' + } + default = models.BooleanField(default=False, verbose_name=_('Default')) + required = models.BooleanField(default=False, verbose_name=_('Required')) + name = models.CharField(max_length=32, verbose_name=_('Name')) + port = models.IntegerField(verbose_name=_('Port')) + setting = models.JSONField(verbose_name=_('Setting'), default=dict) + platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols') + + def __str__(self): + return '{}/{}'.format(self.name, self.port) + + @property + def primary(self): + primary_protocol_name = AllTypes.get_primary_protocol_name( + self.platform.category, self.platform.type + ) + return self.name == primary_protocol_name + + @property + def secret_types(self): + return Protocol.settings().get(self.name, {}).get('secret_types') + + +class PlatformAutomation(models.Model): + ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled")) + ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config")) + ping_enabled = models.BooleanField(default=False, verbose_name=_("Ping enabled")) + 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")) + verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled")) + verify_account_method = models.TextField( + max_length=32, blank=True, null=True, verbose_name=_("Verify account method")) + gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) + gather_accounts_method = models.TextField( + max_length=32, blank=True, null=True, verbose_name=_("Gather facts method") + ) + + +class Platform(models.Model): + """ + 对资产提供 约束和默认值 + 对资产进行抽象 + """ + + class CharsetChoices(models.TextChoices): + utf8 = 'utf8', 'UTF-8' + gbk = 'gbk', 'GBK' + + name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True) + category = models.CharField(default='host', max_length=32, verbose_name=_("Category")) + type = models.CharField(max_length=32, default='linux', verbose_name=_("Type")) + meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta")) + internal = models.BooleanField(default=False, verbose_name=_("Internal")) + comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) + # 资产有关的 + charset = models.CharField( + default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset") + ) + domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled")) + 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")) + automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform', + blank=True, null=True, verbose_name=_("Automation")) + + @property + def type_constraints(self): + return AllTypes.get_constraints(self.category, self.type) + + @classmethod + def default(cls): + linux, created = cls.objects.get_or_create( + defaults={'name': 'Linux'}, name='Linux' + ) + return linux.id + + @property + def primary_protocol(self): + primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type) + return self.protocols.filter(name=primary_protocol_name).first() + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Platform") + # ordering = ('name',) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py deleted file mode 100644 index 4c7454594..000000000 --- a/apps/assets/models/user.py +++ /dev/null @@ -1,395 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import logging - -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.core.validators import MinValueValidator, MaxValueValidator -from django.core.cache import cache - -from common.utils import signer, get_object_or_none -from .base import BaseUser -from .asset import Asset -from .authbook import AuthBook - - -__all__ = ['AdminUser', 'SystemUser'] -logger = logging.getLogger(__name__) - - -class ProtocolMixin: - protocol: str - - class Protocol(models.TextChoices): - ssh = 'ssh', 'SSH' - rdp = 'rdp', 'RDP' - telnet = 'telnet', 'Telnet' - vnc = 'vnc', 'VNC' - mysql = 'mysql', 'MySQL' - oracle = 'oracle', 'Oracle' - mariadb = 'mariadb', 'MariaDB' - postgresql = 'postgresql', 'PostgreSQL' - sqlserver = 'sqlserver', 'SQLServer' - redis = 'redis', 'Redis' - mongodb = 'mongodb', 'MongoDB' - clickhouse = 'clickhouse', 'ClickHouse' - k8s = 'k8s', 'K8S' - - SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp] - - ASSET_CATEGORY_PROTOCOLS = [ - Protocol.ssh, Protocol.rdp, Protocol.telnet, Protocol.vnc - ] - APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [ - Protocol.rdp - ] - APPLICATION_CATEGORY_DB_PROTOCOLS = [ - Protocol.mysql, Protocol.mariadb, Protocol.oracle, - Protocol.postgresql, Protocol.sqlserver, Protocol.clickhouse, - Protocol.redis, Protocol.mongodb - ] - APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [ - Protocol.k8s - ] - APPLICATION_CATEGORY_PROTOCOLS = [ - *APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS, - *APPLICATION_CATEGORY_DB_PROTOCOLS, - *APPLICATION_CATEGORY_CLOUD_PROTOCOLS - ] - - @property - def is_protocol_support_push(self): - return self.protocol in self.SUPPORT_PUSH_PROTOCOLS - - @classmethod - def get_protocol_by_application_type(cls, app_type): - from applications.const import AppType - if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: - protocol = app_type - elif app_type in AppType.remote_app_types(): - protocol = cls.Protocol.rdp - else: - protocol = None - return protocol - - @property - def can_perm_to_asset(self): - return self.protocol in self.ASSET_CATEGORY_PROTOCOLS - - @property - def is_asset_protocol(self): - return self.protocol in self.ASSET_CATEGORY_PROTOCOLS - - -class AuthMixin: - username_same_with_user: bool - protocol: str - ASSET_CATEGORY_PROTOCOLS: list - login_mode: str - LOGIN_MANUAL: str - id: str - username: str - password: str - private_key: str - public_key: str - - def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300): - if not auth: - raise ValueError('Auth not set') - key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) - logger.debug(f'Set system user temp auth: {key}') - cache.set(key, auth, ttl) - - def get_temp_auth(self, asset_or_app_id, user_id): - key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) - logger.debug(f'Get system user temp auth: {key}') - password = cache.get(key) - return password - - def _clean_auth_info_if_manual_login_mode(self): - if self.login_mode == self.LOGIN_MANUAL: - self.password = '' - self.private_key = '' - self.public_key = '' - - def _load_tmp_auth_if_has(self, asset_or_app_id, user_id): - if self.login_mode != self.LOGIN_MANUAL: - return - - if not asset_or_app_id or not user_id: - return - - auth = self.get_temp_auth(asset_or_app_id, user_id) - if not auth: - return - - username = auth.get('username') - password = auth.get('password') - - if username: - self.username = username - if password: - self.password = password - - def load_app_more_auth(self, app_id=None, username=None, user_id=None): - # 清除认证信息 - self._clean_auth_info_if_manual_login_mode() - - # 先加载临时认证信息 - if self.login_mode == self.LOGIN_MANUAL: - self._load_tmp_auth_if_has(app_id, user_id) - return - - # Remote app - from applications.models import Application - app = get_object_or_none(Application, pk=app_id) - if app and app.category_remote_app: - # Remote app - self._load_remoteapp_more_auth(app, username, user_id) - return - - # Other app - # 更新用户名 - from users.models import User - user = get_object_or_none(User, pk=user_id) if user_id else None - if self.username_same_with_user: - if user and not username: - _username = user.username - else: - _username = username - self.username = _username - - def _load_remoteapp_more_auth(self, app, username, user_id): - asset = app.get_remote_app_asset(raise_exception=False) - if asset: - self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id) - - def load_asset_special_auth(self, asset, username=''): - """ - AuthBook 的数据状态 - | asset | systemuser | username | - 1 | * | * | x | - 2 | * | x | * | - - 当前 AuthBook 只有以上两种状态,systemuser 与 username 不会并存。 - 正常的资产与系统用户关联产生的是第1种状态,改密则产生第2种状态。改密之后 - 只有 username 而没有 systemuser 。 - - Freq: 关联同一资产的多个系统用户指定同一用户名时,修改用户密码会影响所有系统用户 - - 这里有一个不对称的行为,同名系统用户密码覆盖 - 当有相同 username 的多个系统用户时,有改密动作之后,所有的同名系统用户都使用最后 - 一次改动,但如果没有发生过改密,同名系统用户使用的密码还是各自的。 - - """ - if username == '': - username = self.username - - authbook = AuthBook.objects.filter( - asset=asset, username=username, systemuser__isnull=True - ).order_by('-date_created').first() - - if not authbook: - authbook = AuthBook.objects.filter( - asset=asset, systemuser=self - ).order_by('-date_created').first() - - if not authbook: - return None - - authbook.load_auth() - self.password = authbook.password - self.private_key = authbook.private_key - self.public_key = authbook.public_key - - def load_asset_more_auth(self, asset_id=None, username=None, user_id=None): - from users.models import User - self._clean_auth_info_if_manual_login_mode() - # 加载临时认证信息 - if self.login_mode == self.LOGIN_MANUAL: - self._load_tmp_auth_if_has(asset_id, user_id) - return - # 更新用户名 - user = get_object_or_none(User, pk=user_id) if user_id else None - if self.username_same_with_user: - if user and not username: - _username = user.username - else: - _username = username - self.username = _username - # 加载某个资产的特殊配置认证信息 - asset = get_object_or_none(Asset, pk=asset_id) if asset_id else None - if not asset: - logger.debug('Asset not found, pass') - return - self.load_asset_special_auth(asset, self.username) - - -class SystemUser(ProtocolMixin, AuthMixin, BaseUser): - LOGIN_AUTO = 'auto' - LOGIN_MANUAL = 'manual' - LOGIN_MODE_CHOICES = ( - (LOGIN_AUTO, _('Automatic managed')), - (LOGIN_MANUAL, _('Manually input')) - ) - - class Type(models.TextChoices): - common = 'common', _('Common user') - admin = 'admin', _('Admin user') - - username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user")) - nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) - assets = models.ManyToManyField( - 'assets.Asset', blank=True, verbose_name=_("Assets"), - through='assets.AuthBook', through_fields=['systemuser', 'asset'], - related_name='system_users' - ) - users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users")) - groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups")) - type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type')) - priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) - protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices, default='ssh', verbose_name=_('Protocol')) - auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) - sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) - shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) - login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) - sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) - token = models.TextField(default='', verbose_name=_('Token')) - home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) - system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) - ad_domain = models.CharField(default='', max_length=256) - # linux su 命令 (switch user) - su_enabled = models.BooleanField(default=False, verbose_name=_('User switch')) - su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from")) - - def __str__(self): - username = self.username - if self.username_same_with_user: - username = '*' - return '{0.name}({1})'.format(self, username) - - @property - def nodes_amount(self): - return self.nodes.all().count() - - @property - def login_mode_display(self): - return self.get_login_mode_display() - - def is_need_push(self): - if self.auto_push and self.is_protocol_support_push: - return True - else: - return False - - @property - def is_admin_user(self): - return self.type == self.Type.admin - - @property - def is_need_cmd_filter(self): - return self.protocol not in [self.Protocol.rdp, self.Protocol.vnc] - - @property - def is_need_test_asset_connective(self): - return self.protocol in self.ASSET_CATEGORY_PROTOCOLS - - @property - def cmd_filter_rules(self): - from .cmd_filter import CommandFilterRule - rules = CommandFilterRule.objects.filter( - filter__in=self.cmd_filters.all() - ).distinct() - return rules - - def is_command_can_run(self, command): - for rule in self.cmd_filter_rules: - action, matched_cmd = rule.match(command) - if action == rule.ActionChoices.allow: - return True, None - elif action == rule.ActionChoices.deny: - return False, matched_cmd - return True, None - - def get_all_assets(self): - from assets.models import Node - nodes_keys = self.nodes.all().values_list('key', flat=True) - asset_ids = set(self.assets.all().values_list('id', flat=True)) - nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys) - asset_ids.update(nodes_asset_ids) - assets = Asset.objects.filter(id__in=asset_ids) - return assets - - def add_related_assets(self, assets_or_ids): - self.assets.add(*tuple(assets_or_ids)) - self.add_related_assets_to_su_from_if_need(assets_or_ids) - - def add_related_assets_to_su_from_if_need(self, assets_or_ids): - if self.protocol not in [self.Protocol.ssh.value]: - return - if not self.su_enabled: - return - if not self.su_from: - return - if self.su_from.protocol != self.protocol: - return - self.su_from.assets.add(*tuple(assets_or_ids)) - - class Meta: - ordering = ['name'] - unique_together = [('name', 'org_id')] - verbose_name = _("System user") - permissions = [ - ('match_systemuser', _('Can match system user')), - ] - - -# Deprecated: 准备废弃 -class AdminUser(BaseUser): - """ - A privileged user that ansible can use it to push system user and so on - """ - BECOME_METHOD_CHOICES = ( - ('sudo', 'sudo'), - ('su', 'su'), - ) - become = models.BooleanField(default=True) - become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) - become_user = models.CharField(default='root', max_length=64) - _become_pass = models.CharField(default='', blank=True, max_length=128) - CONNECTIVITY_CACHE_KEY = '_ADMIN_USER_CONNECTIVE_{}' - _prefer = "admin_user" - - def __str__(self): - return self.name - - @property - def become_pass(self): - password = signer.unsign(self._become_pass) - if password: - return password - else: - return "" - - @become_pass.setter - def become_pass(self, password): - self._become_pass = signer.sign(password) - - @property - def become_info(self): - if self.become: - info = { - "method": self.become_method, - "user": self.become_user, - "pass": self.become_pass, - } - else: - info = None - return info - - class Meta: - ordering = ['name'] - unique_together = [('name', 'org_id')] - verbose_name = _("Admin user") diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index 90b0ee178..d60c5a28c 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- # -from django.utils import timezone -from django.core.cache import cache from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -11,27 +9,77 @@ from common.utils import validate_ssh_private_key __all__ = [ - 'init_model', 'generate_fake', 'private_key_validator', + 'private_key_validator', ] -def init_model(): - from . import SystemUser, AdminUser, Asset - for cls in [SystemUser, AdminUser, Asset]: - if hasattr(cls, 'initial'): - cls.initial() - - -def generate_fake(): - from . import SystemUser, AdminUser, Asset - for cls in [SystemUser, AdminUser, Asset]: - if hasattr(cls, 'generate_fake'): - cls.generate_fake() - - def private_key_validator(value): if not validate_ssh_private_key(value): raise ValidationError( _('%(value)s is not an even number'), params={'value': 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'}, + {'name': 'Unix', 'category': 'host', 'type': 'unix'}, + {'name': 'MacOS', 'category': 'host', 'type': 'unix'}, + {'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'}, + { + 'name': 'Windows-TLS', 'category': 'host', 'type': 'windows', + 'protocols': [ + {'name': 'rdp', 'port': 3389, 'setting': {'security': 'tls'}}, + {'name': 'ssh', 'port': 22}, + ] + }, + { + 'name': 'Windows-RDP', 'category': 'host', 'type': 'windows', + 'protocols': [ + {'name': 'rdp', 'port': 3389, 'setting': {'security': 'rdp'}}, + {'name': 'ssh', 'port': 22}, + ] + }, + # 数据库 + {'name': 'MySQL', 'category': 'database', 'type': 'mysql'}, + {'name': 'PostgreSQL', 'category': 'database', 'type': 'postgresql'}, + {'name': 'Oracle', 'category': 'database', 'type': 'oracle'}, + {'name': 'SQLServer', 'category': 'database', 'type': 'sqlserver'}, + {'name': 'MongoDB', 'category': 'database', 'type': 'mongodb'}, + {'name': 'Redis', 'category': 'database', 'type': 'redis'}, + + # 网络设备 + {'name': 'Generic', 'category': 'device', 'type': 'general'}, + {'name': 'Huawei', 'category': 'device', 'type': 'general'}, + {'name': 'Cisco', 'category': 'device', 'type': 'general'}, + {'name': 'H3C', 'category': 'device', 'type': 'general'}, + + # Web + {'name': 'Website', 'category': 'web', 'type': 'general'}, + + # Cloud + {'name': 'Kubernetes', 'category': 'cloud', 'type': 'k8s'}, + {'name': 'VMware vSphere', 'category': 'cloud', 'type': 'private'}, + ] + + platforms = platform_model.objects.all() + + updated = [] + for p in platforms: + attrs = platform_ops_map.get((p.category, p.type), {}) + if not attrs: + continue + for k, v in attrs.items(): + setattr(p, k, v) + updated.append(p) + platform_model.objects.bulk_update(updated, list(default_ok.keys())) diff --git a/apps/assets/notifications.py b/apps/assets/notifications.py index cded5a005..a797bc845 100644 --- a/apps/assets/notifications.py +++ b/apps/assets/notifications.py @@ -15,11 +15,35 @@ class AccountBackupExecutionTaskMsg(object): def message(self): name = self.name if self.user.secret_key: - return _('{} - The account backup passage task has been completed. See the attachment for details').format(name) + return _('{} - The account backup passage task has been completed. See the attachment for details').format( + name) return _("{} - 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").format(name) + "please go to personal information -> file encryption password to set the encryption password").format( + name) def publish(self, attachment_list=None): send_mail_attachment_async( self.subject, self.message, [self.user.email], attachment_list ) + + +class ChangeSecretExecutionTaskMsg(object): + subject = _('Notification of implementation result of encryption change plan') + + def __init__(self, name: str, user: User): + self.name = name + self.user = user + + @property + def message(self): + name = self.name + if self.user.secret_key: + return _('{} - The encryption change task has been completed. See the attachment for details').format(name) + return _("{} - 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").format( + name) + + def publish(self, attachments=None): + send_mail_attachment_async( + self.subject, self.message, [self.user.email], attachments + ) diff --git a/apps/assets/pagination.py b/apps/assets/pagination.py index f75ff023c..8ae42ef16 100644 --- a/apps/assets/pagination.py +++ b/apps/assets/pagination.py @@ -8,6 +8,9 @@ logger = get_logger(__name__) class AssetPaginationBase(LimitOffsetPagination): + _request = None + _view = None + _user = None def init_attrs(self, queryset, request: Request, view=None): self._request = request @@ -28,7 +31,8 @@ class AssetPaginationBase(LimitOffsetPagination): } for k, v in self._request.query_params.items(): if k not in exclude_query_params and v is not None: - logger.warn(f'Not hit node.assets_amount because find a unknow query_param `{k}` -> {self._request.get_full_path()}') + logger.warn(f'Not hit node.assets_amount because find a unknown query_param ' + f'`{k}` -> {self._request.get_full_path()}') return super().get_count(queryset) node_assets_count = self.get_count_from_nodes(queryset) if node_assets_count is None: @@ -42,11 +46,11 @@ class AssetPaginationBase(LimitOffsetPagination): class NodeAssetTreePagination(AssetPaginationBase): def get_count_from_nodes(self, queryset): is_query_all = self._view.is_query_node_all_assets - if is_query_all: - node = self._view.node - if not node: - node = Node.org_root() - if node: - logger.debug(f'Hit node.assets_amount[{node.assets_amount}] -> {self._request.get_full_path()}') - return node.assets_amount - return None + if not is_query_all: + return None + node = self._view.node + if not node: + node = Node.org_root() + if node: + logger.debug(f'Hit node assets_amount cache: [{node.assets_amount}]') + return node.assets_amount diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 3f1222fc5..9876e3aa6 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -2,14 +2,13 @@ # from .asset import * -from .admin_user import * from .label import * -from .system_user import * from .node import * from .domain import * -from .cmd_filter import * from .gathered_user import * from .favorite_asset import * from .account import * -from .account_history import * -from .backup import * +from assets.serializers.account.backup import * +from .platform import * +from .cagegory import * +from .automations import * diff --git a/apps/assets/serializers/account.py b/apps/assets/serializers/account.py index 9b06944c3..e69de29bb 100644 --- a/apps/assets/serializers/account.py +++ b/apps/assets/serializers/account.py @@ -1,108 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from assets.models import AuthBook -from orgs.mixins.serializers import BulkOrgResourceModelSerializer - -from .base import AuthSerializerMixin -from common.utils.encode import ssh_pubkey_gen -from common.drf.serializers import SecretReadableMixin - - -class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): - ip = serializers.ReadOnlyField(label=_("IP")) - hostname = serializers.ReadOnlyField(label=_("Hostname")) - platform = serializers.ReadOnlyField(label=_("Platform")) - protocols = serializers.SerializerMethodField(label=_("Protocols")) - date_created = serializers.DateTimeField( - label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True - ) - date_updated = serializers.DateTimeField( - label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True - ) - - class Meta: - model = AuthBook - fields_mini = ['id', 'username', 'ip', 'hostname', 'platform', 'protocols', 'version'] - fields_write_only = ['password', 'private_key', "public_key", 'passphrase'] - fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment'] - fields_small = fields_mini + fields_write_only + fields_other - fields_fk = ['asset', 'systemuser', 'systemuser_display'] - fields = fields_small + fields_fk - extra_kwargs = { - 'username': {'required': True}, - 'private_key': {'write_only': True}, - 'public_key': {'write_only': True}, - 'systemuser_display': {'label': _('System user display')} - } - ref_name = 'AssetAccountSerializer' - - def _validate_gen_key(self, attrs): - private_key = attrs.get('private_key') - if not private_key: - return attrs - - password = attrs.get('passphrase') - username = attrs.get('username') - public_key = ssh_pubkey_gen(private_key, password=password, username=username) - attrs['public_key'] = public_key - return attrs - - def validate(self, attrs): - attrs = self._validate_gen_key(attrs) - return attrs - - def get_protocols(self, v): - """ protocols 是 queryset 中返回的,Post 创建成功后返回序列化时没有这个字段 """ - if hasattr(v, 'protocols'): - protocols = v.protocols - elif hasattr(v, 'asset') and v.asset: - protocols = v.asset.protocols - else: - protocols = '' - protocols = protocols.replace(' ', ', ') - return protocols - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('systemuser', 'asset') - return queryset - - def to_representation(self, instance): - instance.load_auth() - return super().to_representation(instance) - - -class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): - class Meta(AccountSerializer.Meta): - extra_kwargs = { - 'password': {'write_only': False}, - 'private_key': {'write_only': False}, - 'public_key': {'write_only': False}, - 'systemuser_display': {'label': _('System user display')} - } - - -class AccountBackUpSerializer(AccountSecretSerializer): - class Meta(AccountSecretSerializer.Meta): - fields = [ - 'id', 'hostname', 'ip', 'username', 'password', - 'private_key', 'public_key', 'date_created', - 'date_updated', 'version' - ] - - @classmethod - def setup_eager_loading(cls, queryset): - return queryset - - def to_representation(self, instance): - return super(AccountSerializer, self).to_representation(instance) - - -class AccountTaskSerializer(serializers.Serializer): - ACTION_CHOICES = ( - ('test', 'test'), - ) - action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) - task = serializers.CharField(read_only=True) diff --git a/apps/assets/serializers/account/__init__.py b/apps/assets/serializers/account/__init__.py new file mode 100644 index 000000000..062b7064c --- /dev/null +++ b/apps/assets/serializers/account/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .template import * diff --git a/apps/assets/serializers/account/account.py b/apps/assets/serializers/account/account.py new file mode 100644 index 000000000..c7dad4f5b --- /dev/null +++ b/apps/assets/serializers/account/account.py @@ -0,0 +1,110 @@ +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.models import Account, AccountTemplate, Asset +from .base import BaseAccountSerializer +from assets.const import SecretType + + +class AccountSerializerCreateMixin(serializers.ModelSerializer): + template = serializers.UUIDField( + required=False, allow_null=True, write_only=True, + label=_('Account template') + ) + push_now = serializers.BooleanField( + default=False, label=_("Push now"), write_only=True + ) + has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) + + @staticmethod + def validate_template(value): + try: + return AccountTemplate.objects.get(id=value) + except AccountTemplate.DoesNotExist: + raise serializers.ValidationError(_('Account template not found')) + + @staticmethod + def replace_attrs(account_template: AccountTemplate, attrs: dict): + exclude_fields = [ + '_state', 'org_id', 'id', 'date_created', + 'date_updated' + ] + template_attrs = { + k: v for k, v in account_template.__dict__.items() + if k not in exclude_fields + } + for k, v in template_attrs.items(): + attrs.setdefault(k, v) + + def validate(self, attrs): + account_template = attrs.pop('template', None) + if account_template: + self.replace_attrs(account_template, attrs) + self.push_now = attrs.pop('push_now', False) + return super().validate(attrs) + + def create(self, validated_data): + instance = super().create(validated_data) + if self.push_now: + push_accounts_to_assets.delay([instance.id], [instance.asset_id]) + return instance + + +class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer): + asset = ObjectRelatedField( + required=False, queryset=Asset.objects, + label=_('Asset'), attrs=('id', 'name', 'address', 'platform_id') + ) + + class Meta(BaseAccountSerializer.Meta): + model = Account + fields = BaseAccountSerializer.Meta.fields \ + + ['su_from', 'version', 'asset'] \ + + ['template', 'push_now'] + 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') + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related('asset', 'asset__platform') + return queryset + + +class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): + class Meta(AccountSerializer.Meta): + extra_kwargs = { + 'secret': {'write_only': False}, + } + + +class AccountHistorySerializer(serializers.ModelSerializer): + secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type')) + + class Meta: + model = Account.history.model + fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user'] + read_only_fields = fields + + +class AccountTaskSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ('test', 'test'), + ) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) + task = serializers.CharField(read_only=True) diff --git a/apps/assets/serializers/backup.py b/apps/assets/serializers/account/backup.py similarity index 71% rename from apps/assets/serializers/backup.py rename to apps/assets/serializers/account/backup.py index c95d0f394..06cf4e2f9 100644 --- a/apps/assets/serializers/backup.py +++ b/apps/assets/serializers/account/backup.py @@ -6,10 +6,10 @@ from rest_framework import serializers from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ops.mixin import PeriodTaskSerializerMixin from common.utils import get_logger +from common.const.choices import Trigger +from common.drf.fields import LabeledChoiceField -from .base import TypesField - -from ..models import AccountBackupPlan, AccountBackupPlanExecution +from assets.models import AccountBackupPlan, AccountBackupPlanExecution logger = get_logger(__file__) @@ -17,8 +17,6 @@ __all__ = ['AccountBackupPlanSerializer', 'AccountBackupPlanExecutionSerializer' class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): - types = TypesField(required=False, allow_null=True, label=_("Actions")) - class Meta: model = AccountBackupPlan fields = [ @@ -36,17 +34,12 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): - trigger_display = serializers.ReadOnlyField( - source='get_trigger_display', label=_('Trigger mode') - ) + trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode')) class Meta: model = AccountBackupPlanExecution - fields = [ - 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', - 'is_success', 'plan', 'org_id', 'recipients', 'trigger_display' - ] - read_only_fields = ( + read_only_fields = [ 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', 'is_success', 'org_id', 'recipients' - ) + ] + fields = read_only_fields + ['plan'] diff --git a/apps/assets/serializers/account/base.py b/apps/assets/serializers/account/base.py new file mode 100644 index 000000000..c0e3553e8 --- /dev/null +++ b/apps/assets/serializers/account/base.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import gettext_lazy as _ + +from assets.models import BaseAccount +from assets.serializers.base import AuthValidateMixin +from orgs.mixins.serializers import BulkOrgResourceModelSerializer + +__all__ = ['BaseAccountSerializer'] + + +class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): + class Meta: + model = BaseAccount + fields_mini = ['id', 'name', 'username'] + fields_small = fields_mini + [ + 'secret_type', 'secret', 'has_secret', 'passphrase', + 'privileged', 'is_active', 'specific', + ] + fields_other = ['created_by', 'date_created', 'date_updated', 'comment'] + fields = fields_small + fields_other + read_only_fields = [ + 'has_secret', 'specific', + 'date_verified', 'created_by', 'date_created', + ] + extra_kwargs = { + 'specific': {'label': _('Specific')}, + } diff --git a/apps/assets/serializers/account/template.py b/apps/assets/serializers/account/template.py new file mode 100644 index 000000000..7a7de7f11 --- /dev/null +++ b/apps/assets/serializers/account/template.py @@ -0,0 +1,27 @@ +from common.drf.serializers import SecretReadableMixin +from assets.models import AccountTemplate +from .base import BaseAccountSerializer + + +class AccountTemplateSerializer(BaseAccountSerializer): + class Meta(BaseAccountSerializer.Meta): + model = AccountTemplate + + # @classmethod + # def validate_required(cls, attrs): + # # TODO 选择模版后检查一些必填项 + # required_field_dict = {} + # error = _('This field is required.') + # for k, v in cls().fields.items(): + # if v.required and k not in attrs: + # required_field_dict[k] = error + # if not required_field_dict: + # return + # raise serializers.ValidationError(required_field_dict) + + +class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer): + class Meta(AccountTemplateSerializer.Meta): + extra_kwargs = { + 'secret': {'write_only': False}, + } diff --git a/apps/assets/serializers/account_history.py b/apps/assets/serializers/account_history.py deleted file mode 100644 index fb2844182..000000000 --- a/apps/assets/serializers/account_history.py +++ /dev/null @@ -1,38 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from assets.models import AuthBook -from common.drf.serializers import SecretReadableMixin -from .account import AccountSerializer, AccountSecretSerializer - - -class AccountHistorySerializer(AccountSerializer): - systemuser_display = serializers.SerializerMethodField(label=_('System user display')) - - class Meta: - model = AuthBook.history.model - fields = AccountSerializer.Meta.fields_mini + \ - AccountSerializer.Meta.fields_write_only + \ - AccountSerializer.Meta.fields_fk + \ - ['history_id', 'date_created', 'date_updated'] - read_only_fields = fields - ref_name = 'AccountHistorySerializer' - - @staticmethod - def get_systemuser_display(instance): - if not instance.systemuser: - return '' - return str(instance.systemuser) - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields = list(set(fields) - {'org_name'}) - return fields - - def to_representation(self, instance): - return super(AccountSerializer, self).to_representation(instance) - - -class AccountHistorySecretSerializer(SecretReadableMixin, AccountHistorySerializer): - class Meta(AccountHistorySerializer.Meta): - extra_kwargs = AccountSecretSerializer.Meta.extra_kwargs diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py deleted file mode 100644 index b6ab18af3..000000000 --- a/apps/assets/serializers/admin_user.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# -from ..models import SystemUser -from .system_user import SystemUserSerializer as SuS - - -class AdminUserSerializer(SuS): - """ - 管理用户 - """ - - class Meta(SuS.Meta): - fields = SuS.Meta.fields_mini + \ - SuS.Meta.fields_write_only + \ - SuS.Meta.fields_m2m + \ - [ - 'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint', - 'su_enabled', 'su_from', - 'date_created', 'date_updated', 'comment', 'created_by', - ] - - def validate_type(self, val): - return SystemUser.Type.admin - - def validate_protocol(self, val): - return 'ssh' diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py deleted file mode 100644 index 5211cfef6..000000000 --- a/apps/assets/serializers/asset.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework import serializers -from django.core.validators import RegexValidator -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import Asset, Node, Platform, SystemUser - -__all__ = [ - 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', - 'ProtocolsField', 'PlatformSerializer', - 'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField', -] - - -class ProtocolField(serializers.RegexField): - protocols = '|'.join(dict(Asset.Protocol.choices).keys()) - default_error_messages = { - 'invalid': _('Protocol format should {}/{}').format(protocols, '1-65535') - } - regex = r'^(%s)/(\d{1,5})$' % protocols - - def __init__(self, *args, **kwargs): - super().__init__(self.regex, **kwargs) - - -def validate_duplicate_protocols(values): - errors = [] - names = [] - - for value in values: - if not value or '/' not in value: - continue - name = value.split('/')[0] - if name in names: - errors.append(_("Protocol duplicate: {}").format(name)) - names.append(name) - errors.append('') - if any(errors): - raise serializers.ValidationError(errors) - - -class ProtocolsField(serializers.ListField): - default_validators = [validate_duplicate_protocols] - - def __init__(self, *args, **kwargs): - kwargs['child'] = ProtocolField() - kwargs['allow_null'] = True - kwargs['allow_empty'] = True - kwargs['min_length'] = 1 - kwargs['max_length'] = 4 - super().__init__(*args, **kwargs) - - def to_representation(self, value): - if not value: - return [] - return value.split(' ') - - -class AssetSerializer(BulkOrgResourceModelSerializer): - platform = serializers.SlugRelatedField( - slug_field='name', queryset=Platform.objects.all(), label=_("Platform") - ) - protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22']) - domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name')) - nodes_display = serializers.ListField( - child=serializers.CharField(), label=_('Nodes name'), required=False - ) - labels_display = serializers.ListField( - child=serializers.CharField(), label=_('Labels name'), required=False, read_only=True - ) - - """ - 资产的数据结构 - """ - - class Meta: - model = Asset - fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols'] - fields_small = fields_mini + [ - 'protocol', 'port', 'protocols', 'is_active', - 'public_ip', 'number', 'comment', - ] - fields_hardware = [ - 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', - 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', - 'os', 'os_version', 'os_arch', 'hostname_raw', - 'cpu_info', 'hardware_info', - ] - fields_fk = [ - 'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display' - ] - fields_m2m = [ - 'nodes', 'nodes_display', 'labels', 'labels_display', - ] - read_only_fields = [ - 'connectivity', 'date_verified', 'cpu_info', 'hardware_info', - 'created_by', 'date_created', - ] - fields = fields_small + fields_hardware + fields_fk + fields_m2m + read_only_fields - extra_kwargs = { - 'protocol': {'write_only': True}, - 'port': {'write_only': True}, - 'hardware_info': {'label': _('Hardware info'), 'read_only': True}, - 'admin_user_display': {'label': _('Admin user display'), 'read_only': True}, - 'cpu_info': {'label': _('CPU info')}, - } - - def get_fields(self): - fields = super().get_fields() - - admin_user_field = fields.get('admin_user') - # 因为 mixin 中对 fields 有处理,可能不需要返回 admin_user - if admin_user_field: - admin_user_field.queryset = SystemUser.objects.filter(type=SystemUser.Type.admin) - return fields - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('domain', 'platform', 'admin_user') - queryset = queryset.prefetch_related('nodes', 'labels') - return queryset - - def compatible_with_old_protocol(self, validated_data): - protocols_data = validated_data.pop("protocols", []) - - # 兼容老的api - name = validated_data.get("protocol") - port = validated_data.get("port") - if not protocols_data and name and port: - protocols_data.insert(0, '/'.join([name, str(port)])) - elif not name and not port and protocols_data: - protocol = protocols_data[0].split('/') - validated_data["protocol"] = protocol[0] - validated_data["port"] = int(protocol[1]) - if protocols_data: - validated_data["protocols"] = ' '.join(protocols_data) - - def perform_nodes_display_create(self, instance, nodes_display): - if not nodes_display: - return - nodes_to_set = [] - for full_value in nodes_display: - node = Node.objects.filter(full_value=full_value).first() - if node: - nodes_to_set.append(node) - else: - node = Node.create_node_by_full_value(full_value) - nodes_to_set.append(node) - instance.nodes.set(nodes_to_set) - - def create(self, validated_data): - self.compatible_with_old_protocol(validated_data) - nodes_display = validated_data.pop('nodes_display', '') - instance = super().create(validated_data) - self.perform_nodes_display_create(instance, nodes_display) - return instance - - def update(self, instance, validated_data): - nodes_display = validated_data.pop('nodes_display', '') - self.compatible_with_old_protocol(validated_data) - instance = super().update(instance, validated_data) - self.perform_nodes_display_create(instance, nodes_display) - return instance - - -class MiniAssetSerializer(serializers.ModelSerializer): - class Meta: - model = Asset - fields = AssetSerializer.Meta.fields_mini - - -class PlatformSerializer(serializers.ModelSerializer): - meta = serializers.DictField(required=False, allow_null=True, label=_('Meta')) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # TODO 修复 drf SlugField RegexValidator bug,之后记得删除 - validators = self.fields['name'].validators - if isinstance(validators[-1], RegexValidator): - validators.pop() - - class Meta: - model = Platform - fields = [ - 'id', 'name', 'base', 'charset', - 'internal', 'meta', 'comment' - ] - extra_kwargs = { - 'internal': {'read_only': True}, - } - - -class AssetSimpleSerializer(serializers.ModelSerializer): - class Meta: - model = Asset - fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified'] - - -class AssetsTaskSerializer(serializers.Serializer): - ACTION_CHOICES = ( - ('refresh', 'refresh'), - ('test', 'test'), - ) - task = serializers.CharField(read_only=True) - action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) - assets = serializers.PrimaryKeyRelatedField( - queryset=Asset.objects, required=False, allow_empty=True, many=True - ) - - -class AssetTaskSerializer(AssetsTaskSerializer): - ACTION_CHOICES = tuple(list(AssetsTaskSerializer.ACTION_CHOICES) + [ - ('push_system_user', 'push_system_user'), - ('test_system_user', 'test_system_user') - ]) - action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) - asset = serializers.PrimaryKeyRelatedField( - queryset=Asset.objects, required=False, allow_empty=True, many=False - ) - system_users = serializers.PrimaryKeyRelatedField( - queryset=SystemUser.objects, required=False, allow_empty=True, many=True - ) diff --git a/apps/assets/serializers/asset/__init__.py b/apps/assets/serializers/asset/__init__.py new file mode 100644 index 000000000..12f1eb66c --- /dev/null +++ b/apps/assets/serializers/asset/__init__.py @@ -0,0 +1,6 @@ +from .common import * +from .host import * +from .database import * +from .device import * +from .cloud import * +from .web import * diff --git a/apps/assets/serializers/asset/cloud.py b/apps/assets/serializers/asset/cloud.py new file mode 100644 index 000000000..fa1e7d33a --- /dev/null +++ b/apps/assets/serializers/asset/cloud.py @@ -0,0 +1,16 @@ +from assets.models import Cloud +from .common import AssetSerializer + +__all__ = ['CloudSerializer'] + + +class CloudSerializer(AssetSerializer): + class Meta(AssetSerializer.Meta): + model = Cloud + fields = AssetSerializer.Meta.fields + extra_kwargs = { + **AssetSerializer.Meta.extra_kwargs, + 'address': { + 'label': 'URL' + } + } diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py new file mode 100644 index 000000000..0fd9e9e8a --- /dev/null +++ b/apps/assets/serializers/asset/common.py @@ -0,0 +1,225 @@ +# -*- 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 common.drf.serializers import WritableNestedModelSerializer +from common.drf.fields import LabeledChoiceField, ObjectRelatedField +from orgs.mixins.serializers import OrgResourceSerializerMixin +from ..account import AccountSerializer +from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol +from ...const import Category, AllTypes + +__all__ = [ + 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', + 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', +] + + +class AssetProtocolsSerializer(serializers.ModelSerializer): + class Meta: + model = Protocol + fields = ['id', 'name', 'port'] + + +class AssetLabelSerializer(serializers.ModelSerializer): + class Meta: + model = Label + fields = ['id', 'name', 'value'] + extra_kwargs = { + 'name': {'required': False}, + 'value': {'required': False} + } + + +class AssetPlatformSerializer(serializers.ModelSerializer): + class Meta: + model = Platform + fields = ['id', 'name'] + extra_kwargs = { + 'name': {'required': False} + } + + +class AssetAccountSerializer(AccountSerializer): + add_org_fields = False + + class Meta(AccountSerializer.Meta): + fields_mini = [ + 'id', 'name', 'username', 'privileged', + 'version', 'secret_type', + ] + fields_write_only = [ + 'secret', 'push_now' + ] + fields = fields_mini + fields_write_only + + +class AssetSerializer(OrgResourceSerializerMixin, 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) + platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) + 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')) + automation_enabled_info = serializers.SerializerMethodField() + + class Meta: + model = Asset + fields_mini = ['id', 'name', 'address', 'automation_enabled_info'] + 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' + ] + 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_automation_enabled_info(obj): + automation = obj.platform.automation + return { + 'ping_enabled': automation.ping_enabled, + 'ansible_enabled': automation.ansible_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, + } + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related('domain', 'platform', 'protocols') \ + .annotate(category=F("platform__category")) \ + .annotate(type=F("platform__type")) + queryset = queryset.prefetch_related('nodes', 'labels') + return queryset + + def perform_nodes_display_create(self, instance, nodes_display): + if not nodes_display: + return + nodes_to_set = [] + for full_value in nodes_display: + if not full_value.startswith('/'): + full_value = '/' + instance.org.name + '/' + full_value + node = Node.objects.filter(full_value=full_value).first() + if node: + nodes_to_set.append(node) + else: + node = Node.create_node_by_full_value(full_value) + nodes_to_set.append(node) + instance.nodes.set(nodes_to_set) + + def validate_nodes(self, nodes): + if nodes: + return nodes + request = self.context.get('request') + if not request: + return [] + node_id = request.query_params.get('node_id') + if not node_id: + return [] + + def validate_protocols(self, protocols_data): + if not protocols_data: + protocols_data = [] + platform_id = self.initial_data.get('platform') + if isinstance(platform_id, dict): + platform_id = platform_id.get('id') or platform_id.get('pk') + platform = Platform.objects.filter(id=platform_id).first() + if not platform: + raise serializers.ValidationError({'platform': _("Platform not exist")}) + + protocols_data_map = {p['name']: p for p in protocols_data} + platform_protocols = platform.protocols.all() + protocols_default = [p for p in platform_protocols if p.default] + protocols_required = [p for p in platform_protocols if p.required or p.primary] + + if not protocols_data_map: + protocols_data_map = { + p.name: {'name': p.name, 'port': p.port} + for p in protocols_required + protocols_default + } + + protocols_not_found = [p.name for p in protocols_required if p.name not in protocols_data_map] + if protocols_not_found: + raise serializers.ValidationError({ + 'protocols': _("Protocol is required: {}").format(', '.join(protocols_not_found)) + }) + return protocols_data_map.values() + + @atomic + def create(self, validated_data): + nodes_display = validated_data.pop('nodes_display', '') + instance = super().create(validated_data) + self.perform_nodes_display_create(instance, nodes_display) + return instance + + @atomic + def update(self, instance, validated_data): + nodes_display = validated_data.pop('nodes_display', '') + instance = super().update(instance, validated_data) + self.perform_nodes_display_create(instance, nodes_display) + return instance + + +class MiniAssetSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + fields = AssetSerializer.Meta.fields_mini + + +class AssetSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + fields = [ + 'id', 'name', 'address', 'port', + 'connectivity', 'date_verified' + ] + + +class AssetsTaskSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ('refresh', 'refresh'), + ('test', 'test'), + ) + task = serializers.CharField(read_only=True) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) + assets = serializers.PrimaryKeyRelatedField( + queryset=Asset.objects, required=False, allow_empty=True, many=True + ) + + +class AssetTaskSerializer(AssetsTaskSerializer): + ACTION_CHOICES = tuple(list(AssetsTaskSerializer.ACTION_CHOICES) + [ + ('push_system_user', 'push_system_user'), + ('test_system_user', 'test_system_user') + ]) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) + asset = serializers.PrimaryKeyRelatedField( + queryset=Asset.objects, required=False, allow_empty=True, many=False + ) + accounts = serializers.PrimaryKeyRelatedField( + queryset=Account.objects, required=False, allow_empty=True, many=True + ) diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py new file mode 100644 index 000000000..d168d2ffe --- /dev/null +++ b/apps/assets/serializers/asset/database.py @@ -0,0 +1,11 @@ + +from assets.models import Database +from .common import AssetSerializer + +__all__ = ['DatabaseSerializer'] + + +class DatabaseSerializer(AssetSerializer): + class Meta(AssetSerializer.Meta): + model = Database + fields = AssetSerializer.Meta.fields + ['db_name'] diff --git a/apps/assets/serializers/asset/device.py b/apps/assets/serializers/asset/device.py new file mode 100644 index 000000000..edad96ad1 --- /dev/null +++ b/apps/assets/serializers/asset/device.py @@ -0,0 +1,10 @@ + +from assets.models import Device +from .common import AssetSerializer + +__all__ = ['DeviceSerializer'] + + +class DeviceSerializer(AssetSerializer): + class Meta(AssetSerializer.Meta): + model = Device diff --git a/apps/assets/serializers/asset/host.py b/apps/assets/serializers/asset/host.py new file mode 100644 index 000000000..e01e1bab9 --- /dev/null +++ b/apps/assets/serializers/asset/host.py @@ -0,0 +1,44 @@ +from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ + +from assets.models import Host +from .common import AssetSerializer + + +__all__ = ['HostInfoSerializer', 'HostSerializer'] + + +class HostInfoSerializer(serializers.Serializer): + vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor')) + model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model')) + sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number')) + + cpu_model = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU model')) + cpu_count = serializers.IntegerField(required=False, label=_('CPU count')) + cpu_cores = serializers.IntegerField(required=False, label=_('CPU cores')) + cpu_vcpus = serializers.IntegerField(required=False, label=_('CPU vcpus')) + memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory')) + disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total')) + disk_info = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk info')) + + os = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS')) + os_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version')) + os_arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch')) + hostname_raw = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Hostname raw')) + number = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Asset number')) + + +class HostSerializer(AssetSerializer): + info = HostInfoSerializer(required=False) + + class Meta(AssetSerializer.Meta): + model = Host + fields = AssetSerializer.Meta.fields + ['info'] + extra_kwargs = { + **AssetSerializer.Meta.extra_kwargs, + 'address': { + 'label': _("IP/Host") + }, + } + + diff --git a/apps/assets/serializers/asset/web.py b/apps/assets/serializers/asset/web.py new file mode 100644 index 000000000..333795473 --- /dev/null +++ b/apps/assets/serializers/asset/web.py @@ -0,0 +1,30 @@ + +from assets.models import Web +from .common import AssetSerializer + +__all__ = ['WebSerializer'] + + +class WebSerializer(AssetSerializer): + class Meta(AssetSerializer.Meta): + model = Web + fields = AssetSerializer.Meta.fields + [ + 'autofill', 'username_selector', + 'password_selector', 'submit_selector', + 'script' + ] + extra_kwargs = { + **AssetSerializer.Meta.extra_kwargs, + 'address': { + 'label': 'URL' + }, + 'username_selector': { + 'default': 'input[type=text]' + }, + 'password_selector': { + 'default': 'input[type=password]' + }, + 'submit_selector': { + 'default': 'button[type=submit]', + }, + } diff --git a/apps/assets/serializers/automations/__init__.py b/apps/assets/serializers/automations/__init__.py new file mode 100644 index 000000000..e4daeda95 --- /dev/null +++ b/apps/assets/serializers/automations/__init__.py @@ -0,0 +1,3 @@ +from .base import * +from .change_secret import * +from .gather_accounts import * diff --git a/apps/assets/serializers/automations/base.py b/apps/assets/serializers/automations/base.py new file mode 100644 index 000000000..3ff3f6686 --- /dev/null +++ b/apps/assets/serializers/automations/base.py @@ -0,0 +1,81 @@ +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from ops.mixin import PeriodTaskSerializerMixin +from assets.const import AutomationTypes +from assets.models import Asset, Node, BaseAutomation, AutomationExecution +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.utils import get_logger +from common.drf.fields import ObjectRelatedField + +logger = get_logger(__file__) + +__all__ = [ + 'BaseAutomationSerializer', 'AutomationExecutionSerializer', + 'UpdateAssetSerializer', 'UpdateNodeSerializer', 'AutomationAssetsSerializer', +] + + +class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): + assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets')) + nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes')) + + class Meta: + read_only_fields = [ + 'date_created', 'date_updated', 'created_by', 'periodic_display' + ] + fields = read_only_fields + [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', + 'type', 'accounts', 'nodes', 'assets', 'is_active' + ] + extra_kwargs = { + 'name': {'required': True}, + 'type': {'read_only': True}, + 'periodic_display': {'label': _('Periodic perform')}, + } + + +class AutomationExecutionSerializer(serializers.ModelSerializer): + snapshot = serializers.SerializerMethodField(label=_('Automation snapshot')) + type = serializers.ChoiceField(choices=AutomationTypes.choices, write_only=True, label=_('Type')) + trigger_display = serializers.ReadOnlyField(source='get_trigger_display', label=_('Trigger mode')) + + class Meta: + model = AutomationExecution + read_only_fields = [ + 'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status' + ] + fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields + + @staticmethod + def get_snapshot(obj): + tp = obj.snapshot['type'] + snapshot = { + 'type': tp, + 'name': obj.snapshot['name'], + 'comment': obj.snapshot['comment'], + 'accounts': obj.snapshot['accounts'], + 'node_amount': len(obj.snapshot['nodes']), + 'asset_amount': len(obj.snapshot['assets']), + 'type_display': getattr(AutomationTypes, tp).label, + } + return snapshot + + +class UpdateAssetSerializer(serializers.ModelSerializer): + class Meta: + model = BaseAutomation + fields = ['id', 'assets'] + + +class UpdateNodeSerializer(serializers.ModelSerializer): + class Meta: + model = BaseAutomation + fields = ['id', 'nodes'] + + +class AutomationAssetsSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + only_fields = ['id', 'name', 'address'] + fields = tuple(only_fields) diff --git a/apps/assets/serializers/automations/change_secret.py b/apps/assets/serializers/automations/change_secret.py new file mode 100644 index 000000000..b0149334d --- /dev/null +++ b/apps/assets/serializers/automations/change_secret.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from common.utils import get_logger +from common.drf.fields import LabeledChoiceField, ObjectRelatedField +from assets.serializers.base import AuthValidateMixin +from assets.const import DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy +from assets.models import Asset, Account, ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution + +from .base import BaseAutomationSerializer + +logger = get_logger(__file__) + +__all__ = [ + 'ChangeSecretAutomationSerializer', + 'ChangeSecretRecordSerializer', + 'ChangeSecretRecordBackUpSerializer' +] + + +class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer): + secret_strategy = LabeledChoiceField( + choices=SecretStrategy.choices, required=True, label=_('Secret strategy') + ) + ssh_key_change_strategy = LabeledChoiceField( + choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy') + ) + password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES) + + class Meta: + model = ChangeSecretAutomation + read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [ + 'secret_type', 'secret_strategy', 'secret', 'password_rules', + 'ssh_key_change_strategy', 'passphrase', 'recipients', + ] + extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{ + 'recipients': {'label': _('Recipient'), 'help_text': _( + "Currently only mail sending is supported" + )}, + }} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_secret_type_choices() + + def set_secret_type_choices(self): + secret_type = self.fields.get('secret_type') + if not secret_type: + return + choices = secret_type._choices + choices.pop(SecretType.ACCESS_KEY, None) + choices.pop(SecretType.TOKEN, None) + secret_type._choices = choices + + def validate_password_rules(self, password_rules): + secret_type = self.initial_secret_type + if secret_type != SecretType.PASSWORD: + return password_rules + + length = password_rules.get('length') + symbol_set = password_rules.get('symbol_set', '') + + try: + length = int(length) + except Exception as e: + logger.error(e) + msg = _("* Please enter the correct password length") + raise serializers.ValidationError(msg) + if length < 6 or length > 30: + msg = _('* Password length range 6-30 bits') + raise serializers.ValidationError(msg) + + if not isinstance(symbol_set, str): + symbol_set = str(symbol_set) + + password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)} + return password_rules + + def validate(self, attrs): + secret_type = attrs.get('secret_type') + secret_strategy = attrs.get('secret_strategy') + if secret_type == SecretType.PASSWORD: + attrs.pop('ssh_key_change_strategy', None) + if secret_strategy == SecretStrategy.custom: + attrs.pop('password_rules', None) + else: + attrs.pop('secret', None) + elif secret_type == SecretType.SSH_KEY: + attrs.pop('password_rules', None) + if secret_strategy != SecretStrategy.custom: + attrs.pop('secret', None) + return attrs + + +class ChangeSecretRecordSerializer(serializers.ModelSerializer): + is_success = serializers.SerializerMethodField(label=_('Is success')) + asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset')) + account = ObjectRelatedField(queryset=Account.objects, label=_('Account')) + execution = ObjectRelatedField( + queryset=AutomationExecution.objects, label=_('Automation task execution') + ) + + class Meta: + model = ChangeSecretRecord + fields = [ + 'id', 'asset', 'account', 'date_started', 'date_finished', + 'timedelta', 'is_success', 'error', 'execution', + ] + read_only_fields = fields + + @staticmethod + def get_is_success(obj): + if obj.status == 'success': + return _("Success") + return _("Failed") + + +class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer): + asset = serializers.SerializerMethodField(label=_('Asset')) + account = serializers.SerializerMethodField(label=_('Account')) + is_success = serializers.SerializerMethodField(label=_('Is success')) + + class Meta: + model = ChangeSecretRecord + fields = [ + 'id', 'asset', 'account', 'old_secret', 'new_secret', + 'status', 'error', 'is_success' + ] + read_only_fields = fields + + @staticmethod + def get_asset(instance): + return str(instance.asset) + + @staticmethod + def get_account(instance): + return str(instance.account) + + @staticmethod + def get_is_success(obj): + if obj.status == 'success': + return _("Success") + return _("Failed") diff --git a/apps/assets/serializers/automations/gather_accounts.py b/apps/assets/serializers/automations/gather_accounts.py new file mode 100644 index 000000000..6b86fd64c --- /dev/null +++ b/apps/assets/serializers/automations/gather_accounts.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext_lazy as _ +from assets.models import GatherAccountsAutomation +from common.utils import get_logger + +from .base import BaseAutomationSerializer + +logger = get_logger(__file__) + +__all__ = [ + 'GatherAccountAutomationSerializer', +] + + +class GatherAccountAutomationSerializer(BaseAutomationSerializer): + class Meta: + model = GatherAccountsAutomation + read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + ['executed_amount'] + fields = BaseAutomationSerializer.Meta.fields + read_only_fields + + extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{ + 'executed_amount': {'label': _('Executed amount')} + }} diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 92249705d..9641ce786 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -1,76 +1,50 @@ # -*- coding: utf-8 -*- # -from io import StringIO - from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key -from common.drf.fields import EncryptedField -from assets.models import Type -from .utils import validate_password_for_ansible +from assets.const import SecretType +from common.drf.fields import EncryptedField, LabeledChoiceField +from .utils import validate_password_for_ansible, validate_ssh_key -class AuthSerializer(serializers.ModelSerializer): - password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password')) - private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key')) - - def gen_keys(self, private_key=None, password=None): - if private_key is None: - return None, None - public_key = ssh_pubkey_gen(private_key=private_key, password=password) - return private_key, public_key - - def save(self, **kwargs): - password = self.validated_data.pop('password', None) or None - private_key = self.validated_data.pop('private_key', None) or None - self.instance = super().save(**kwargs) - if password or private_key: - private_key, public_key = self.gen_keys(private_key, password) - self.instance.set_auth(password=password, private_key=private_key, - public_key=public_key) - return self.instance - - -class AuthSerializerMixin(serializers.ModelSerializer): - password = EncryptedField( - label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024, - validators=[validate_password_for_ansible] +class AuthValidateMixin(serializers.Serializer): + secret_type = LabeledChoiceField( + choices=SecretType.choices, required=True, label=_('Secret type') ) - private_key = EncryptedField( - label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384 + secret = EncryptedField( + label=_('Secret'), required=False, max_length=40960, allow_blank=True, + allow_null=True, write_only=True, ) passphrase = serializers.CharField( allow_blank=True, allow_null=True, required=False, max_length=512, write_only=True, label=_('Key password') ) - def validate_password(self, password): - return password + @property + def initial_secret_type(self): + secret_type = self.initial_data.get('secret_type') + return secret_type - def validate_private_key(self, private_key): - if not private_key: - return - passphrase = self.initial_data.get('passphrase') - passphrase = passphrase if passphrase else None - valid = validate_ssh_private_key(private_key, password=passphrase) - if not valid: - raise serializers.ValidationError(_("private key invalid or passphrase error")) - - private_key = ssh_private_key_gen(private_key, password=passphrase) - string_io = StringIO() - private_key.write_private_key(string_io) - private_key = string_io.getvalue() - return private_key - - def validate_public_key(self, public_key): - return public_key + def validate_secret(self, secret): + if not secret: + return '' + secret_type = self.initial_secret_type + if secret_type == SecretType.PASSWORD: + validate_password_for_ansible(secret) + return secret + elif secret_type == SecretType.SSH_KEY: + passphrase = self.initial_data.get('passphrase') + passphrase = passphrase if passphrase else None + return validate_ssh_key(secret, passphrase) + else: + return secret @staticmethod def clean_auth_fields(validated_data): - for field in ('password', 'private_key', 'public_key'): + for field in ('secret',): value = validated_data.get(field) - if not value: + if value is None: validated_data.pop(field, None) validated_data.pop('passphrase', None) @@ -81,24 +55,3 @@ class AuthSerializerMixin(serializers.ModelSerializer): def update(self, instance, validated_data): self.clean_auth_fields(validated_data) return super().update(instance, validated_data) - - -class TypesField(serializers.MultipleChoiceField): - def __init__(self, *args, **kwargs): - kwargs['choices'] = Type.CHOICES - super().__init__(*args, **kwargs) - - def to_representation(self, value): - return Type.value_to_choices(value) - - def to_internal_value(self, data): - if data is None: - return data - return Type.choices_to_value(data) - - -class ActionsDisplayField(TypesField): - def to_representation(self, value): - values = super().to_representation(value) - choices = dict(Type.CHOICES) - return [choices.get(i) for i in values] diff --git a/apps/assets/serializers/cagegory.py b/apps/assets/serializers/cagegory.py new file mode 100644 index 000000000..7f8b61571 --- /dev/null +++ b/apps/assets/serializers/cagegory.py @@ -0,0 +1,15 @@ +from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ + + +class TypeSerializer(serializers.Serializer): + label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label')) + value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value')) + category = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Category')) + constraints = serializers.JSONField(required=False, allow_null=True, label=_('Constraints')) + + +class CategorySerializer(serializers.Serializer): + label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label')) + value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value')) + types = TypeSerializer(many=True, required=False, label=_('Types'), read_only=True) diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py deleted file mode 100644 index 9bd5d38cc..000000000 --- a/apps/assets/serializers/cmd_filter.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# -import re -from rest_framework import serializers - -from django.utils.translation import ugettext_lazy as _ -from ..models import CommandFilter, CommandFilterRule -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from orgs.utils import tmp_to_root_org -from common.utils import get_object_or_none, lazyproperty -from terminal.models import Session - - -class CommandFilterSerializer(BulkOrgResourceModelSerializer): - class Meta: - model = CommandFilter - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'org_id', 'org_name', 'is_active', - 'date_created', 'date_updated', - 'comment', 'created_by', - ] - fields_fk = ['rules'] - fields_m2m = ['users', 'user_groups', 'system_users', 'nodes', 'assets', 'applications'] - fields = fields_small + fields_fk + fields_m2m - extra_kwargs = { - 'rules': {'read_only': True}, - 'date_created': {'label': _("Date created")}, - 'date_updated': {'label': _("Date updated")}, - } - - -class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display")) - action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display")) - - class Meta: - model = CommandFilterRule - fields_mini = ['id'] - fields_small = fields_mini + [ - 'type', 'type_display', 'content', 'ignore_case', 'pattern', - 'priority', 'action', 'action_display', 'reviewers', - 'date_created', 'date_updated', 'comment', 'created_by', - ] - fields_fk = ['filter'] - fields = fields_small + fields_fk - extra_kwargs = { - 'date_created': {'label': _("Date created")}, - 'date_updated': {'label': _("Date updated")}, - 'action_display': {'label': _("Action display")}, - 'pattern': {'label': _("Pattern")} - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_action_choices() - - def set_action_choices(self): - from django.conf import settings - action = self.fields.get('action') - if not action: - return - choices = action._choices - if not settings.XPACK_ENABLED: - choices.pop(CommandFilterRule.ActionChoices.confirm, None) - action._choices = choices - - def validate_content(self, content): - tp = self.initial_data.get("type") - if tp == CommandFilterRule.TYPE_COMMAND: - regex = CommandFilterRule.construct_command_regex(content) - else: - regex = content - ignore_case = self.initial_data.get('ignore_case') - succeed, error, pattern = CommandFilterRule.compile_regex(regex, ignore_case) - if not succeed: - raise serializers.ValidationError(error) - return content - - -class CommandConfirmSerializer(serializers.Serializer): - session_id = serializers.UUIDField(required=True, allow_null=False) - cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False) - run_command = serializers.CharField(required=True, allow_null=False) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.session = None - self.cmd_filter_rule = None - - def validate_session_id(self, session_id): - self.session = self.validate_object_exist(Session, session_id) - return session_id - - def validate_cmd_filter_rule_id(self, cmd_filter_rule_id): - self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id) - return cmd_filter_rule_id - - @staticmethod - def validate_object_exist(model, field_id): - with tmp_to_root_org(): - obj = get_object_or_none(model, id=field_id) - if not obj: - error = '{} Model object does not exist'.format(model.__name__) - raise serializers.ValidationError(error) - return obj - - @lazyproperty - def org(self): - return self.session.org diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index fa1a39574..b06a1afc8 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,29 +3,28 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.validators import alphanumeric from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.drf.serializers import SecretReadableMixin -from ..models import Domain, Gateway -from .base import AuthSerializerMixin +from common.drf.fields import ObjectRelatedField +from ..serializers import HostSerializer +from ..models import Domain, Gateway, Asset class DomainSerializer(BulkOrgResourceModelSerializer): asset_count = serializers.SerializerMethodField(label=_('Assets amount')) - application_count = serializers.SerializerMethodField(label=_('Applications amount')) gateway_count = serializers.SerializerMethodField(label=_('Gateways count')) + assets = ObjectRelatedField( + many=True, required=False, queryset=Asset.objects, label=_('Asset') + ) class Meta: model = Domain fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'comment', 'date_created' - ] - fields_m2m = [ - 'asset_count', 'assets', 'application_count', 'gateway_count', - ] - fields = fields_small + fields_m2m - read_only_fields = ('asset_count', 'gateway_count', 'date_created') + fields_small = fields_mini + ['comment'] + fields_m2m = ['assets'] + read_only_fields = ['asset_count', 'gateway_count', 'date_created'] + fields = fields_small + fields_m2m + read_only_fields + extra_kwargs = { 'assets': {'required': False, 'label': _('Assets')}, } @@ -35,50 +34,31 @@ class DomainSerializer(BulkOrgResourceModelSerializer): return obj.assets.count() @staticmethod - def get_application_count(obj): - return obj.applications.count() + def get_gateway_count(obj): + return obj.gateways.count() + + +class GatewaySerializer(HostSerializer): + effective_accounts = serializers.SerializerMethodField() + + class Meta(HostSerializer.Meta): + model = Gateway + fields = HostSerializer.Meta.fields + ['effective_accounts'] @staticmethod - def get_gateway_count(obj): - return obj.gateway_set.all().count() - - -class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): - is_connective = serializers.BooleanField(required=False, label=_('Connectivity')) - - class Meta: - model = Gateway - fields_mini = ['id', 'name'] - fields_write_only = [ - 'password', 'private_key', 'public_key', 'passphrase' + def get_effective_accounts(obj): + accounts = obj.select_accounts.values() + return [ + { + 'id': account.id, + 'username': account.username, + 'secret_type': account.secret_type, + } for account in accounts ] - fields_small = fields_mini + fields_write_only + [ - 'username', 'ip', 'port', 'protocol', - 'is_active', 'is_connective', - 'date_created', 'date_updated', - 'created_by', 'comment', - ] - fields_fk = ['domain'] - fields = fields_small + fields_fk - extra_kwargs = { - 'username': {"validators": [alphanumeric]}, - 'password': {'write_only': True}, - 'private_key': {"write_only": True}, - 'public_key': {"write_only": True}, - } - - -class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer): - class Meta(GatewaySerializer.Meta): - extra_kwargs = { - 'password': {'write_only': False}, - 'private_key': {"write_only": False}, - 'public_key': {"write_only": False}, - } class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer): - gateways = GatewayWithAuthSerializer(many=True, read_only=True) + gateways = GatewaySerializer(many=True, read_only=True) class Meta: model = Domain diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index 7c024bf1a..cc0647943 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,7 +4,7 @@ from rest_framework import serializers from orgs.utils import tmp_to_root_org -from common.mixins import BulkSerializerMixin +from common.drf.serializers import BulkSerializerMixin from ..models import FavoriteAsset diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py index 86e7dcfae..a0b58de45 100644 --- a/apps/assets/serializers/gathered_user.py +++ b/apps/assets/serializers/gathered_user.py @@ -1,25 +1,26 @@ # -*- coding: utf-8 -*- # - from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import OrgResourceModelSerializerMixin -from ..models import GatheredUser +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', + 'username', 'ip_last_login', 'present', 'name', 'date_last_login', 'date_created', 'date_updated' ] - fields_fk = ['asset', 'hostname', 'ip'] + fields_fk = ['asset', 'ip'] fields = fields_small + fields_fk read_only_fields = fields extra_kwargs = { - 'hostname': {'label': _("Hostname")}, + 'name': {'label': _("Hostname")}, 'ip': {'label': 'IP'}, } diff --git a/apps/assets/serializers/mixin.py b/apps/assets/serializers/mixin.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py new file mode 100644 index 000000000..ccb536bb2 --- /dev/null +++ b/apps/assets/serializers/platform.py @@ -0,0 +1,142 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.drf.fields import LabeledChoiceField +from common.drf.serializers import WritableNestedModelSerializer +from ..const import Category, AllTypes +from ..models import Platform, PlatformProtocol, PlatformAutomation + +__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer"] + + +class ProtocolSettingSerializer(serializers.Serializer): + SECURITY_CHOICES = [ + ("any", "Any"), + ("rdp", "RDP"), + ("tls", "TLS"), + ("nla", "NLA"), + ] + # RDP + console = serializers.BooleanField(required=False) + security = serializers.ChoiceField(choices=SECURITY_CHOICES, default="any") + + # SFTP + sftp_enabled = serializers.BooleanField(default=True, label=_("SFTP enabled")) + sftp_home = serializers.CharField(default="/tmp", label=_("SFTP home")) + + # HTTP + auto_fill = serializers.BooleanField(default=False, label=_("Auto fill")) + username_selector = serializers.CharField( + default="", allow_blank=True, label=_("Username selector") + ) + password_selector = serializers.CharField( + default="", allow_blank=True, label=_("Password selector") + ) + submit_selector = serializers.CharField( + default="", allow_blank=True, label=_("Submit selector") + ) + + +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", + ] + extra_kwargs = { + "ping_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": "账号创建改密方式"}, + "gather_accounts_enabled": {"label": "启用账号收集"}, + "gather_accounts_method": {"label": "收集账号方式"}, + } + + +class PlatformProtocolsSerializer(serializers.ModelSerializer): + setting = ProtocolSettingSerializer(required=False, allow_null=True) + primary = serializers.BooleanField(read_only=True, label=_("Primary")) + + class Meta: + model = PlatformProtocol + fields = [ + "id", + "name", + "port", + "primary", + "default", + "required", + "secret_types", + "setting", + ] + + +class PlatformSerializer(WritableNestedModelSerializer): + charset = LabeledChoiceField( + choices=Platform.CharsetChoices.choices, label=_("Charset") + ) + type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type")) + category = LabeledChoiceField(choices=Category.choices, label=_("Category")) + protocols = PlatformProtocolsSerializer( + label=_("Protocols"), many=True, required=False + ) + automation = PlatformAutomationSerializer(label=_("Automation"), required=False) + su_method = LabeledChoiceField( + choices=[("sudo", "sudo su -"), ("su", "su - ")], + label="切换方式", + required=False, + default="sudo", + ) + + class Meta: + model = Platform + fields_mini = ["id", "name", "internal"] + fields_small = fields_mini + [ + "category", + "type", + "charset", + ] + fields = fields_small + [ + "protocols_enabled", + "protocols", + "domain_enabled", + "su_enabled", + "su_method", + "automation", + "comment", + ] + extra_kwargs = { + "su_enabled": {"label": "启用切换账号"}, + "protocols_enabled": {"label": "启用协议"}, + "domain_enabled": {"label": "启用网域"}, + "domain_default": {"label": "默认网域"}, + } + + +class PlatformOpsMethodSerializer(serializers.Serializer): + id = serializers.CharField(read_only=True) + name = serializers.CharField(max_length=50, label=_("Name")) + category = serializers.CharField(max_length=50, label=_("Category")) + type = serializers.ListSerializer(child=serializers.CharField()) + method = serializers.CharField() diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py deleted file mode 100644 index 54aff3e82..000000000 --- a/apps/assets/serializers/system_user.py +++ /dev/null @@ -1,355 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ -from django.db.models import Count - -from common.mixins.serializers import BulkSerializerMixin -from common.utils import ssh_pubkey_gen -from common.drf.fields import EncryptedField -from common.drf.serializers import SecretReadableMixin -from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import SystemUser, Asset -from .utils import validate_password_for_ansible -from .base import AuthSerializerMixin - -__all__ = [ - 'SystemUserSerializer', 'MiniSystemUserSerializer', - 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', - 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', - 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', - 'SystemUserTempAuthSerializer', 'RelationMixin', -] - - -class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): - """ - 系统用户 - """ - password = EncryptedField( - label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024, - trim_whitespace=False, validators=[validate_password_for_ansible], - write_only=True - ) - auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True) - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint')) - token = EncryptedField( - label=_('Token'), required=False, write_only=True, style={'base_template': 'textarea.html'} - ) - applications_amount = serializers.IntegerField( - source='apps_amount', read_only=True, label=_('Apps amount') - ) - - class Meta: - model = SystemUser - fields_mini = ['id', 'name', 'username'] - fields_write_only = ['password', 'public_key', 'private_key', 'passphrase'] - fields_small = fields_mini + fields_write_only + [ - 'token', 'ssh_key_fingerprint', - 'type', 'type_display', 'protocol', 'is_asset_protocol', - 'login_mode', 'login_mode_display', 'priority', - 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain', - 'username_same_with_user', 'auto_push', 'auto_generate_key', - 'su_enabled', 'su_from', - 'date_created', 'date_updated', 'comment', 'created_by', - ] - fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes'] - fields = fields_small + fields_m2m - extra_kwargs = { - 'cmd_filters': {"required": False, 'label': _('Command filter')}, - 'public_key': {"write_only": True}, - 'private_key': {"write_only": True}, - 'nodes_amount': {'label': _('Nodes amount')}, - 'assets_amount': {'label': _('Assets amount')}, - 'login_mode_display': {'label': _('Login mode display')}, - 'created_by': {'read_only': True}, - 'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')}, - 'is_asset_protocol': {'label': _('Is asset protocol')}, - 'su_from': {'help_text': _('Only ssh and automatic login system users are supported')} - } - - def validate_auto_push(self, value): - login_mode = self.get_initial_value("login_mode") - protocol = self.get_initial_value("protocol") - - if login_mode == SystemUser.LOGIN_MANUAL: - value = False - elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: - value = False - return value - - def validate_auto_generate_key(self, value): - login_mode = self.get_initial_value("login_mode") - protocol = self.get_initial_value("protocol") - - if self.context["request"].method.lower() != "post": - value = False - elif self.instance: - value = False - elif login_mode == SystemUser.LOGIN_MANUAL: - value = False - elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: - value = False - return value - - def validate_username_same_with_user(self, username_same_with_user): - if not username_same_with_user: - return username_same_with_user - protocol = self.get_initial_value("protocol", "ssh") - queryset = SystemUser.objects.filter( - protocol=protocol, - username_same_with_user=True - ) - if self.instance: - queryset = queryset.exclude(id=self.instance.id) - exists = queryset.exists() - if not exists: - return username_same_with_user - error = _("Username same with user with protocol {} only allow 1").format(protocol) - raise serializers.ValidationError(error) - - def validate_username(self, username): - protocol = self.get_initial_value("protocol") - if username: - if protocol == SystemUser.Protocol.telnet: - regx = alphanumeric_cn_re - elif protocol == SystemUser.Protocol.rdp: - regx = alphanumeric_win_re - else: - regx = alphanumeric_re - if not regx.match(username): - raise serializers.ValidationError(_('Special char not allowed')) - return username - - username_same_with_user = self.get_initial_value("username_same_with_user") - if username_same_with_user: - return '' - - login_mode = self.get_initial_value("login_mode") - if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc \ - and protocol != SystemUser.Protocol.redis: - msg = _('* Automatic login mode must fill in the username.') - raise serializers.ValidationError(msg) - return username - - def validate_home(self, home): - username_same_with_user = self.get_initial_value("username_same_with_user") - if username_same_with_user: - return '' - return home - - @staticmethod - def validate_sftp_root(value): - if value in ['home', 'tmp']: - return value - if not value.startswith('/'): - error = _("Path should starts with /") - raise serializers.ValidationError(error) - return value - - def validate_password(self, password): - super().validate_password(password) - auto_gen_key = self.get_initial_value('auto_generate_key', False) - private_key = self.get_initial_value('private_key') - login_mode = self.get_initial_value('login_mode') - - if not self.instance and not auto_gen_key and not password and \ - not private_key and login_mode == SystemUser.LOGIN_AUTO: - raise serializers.ValidationError(_("Password or private key required")) - return password - - def validate_su_from(self, su_from: SystemUser): - # self: su enabled - su_enabled = self.get_initial_value('su_enabled', default=False) - if not su_enabled: - return - if not su_from: - error = _('This field is required.') - raise serializers.ValidationError(error) - # self: protocol ssh - protocol = self.get_initial_value('protocol', default=SystemUser.Protocol.ssh.value) - if protocol not in [SystemUser.Protocol.ssh.value]: - error = _('Only ssh protocol system users are allowed') - raise serializers.ValidationError(error) - # su_from: protocol same - if su_from.protocol != protocol: - error = _('The protocol must be consistent with the current user: {}').format(protocol) - raise serializers.ValidationError(error) - # su_from: login model auto - if su_from.login_mode != su_from.LOGIN_AUTO: - error = _('Only system users with automatic login are allowed') - raise serializers.ValidationError(error) - return su_from - - def _validate_admin_user(self, attrs): - if self.instance: - tp = self.instance.type - else: - tp = attrs.get('type') - if tp != SystemUser.Type.admin: - return attrs - attrs['protocol'] = SystemUser.Protocol.ssh - attrs['login_mode'] = SystemUser.LOGIN_AUTO - attrs['username_same_with_user'] = False - attrs['auto_push'] = False - return attrs - - def _validate_gen_key(self, attrs): - username = attrs.get('username', 'manual') - auto_gen_key = attrs.pop('auto_generate_key', False) - protocol = attrs.get('protocol') - - if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: - return attrs - - # 自动生成 - if auto_gen_key and not self.instance: - password = SystemUser.gen_password() - attrs['password'] = password - if protocol == SystemUser.Protocol.ssh: - private_key, public_key = SystemUser.gen_key(username) - attrs['private_key'] = private_key - attrs['public_key'] = public_key - # 如果设置了private key,没有设置public key则生成 - elif attrs.get('private_key'): - private_key = attrs['private_key'] - password = attrs.get('password') - public_key = ssh_pubkey_gen(private_key, password=password, username=username) - attrs['public_key'] = public_key - return attrs - - def _validate_login_mode(self, attrs): - if 'login_mode' in attrs: - login_mode = attrs['login_mode'] - else: - login_mode = self.instance.login_mode if self.instance else SystemUser.LOGIN_AUTO - - if login_mode == SystemUser.LOGIN_MANUAL: - attrs['password'] = '' - attrs['private_key'] = '' - attrs['public_key'] = '' - - return attrs - - def validate(self, attrs): - attrs = self._validate_admin_user(attrs) - attrs = self._validate_gen_key(attrs) - attrs = self._validate_login_mode(attrs) - return attrs - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset \ - .annotate(assets_amount=Count("assets")) \ - .prefetch_related('nodes', 'cmd_filters') - return queryset - - -class MiniSystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - fields = SystemUserSerializer.Meta.fields_mini - - -class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer): - class Meta(SystemUserSerializer.Meta): - fields_mini = ['id', 'name', 'username'] - fields_write_only = ['password', 'public_key', 'private_key'] - fields_small = fields_mini + fields_write_only + [ - 'protocol', 'login_mode', 'login_mode_display', 'priority', - 'sudo', 'shell', 'ad_domain', 'sftp_root', 'token', - "username_same_with_user", 'auto_push', 'auto_generate_key', - 'comment', - ] - fields = fields_small - extra_kwargs = { - 'nodes_amount': {'label': _('Node')}, - 'assets_amount': {'label': _('Asset')}, - 'login_mode_display': {'label': _('Login mode display')}, - 'created_by': {'read_only': True}, - 'password': {'write_only': False}, - 'private_key': {'write_only': False}, - 'token': {'write_only': False} - } - - -class SystemUserSimpleSerializer(serializers.ModelSerializer): - """ - 系统用户最基本信息的数据结构 - """ - - class Meta: - model = SystemUser - fields = ('id', 'name', 'username') - - -class RelationMixin(BulkSerializerMixin, serializers.Serializer): - systemuser_display = serializers.ReadOnlyField(label=_("System user name")) - org_name = serializers.ReadOnlyField(label=_("Org name")) - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(['systemuser', "systemuser_display", "org_name"]) - return fields - - -class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): - asset_display = serializers.ReadOnlyField(label=_('Asset hostname')) - - class Meta: - model = SystemUser.assets.through - fields = [ - "id", "asset", "asset_display", 'systemuser', 'systemuser_display', - "connectivity", 'date_verified', 'org_id' - ] - use_model_bulk_create = True - model_bulk_create_kwargs = { - 'ignore_conflicts': True - } - - -class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): - node_display = serializers.SerializerMethodField() - - class Meta: - model = SystemUser.nodes.through - fields = [ - 'id', 'node', "node_display", - ] - - def get_node_display(self, obj): - return obj.node.full_value - - -class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - user_display = serializers.ReadOnlyField() - - class Meta: - model = SystemUser.users.through - fields = [ - 'id', "user", "user_display", - ] - - -class SystemUserTaskSerializer(serializers.Serializer): - ACTION_CHOICES = ( - ("test", "test"), - ("push", "push"), - ) - action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) - asset = serializers.PrimaryKeyRelatedField( - queryset=Asset.objects, allow_null=True, required=False, write_only=True - ) - assets = serializers.PrimaryKeyRelatedField( - queryset=Asset.objects, allow_null=True, required=False, write_only=True, - many=True - ) - task = serializers.CharField(read_only=True) - - -class SystemUserTempAuthSerializer(SystemUserSerializer): - instance_id = serializers.CharField() - - class Meta(SystemUserSerializer.Meta): - fields = ['instance_id', 'username', 'password'] diff --git a/apps/assets/serializers/utils.py b/apps/assets/serializers/utils.py index 52527e723..770710843 100644 --- a/apps/assets/serializers/utils.py +++ b/apps/assets/serializers/utils.py @@ -1,6 +1,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from common.utils import validate_ssh_private_key, parse_ssh_private_key_str + def validate_password_for_ansible(password): """ 校验 Ansible 不支持的特殊字符 """ @@ -15,3 +17,9 @@ def validate_password_for_ansible(password): if '"' in password: raise serializers.ValidationError(_('Password can not contains `"` ')) + +def validate_ssh_key(ssh_key, passphrase=None): + valid = validate_ssh_private_key(ssh_key, password=passphrase) + if not valid: + raise serializers.ValidationError(_("private key invalid or passphrase error")) + return parse_ssh_private_key_str(ssh_key, passphrase) diff --git a/apps/assets/signal_handlers/__init__.py b/apps/assets/signal_handlers/__init__.py index 8a895544f..b337df001 100644 --- a/apps/assets/signal_handlers/__init__.py +++ b/apps/assets/signal_handlers/__init__.py @@ -1,5 +1,4 @@ from .asset import * -from .system_user import * -from .authbook import * +from .account import * from .node_assets_amount import * from .node_assets_mapping import * diff --git a/apps/assets/signal_handlers/account.py b/apps/assets/signal_handlers/account.py new file mode 100644 index 000000000..8020e4087 --- /dev/null +++ b/apps/assets/signal_handlers/account.py @@ -0,0 +1,15 @@ +from django.dispatch import receiver +from django.db.models.signals import pre_save + +from common.utils import get_logger +from ..models import Account + +logger = get_logger(__name__) + + +@receiver(pre_save, sender=Account) +def on_account_pre_create(sender, instance, **kwargs): + # 升级版本号 + instance.version += 1 + # 即使在 root 组织也不怕 + instance.org_id = instance.asset.org_id diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py index 97f727a46..5aac26319 100644 --- a/apps/assets/signal_handlers/asset.py +++ b/apps/assets/signal_handlers/asset.py @@ -8,11 +8,10 @@ from django.dispatch import receiver from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE from common.utils import get_logger from common.decorator import on_transaction_commit -from assets.models import Asset, SystemUser, Node +from assets.models import Asset, Node from assets.tasks import ( update_assets_hardware_info_util, test_asset_connectivity_util, - push_system_user_to_assets, ) logger = get_logger(__file__) @@ -52,8 +51,6 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): if not has_node: instance.nodes.add(Node.org_root()) - instance.set_admin_user_relation() - @receiver(m2m_changed, sender=Asset.nodes.through) def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): @@ -79,32 +76,32 @@ def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True)) # 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的 - system_user_ids = SystemUser.objects.filter( - nodes__key__in=nodes_ancestors_keys - ).distinct().values_list('id', flat=True) + # system_user_ids = SystemUser.objects.filter( + # nodes__key__in=nodes_ancestors_keys + # ).distinct().values_list('id', flat=True) # 查询所有已存在的关系 - m2m_model = SystemUser.assets.through - exist = set(m2m_model.objects.filter( - systemuser_id__in=system_user_ids, asset_id__in=asset_ids - ).values_list('systemuser_id', 'asset_id')) + # m2m_model = SystemUser.assets.through + # exist = set(m2m_model.objects.filter( + # systemuser_id__in=system_user_ids, asset_id__in=asset_ids + # ).values_list('systemuser_id', 'asset_id')) # TODO 优化 - to_create = [] - for system_user_id in system_user_ids: - asset_ids_to_push = [] - for asset_id in asset_ids: - if (system_user_id, asset_id) in exist: - continue - asset_ids_to_push.append(asset_id) - to_create.append(m2m_model( - systemuser_id=system_user_id, - asset_id=asset_id, - org_id=instance.org_id - )) - if asset_ids_to_push: - push_system_user_to_assets.delay(system_user_id, asset_ids_to_push) - m2m_model.objects.bulk_create(to_create) - + # to_create = [] + # for system_user_id in system_user_ids: + # asset_ids_to_push = [] + # for asset_id in asset_ids: + # if (system_user_id, asset_id) in exist: + # continue + # asset_ids_to_push.append(asset_id) + # to_create.append(m2m_model( + # systemuser_id=system_user_id, + # asset_id=asset_id, + # org_id=instance.org_id + # )) + # if asset_ids_to_push: + # push_system_user_to_assets.delay(system_user_id, asset_ids_to_push) + # m2m_model.objects.bulk_create(to_create) + # RELATED_NODE_IDS = '_related_node_ids' diff --git a/apps/assets/signal_handlers/authbook.py b/apps/assets/signal_handlers/authbook.py deleted file mode 100644 index 513488763..000000000 --- a/apps/assets/signal_handlers/authbook.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.dispatch import receiver -from django.apps import apps -from simple_history.signals import pre_create_historical_record -from django.db.models.signals import post_save, pre_save, pre_delete - -from common.utils import get_logger -from ..models import AuthBook, SystemUser - -AuthBookHistory = apps.get_model('assets', 'HistoricalAuthBook') -logger = get_logger(__name__) - - -@receiver(pre_create_historical_record, sender=AuthBookHistory) -def pre_create_historical_record_callback(sender, history_instance=None, **kwargs): - attrs_to_copy = ['username', 'password', 'private_key'] - - for attr in attrs_to_copy: - if getattr(history_instance, attr): - continue - try: - system_user = history_instance.systemuser - except SystemUser.DoesNotExist: - continue - if not system_user: - continue - system_user_attr_value = getattr(history_instance.systemuser, attr) - if system_user_attr_value: - setattr(history_instance, attr, system_user_attr_value) - - -@receiver(pre_delete, sender=AuthBook) -def on_authbook_post_delete(sender, instance, **kwargs): - instance.remove_asset_admin_user_if_need() - - -@receiver(post_save, sender=AuthBook) -def on_authbook_post_create(sender, instance, created, **kwargs): - instance.sync_to_system_user_account() - if created: - pass - # # 不再自动更新资产管理用户,只允许用户手动指定。 - # 只在创建时进行更新资产的管理用户 - # instance.update_asset_admin_user_if_need() - - -@receiver(pre_save, sender=AuthBook) -def on_authbook_pre_create(sender, instance, **kwargs): - # 升级版本号 - instance.version += 1 - # 即使在 root 组织也不怕 - instance.org_id = instance.asset.org_id diff --git a/apps/assets/signal_handlers/system_user.py b/apps/assets/signal_handlers/system_user.py deleted file mode 100644 index 00b19e110..000000000 --- a/apps/assets/signal_handlers/system_user.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.db.models.signals import ( - post_save, m2m_changed, pre_save, pre_delete, post_delete -) -from django.dispatch import receiver - -from common.exceptions import M2MReverseNotAllowed -from common.const.signals import POST_ADD -from common.utils import get_logger -from common.decorator import on_transaction_commit -from assets.models import Asset, SystemUser, Node, AuthBook -from users.models import User -from orgs.utils import tmp_to_root_org -from assets.tasks import ( - push_system_user_to_assets_manual, - push_system_user_to_assets, - add_nodes_assets_to_system_users -) - -logger = get_logger(__file__) - - -@receiver(m2m_changed, sender=SystemUser.assets.through) -@on_transaction_commit -def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): - """ - 当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中 - """ - logger.debug("System user assets change signal recv: {}".format(instance)) - - if not instance: - logger.debug('No system user found') - return - - if model == Asset: - system_user_ids = [instance.id] - asset_ids = pk_set - else: - system_user_ids = pk_set - asset_ids = [instance.id] - - org_id = instance.org_id - - # 关联创建的 authbook 没有系统用户id - with tmp_to_root_org(): - authbooks = AuthBook.objects.filter( - asset_id__in=asset_ids, - systemuser_id__in=system_user_ids - ) - if action == POST_ADD: - authbooks.update(org_id=org_id) - - save_action_mapper = { - 'pre_add': pre_save, - 'post_add': post_save, - 'pre_remove': pre_delete, - 'post_remove': post_delete - } - - for ab in authbooks: - ab.org_id = org_id - - save_action = save_action_mapper[action] - logger.debug('Send AuthBook post save signal: {} -> {}'.format(action, ab.id)) - save_action.send(sender=AuthBook, instance=ab, created=True) - - if action == POST_ADD: - for system_user_id in system_user_ids: - push_system_user_to_assets.delay(system_user_id, asset_ids) - - -@receiver(m2m_changed, sender=SystemUser.users.through) -@on_transaction_commit -def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs): - """ - 当系统用户和用户关系发生变化时,应该重新推送系统用户资产中 - """ - if action != POST_ADD: - return - - if reverse: - raise M2MReverseNotAllowed - - if not instance.username_same_with_user: - return - - logger.debug("System user users change signal recv: {}".format(instance)) - usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True) - - for username in usernames: - push_system_user_to_assets_manual.delay(instance, username) - - -@receiver(m2m_changed, sender=SystemUser.nodes.through) -@on_transaction_commit -def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs): - """ - 当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上 - """ - if action != POST_ADD: - return - logger.info("System user nodes update signal recv: {}".format(instance)) - - queryset = model.objects.filter(pk__in=pk_set) - if model == Node: - nodes_keys = queryset.values_list('key', flat=True) - system_users = [instance] - else: - nodes_keys = [instance.key] - system_users = queryset - add_nodes_assets_to_system_users.delay(nodes_keys, system_users) - - -@receiver(m2m_changed, sender=SystemUser.groups.through) -def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs): - """ - 当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上 - """ - if action != POST_ADD: - return - if reverse: - raise M2MReverseNotAllowed - logger.info("System user groups update signal recv: {}".format(instance)) - - users = User.objects.filter(groups__id__in=pk_set).distinct() - instance.users.add(*users) - - -@receiver(post_save, sender=SystemUser, dispatch_uid="jms") -@on_transaction_commit -def on_system_user_update(instance: SystemUser, created, **kwargs): - """ - 当系统用户更新时,可能更新了密钥,用户名等,这时要自动推送系统用户到资产上, - 其实应该当 用户名,密码,密钥 sudo等更新时再推送,这里偷个懒, - 这里直接取了 instance.assets 因为nodes和系统用户发生变化时,会自动将nodes下的资产 - 关联到上面 - """ - if instance and not created: - logger.info("System user update signal recv: {}".format(instance)) - assets = instance.assets.all().valid() - push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets]) - # add assets to su_from - instance.add_related_assets_to_su_from_if_need(assets) diff --git a/apps/assets/task_handlers/__init__.py b/apps/assets/task_handlers/__init__.py deleted file mode 100644 index d557c449e..000000000 --- a/apps/assets/task_handlers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .endpoint import * diff --git a/apps/assets/task_handlers/endpoint.py b/apps/assets/task_handlers/endpoint.py deleted file mode 100644 index 729fc8648..000000000 --- a/apps/assets/task_handlers/endpoint.py +++ /dev/null @@ -1,10 +0,0 @@ -from .backup.manager import AccountBackupExecutionManager - - -class ExecutionManager: - manager_type = { - 'backup': AccountBackupExecutionManager - } - - def __new__(cls, execution): - return AccountBackupExecutionManager(execution) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index 22ccbf503..060f4d2d9 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # +from .ping import * from .utils import * from .common import * -from .asset_connectivity import * -from .account_connectivity import * -from .gather_asset_users import * -from .gather_asset_hardware_info import * -from .push_system_user import * -from .system_user_connectivity import * -from .nodes_amount import * from .backup import * +from .automation import * +from .gather_facts import * +from .nodes_amount import * +from .push_account import * +from .verify_account import * +from .gather_accounts import * diff --git a/apps/assets/tasks/account_connectivity.py b/apps/assets/tasks/account_connectivity.py deleted file mode 100644 index 197979055..000000000 --- a/apps/assets/tasks/account_connectivity.py +++ /dev/null @@ -1,109 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from celery import shared_task -from django.utils.translation import ugettext as _, gettext_noop - -from common.utils import get_logger -from orgs.utils import org_aware_func -from ..models import Connectivity -from . import const -from .utils import check_asset_can_run_ansible - - -logger = get_logger(__file__) - - -__all__ = [ - 'test_account_connectivity_util', 'test_accounts_connectivity_manual', - 'get_test_account_connectivity_tasks', 'test_user_connectivity', - 'run_adhoc', -] - - -def get_test_account_connectivity_tasks(asset): - if asset.is_unixlike(): - tasks = const.PING_UNIXLIKE_TASKS - elif asset.is_windows(): - tasks = const.PING_WINDOWS_TASKS - else: - msg = _( - "The asset {} system platform {} does not " - "support run Ansible tasks".format(asset.hostname, asset.platform) - ) - logger.info(msg) - tasks = [] - return tasks - - -def run_adhoc(task_name, tasks, inventory): - """ - :param task_name - :param tasks - :param inventory - """ - from ops.ansible.runner import AdHocRunner - runner = AdHocRunner(inventory, options=const.TASK_OPTIONS) - result = runner.run(tasks, 'all', task_name) - return result.results_raw, result.results_summary - - -def test_user_connectivity(task_name, asset, username, password=None, private_key=None): - """ - :param task_name - :param asset - :param username - :param password - :param private_key - """ - from ops.inventory import JMSCustomInventory - - tasks = get_test_account_connectivity_tasks(asset) - if not tasks: - logger.debug("No tasks ") - return {}, {} - inventory = JMSCustomInventory( - assets=[asset], username=username, password=password, - private_key=private_key - ) - raw, summary = run_adhoc( - task_name=task_name, tasks=tasks, inventory=inventory - ) - return raw, summary - - -@org_aware_func("account") -def test_account_connectivity_util(account, task_name): - """ - :param account: 对象 - :param task_name: - :return: - """ - if not check_asset_can_run_ansible(account.asset): - return - - account.load_auth() - try: - raw, summary = test_user_connectivity( - task_name=task_name, asset=account.asset, - username=account.username, password=account.password, - private_key=account.private_key_file - ) - except Exception as e: - logger.warn("Failed run adhoc {}, {}".format(task_name, e)) - return - - if summary.get('success'): - account.set_connectivity(Connectivity.ok) - else: - account.set_connectivity(Connectivity.failed) - - -@shared_task(queue="ansible") -def test_accounts_connectivity_manual(accounts): - """ - :param accounts: 对象 - """ - for account in accounts: - task_name = gettext_noop("Test account connectivity: ") + str(account) - test_account_connectivity_util(account, task_name) - print(".\n") diff --git a/apps/assets/tasks/asset_connectivity.py b/apps/assets/tasks/asset_connectivity.py deleted file mode 100644 index 0038c38d3..000000000 --- a/apps/assets/tasks/asset_connectivity.py +++ /dev/null @@ -1,116 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -from itertools import groupby -from collections import defaultdict -from celery import shared_task -from django.utils.translation import gettext_noop - -from common.utils import get_logger -from orgs.utils import org_aware_func -from ..models import Asset, Connectivity, AuthBook -from . import const -from .utils import clean_ansible_task_hosts, group_asset_by_platform - - -logger = get_logger(__file__) -__all__ = [ - 'test_asset_connectivity_util', 'test_asset_connectivity_manual', - 'test_node_assets_connectivity_manual', 'test_assets_connectivity_manual', -] - - -def set_assets_accounts_connectivity(assets, results_summary): - asset_ids_ok = set() - asset_ids_failed = set() - - asset_hostnames_ok = results_summary.get('contacted', {}).keys() - - for asset in assets: - if asset.hostname in asset_hostnames_ok: - asset_ids_ok.add(asset.id) - else: - asset_ids_failed.add(asset.id) - - Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok) - Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed) - - accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin') - accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin') - - AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok) - AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed) - - -@shared_task(queue="ansible") -@org_aware_func("assets") -def test_asset_connectivity_util(assets, task_name=None): - from ops.utils import update_or_create_ansible_task - - if task_name is None: - task_name = gettext_noop("Test assets connectivity. ") - - hosts = clean_ansible_task_hosts(assets) - if not hosts: - return {} - platform_hosts_map = {} - hosts_sorted = sorted(hosts, key=group_asset_by_platform) - platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) - for i in platform_hosts: - platform_hosts_map[i[0]] = list(i[1]) - - platform_tasks_map = { - "unixlike": const.PING_UNIXLIKE_TASKS, - "windows": const.PING_WINDOWS_TASKS - } - results_summary = dict( - contacted=defaultdict(dict), dark=defaultdict(dict), success=True - ) - for platform, _hosts in platform_hosts_map.items(): - if not _hosts: - continue - logger.debug("System user not has special auth") - tasks = platform_tasks_map.get(platform) - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=_hosts, tasks=tasks, - pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, - ) - raw, summary = task.run() - success = summary.get('success', False) - contacted = summary.get('contacted', {}) - dark = summary.get('dark', {}) - - results_summary['success'] &= success - results_summary['contacted'].update(contacted) - results_summary['dark'].update(dark) - continue - set_assets_accounts_connectivity(assets, results_summary) - return results_summary - - -@shared_task(queue="ansible") -def test_asset_connectivity_manual(asset): - task_name = gettext_noop("Test assets connectivity: ") + str(asset) - summary = test_asset_connectivity_util([asset], task_name=task_name) - - if summary.get('dark'): - return False, summary['dark'] - else: - return True, "" - - -@shared_task(queue="ansible") -def test_assets_connectivity_manual(assets): - task_name = gettext_noop("Test assets connectivity: ") + str([asset.hostname for asset in assets]) - summary = test_asset_connectivity_util(assets, task_name=task_name) - - if summary.get('dark'): - return False, summary['dark'] - else: - return True, "" - - -@shared_task(queue="ansible") -def test_node_assets_connectivity_manual(node): - task_name = gettext_noop("Test if the assets under the node are connectable: ") + node.name - assets = node.get_all_assets() - result = test_asset_connectivity_util(assets, task_name=task_name) - return result diff --git a/apps/assets/tasks/automation.py b/apps/assets/tasks/automation.py new file mode 100644 index 000000000..60f01836f --- /dev/null +++ b/apps/assets/tasks/automation.py @@ -0,0 +1,20 @@ +from celery import shared_task +from django.utils.translation import gettext_lazy as _ + +from orgs.utils import tmp_to_root_org, tmp_to_org +from common.utils import get_logger, get_object_or_none +from assets.const import AutomationTypes + +logger = get_logger(__file__) + + +@shared_task(queue='ansible', verbose_name=_('Execute automation')) +def execute_automation(pid, trigger, tp): + model = AutomationTypes.get_type_model(tp) + with tmp_to_root_org(): + instance = get_object_or_none(model, pk=pid) + if not instance: + logger.error("No automation task found: {}".format(pid)) + return + with tmp_to_org(instance.org): + instance.execute(trigger) diff --git a/apps/assets/tasks/backup.py b/apps/assets/tasks/backup.py index 5d4e91011..a82a6abd1 100644 --- a/apps/assets/tasks/backup.py +++ b/apps/assets/tasks/backup.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from celery import shared_task +from django.utils.translation import gettext_lazy as _ from common.utils import get_object_or_none, get_logger from orgs.utils import tmp_to_org, tmp_to_root_org @@ -9,7 +10,7 @@ from assets.models import AccountBackupPlan logger = get_logger(__file__) -@shared_task +@shared_task(verbose_name=_('Execute account backup plan')) def execute_account_backup_plan(pid, trigger): with tmp_to_root_org(): plan = get_object_or_none(AccountBackupPlan, pk=pid) diff --git a/apps/assets/tasks/common.py b/apps/assets/tasks/common.py index b6c2326e9..ec51c5a2b 100644 --- a/apps/assets/tasks/common.py +++ b/apps/assets/tasks/common.py @@ -1,36 +1,2 @@ # -*- coding: utf-8 -*- # - -from celery import shared_task - -from orgs.utils import tmp_to_root_org -from assets.models import AuthBook - -__all__ = ['add_nodes_assets_to_system_users'] - - -@shared_task -@tmp_to_root_org() -def add_nodes_assets_to_system_users(nodes_keys, system_users): - from ..models import Node - from assets.tasks import push_system_user_to_assets - - nodes = Node.objects.filter(key__in=nodes_keys) - assets = Node.get_nodes_all_assets(*nodes) - for system_user in system_users: - """ 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号, - 无法更新节点下所有资产的管理用户的问题 """ - need_push_asset_ids = [] - for asset in assets: - defaults = {'asset': asset, 'systemuser': system_user, 'org_id': asset.org_id} - instance, created = AuthBook.objects.update_or_create( - defaults=defaults, asset=asset, systemuser=system_user - ) - if created: - need_push_asset_ids.append(asset.id) - # # 不再自动更新资产管理用户,只允许用户手动指定。 - # 只要关联都需要更新资产的管理用户 - # instance.update_asset_admin_user_if_need() - - if need_push_asset_ids: - push_system_user_to_assets.delay(system_user.id, need_push_asset_ids) diff --git a/apps/assets/tasks/const.py b/apps/assets/tasks/const.py deleted file mode 100644 index 51f92bd51..000000000 --- a/apps/assets/tasks/const.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# -import os - -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ - - -ENV_PERIOD_TASK = os.environ.get("PERIOD_TASK", "on") == 'on' -PERIOD_TASK_ENABLED = settings.PERIOD_TASK_ENABLED and ENV_PERIOD_TASK - -UPDATE_ASSETS_HARDWARE_TASKS = [ - { - 'name': "setup", - 'action': { - 'module': 'setup' - } - } -] - -ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}" - -SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}" -PING_UNIXLIKE_TASKS = [ - { - "name": "ping", - "action": { - "module": "ping", - } - } -] -PING_WINDOWS_TASKS = [ - { - "name": "ping", - "action": { - "module": "win_ping", - } - } -] - -TASK_OPTIONS = { - 'timeout': 10, - 'forks': 10, -} - -CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}' -CONN_UNREACHABLE, CONN_REACHABLE, CONN_UNKNOWN = range(0, 3) -CONNECTIVITY_CHOICES = ( - (CONN_UNREACHABLE, _("Unreachable")), - (CONN_REACHABLE, _('Reachable')), - (CONN_UNKNOWN, _("Unknown")), -) - -GATHER_ASSET_USERS_TASKS = [ - { - "name": "gather host users", - "action": { - "module": 'getent', - "args": "database=passwd" - }, - }, - { - "name": "get last login", - "action": { - "module": "shell", - "args": "users=$(getent passwd | grep -v 'nologin' | " - "grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -w -F $i -1 | " - "head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done" - } - } -] - -GATHER_ASSET_USERS_TASKS_WINDOWS = [ - { - "name": "gather windows host users", - "action": { - "module": 'win_shell', - "args": "net user" - } - } -] diff --git a/apps/assets/tasks/gather_accounts.py b/apps/assets/tasks/gather_accounts.py new file mode 100644 index 000000000..5e20bfe73 --- /dev/null +++ b/apps/assets/tasks/gather_accounts.py @@ -0,0 +1,35 @@ +# ~*~ coding: utf-8 ~*~ +from celery import shared_task +from django.utils.translation import gettext_noop +from django.utils.translation import gettext_lazy as _ + +from orgs.utils import tmp_to_root_org, org_aware_func +from common.utils import get_logger +from assets.models import Node + +__all__ = ['gather_asset_accounts'] +logger = get_logger(__name__) + + +@org_aware_func("nodes") +def gather_asset_accounts_util(nodes, task_name): + from assets.models import GatherAccountsAutomation + task_name = GatherAccountsAutomation.generate_unique_name(task_name) + + data = { + 'name': task_name, + 'comment': ', '.join([str(i) for i in nodes]) + } + instance = GatherAccountsAutomation.objects.create(**data) + instance.nodes.add(*nodes) + instance.execute() + + +@shared_task(queue="ansible", verbose_name=_('Gather asset accounts')) +def gather_asset_accounts(node_ids, task_name=None): + if task_name is None: + task_name = gettext_noop("Gather assets accounts") + + with tmp_to_root_org(): + nodes = Node.objects.filter(id__in=node_ids) + gather_asset_accounts_util(nodes=nodes, task_name=task_name) diff --git a/apps/assets/tasks/gather_asset_hardware_info.py b/apps/assets/tasks/gather_asset_hardware_info.py deleted file mode 100644 index ae8107cb2..000000000 --- a/apps/assets/tasks/gather_asset_hardware_info.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -# -import json -import re - -from celery import shared_task -from django.utils.translation import ugettext as _, gettext_noop - -from common.utils import ( - capacity_convert, sum_capacity, get_logger -) -from orgs.utils import org_aware_func -from . import const -from .utils import clean_ansible_task_hosts - - -logger = get_logger(__file__) -disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') -__all__ = [ - 'update_assets_hardware_info_util', 'update_asset_hardware_info_manual', - 'update_assets_hardware_info_period', 'update_node_assets_hardware_info_manual', - 'update_assets_hardware_info_manual', -] - - -def set_assets_hardware_info(assets, result, **kwargs): - """ - Using ops task run result, to update asset info - - @shared_task must be exit, because we using it as a task callback, is must - be a celery task also - :param assets: - :param result: - :param kwargs: {task_name: ""} - :return: - """ - result_raw = result[0] - assets_updated = [] - success_result = result_raw.get('ok', {}) - - for asset in assets: - hostname = asset.hostname - info = success_result.get(hostname, {}) - info = info.get('setup', {}).get('ansible_facts', {}) - if not info: - logger.error(_("Get asset info failed: {}").format(hostname)) - continue - ___vendor = info.get('ansible_system_vendor', 'Unknown') - ___model = info.get('ansible_product_name', 'Unknown') - ___sn = info.get('ansible_product_serial', 'Unknown') - - for ___cpu_model in info.get('ansible_processor', []): - if ___cpu_model.endswith('GHz') or ___cpu_model.startswith("Intel"): - break - else: - ___cpu_model = 'Unknown' - ___cpu_model = ___cpu_model[:48] - ___cpu_count = info.get('ansible_processor_count', 0) - ___cpu_cores = info.get('ansible_processor_cores', None) or \ - len(info.get('ansible_processor', [])) - ___cpu_vcpus = info.get('ansible_processor_vcpus', 0) - ___memory = '%s %s' % capacity_convert( - '{} MB'.format(info.get('ansible_memtotal_mb')) - ) - disk_info = {} - for dev, dev_info in info.get('ansible_devices', {}).items(): - if disk_pattern.match(dev) and dev_info['removable'] == '0': - disk_info[dev] = dev_info['size'] - ___disk_total = '%.1f %s' % sum_capacity(disk_info.values()) - ___disk_info = json.dumps(disk_info) - - # ___platform = info.get('ansible_system', 'Unknown') - ___os = info.get('ansible_distribution', 'Unknown') - ___os_version = info.get('ansible_distribution_version', 'Unknown') - ___os_arch = info.get('ansible_architecture', 'Unknown') - ___hostname_raw = info.get('ansible_hostname', 'Unknown') - - for k, v in locals().items(): - if k.startswith('___'): - setattr(asset, k.strip('_'), v) - asset.save() - assets_updated.append(asset) - return assets_updated - - -@shared_task -@org_aware_func("assets") -def update_assets_hardware_info_util(assets, task_name=None): - """ - Using ansible api to update asset hardware info - :param assets: asset seq - :param task_name: task_name running - :return: result summary ['contacted': {}, 'dark': {}] - """ - from ops.utils import update_or_create_ansible_task - if task_name is None: - task_name = gettext_noop("Update some assets hardware info. ") - tasks = const.UPDATE_ASSETS_HARDWARE_TASKS - hosts = clean_ansible_task_hosts(assets) - if not hosts: - return {} - task, created = update_or_create_ansible_task( - task_name, hosts=hosts, tasks=tasks, - pattern='all', options=const.TASK_OPTIONS, - run_as_admin=True, - ) - result = task.run() - set_assets_hardware_info(assets, result) - return True - - -@shared_task(queue="ansible") -def update_asset_hardware_info_manual(asset): - task_name = gettext_noop("Update asset hardware info: ") + str(asset.hostname) - update_assets_hardware_info_util([asset], task_name=task_name) - - -@shared_task(queue="ansible") -def update_assets_hardware_info_manual(assets): - task_name = gettext_noop("Update assets hardware info: ") + str([asset.hostname for asset in assets]) - update_assets_hardware_info_util(assets, task_name=task_name) - - -@shared_task(queue="ansible") -def update_assets_hardware_info_period(): - """ - Update asset hardware period task - :return: - """ - if not const.PERIOD_TASK_ENABLED: - logger.debug("Period task disabled, update assets hardware info pass") - return - - -@shared_task(queue="ansible") -def update_node_assets_hardware_info_manual(node): - task_name = gettext_noop("Update node asset hardware information: ") + str(node.name) - assets = node.get_all_assets() - result = update_assets_hardware_info_util(assets, task_name=task_name) - return result diff --git a/apps/assets/tasks/gather_asset_users.py b/apps/assets/tasks/gather_asset_users.py deleted file mode 100644 index abc69997a..000000000 --- a/apps/assets/tasks/gather_asset_users.py +++ /dev/null @@ -1,151 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -import re -from collections import defaultdict - -from celery import shared_task -from django.utils.translation import gettext_noop -from django.utils import timezone - -from orgs.utils import tmp_to_org, org_aware_func -from common.utils import get_logger -from ..models import GatheredUser, Node -from .utils import clean_ansible_task_hosts -from . import const - -__all__ = ['gather_asset_users', 'gather_nodes_asset_users'] -logger = get_logger(__name__) -space = re.compile('\s+') -ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$') - - -def parse_linux_result_to_users(result): - users = defaultdict(dict) - users_result = result.get('gather host users', {})\ - .get('ansible_facts', {})\ - .get('getent_passwd') - if not isinstance(users_result, dict): - users_result = {} - for username, attr in users_result.items(): - if ignore_login_shell.search(attr[-1]): - continue - users[username] = {} - last_login_result = result.get('get last login', {}).get('stdout_lines', []) - for line in last_login_result: - data = line.split('@') - if len(data) != 3: - continue - username, ip, dt = data - dt += ' +0800' - date = timezone.datetime.strptime(dt, '%b %d %H:%M:%S %Y %z') - users[username] = {"ip": ip, "date": date} - return users - - -def parse_windows_result_to_users(result): - task_result = [] - for task_name, raw in result.items(): - res = raw.get('stdout_lines', {}) - if res: - task_result = res - break - if not task_result: - return [] - - users = {} - - for i in range(4): - task_result.pop(0) - for i in range(2): - task_result.pop() - - for line in task_result: - username_list = space.split(line) - # such as: ['Admini', 'appadm', 'DefaultAccount', ''] - for username in username_list: - if not username: - continue - users[username] = {} - return users - - -def add_asset_users(assets, results): - assets_map = {a.hostname: a for a in assets} - parser_map = { - 'linux': parse_linux_result_to_users, - 'windows': parse_windows_result_to_users - } - - assets_users_map = {} - - for platform, platform_results in results.items(): - for hostname, res in platform_results.items(): - parse = parser_map.get(platform) - users = parse(res) - logger.debug('Gathered host users: {} {}'.format(hostname, users)) - asset = assets_map.get(hostname) - if not asset: - continue - assets_users_map[asset] = users - - for asset, users in assets_users_map.items(): - with tmp_to_org(asset.org_id): - GatheredUser.objects.filter(asset=asset, present=True)\ - .update(present=False) - for username, data in users.items(): - defaults = {'asset': asset, 'username': username, 'present': True} - if data.get("ip"): - defaults["ip_last_login"] = data["ip"][:32] - if data.get("date"): - defaults["date_last_login"] = data["date"] - GatheredUser.objects.update_or_create( - defaults=defaults, asset=asset, username=username, - ) - - -@shared_task(queue="ansible") -@org_aware_func("assets") -def gather_asset_users(assets, task_name=None): - from ops.utils import update_or_create_ansible_task - if task_name is None: - task_name = gettext_noop("Gather assets users") - assets = clean_ansible_task_hosts(assets) - if not assets: - return - hosts_category = { - 'linux': { - 'hosts': [], - 'tasks': const.GATHER_ASSET_USERS_TASKS - }, - 'windows': { - 'hosts': [], - 'tasks': const.GATHER_ASSET_USERS_TASKS_WINDOWS - } - } - for asset in assets: - hosts_list = hosts_category['windows']['hosts'] if asset.is_windows() \ - else hosts_category['linux']['hosts'] - hosts_list.append(asset) - - results = {'linux': defaultdict(dict), 'windows': defaultdict(dict)} - for k, value in hosts_category.items(): - if not value['hosts']: - continue - _task_name = '{}: {}'.format(task_name, k) - task, created = update_or_create_ansible_task( - task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'], - pattern='all', options=const.TASK_OPTIONS, - run_as_admin=True, - ) - raw, summary = task.run() - results[k].update(raw['ok']) - add_asset_users(assets, results) - - -@shared_task(queue="ansible") -def gather_nodes_asset_users(nodes_key): - nodes = Node.objects.filter(key__in=nodes_key) - assets = Node.get_nodes_all_assets(*nodes) - assets_groups_by_100 = [assets[i:i+100] for i in range(0, len(assets), 100)] - for _assets in assets_groups_by_100: - gather_asset_users(_assets) diff --git a/apps/assets/tasks/gather_facts.py b/apps/assets/tasks/gather_facts.py new file mode 100644 index 000000000..b3196abf5 --- /dev/null +++ b/apps/assets/tasks/gather_facts.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +from celery import shared_task +from django.utils.translation import gettext_noop +from django.utils.translation import gettext_lazy as _ + +from common.utils import get_logger +from orgs.utils import org_aware_func, tmp_to_root_org + +logger = get_logger(__file__) +__all__ = [ + 'update_assets_hardware_info_util', + 'update_node_assets_hardware_info_manual', + 'update_assets_hardware_info_manual', +] + + +@org_aware_func('assets') +def update_assets_hardware_info_util(assets=None, nodes=None, task_name=None): + from assets.models import GatherFactsAutomation + if not assets and not nodes: + logger.info("No assets or nodes to update hardware info") + return + + if task_name is None: + task_name = gettext_noop("Update some assets hardware info. ") + task_name = GatherFactsAutomation.generate_unique_name(task_name) + comment = '' + if assets: + comment += 'asset:' + ', '.join([str(i) for i in assets]) + '\n' + if nodes: + comment += 'node:' + ', '.join([str(i) for i in nodes]) + + data = {'name': task_name, 'comment': comment} + instance = GatherFactsAutomation.objects.create(**data) + + if assets: + instance.assets.add(*assets) + if nodes: + instance.nodes.add(*nodes) + instance.execute() + + +@shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets')) +def update_assets_hardware_info_manual(asset_ids): + from assets.models import Asset + with tmp_to_root_org(): + assets = Asset.objects.filter(id__in=asset_ids) + task_name = gettext_noop("Update assets hardware info: ") + update_assets_hardware_info_util(assets=assets, task_name=task_name) + + +@shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets under a node')) +def update_node_assets_hardware_info_manual(node_id): + from assets.models import Node + with tmp_to_root_org(): + node = Node.objects.get(id=node_id) + + task_name = gettext_noop("Update node asset hardware information: ") + update_assets_hardware_info_util(nodes=[node], task_name=task_name) diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index 2f8f0592d..f8d8d38a4 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -8,11 +8,12 @@ from assets.utils import check_node_assets_amount from common.utils.lock import AcquireFailed from common.utils import get_logger +from common.const.crontab import CRONTAB_AT_AM_TWO logger = get_logger(__file__) -@shared_task +@shared_task(verbose_name=_('Check the amount of assets under the node')) def check_node_assets_amount_task(org_id=None): if org_id is None: orgs = Organization.objects.all() @@ -29,7 +30,7 @@ def check_node_assets_amount_task(org_id=None): logger.error(error) -@register_as_period_task(crontab='0 2 * * *') -@shared_task +@register_as_period_task(crontab=CRONTAB_AT_AM_TWO) +@shared_task(verbose_name=_('Periodic check the amount of assets under the node')) def check_node_assets_amount_period_task(): check_node_assets_amount_task() diff --git a/apps/assets/tasks/ping.py b/apps/assets/tasks/ping.py new file mode 100644 index 000000000..817f64b64 --- /dev/null +++ b/apps/assets/tasks/ping.py @@ -0,0 +1,51 @@ +# ~*~ coding: utf-8 ~*~ +from celery import shared_task +from django.utils.translation import gettext_noop +from django.utils.translation import gettext_lazy as _ + +from common.utils import get_logger +from orgs.utils import org_aware_func, tmp_to_root_org + +logger = get_logger(__file__) +__all__ = [ + 'test_asset_connectivity_util', + 'test_assets_connectivity_manual', + 'test_node_assets_connectivity_manual', +] + + +@org_aware_func('assets') +def test_asset_connectivity_util(assets, task_name=None): + from assets.models import PingAutomation + if task_name is None: + task_name = gettext_noop("Test assets connectivity ") + + task_name = PingAutomation.generate_unique_name(task_name) + data = { + 'name': task_name, + 'comment': ', '.join([str(i) for i in assets]) + } + instance = PingAutomation.objects.create(**data) + instance.assets.add(*assets) + instance.execute() + + +@shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of a asset')) +def test_assets_connectivity_manual(asset_ids): + from assets.models import Asset + with tmp_to_root_org(): + assets = Asset.objects.filter(id__in=asset_ids) + + task_name = gettext_noop("Test assets connectivity ") + test_asset_connectivity_util(assets, task_name=task_name) + + +@shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of assets under a node')) +def test_node_assets_connectivity_manual(node_id): + from assets.models import Node + with tmp_to_root_org(): + node = Node.objects.get(id=node_id) + + task_name = gettext_noop("Test if the assets under the node are connectable ") + assets = node.get_all_assets() + test_asset_connectivity_util(assets, task_name=task_name) diff --git a/apps/assets/tasks/push_account.py b/apps/assets/tasks/push_account.py new file mode 100644 index 000000000..7c596c8f2 --- /dev/null +++ b/apps/assets/tasks/push_account.py @@ -0,0 +1,41 @@ +from celery import shared_task +from django.utils.translation import gettext_noop + +from common.utils import get_logger +from orgs.utils import org_aware_func, tmp_to_root_org +from django.utils.translation import ugettext_lazy as _ + +logger = get_logger(__file__) +__all__ = [ + 'push_accounts_to_assets', +] + + +@org_aware_func("assets") +def push_accounts_to_assets_util(accounts, assets, username=None): + from assets.models import PushAccountAutomation + task_name = gettext_noop("Push accounts to assets") + task_name = PushAccountAutomation.generate_unique_name(task_name) + if username is None: + account_usernames = list(accounts.values_list('username', flat=True)) + else: + account_usernames = [username] + + data = { + 'name': task_name, + 'accounts': account_usernames, + 'comment': ', '.join([str(i) for i in assets]) + } + instance = PushAccountAutomation.objects.create(**data) + instance.assets.add(*assets) + instance.execute() + + +@shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) +def push_accounts_to_assets(account_ids, asset_ids, username=None): + from assets.models import Asset, Account + with tmp_to_root_org(): + assets = Asset.objects.filter(id__in=asset_ids) + accounts = Account.objects.filter(id__in=account_ids) + + return push_accounts_to_assets_util(accounts, assets, username) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py deleted file mode 100644 index 8834a29e9..000000000 --- a/apps/assets/tasks/push_system_user.py +++ /dev/null @@ -1,307 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from itertools import groupby -from celery import shared_task -from common.db.utils import get_object_if_need, get_objects -from django.utils.translation import ugettext as _, gettext_noop -from django.db.models import Empty - -from common.utils import encrypt_password, get_logger -from assets.models import SystemUser, Asset -from orgs.utils import org_aware_func, tmp_to_root_org -from . import const -from .utils import clean_ansible_task_hosts, group_asset_by_platform - - -logger = get_logger(__file__) -__all__ = [ - 'push_system_user_util', 'push_system_user_to_assets', - 'push_system_user_to_assets_manual', 'push_system_user_a_asset_manual', - 'push_system_users_a_asset' -] - - -def _split_by_comma(raw: str): - try: - return [i.strip() for i in raw.split(',')] - except AttributeError: - return [] - - -def _dump_args(args: dict): - return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty]) - - -def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs): - algorithm = kwargs.get('algorithm') - if username is None: - username = system_user.username - - comment = system_user.name - if system_user.username_same_with_user: - from users.models import User - user = User.objects.filter(username=username).only('name', 'username').first() - if user: - comment = f'{system_user.name}[{str(user)}]' - comment = comment.replace(' ', '') - - password = system_user.password - public_key = system_user.public_key - - groups = _split_by_comma(system_user.system_groups) - - if groups: - groups = '"%s"' % ','.join(groups) - - add_user_args = { - 'name': username, - 'shell': system_user.shell or Empty, - 'state': 'present', - 'home': system_user.home or Empty, - 'expires': -1, - 'groups': groups or Empty, - 'comment': comment - } - - tasks = [ - { - 'name': 'Add user {}'.format(username), - 'action': { - 'module': 'user', - 'args': _dump_args(add_user_args), - } - }, - { - 'name': 'Add group {}'.format(username), - 'action': { - 'module': 'group', - 'args': 'name={} state=present'.format(username), - } - } - ] - if not system_user.home: - tasks.extend([ - { - 'name': 'Check home dir exists', - 'action': { - 'module': 'stat', - 'args': 'path=/home/{}'.format(username) - }, - 'register': 'home_existed' - }, - { - 'name': "Set home dir permission", - 'action': { - 'module': 'file', - 'args': "path=/home/{0} owner={0} group={0} mode=700".format(username) - }, - 'when': 'home_existed.stat.exists == true' - } - ]) - if password: - tasks.append({ - 'name': 'Set {} password'.format(username), - 'action': { - 'module': 'user', - 'args': 'name={} shell={} state=present password={}'.format( - username, system_user.shell, - encrypt_password(password, salt="K3mIlKK", algorithm=algorithm), - ), - } - }) - if public_key: - tasks.append({ - 'name': 'Set {} authorized key'.format(username), - 'action': { - 'module': 'authorized_key', - 'args': "user={} state=present key='{}'".format( - username, public_key - ) - } - }) - if system_user.sudo: - sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n') - sudo_list = sudo.split('\n') - sudo_tmp = [] - for s in sudo_list: - sudo_tmp.append(s.strip(',')) - sudo = ','.join(sudo_tmp) - tasks.append({ - 'name': 'Set {} sudo setting'.format(username), - 'action': { - 'module': 'lineinfile', - 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " - "line='{0} ALL=(ALL) NOPASSWD: {1}' " - "validate='visudo -cf %s'".format(username, sudo) - } - }) - - return tasks - - -def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs): - if username is None: - username = system_user.username - password = system_user.password - groups = {'Users', 'Remote Desktop Users'} - if system_user.system_groups: - groups.update(_split_by_comma(system_user.system_groups)) - groups = ','.join(groups) - - tasks = [] - if not password: - logger.error("Error: no password found") - return tasks - - if system_user.ad_domain: - logger.error('System user with AD domain do not support push.') - return tasks - - task = { - 'name': 'Add user {}'.format(username), - 'action': { - 'module': 'win_user', - 'args': 'fullname={} ' - 'name={} ' - 'password={} ' - 'state=present ' - 'update_password=always ' - 'password_expired=no ' - 'password_never_expires=yes ' - 'groups="{}" ' - 'groups_action=add ' - ''.format(username, username, password, groups), - } - } - tasks.append(task) - return tasks - - -def get_push_system_user_tasks(system_user, platform="unixlike", username=None, algorithm=None): - """ - 获取推送系统用户的 ansible 命令,跟资产无关 - :param system_user: - :param platform: - :param username: 当动态时,近推送某个 - :return: - """ - get_task_map = { - "unixlike": get_push_unixlike_system_user_tasks, - "windows": get_push_windows_system_user_tasks, - } - get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks) - if not system_user.username_same_with_user: - return get_tasks(system_user, algorithm=algorithm) - tasks = [] - # 仅推送这个username - if username is not None: - tasks.extend(get_tasks(system_user, username, algorithm=algorithm)) - return tasks - users = system_user.users.all().values_list('username', flat=True) - print(_("System user is dynamic: {}").format(list(users))) - for _username in users: - tasks.extend(get_tasks(system_user, _username, algorithm=algorithm)) - return tasks - - -@org_aware_func("system_user") -def push_system_user_util(system_user, assets, task_name, username=None): - from ops.utils import update_or_create_ansible_task - assets = clean_ansible_task_hosts(assets, system_user=system_user) - if not assets: - return {} - - # 资产按平台分类 - assets_sorted = sorted(assets, key=group_asset_by_platform) - platform_hosts = groupby(assets_sorted, key=group_asset_by_platform) - - if system_user.username_same_with_user: - if username is None: - # 动态系统用户,但是没有指定 username - usernames = list(system_user.users.all().values_list('username', flat=True).distinct()) - else: - usernames = [username] - else: - # 非动态系统用户指定 username 无效 - assert username is None, 'Only Dynamic user can assign `username`' - usernames = [system_user.username] - - def run_task(_tasks, _hosts): - if not _tasks: - return - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, - ) - task.run() - - for platform, _assets in platform_hosts: - _assets = list(_assets) - if not _assets: - continue - print(_("Start push system user for platform: [{}]").format(platform)) - print(_("Hosts count: {}").format(len(_assets))) - - for u in usernames: - for a in _assets: - system_user.load_asset_special_auth(a, u) - algorithm = 'des' if a.platform.name == 'AIX' else 'sha512' - tasks = get_push_system_user_tasks( - system_user, platform, username=u, - algorithm=algorithm - ) - run_task(tasks, [a]) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_user_to_assets_manual(system_user, username=None): - """ - 将系统用户推送到与它关联的所有资产上 - """ - system_user = get_object_if_need(SystemUser, system_user) - assets = system_user.get_related_assets() - task_name = gettext_noop("Push system users to assets: ") + system_user.name - return push_system_user_util(system_user, assets, task_name=task_name, username=username) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_user_a_asset_manual(system_user, asset, username=None): - """ - 将系统用户推送到一个资产上 - """ - # if username is None: - # username = system_user.username - task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format( - system_user.name, username or system_user.username, asset - ) - return push_system_user_util(system_user, [asset], task_name=task_name, username=username) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_users_a_asset(system_users, asset): - for system_user in system_users: - push_system_user_a_asset_manual(system_user, asset) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_user_to_assets(system_user_id, asset_ids, username=None): - """ - 推送系统用户到指定的若干资产上 - """ - system_user = SystemUser.objects.get(id=system_user_id) - assets = get_objects(Asset, asset_ids) - task_name = gettext_noop("Push system users to assets: ") + system_user.name - - return push_system_user_util(system_user, assets, task_name, username=username) - -# @shared_task -# @register_as_period_task(interval=3600) -# @after_app_ready_start -# @after_app_shutdown_clean_periodic -# def push_system_user_period(): -# for system_user in SystemUser.objects.all(): -# push_system_user_related_nodes(system_user) diff --git a/apps/assets/tasks/system_user_connectivity.py b/apps/assets/tasks/system_user_connectivity.py deleted file mode 100644 index 893081163..000000000 --- a/apps/assets/tasks/system_user_connectivity.py +++ /dev/null @@ -1,151 +0,0 @@ - -from itertools import groupby -from collections import defaultdict - -from celery import shared_task -from django.utils.translation import ugettext as _, gettext_noop - -from assets.models import Asset -from common.utils import get_logger -from orgs.utils import tmp_to_org, org_aware_func -from ..models import SystemUser, Connectivity, AuthBook -from . import const -from .utils import ( - clean_ansible_task_hosts, group_asset_by_platform -) - -logger = get_logger(__name__) -__all__ = [ - 'test_system_user_connectivity_util', 'test_system_user_connectivity_manual', - 'test_system_user_connectivity_period', 'test_system_user_connectivity_a_asset', - 'test_system_users_connectivity_a_asset' -] - - -def set_assets_accounts_connectivity(system_user, assets, results_summary): - asset_ids_ok = set() - asset_ids_failed = set() - - asset_hostnames_ok = results_summary.get('contacted', {}).keys() - - for asset in assets: - if asset.hostname in asset_hostnames_ok: - asset_ids_ok.add(asset.id) - else: - asset_ids_failed.add(asset.id) - - accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser=system_user) - accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser=system_user) - - AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok) - AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed) - - -@org_aware_func("system_user") -def test_system_user_connectivity_util(system_user, assets, task_name): - """ - Test system cant connect his assets or not. - :param system_user: - :param assets: - :param task_name: - :return: - """ - from ops.utils import update_or_create_ansible_task - - if system_user.username_same_with_user: - logger.error(_("Dynamic system user not support test")) - return - - # hosts = clean_ansible_task_hosts(assets, system_user=system_user) - # TODO: 这里不传递系统用户,因为clean_ansible_task_hosts会通过system_user来判断是否可以推送, - # 不符合测试可连接性逻辑, 后面需要优化此逻辑 - hosts = clean_ansible_task_hosts(assets) - if not hosts: - return {} - platform_hosts_map = {} - hosts_sorted = sorted(hosts, key=group_asset_by_platform) - platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) - for i in platform_hosts: - platform_hosts_map[i[0]] = list(i[1]) - - platform_tasks_map = { - "unixlike": const.PING_UNIXLIKE_TASKS, - "windows": const.PING_WINDOWS_TASKS - } - - results_summary = dict( - contacted=defaultdict(dict), dark=defaultdict(dict), success=True - ) - - def run_task(_tasks, _hosts, _username): - old_name = "{}".format(system_user) - new_name = "{}({})".format(system_user.name, _username) - _task_name = task_name.replace(old_name, new_name) - _task, created = update_or_create_ansible_task( - task_name=_task_name, hosts=_hosts, tasks=_tasks, - pattern='all', options=const.TASK_OPTIONS, - run_as=_username, system_user=system_user - ) - raw, summary = _task.run() - success = summary.get('success', False) - contacted = summary.get('contacted', {}) - dark = summary.get('dark', {}) - - results_summary['success'] &= success - results_summary['contacted'].update(contacted) - results_summary['dark'].update(dark) - - for platform, _hosts in platform_hosts_map.items(): - if not _hosts: - continue - if platform not in ["unixlike", "windows"]: - continue - - tasks = platform_tasks_map[platform] - print(_("Start test system user connectivity for platform: [{}]").format(platform)) - print(_("Hosts count: {}").format(len(_hosts))) - # 用户名不是动态的,用户名则是一个 - logger.debug("System user not has special auth") - run_task(tasks, _hosts, system_user.username) - - set_assets_accounts_connectivity(system_user, hosts, results_summary) - return results_summary - - -@shared_task(queue="ansible") -@org_aware_func("system_user") -def test_system_user_connectivity_manual(system_user, asset_ids=None): - task_name = gettext_noop("Test system user connectivity: ") + str(system_user) - if asset_ids: - assets = Asset.objects.filter(id__in=asset_ids) - else: - assets = system_user.get_related_assets() - test_system_user_connectivity_util(system_user, assets, task_name) - - -@shared_task(queue="ansible") -@org_aware_func("system_user") -def test_system_user_connectivity_a_asset(system_user, asset): - task_name = gettext_noop("Test system user connectivity: ") + "{} => {}".format( - system_user, asset - ) - test_system_user_connectivity_util(system_user, [asset], task_name) - - -@shared_task(queue="ansible") -def test_system_users_connectivity_a_asset(system_users, asset): - for system_user in system_users: - test_system_user_connectivity_a_asset(system_user, asset) - - -@shared_task(queue="ansible") -def test_system_user_connectivity_period(): - if not const.PERIOD_TASK_ENABLED: - logger.debug("Period task disabled, test system user connectivity pass") - return - queryset_map = SystemUser.objects.all_group_by_org() - for org, system_user in queryset_map.items(): - task_name = gettext_noop("Test system user connectivity period: ") + str(system_user) - with tmp_to_org(org): - assets = system_user.get_related_assets() - test_system_user_connectivity_util(system_user, assets, task_name) diff --git a/apps/assets/tasks/utils.py b/apps/assets/tasks/utils.py index 93aaa4bfc..a60f1b494 100644 --- a/apps/assets/tasks/utils.py +++ b/apps/assets/tasks/utils.py @@ -25,7 +25,7 @@ def check_asset_can_run_ansible(asset): def check_system_user_can_run_ansible(system_user): - if not system_user.auto_push: + if not system_user.auto_push_account: logger.warn(f'Push system user task skip, auto push not enable: system_user={system_user.name}') return False if not system_user.is_protocol_support_push: diff --git a/apps/assets/tasks/verify_account.py b/apps/assets/tasks/verify_account.py new file mode 100644 index 000000000..4538f2b2d --- /dev/null +++ b/apps/assets/tasks/verify_account.py @@ -0,0 +1,38 @@ +from celery import shared_task +from django.utils.translation import gettext_noop +from django.utils.translation import ugettext as _ + +from common.utils import get_logger +from orgs.utils import org_aware_func, tmp_to_root_org + +logger = get_logger(__name__) +__all__ = [ + 'verify_accounts_connectivity' +] + + +@org_aware_func("assets") +def verify_accounts_connectivity_util(accounts, assets, task_name): + from assets.models import VerifyAccountAutomation + task_name = VerifyAccountAutomation.generate_unique_name(task_name) + account_usernames = list(accounts.values_list('username', flat=True)) + + data = { + 'name': task_name, + 'accounts': account_usernames, + 'comment': ', '.join([str(i) for i in assets]) + } + instance = VerifyAccountAutomation.objects.create(**data) + instance.assets.add(*assets) + instance.execute() + + +@shared_task(queue="ansible", verbose_name=_('Verify asset account availability')) +def verify_accounts_connectivity(account_ids, asset_ids): + from assets.models import Asset, Account + with tmp_to_root_org(): + assets = Asset.objects.filter(id__in=asset_ids) + accounts = Account.objects.filter(id__in=account_ids) + + task_name = gettext_noop("Verify accounts connectivity") + return verify_accounts_connectivity_util(accounts, assets, task_name) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index e8658ec0f..773f5c348 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -1,61 +1,55 @@ # coding:utf-8 -from django.urls import path, re_path -from rest_framework_nested import routers +from django.urls import path from rest_framework_bulk.routes import BulkRouter -from common import api as capi - from .. import api app_name = 'assets' router = BulkRouter() +router.register(r'categories', api.CategoryViewSet, 'category') router.register(r'assets', api.AssetViewSet, 'asset') +router.register(r'hosts', api.HostViewSet, 'host') +router.register(r'devices', api.DeviceViewSet, 'device') +router.register(r'databases', api.DatabaseViewSet, 'database') +router.register(r'webs', api.WebViewSet, 'web') +router.register(r'clouds', api.CloudViewSet, 'cloud') router.register(r'accounts', api.AccountViewSet, 'account') +router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') +router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') -router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history') -router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') -router.register(r'system-users', api.SystemUserViewSet, 'system-user') -router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') 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'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') -router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') -router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') -router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation') router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') -cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') -cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') - +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-records', api.ChangeSecretRecordViewSet, 'change-secret-record') +router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation') urlpatterns = [ - path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), - path('assets//platform/', api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'), + # path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), path('assets//tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'), path('assets//perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'), - path('assets//perm-users//permissions/', api.AssetPermUserPermissionsListApi.as_view(), name='asset-perm-user-permission-list'), - path('assets//perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'), - path('assets//perm-user-groups//permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), - - path('system-users//auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), - path('system-users//assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), - path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), - path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), - path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'), - path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), - path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), - path('cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='cmd-filter-rules'), + path('assets//perm-users//permissions/', api.AssetPermUserPermissionsListApi.as_view(), + name='asset-perm-user-permission-list'), + path('assets//perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), + name='asset-perm-user-group-list'), + path('assets//perm-user-groups//permissions/', + api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'), + path('account-secrets//histories/', api.AccountHistoriesSecretAPI.as_view(), + 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'), @@ -69,13 +63,10 @@ urlpatterns = [ path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), - path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'), - + path('automation//asset/remove/', api.AutomationRemoveAssetApi.as_view(), name='automation-remove-asset'), + path('automation//asset/add/', api.AutomationAddAssetApi.as_view(), name='automation-add-asset'), + path('automation//nodes/', api.AutomationNodeAddRemoveApi.as_view(), name='automation-add-or-remove-node'), + path('automation//assets/', api.AutomationAssetsListApi.as_view(), name='automation-assets'), ] -old_version_urlpatterns = [ - re_path('(?Padmin-user|system-user|domain|gateway|cmd-filter|asset-user)/.*', capi.redirect_plural_name_api) -] - -urlpatterns += router.urls + cmd_filter_router.urls + old_version_urlpatterns - +urlpatterns += router.urls diff --git a/apps/assets/utils.py b/apps/assets/utils.py index c9857f802..6c39fffa5 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -53,7 +53,7 @@ def is_query_node_all_assets(request): return is_true(query_all_arg) -def get_node(request): +def get_node_from_request(request): node_id = dict_get_any(request.query_params, ['node', 'node_id']) if not node_id: return None @@ -126,8 +126,8 @@ class NodeAssetsUtil: from assets.models import Node, Asset nodes = list(Node.objects.all()) - nodes_assets = Asset.nodes.through.objects.all()\ - .annotate(aid=output_as_string('asset_id'))\ + nodes_assets = Asset.nodes.through.objects.all() \ + .annotate(aid=output_as_string('asset_id')) \ .values_list('node__key', 'aid') mapping = defaultdict(set) diff --git a/apps/audits/api.py b/apps/audits/api.py index 4cd73f12b..397c96c6b 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -15,20 +15,18 @@ from common.drf.filters import DatetimeRangeFilter from common.api import CommonGenericViewSet from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin from orgs.utils import current_org -from ops.models import CommandExecution +# from ops.models import CommandExecution from . import filters from .backends import TYPE_ENGINE_MAPPING from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog -from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer +from .serializers import FTPLogSerializer, UserLoginLogSerializer from .serializers import ( OperateLogSerializer, OperateLogActionDetailSerializer, - PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer + PasswordChangeLogSerializer ) -class FTPLogViewSet(CreateModelMixin, - ListModelMixin, - OrgGenericViewSet): +class FTPLogViewSet(CreateModelMixin, ListModelMixin, OrgGenericViewSet): model = FTPLog serializer_class = FTPLogSerializer extra_filter_backends = [DatetimeRangeFilter] @@ -122,49 +120,53 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): ) return queryset - -class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): - model = CommandExecution - serializer_class = CommandExecutionSerializer - extra_filter_backends = [DatetimeRangeFilter] - date_range_filter_fields = [ - ('date_start', ('date_from', 'date_to')) - ] - filterset_fields = [ - 'user__name', 'user__username', 'command', - 'run_as__name', 'run_as__username', 'is_finished' - ] - search_fields = [ - 'command', 'user__name', 'user__username', - 'run_as__name', 'run_as__username', - ] - ordering = ['-date_created'] - - def get_queryset(self): - queryset = super().get_queryset() - if current_org.is_root(): - return queryset - queryset = queryset.filter(run_as__org_id=current_org.org_id()) - return queryset - - -class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): - serializer_class = CommandExecutionHostsRelationSerializer - m2m_field = CommandExecution.hosts.field - filterset_class = filters.CommandExecutionFilter - search_fields = ('asset__hostname', ) - http_method_names = ['options', 'get'] - rbac_perms = { - 'GET': 'ops.view_commandexecution', - 'list': 'ops.view_commandexecution', - } - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - asset_display=Concat( - F('asset__hostname'), Value('('), - F('asset__ip'), Value(')') - ) - ) - return queryset +# Todo: 看看怎么搞 +# class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): +# model = CommandExecution +# serializer_class = CommandExecutionSerializer +# extra_filter_backends = [DatetimeRangeFilter] +# date_range_filter_fields = [ +# ('date_start', ('date_from', 'date_to')) +# ] +# filterset_fields = [ +# 'user__name', 'user__username', 'command', +# 'account', 'is_finished' +# ] +# search_fields = [ +# 'command', 'user__name', 'user__username', +# 'account__username', +# ] +# ordering = ['-date_created'] +# +# def get_queryset(self): +# queryset = super().get_queryset() +# if getattr(self, 'swagger_fake_view', False): +# return queryset.model.objects.none() +# if current_org.is_root(): +# return queryset +# # queryset = queryset.filter(run_as__org_id=current_org.org_id()) +# return queryset +# +# +# class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): +# serializer_class = CommandExecutionHostsRelationSerializer +# m2m_field = CommandExecution.hosts.field +# filterset_fields = [ +# 'id', 'asset', 'commandexecution' +# ] +# search_fields = ('asset__name', ) +# http_method_names = ['options', 'get'] +# rbac_perms = { +# 'GET': 'ops.view_commandexecution', +# 'list': 'ops.view_commandexecution', +# } +# +# def get_queryset(self): +# queryset = super().get_queryset() +# queryset = queryset.annotate( +# asset_display=Concat( +# F('asset__name'), Value('('), +# F('asset__address'), Value(')') +# ) +# ) +# return queryset diff --git a/apps/audits/const.py b/apps/audits/const.py index 17380311b..62edc62a2 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ +from django.db.models import TextChoices, IntegerChoices DEFAULT_CITY = _("Unknown") @@ -9,8 +10,12 @@ MODELS_NEED_RECORD = ( 'User', 'UserGroup', # authentication 'AccessKey', 'TempToken', + "User", + "UserGroup", # acls - 'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting', + "LoginACL", + "LoginAssetACL", + "LoginConfirmSetting", # assets 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', 'CommandFilter', 'Platform', 'Label', @@ -19,11 +24,11 @@ MODELS_NEED_RECORD = ( # account 'AuthBook', # orgs - 'Organization', + "Organization", # settings - 'Setting', + "Setting", # perms - 'AssetPermission', 'ApplicationPermission', + 'AssetPermission', # notifications 'SystemMsgSubscription', 'UserMsgSubscription', # Terminal @@ -31,6 +36,40 @@ MODELS_NEED_RECORD = ( # rbac 'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding', # xpack - 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'ApplicationChangeAuthPlan', + 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', 'Interface', ) + + +class OperateChoices(TextChoices): + mkdir = "mkdir", _("Mkdir") + rmdir = "rmdir", _("Rmdir") + delete = "delete", _("Delete") + upload = "upload", _("Upload") + rename = "rename", _("Rename") + symlink = "symlink", _("Symlink") + download = "download", _("Download") + + +class ActionChoices(TextChoices): + view = "view", _("View") + update = "update", _("Update") + delete = "delete", _("Delete") + create = "create", _("Create") + + +class LoginTypeChoices(TextChoices): + web = "W", _("Web") + terminal = "T", _("Terminal") + unknown = "U", _("Unknown") + + +class MFAChoices(IntegerChoices): + disabled = 0, _("Disabled") + enabled = 1, _("Enabled") + unknown = 2, _("-") + + +class LoginStatusChoices(IntegerChoices): + success = True, _("Success") + failed = False, _("Failed") diff --git a/apps/audits/filters.py b/apps/audits/filters.py index b8bf466ec..c15c22b56 100644 --- a/apps/audits/filters.py +++ b/apps/audits/filters.py @@ -5,10 +5,9 @@ from rest_framework import filters from rest_framework.compat import coreapi, coreschema from orgs.utils import current_org -from ops.models import CommandExecution from common.drf.filters import BaseFilterSet -__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter'] +__all__ = ['CurrentOrgMembersFilter'] class CurrentOrgMembersFilter(filters.BaseFilterBackend): @@ -35,21 +34,21 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend): queryset = queryset.filter(user__in=self._get_user_list()) return queryset - -class CommandExecutionFilter(BaseFilterSet): - hostname_ip = CharFilter(method='filter_hostname_ip') - - class Meta: - model = CommandExecution.hosts.through - fields = ( - 'id', 'asset', 'commandexecution', 'hostname_ip' - ) - - def filter_hostname_ip(self, queryset, name, value): - queryset = queryset.annotate( - hostname_ip=Concat( - F('asset__hostname'), Value('('), - F('asset__ip'), Value(')') - ) - ).filter(hostname_ip__icontains=value) - return queryset +# +# class CommandExecutionFilter(BaseFilterSet): +# hostname_ip = CharFilter(method='filter_hostname_ip') +# +# class Meta: +# model = CommandExecution.hosts.through +# fields = ( +# 'id', 'asset', 'commandexecution', 'hostname_ip' +# ) +# +# def filter_hostname_ip(self, queryset, name, value): +# queryset = queryset.annotate( +# hostname_ip=Concat( +# F('asset__hostname'), Value('('), +# F('asset__address'), Value(')') +# ) +# ).filter(hostname_ip__icontains=value) +# return queryset diff --git a/apps/audits/migrations/0015_auto_20221111_1919.py b/apps/audits/migrations/0015_auto_20221111_1919.py new file mode 100644 index 000000000..b638474ee --- /dev/null +++ b/apps/audits/migrations/0015_auto_20221111_1919.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.14 on 2022-11-11 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0014_auto_20220505_1902'), + ] + + operations = [ + migrations.AlterField( + model_name='ftplog', + name='operate', + field=models.CharField(choices=[('mkdir', 'Mkdir'), ('rmdir', 'Rmdir'), ('delete', 'Delete'), ('upload', 'Upload'), ('rename', 'Rename'), ('symlink', 'Symlink'), ('download', 'Download')], max_length=16, verbose_name='Operate'), + ), + migrations.AlterField( + model_name='operatelog', + name='action', + field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create')], max_length=16, verbose_name='Action'), + ), + migrations.AlterField( + model_name='userloginlog', + name='status', + field=models.BooleanField(choices=[(1, 'Success'), (0, 'Failed')], default=1, verbose_name='Status'), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index affd6045c..1e65ea233 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -9,59 +9,47 @@ from common.utils import lazyproperty from common.db.encoder import ModelJSONFieldEncoder from orgs.mixins.models import OrgModelMixin, Organization from orgs.utils import current_org +from .const import ( + OperateChoices, + ActionChoices, + LoginTypeChoices, + MFAChoices, + LoginStatusChoices, +) __all__ = [ - 'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog', + "FTPLog", + "OperateLog", + "PasswordChangeLog", + "UserLoginLog", ] class FTPLog(OrgModelMixin): - OPERATE_DELETE = 'Delete' - OPERATE_UPLOAD = 'Upload' - OPERATE_DOWNLOAD = 'Download' - OPERATE_RMDIR = 'Rmdir' - OPERATE_RENAME = 'Rename' - OPERATE_MKDIR = 'Mkdir' - OPERATE_SYMLINK = 'Symlink' - - OPERATE_CHOICES = ( - (OPERATE_DELETE, _('Delete')), - (OPERATE_UPLOAD, _('Upload')), - (OPERATE_DOWNLOAD, _('Download')), - (OPERATE_RMDIR, _('Rmdir')), - (OPERATE_RENAME, _('Rename')), - (OPERATE_MKDIR, _('Mkdir')), - (OPERATE_SYMLINK, _('Symlink')) - ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_('User')) - remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) + user = models.CharField(max_length=128, verbose_name=_("User")) + remote_addr = models.CharField( + 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")) - operate = models.CharField(max_length=16, verbose_name=_("Operate"), choices=OPERATE_CHOICES) + operate = models.CharField( + max_length=16, verbose_name=_("Operate"), choices=OperateChoices.choices + ) filename = models.CharField(max_length=1024, verbose_name=_("Filename")) is_success = models.BooleanField(default=True, verbose_name=_("Success")) - date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start')) + date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start")) class Meta: verbose_name = _("File transfer log") class OperateLog(OrgModelMixin): - ACTION_CREATE = 'create' - ACTION_VIEW = 'view' - ACTION_UPDATE = 'update' - ACTION_DELETE = 'delete' - ACTION_CHOICES = ( - (ACTION_CREATE, _("Create")), - (ACTION_VIEW, _("View")), - (ACTION_UPDATE, _("Update")), - (ACTION_DELETE, _("Delete")) - ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_('User')) - action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action")) + user = models.CharField(max_length=128, verbose_name=_("User")) + action = models.CharField( + max_length=16, choices=ActionChoices.choices, verbose_name=_("Action") + ) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) @@ -102,50 +90,48 @@ class OperateLog(OrgModelMixin): class PasswordChangeLog(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) - user = models.CharField(max_length=128, verbose_name=_('User')) + user = models.CharField(max_length=128, verbose_name=_("User")) change_by = models.CharField(max_length=128, verbose_name=_("Change by")) - remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) - datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime')) + remote_addr = models.CharField( + max_length=128, verbose_name=_("Remote addr"), blank=True, null=True + ) + datetime = models.DateTimeField(auto_now=True, verbose_name=_("Datetime")) def __str__(self): return "{} change {}'s password".format(self.change_by, self.user) class Meta: - verbose_name = _('Password change log') + verbose_name = _("Password change log") class UserLoginLog(models.Model): - LOGIN_TYPE_CHOICE = ( - ('W', 'Web'), - ('T', 'Terminal'), - ('U', 'Unknown'), - ) - - MFA_DISABLED = 0 - MFA_ENABLED = 1 - MFA_UNKNOWN = 2 - - MFA_CHOICE = ( - (MFA_DISABLED, _('Disabled')), - (MFA_ENABLED, _('Enabled')), - (MFA_UNKNOWN, _('-')), - ) - - STATUS_CHOICE = ( - (True, _('Success')), - (False, _('Failed')) - ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) - username = models.CharField(max_length=128, verbose_name=_('Username')) - type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) - ip = models.GenericIPAddressField(verbose_name=_('Login ip')) - city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city')) - user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent')) - mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA')) - reason = models.CharField(default='', max_length=128, blank=True, verbose_name=_('Reason')) - status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status')) - datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login')) - backend = models.CharField(max_length=32, default='', verbose_name=_('Authentication backend')) + username = models.CharField(max_length=128, verbose_name=_("Username")) + type = models.CharField( + choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type") + ) + ip = models.GenericIPAddressField(verbose_name=_("Login ip")) + city = models.CharField( + max_length=254, blank=True, null=True, verbose_name=_("Login city") + ) + user_agent = models.CharField( + max_length=254, blank=True, null=True, verbose_name=_("User agent") + ) + mfa = models.SmallIntegerField( + default=MFAChoices.unknown, choices=MFAChoices.choices, verbose_name=_("MFA") + ) + reason = models.CharField( + default="", max_length=128, blank=True, verbose_name=_("Reason") + ) + status = models.BooleanField( + default=LoginStatusChoices.success, + choices=LoginStatusChoices.choices, + verbose_name=_("Status"), + ) + datetime = models.DateTimeField(default=timezone.now, verbose_name=_("Date login")) + backend = models.CharField( + max_length=32, default="", verbose_name=_("Authentication backend") + ) @property def backend_display(self): @@ -155,8 +141,8 @@ class UserLoginLog(models.Model): def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None): login_logs = cls.objects.all() if date_from and date_to: - date_from = "{} {}".format(date_from, '00:00:00') - date_to = "{} {}".format(date_to, '23:59:59') + date_from = "{} {}".format(date_from, "00:00:00") + date_to = "{} {}".format(date_to, "23:59:59") login_logs = login_logs.filter( datetime__gte=date_from, datetime__lte=date_to ) @@ -164,18 +150,19 @@ class UserLoginLog(models.Model): login_logs = login_logs.filter(username=user) if keyword: login_logs = login_logs.filter( - Q(ip__contains=keyword) | - Q(city__contains=keyword) | - Q(username__contains=keyword) + Q(ip__contains=keyword) + | Q(city__contains=keyword) + | Q(username__contains=keyword) ) if not current_org.is_root(): - username_list = current_org.get_members().values_list('username', flat=True) + username_list = current_org.get_members().values_list("username", flat=True) login_logs = login_logs.filter(username__in=username_list) return login_logs @property def reason_display(self): from authentication.errors import reason_choices, old_reason_choices + reason = reason_choices.get(self.reason) if reason: return reason @@ -183,5 +170,5 @@ class UserLoginLog(models.Model): return reason class Meta: - ordering = ['-datetime', 'username'] - verbose_name = _('User login log') + ordering = ["-datetime", "username"] + verbose_name = _("User login log") diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 2b6c20686..10e5a7f16 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -2,48 +2,66 @@ # from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from django.db.models import F -from common.mixins import BulkSerializerMixin +from common.drf.fields import LabeledChoiceField from terminal.models import Session -from ops.models import CommandExecution from . import models +from .const import ( + ActionChoices, + OperateChoices, + MFAChoices, + LoginStatusChoices, + LoginTypeChoices, +) class FTPLogSerializer(serializers.ModelSerializer): - operate_display = serializers.ReadOnlyField(source='get_operate_display', label=_('Operate display')) + operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate")) class Meta: model = models.FTPLog - fields_mini = ['id'] + fields_mini = ["id"] fields_small = fields_mini + [ - 'user', 'remote_addr', 'asset', 'system_user', 'org_id', - 'operate', 'filename', 'operate_display', - 'is_success', - 'date_start', + "user", + "remote_addr", + "asset", + "system_user", + "org_id", + "operate", + "filename", + "is_success", + "date_start", ] fields = fields_small class UserLoginLogSerializer(serializers.ModelSerializer): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display')) - mfa_display = serializers.ReadOnlyField(source='get_mfa_display', label=_('MFA display')) + mfa = LabeledChoiceField(choices=MFAChoices.choices, label=_("MFA")) + type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type")) + status = LabeledChoiceField(choices=LoginStatusChoices.choices, label=_("Status")) class Meta: model = models.UserLoginLog - fields_mini = ['id'] + fields_mini = ["id"] fields_small = fields_mini + [ - 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', - 'mfa', 'mfa_display', 'reason', 'reason_display', 'backend', 'backend_display', - 'status', 'status_display', - 'datetime', + "username", + "type", + "ip", + "city", + "user_agent", + "mfa", + "reason", + "reason_display", + "backend", + "backend_display", + "status", + "datetime", ] fields = fields_small extra_kwargs = { - "user_agent": {'label': _('User agent')}, - "reason_display": {'label': _('Reason display')}, - 'backend_display': {'label': _('Authentication backend')} + "user_agent": {"label": _("User agent")}, + "reason_display": {"label": _("Reason display")}, + "backend_display": {"label": _("Authentication backend")}, } @@ -54,73 +72,73 @@ class OperateLogActionDetailSerializer(serializers.ModelSerializer): class OperateLogSerializer(serializers.ModelSerializer): - action_display = serializers.CharField(source='get_action_display', label=_('Action')) + action = LabeledChoiceField(choices=ActionChoices.choices, label=_("Action")) class Meta: model = models.OperateLog - fields_mini = ['id'] + fields_mini = ["id"] fields_small = fields_mini + [ - 'user', 'action', 'action_display', - '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')} - } + extra_kwargs = {"resource_type_display": {"label": _("Resource Type")}} class PasswordChangeLogSerializer(serializers.ModelSerializer): class Meta: model = models.PasswordChangeLog - fields = ( - 'id', 'user', 'change_by', 'remote_addr', 'datetime' - ) + fields = ("id", "user", "change_by", "remote_addr", "datetime") class SessionAuditSerializer(serializers.ModelSerializer): class Meta: model = Session - fields = '__all__' + 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 + [ - 'run_as', 'command', 'is_finished', 'user', - 'date_start', 'result', 'is_success', 'org_id' - ] - fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display'] - extra_kwargs = { - 'result': {'label': _('Result')}, # model 上的方法,只能在这修改 - 'is_success': {'label': _('Is success')}, - 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改 - 'run_as': {'label': _('Run as')}, - 'user': {'label': _('User')}, - 'run_as_display': {'label': _('Run as display')}, - 'user_display': {'label': _('User display')}, - } - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('user', 'run_as', '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' - ] +# +# 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/signal_handlers.py b/apps/audits/signal_handlers.py index 19186b568..d21beb4a4 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -2,26 +2,19 @@ # import uuid -from django.db.models.signals import ( - post_save, m2m_changed, pre_delete, pre_save -) from django.dispatch import receiver from django.conf import settings from django.db import transaction -from django.utils import timezone +from django.utils import timezone, translation from django.utils.functional import LazyObject from django.contrib.auth import BACKEND_SESSION_KEY from django.utils.translation import ugettext_lazy as _ -from django.utils import translation -from rest_framework.renderers import JSONRenderer +from django.db.models.signals import post_save, pre_save, m2m_changed, pre_delete from rest_framework.request import Request +from rest_framework.renderers import JSONRenderer + from users.models import User -from assets.models import Asset, SystemUser, CommandFilter -from terminal.models import Session, Command -from perms.models import AssetPermission, ApplicationPermission -from rbac.models import Role - from audits.utils import model_to_dict_for_operate_log as model_to_dict from audits.handler import ( get_instance_current_with_cache_diff, cache_instance_before_data, @@ -29,17 +22,19 @@ from audits.handler import ( ) from authentication.signals import post_auth_failed, post_auth_success from authentication.utils import check_different_city_login_if_need +from terminal.models import Session, Command from jumpserver.utils import current_request from users.signals import post_user_change_password -from .utils import write_login_log -from . import models, serializers from .models import OperateLog from .const import MODELS_NEED_RECORD -from terminal.backends.command.serializers import SessionCommandSerializer from terminal.serializers import SessionSerializer +from terminal.backends.command.serializers import SessionCommandSerializer from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL from common.utils import get_request_ip, get_logger, get_syslogger from common.utils.encode import data_to_json +from . import models, serializers +from .utils import write_login_log + logger = get_logger(__name__) @@ -54,14 +49,14 @@ class AuthBackendLabelMapping(LazyObject): for source, backends in User.SOURCE_BACKEND_MAPPING.items(): for backend in backends: backend_label_mapping[backend] = source.label - backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') - backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') - backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') - backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token') - backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') - backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu') - backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') - backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token') + backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _("SSH Key") + backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _("Password") + backend_label_mapping[settings.AUTH_BACKEND_SSO] = _("SSO") + backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") + backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") + backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") + backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") + backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") return backend_label_mapping def _setup(self): diff --git a/apps/audits/urls/api_urls.py b/apps/audits/urls/api_urls.py index 7301b67fb..902c65fbf 100644 --- a/apps/audits/urls/api_urls.py +++ b/apps/audits/urls/api_urls.py @@ -15,8 +15,8 @@ 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'command-execution-logs', api.CommandExecutionViewSet, 'command-execution-log') +# router.register(r'command-executions-hosts-relations', api.CommandExecutionHostRelationViewSet, 'command-executions-hosts-relation') urlpatterns = [ diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index a27c10d55..97ef3b5ff 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,109 +1,67 @@ -import abc -import os -import json import base64 +import json +import os import urllib.parse + from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import PermissionDenied -from rest_framework.decorators import action -from rest_framework.response import Response +from django.utils import timezone from rest_framework import status +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ValidationError from common.drf.api import JMSModelViewSet from common.http import is_true +from common.utils import random_string +from common.utils.django import get_request_os from orgs.mixins.api import RootOrgViewMixin -from perms.models.base import Action -from terminal.models import EndpointRule +from perms.models import ActionChoices +from terminal.const import NativeClient, TerminalType +from terminal.models import EndpointRule, Applet +from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer, + SuperConnectionTokenSerializer, ) -from ..models import ConnectionToken __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] -class ConnectionTokenMixin: +class RDPFileClientProtocolURLMixin: request: Request + get_serializer: callable @staticmethod - def check_token_valid(token: ConnectionToken): - is_valid, error = token.check_valid() - if not is_valid: - raise PermissionDenied(error) + def set_applet_info(token, rdp_options): + # remote-app + applet = Applet.objects.filter(name=token.connect_method).first() + if not applet: + return rdp_options - @abc.abstractmethod - def get_request_resource_user(self, serializer): - raise NotImplementedError - - def get_request_resources(self, serializer): - user = self.get_request_resource_user(serializer) - asset = serializer.validated_data.get('asset') - application = serializer.validated_data.get('application') - system_user = serializer.validated_data.get('system_user') - return user, asset, application, system_user - - @staticmethod - def check_user_has_resource_permission(user, asset, application, system_user): - from perms.utils.asset import has_asset_system_permission - from perms.utils.application import has_application_system_permission - - if asset and not has_asset_system_permission(user, asset, system_user): - error = f'User not has this asset and system user permission: ' \ - f'user={user.id} system_user={system_user.id} asset={asset.id}' - raise PermissionDenied(error) - - if application and not has_application_system_permission(user, application, system_user): - error = f'User not has this application and system user permission: ' \ - f'user={user.id} system_user={system_user.id} application={application.id}' - raise PermissionDenied(error) - - def get_smart_endpoint(self, protocol, asset=None, application=None): - if asset: - target_instance = asset - target_ip = asset.get_target_ip() - elif application: - target_instance = application - target_ip = application.get_target_ip() - else: - target_instance = None - target_ip = '' - endpoint = EndpointRule.match_endpoint(target_instance, target_ip, protocol, self.request) - return endpoint - - @staticmethod - def parse_env_bool(env_key, env_default, true_value, false_value): - return true_value if is_true(os.getenv(env_key, env_default)) else false_value - - def get_client_protocol_data(self, token: ConnectionToken): - from assets.models import SystemUser - protocol = token.system_user.protocol - username = token.user.username - rdp_config = ssh_token = '' - if protocol == SystemUser.Protocol.rdp: - filename, rdp_config = self.get_rdp_file_info(token) - elif protocol == SystemUser.Protocol.ssh: - filename, ssh_token = self.get_ssh_token(token) - else: - raise ValueError('Protocol not support: {}'.format(protocol)) - filename = urllib.parse.unquote(filename) - return { - "filename": filename, - "protocol": protocol, - "username": username, - "token": ssh_token, - "config": rdp_config + 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': '', 'username:s': '', - # 'screen mode id:i': '1', - # 'desktopwidth:i': '1280', - # 'desktopheight:i': '800', 'use multimon:i': '0', 'session bpp:i': '32', 'audiomode:i': '0', @@ -124,18 +82,12 @@ class ConnectionTokenMixin: 'bookmarktype:i': '3', 'use redirection server name:i': '0', 'smart sizing:i': '1', - # 'drivestoredirect:s': '*', - # 'domain:s': '' - # 'alternate shell:s:': '||MySQLWorkbench', - # 'remoteapplicationname:s': 'Firefox', - # 'remoteapplicationcmdline:s': '', } # 设置磁盘挂载 drives_redirect = is_true(self.request.query_params.get('drives_redirect')) if drives_redirect: - actions = Action.choices_to_value(token.actions) - if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD: + if ActionChoices.contains(token.actions, ActionChoices.transfer()): rdp_options['drivestoredirect:s'] = '*' # 设置全屏 @@ -143,15 +95,12 @@ class ConnectionTokenMixin: rdp_options['screen mode id:i'] = '2' if full_screen else '1' # 设置 RDP Server 地址 - endpoint = self.get_smart_endpoint( - protocol='rdp', asset=token.asset, application=token.application - ) + endpoint = self.get_smart_endpoint(protocol='rdp', asset=token.asset) rdp_options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}' # 设置用户名 rdp_options['username:s'] = '{}|{}'.format(token.user.username, str(token.id)) - if token.system_user.ad_domain: - rdp_options['domain:s'] = token.system_user.ad_domain + # rdp_options['domain:s'] = token.account_ad_domain # 设置宽高 height = self.request.query_params.get('height') @@ -165,17 +114,11 @@ class ConnectionTokenMixin: 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') - if token.asset: - name = token.asset.hostname - elif token.application and token.application.category_remote_app: - app = '||jmservisor' - name = token.application.name - rdp_options['remoteapplicationmode:i'] = '1' - rdp_options['alternate shell:s'] = app - rdp_options['remoteapplicationprogram:s'] = app - rdp_options['remoteapplicationname:s'] = name - else: - name = '*' + # 设置远程应用 + self.set_applet_info(token, rdp_options) + + # 文件名 + name = token.asset.name prefix_name = f'{token.user.username}-{name}' filename = self.get_connect_filename(prefix_name) @@ -194,104 +137,71 @@ class ConnectionTokenMixin: filename = urllib.parse.quote(filename) return filename - def get_ssh_token(self, token: ConnectionToken): - if token.asset: - name = token.asset.hostname - elif token.application: - name = token.application.name - else: - name = '*' - prefix_name = f'{token.user.username}-{name}' - filename = self.get_connect_filename(prefix_name) + @staticmethod + def parse_env_bool(env_key, env_default, true_value, false_value): + return true_value if is_true(os.getenv(env_key, env_default)) else false_value - endpoint = self.get_smart_endpoint( - protocol='ssh', asset=token.asset, application=token.application + def get_client_protocol_data(self, token: ConnectionToken): + _os = get_request_os(self.request) + + connect_method_name = token.connect_method + connect_method_dict = TerminalType.get_connect_method( + token.connect_method, token.protocol, _os ) + if connect_method_dict is None: + raise ValueError('Connect method not support: {}'.format(connect_method_name)) + data = { - 'ip': endpoint.host, - 'port': str(endpoint.ssh_port), - 'username': 'JMS-{}'.format(str(token.id)), - 'password': token.secret + 'id': str(token.id), + 'value': token.value, + 'protocol': token.protocol, + 'command': '', + 'file': {} } - token = json.dumps(data) - return filename, token - -class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet): - filterset_fields = ( - 'type', 'user_display', 'system_user_display', - 'application_display', 'asset_display' - ) - search_fields = filterset_fields - serializer_classes = { - 'default': ConnectionTokenSerializer, - 'list': ConnectionTokenDisplaySerializer, - 'retrieve': ConnectionTokenDisplaySerializer, - 'get_secret_detail': ConnectionTokenSecretSerializer, - } - rbac_perms = { - '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', - } - - def get_queryset(self): - return ConnectionToken.objects.filter(user=self.request.user) - - def get_request_resource_user(self, serializer): - return self.request.user - - def get_object(self): - if self.request.user.is_service_account: - # TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中 - obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk')) + if connect_method_name == NativeClient.mstsc: + filename, content = self.get_rdp_file_info(token) + data.update({ + 'file': { + 'name': filename, + 'content': content, + } + }) else: - obj = super(ConnectionTokenViewSet, self).get_object() - return obj + endpoint = self.get_smart_endpoint( + protocol=connect_method_dict['endpoint_protocol'], + asset=token.asset + ) + cmd = NativeClient.get_launch_command(connect_method_name, token, endpoint) + data.update({'command': cmd}) + return data - def create_connection_token(self): - data = self.request.query_params if self.request.method == 'GET' else self.request.data - serializer = self.get_serializer(data=data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - token: ConnectionToken = serializer.instance - return token + def get_smart_endpoint(self, protocol, asset=None): + target_ip = asset.get_target_ip() if asset else '' + endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request) + return endpoint - def perform_create(self, serializer): - user, asset, application, system_user = self.get_request_resources(serializer) - self.check_user_has_resource_permission(user, asset, application, system_user) - return super(ConnectionTokenViewSet, self).perform_create(serializer) - @action(methods=['POST'], detail=False, url_path='secret-info/detail') - def get_secret_detail(self, request, *args, **kwargs): - # 非常重要的 api,在逻辑层再判断一下,双重保险 - perm_required = 'authentication.view_connectiontokensecret' - if not request.user.has_perm(perm_required): - raise PermissionDenied('Not allow to view secret') - token_id = request.data.get('token') or '' - token = get_object_or_404(ConnectionToken, pk=token_id) - self.check_token_valid(token) - token.load_system_user_auth() - serializer = self.get_serializer(instance=token) - return Response(serializer.data, status=status.HTTP_200_OK) +class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): + request: Request + get_object: callable + get_serializer: callable + perform_create: callable - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') - def get_rdp_file(self, request, *args, **kwargs): - token = self.create_connection_token() - self.check_token_valid(token) + @action(methods=['POST', 'GET'], detail=True, url_path='rdp-file') + def get_rdp_file(self, *args, **kwargs): + token = self.get_object() + token.is_valid() filename, content = self.get_rdp_file_info(token) filename = '{}.rdp'.format(filename) response = HttpResponse(content, content_type='application/octet-stream') response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename return response - @action(methods=['POST', 'GET'], detail=False, url_path='client-url') - def get_client_protocol_url(self, request, *args, **kwargs): - token = self.create_connection_token() - self.check_token_valid(token) + @action(methods=['POST', 'GET'], detail=True, url_path='client-url') + def get_client_protocol_url(self, *args, **kwargs): + token = self.get_object() + token.is_valid() try: protocol_data = self.get_client_protocol_data(token) except ValueError as e: @@ -310,6 +220,88 @@ class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelVie return Response(status=status.HTTP_204_NO_CONTENT) +class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet): + filterset_fields = ( + 'user_display', 'asset_display' + ) + 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) \ + .filter(date_expired__gt=timezone.now()) + return queryset + + def get_user(self, serializer): + return self.request.user + + def perform_create(self, serializer): + self.validate_serializer(serializer) + return super().perform_create(serializer) + + def validate_serializer(self, serializer): + from perms.utils.account import PermAccountUtil + + data = serializer.validated_data + user = self.get_user(serializer) + asset = data.get('asset') + account_name = data.get('account') + data['org_id'] = asset.org_id + data['user'] = user + data['value'] = random_string(16) + + util = PermAccountUtil() + 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( + user, asset, account_name + ) + raise PermissionDenied(msg) + + if permed_account.date_expired < timezone.now(): + raise PermissionDenied('Expired') + + if permed_account.has_secret: + data['input_secret'] = '' + if permed_account.username != '@INPUT': + data['input_username'] = '' + return permed_account + + class SuperConnectionTokenViewSet(ConnectionTokenViewSet): serializer_classes = { 'default': SuperConnectionTokenSerializer, @@ -319,14 +311,17 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): 'renewal': 'authentication.add_superconnectiontoken' } - def get_request_resource_user(self, serializer): + def get_queryset(self): + return ConnectionToken.objects.all() + + def get_user(self, serializer): return serializer.validated_data.get('user') @action(methods=['PATCH'], detail=False) def renewal(self, request, *args, **kwargs): from common.utils.timezone import as_current_tz - token_id = request.data.get('token') or '' + token_id = request.data.get('id') or '' token = get_object_or_404(ConnectionToken, pk=token_id) date_expired = as_current_tz(token.date_expired) if token.is_expired: diff --git a/apps/authentication/api/perm_token.py b/apps/authentication/api/perm_token.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/authentication/api/temp_token.py b/apps/authentication/api/temp_token.py index 6e640edd6..2fa5791e3 100644 --- a/apps/authentication/api/temp_token.py +++ b/apps/authentication/api/temp_token.py @@ -2,10 +2,10 @@ from django.utils import timezone from rest_framework.response import Response from rest_framework.decorators import action +from rbac.permissions import RBACPermission from common.drf.api import JMSModelViewSet from ..models import TempToken from ..serializers import TempTokenSerializer -from rbac.permissions import RBACPermission class TempTokenViewSet(JMSModelViewSet): diff --git a/apps/authentication/errors/const.py b/apps/authentication/errors/const.py index 530bcf150..e9a617f97 100644 --- a/apps/authentication/errors/const.py +++ b/apps/authentication/errors/const.py @@ -48,7 +48,7 @@ block_user_login_msg = _( "(please contact admin to unlock it or try again after {} minutes)" ) block_ip_login_msg = _( - "The ip has been locked " + "The address has been locked " "(please contact admin to unlock it or try again after {} minutes)" ) block_mfa_msg = _( diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py index 5b6d7c06f..8573b086b 100644 --- a/apps/authentication/middleware.py +++ b/apps/authentication/middleware.py @@ -1,4 +1,5 @@ import base64 +import time from django.shortcuts import redirect, reverse, render from django.utils.deprecation import MiddlewareMixin diff --git a/apps/authentication/migrations/0012_auto_20220816_1629.py b/apps/authentication/migrations/0012_auto_20220816_1629.py new file mode 100644 index 000000000..07c9559ce --- /dev/null +++ b/apps/authentication/migrations/0012_auto_20220816_1629.py @@ -0,0 +1,57 @@ +# Generated by Django 3.2.14 on 2022-08-16 08:29 + +from django.db import migrations, models + + +def migrate_system_user_to_account(apps, schema_editor): + connection_token_model = apps.get_model("authentication", "ConnectionToken") + count = 0 + bulk_size = 10000 + + while True: + connection_tokens = connection_token_model.objects \ + .prefetch_related('system_user')[count:bulk_size] + if not connection_tokens: + break + count += len(connection_tokens) + updated = [] + for connection_token in connection_tokens: + connection_token.account = connection_token.system_user.username + updated.append(connection_token) + connection_token_model.objects.bulk_update(updated, ['account_username']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0011_auto_20220705_1940'), + ] + + operations = [ + migrations.RemoveField( + model_name='connectiontoken', + name='application', + ), + migrations.RemoveField( + model_name='connectiontoken', + name='application_display', + ), + migrations.RemoveField( + model_name='connectiontoken', + name='system_user_display', + ), + migrations.AddField( + model_name='connectiontoken', + name='account_username', + field=models.CharField(default='', max_length=128, verbose_name='Account'), + ), + migrations.RunPython(migrate_system_user_to_account), + migrations.RemoveField( + model_name='connectiontoken', + name='system_user', + ), + migrations.RemoveField( + model_name='connectiontoken', + name='type', + ), + ] diff --git a/apps/authentication/migrations/0013_connectiontoken_protocol.py b/apps/authentication/migrations/0013_connectiontoken_protocol.py new file mode 100644 index 000000000..3ba4785b0 --- /dev/null +++ b/apps/authentication/migrations/0013_connectiontoken_protocol.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.14 on 2022-10-27 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('authentication', '0012_auto_20220816_1629'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='protocol', + field=models.CharField(default='ssh', max_length=16, verbose_name='Protocol'), + ), + ] diff --git a/apps/authentication/migrations/0014_auto_20221122_2152.py b/apps/authentication/migrations/0014_auto_20221122_2152.py new file mode 100644 index 000000000..483b6d5f0 --- /dev/null +++ b/apps/authentication/migrations/0014_auto_20221122_2152.py @@ -0,0 +1,51 @@ +# Generated by Django 3.2.14 on 2022-11-22 13:52 + +from django.db import migrations, models + +import common.db.fields + + +class Migration(migrations.Migration): + dependencies = [ + ('authentication', '0013_connectiontoken_protocol'), + ] + + operations = [ + migrations.RenameField( + model_name='connectiontoken', + old_name='account_username', + new_name='account_name' + ), + migrations.AlterField( + model_name='connectiontoken', + name='account_name', + field=models.CharField(max_length=128, verbose_name='Account name'), + ), + migrations.AddField( + model_name='connectiontoken', + name='input_username', + field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input username'), + ), + migrations.AddField( + model_name='connectiontoken', + name='input_secret', + field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, + verbose_name='Input secret'), + ), + migrations.RenameField( + model_name='connectiontoken', + old_name='secret', + new_name='value', + ), + migrations.AlterField( + model_name='connectiontoken', + name='value', + field=models.CharField(default='', max_length=64, verbose_name='Value'), + ), + migrations.AddField( + model_name='connectiontoken', + name='connect_method', + field=models.CharField(default='web_ui', max_length=32, verbose_name='Connect method'), + preserve_default=False, + ), + ] diff --git a/apps/authentication/migrations/0015_auto_20221205_1136.py b/apps/authentication/migrations/0015_auto_20221205_1136.py new file mode 100644 index 000000000..7de71fe5c --- /dev/null +++ b/apps/authentication/migrations/0015_auto_20221205_1136.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.14 on 2022-12-05 03:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('authentication', '0014_auto_20221122_2152'), + ] + + operations = [ + migrations.RenameField( + model_name='connectiontoken', + old_name='account_name', + new_name='account', + ), + ] diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 7341b4bd1..f381edecf 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- # import inspect -from functools import partial import time +from functools import partial from typing import Callable -from django.utils.http import urlencode -from django.core.cache import cache from django.conf import settings from django.contrib import auth -from django.utils.translation import ugettext as _ -from rest_framework.request import Request from django.contrib.auth import ( BACKEND_SESSION_KEY, load_backend, PermissionDenied, user_login_failed, _clean_credentials, ) +from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from django.shortcuts import reverse, redirect, get_object_or_404 +from django.utils.http import urlencode +from django.utils.translation import ugettext as _ +from rest_framework.request import Request -from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil from acls.models import LoginACL +from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil from . import errors @@ -333,13 +333,13 @@ class AuthACLMixin: return acl: LoginACL - if acl.is_action(acl.ActionChoices.allow): + if acl.is_action(acl.ActionChoices.accept): return if acl.is_action(acl.ActionChoices.reject): raise errors.LoginACLIPAndTimePeriodNotAllowed(user.username, request=self.request) - if acl.is_action(acl.ActionChoices.confirm): + if acl.is_action(acl.ActionChoices.review): self.request.session['auth_confirm_required'] = '1' self.request.session['auth_acl_id'] = str(acl.id) return @@ -354,7 +354,7 @@ class AuthACLMixin: acl = LoginACL.filter_acl(user).filter(id=acl_id).first() if not acl: return - if not acl.is_action(acl.ActionChoices.confirm): + if not acl.is_action(acl.ActionChoices.review): return self.get_ticket_or_create(acl) self.check_user_login_confirm() diff --git a/apps/authentication/models.py b/apps/authentication/models.py deleted file mode 100644 index 9b78a8bad..000000000 --- a/apps/authentication/models.py +++ /dev/null @@ -1,283 +0,0 @@ -import uuid -from datetime import datetime, timedelta -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from rest_framework.authtoken.models import Token -from orgs.mixins.models import OrgModelMixin - -from common.db import models -from common.utils import lazyproperty -from common.utils.timezone import as_current_tz - - -class AccessKey(models.Model): - id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, - default=uuid.uuid4, editable=False) - secret = models.UUIDField(verbose_name='AccessKeySecret', - default=uuid.uuid4, editable=False) - user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), - on_delete=models.CASCADE_SIGNAL_SKIP, related_name='access_keys') - is_active = models.BooleanField(default=True, verbose_name=_('Active')) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) - - def get_id(self): - return str(self.id) - - def get_secret(self): - return str(self.secret) - - def get_full_value(self): - return '{}:{}'.format(self.id, self.secret) - - def __str__(self): - return str(self.id) - - class Meta: - verbose_name = _("Access key") - - -class PrivateToken(Token): - """Inherit from auth token, otherwise migration is boring""" - - class Meta: - verbose_name = _('Private Token') - - -class SSOToken(models.JMSBaseModel): - """ - 类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036) - 出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。 - """ - authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) - expired = models.BooleanField(default=False, verbose_name=_('Expired')) - user = models.ForeignKey('users.User', on_delete=models.CASCADE_SIGNAL_SKIP, verbose_name=_('User'), db_constraint=False) - - class Meta: - verbose_name = _('SSO token') - - -def date_expired_default(): - return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION) - - -class ConnectionToken(OrgModelMixin, models.JMSModel): - class Type(models.TextChoices): - asset = 'asset', _('Asset') - application = 'application', _('Application') - - type = models.CharField( - max_length=16, default=Type.asset, choices=Type.choices, verbose_name=_("Type") - ) - secret = models.CharField(max_length=64, default='', verbose_name=_("Secret")) - date_expired = models.DateTimeField( - default=date_expired_default, verbose_name=_("Date expired") - ) - - user = models.ForeignKey( - 'users.User', on_delete=models.SET_NULL, verbose_name=_('User'), - related_name='connection_tokens', null=True, blank=True - ) - user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) - system_user = models.ForeignKey( - 'assets.SystemUser', on_delete=models.SET_NULL, verbose_name=_('System user'), - related_name='connection_tokens', null=True, blank=True - ) - system_user_display = models.CharField( - max_length=128, default='', verbose_name=_("System user display") - ) - asset = models.ForeignKey( - 'assets.Asset', on_delete=models.SET_NULL, verbose_name=_('Asset'), - related_name='connection_tokens', null=True, blank=True - ) - asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) - application = models.ForeignKey( - 'applications.Application', on_delete=models.SET_NULL, verbose_name=_('Application'), - related_name='connection_tokens', null=True, blank=True - ) - application_display = models.CharField( - max_length=128, default='', verbose_name=_("Application display") - ) - - class Meta: - ordering = ('-date_expired',) - verbose_name = _('Connection token') - permissions = [ - ('view_connectiontokensecret', _('Can view connection token secret')) - ] - - @classmethod - def get_default_date_expired(cls): - return date_expired_default() - - @property - def is_expired(self): - return self.date_expired < timezone.now() - - @property - def expire_time(self): - interval = self.date_expired - timezone.now() - seconds = interval.total_seconds() - if seconds < 0: - seconds = 0 - return int(seconds) - - def expire(self): - self.date_expired = timezone.now() - self.save() - - @property - def is_valid(self): - return not self.is_expired - - def is_type(self, tp): - return self.type == tp - - def renewal(self): - """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ - self.date_expired = self.get_default_date_expired() - self.save() - - actions = expired_at = None # actions 和 expired_at 在 check_valid() 中赋值 - - def check_valid(self): - from perms.utils.asset.permission import validate_permission as asset_validate_permission - from perms.utils.application.permission import validate_permission as app_validate_permission - - if self.is_expired: - is_valid = False - error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) - return is_valid, error - - if not self.user: - is_valid = False - error = _('User not exists') - return is_valid, error - if not self.user.is_valid: - is_valid = False - error = _('User invalid, disabled or expired') - return is_valid, error - - if not self.system_user: - is_valid = False - error = _('System user not exists') - return is_valid, error - - if self.is_type(self.Type.asset): - if not self.asset: - is_valid = False - error = _('Asset not exists') - return is_valid, error - if not self.asset.is_active: - is_valid = False - error = _('Asset inactive') - return is_valid, error - has_perm, actions, expired_at = asset_validate_permission( - self.user, self.asset, self.system_user - ) - if not has_perm: - is_valid = False - error = _('User has no permission to access asset or permission expired') - return is_valid, error - self.actions = actions - self.expired_at = expired_at - - elif self.is_type(self.Type.application): - if not self.application: - is_valid = False - error = _('Application not exists') - return is_valid, error - has_perm, actions, expired_at = app_validate_permission( - self.user, self.application, self.system_user - ) - if not has_perm: - is_valid = False - error = _('User has no permission to access application or permission expired') - return is_valid, error - self.actions = actions - self.expired_at = expired_at - - return True, '' - - @lazyproperty - def domain(self): - if self.asset: - return self.asset.domain - if not self.application: - return - if self.application.category_remote_app: - asset = self.application.get_remote_app_asset() - domain = asset.domain if asset else None - else: - domain = self.application.domain - return domain - - @lazyproperty - def gateway(self): - from assets.models import Domain - if not self.domain: - return - self.domain: Domain - return self.domain.random_gateway() - - @lazyproperty - def remote_app(self): - if not self.application: - return {} - if not self.application.category_remote_app: - return {} - return self.application.get_rdp_remote_app_setting() - - @lazyproperty - def asset_or_remote_app_asset(self): - if self.asset: - return self.asset - if self.application and self.application.category_remote_app: - return self.application.get_remote_app_asset() - - @lazyproperty - def cmd_filter_rules(self): - from assets.models import CommandFilterRule - kwargs = { - 'user_id': self.user.id, - 'system_user_id': self.system_user.id, - } - if self.asset: - kwargs['asset_id'] = self.asset.id - elif self.application: - kwargs['application_id'] = self.application_id - rules = CommandFilterRule.get_queryset(**kwargs) - return rules - - def load_system_user_auth(self): - if self.asset: - self.system_user.load_asset_more_auth(self.asset.id, self.user.username, self.user.id) - elif self.application: - self.system_user.load_app_more_auth(self.application.id, self.user.username, self.user.id) - - -class TempToken(models.JMSModel): - username = models.CharField(max_length=128, verbose_name=_("Username")) - secret = models.CharField(max_length=64, verbose_name=_("Secret")) - verified = models.BooleanField(default=False, verbose_name=_("Verified")) - date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified")) - date_expired = models.DateTimeField(verbose_name=_("Date expired")) - - class Meta: - verbose_name = _("Temporary token") - - @property - def user(self): - from users.models import User - return User.objects.filter(username=self.username).first() - - @property - def is_valid(self): - not_expired = self.date_expired and self.date_expired > timezone.now() - return not self.verified and not_expired - - -class SuperConnectionToken(ConnectionToken): - class Meta: - proxy = True - verbose_name = _("Super connection token") diff --git a/apps/authentication/models/__init__.py b/apps/authentication/models/__init__.py new file mode 100644 index 000000000..e889e03b1 --- /dev/null +++ b/apps/authentication/models/__init__.py @@ -0,0 +1,5 @@ +from .access_key import * +from .connection_token import * +from .private_token import * +from .sso_token import * +from .temp_token import * diff --git a/apps/authentication/models/access_key.py b/apps/authentication/models/access_key.py new file mode 100644 index 000000000..67aa6b812 --- /dev/null +++ b/apps/authentication/models/access_key.py @@ -0,0 +1,31 @@ +import uuid +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +from django.db import models + + +class AccessKey(models.Model): + id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, + default=uuid.uuid4, editable=False) + secret = models.UUIDField(verbose_name='AccessKeySecret', + default=uuid.uuid4, editable=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', + on_delete=models.CASCADE, related_name='access_keys') + is_active = models.BooleanField(default=True, verbose_name=_('Active')) + date_created = models.DateTimeField(auto_now_add=True) + + def get_id(self): + return str(self.id) + + def get_secret(self): + return str(self.secret) + + def get_full_value(self): + return '{}:{}'.format(self.id, self.secret) + + def __str__(self): + return str(self.id) + + class Meta: + verbose_name = _("Access key") diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py new file mode 100644 index 000000000..b30ccb376 --- /dev/null +++ b/apps/authentication/models/connection_token.py @@ -0,0 +1,173 @@ +from datetime import timedelta + +from django.conf import settings +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +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.timezone import as_current_tz +from orgs.mixins.models import OrgModelMixin + + +def date_expired_default(): + return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION) + + +class ConnectionToken(OrgModelMixin, JMSBaseModel): + value = models.CharField(max_length=64, default='', verbose_name=_("Value")) + user = models.ForeignKey( + 'users.User', on_delete=models.SET_NULL, null=True, blank=True, + related_name='connection_tokens', verbose_name=_('User') + ) + asset = models.ForeignKey( + 'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True, + related_name='connection_tokens', verbose_name=_('Asset'), + ) + account = models.CharField(max_length=128, verbose_name=_("Account name")) # 登录账号Name + input_username = models.CharField(max_length=128, default='', blank=True, verbose_name=_("Input username")) + input_secret = EncryptCharField(max_length=64, default='', blank=True, verbose_name=_("Input secret")) + protocol = models.CharField(max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")) + connect_method = models.CharField(max_length=32, verbose_name=_("Connect method")) + user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) + asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) + date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_("Date expired")) + + class Meta: + ordering = ('-date_expired',) + verbose_name = _('Connection token') + permissions = [ + ('view_connectiontokensecret', _('Can view connection token secret')) + ] + + @property + def is_expired(self): + return self.date_expired < timezone.now() + + @property + def expire_time(self): + interval = self.date_expired - timezone.now() + seconds = interval.total_seconds() + if seconds < 0: + seconds = 0 + return int(seconds) + + def save(self, *args, **kwargs): + self.asset_display = pretty_string(self.asset, max_length=128) + self.user_display = pretty_string(self.user, max_length=128) + return super().save(*args, **kwargs) + + def expire(self): + self.date_expired = timezone.now() + self.save() + + def renewal(self): + """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ + self.date_expired = date_expired_default() + self.save() + + @lazyproperty + def permed_account(self): + from perms.utils import PermAccountUtil + permed_account = PermAccountUtil().validate_permission( + self.user, self.asset, self.account + ) + return permed_account + + @lazyproperty + def actions(self): + return self.permed_account.actions + + @lazyproperty + def expire_at(self): + return self.permed_account.date_expired.timestamp() + + def is_valid(self): + if self.is_expired: + error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) + raise PermissionDenied(error) + if not self.user or not self.user.is_valid: + error = _('No user or invalid user') + raise PermissionDenied(error) + if not self.asset or not self.asset.is_active: + is_valid = False + error = _('No asset or inactive asset') + return is_valid, error + if not self.account: + error = _('No account') + raise PermissionDenied(error) + + 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 + ) + raise PermissionDenied(msg) + + if self.permed_account.date_expired < timezone.now(): + raise PermissionDenied('Expired') + return True + + @lazyproperty + def platform(self): + return self.asset.platform + + @lazyproperty + def account_object(self): + from assets.models import Account + if not self.asset: + return None + + account = self.asset.accounts.filter(name=self.account).first() + if self.account == '@INPUT' or not account: + data = { + 'name': self.account, + 'username': self.input_username, + 'secret_type': 'password', + 'secret': self.input_secret, + 'su_from': None, + 'org_id': self.asset.org_id + } + else: + data = { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': account.secret or self.input_secret, + 'su_from': account.su_from, + 'org_id': account.org_id + } + return Account(**data) + + @lazyproperty + def domain(self): + domain = self.asset.domain if self.asset else None + return domain + + @lazyproperty + def gateway(self): + from assets.models import Domain + if not self.domain: + return + self.domain: Domain + return self.domain.random_gateway() + + @lazyproperty + def command_filter_acls(self): + from acls.models import CommandFilterACL + kwargs = { + 'user': self.user, + 'asset': self.asset, + 'account': self.account, + } + acls = CommandFilterACL.filter_queryset(**kwargs).valid() + return acls + + +class SuperConnectionToken(ConnectionToken): + class Meta: + proxy = True + verbose_name = _("Super connection token") diff --git a/apps/authentication/models/private_token.py b/apps/authentication/models/private_token.py new file mode 100644 index 000000000..8d83d1e0a --- /dev/null +++ b/apps/authentication/models/private_token.py @@ -0,0 +1,9 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework.authtoken.models import Token + + +class PrivateToken(Token): + """Inherit from auth token, otherwise migration is boring""" + + class Meta: + verbose_name = _('Private Token') diff --git a/apps/authentication/models/sso_token.py b/apps/authentication/models/sso_token.py new file mode 100644 index 000000000..fb4c68827 --- /dev/null +++ b/apps/authentication/models/sso_token.py @@ -0,0 +1,18 @@ +import uuid +from django.utils.translation import ugettext_lazy as _ + +from django.db import models +from common.db.models import BaseCreateUpdateModel + + +class SSOToken(BaseCreateUpdateModel): + """ + 类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036) + 出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。 + """ + authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) + expired = models.BooleanField(default=False, verbose_name=_('Expired')) + user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False) + + class Meta: + verbose_name = _('SSO token') diff --git a/apps/authentication/models/temp_token.py b/apps/authentication/models/temp_token.py new file mode 100644 index 000000000..d76a30a42 --- /dev/null +++ b/apps/authentication/models/temp_token.py @@ -0,0 +1,26 @@ +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from django.db import models +from common.db.models import JMSBaseModel + + +class TempToken(JMSBaseModel): + username = models.CharField(max_length=128, verbose_name=_("Username")) + secret = models.CharField(max_length=64, verbose_name=_("Secret")) + verified = models.BooleanField(default=False, verbose_name=_("Verified")) + date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified")) + date_expired = models.DateTimeField(verbose_name=_("Date expired")) + + class Meta: + verbose_name = _("Temporary token") + + @property + def user(self): + from users.models import User + return User.objects.filter(username=self.username).first() + + @property + def is_valid(self): + not_expired = self.date_expired and self.date_expired > timezone.now() + return not self.verified and not_expired diff --git a/apps/authentication/serializers/__init__.py b/apps/authentication/serializers/__init__.py index 65994a58c..d6e1671cf 100644 --- a/apps/authentication/serializers/__init__.py +++ b/apps/authentication/serializers/__init__.py @@ -1,4 +1,5 @@ -from .token import * +from .confirm import * +from .connect_token_secret import * from .connection_token import * from .password_mfa import * -from .confirm import * +from .token import * diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py new file mode 100644 index 000000000..48f86ba50 --- /dev/null +++ b/apps/authentication/serializers/connect_token_secret.py @@ -0,0 +1,117 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from acls.models import CommandGroup +from assets.models import Asset, Account, Platform +from assets.serializers import PlatformSerializer, AssetProtocolsSerializer +from authentication.models import ConnectionToken +from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from perms.serializers.permission import ActionChoicesField +from users.models import User + +__all__ = [ + 'ConnectionTokenSecretSerializer', +] + + +class _ConnectionTokenUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'name', 'username', 'email'] + + +class _ConnectionTokenAssetSerializer(serializers.ModelSerializer): + protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) + + class Meta: + model = Asset + fields = [ + 'id', 'name', 'address', 'protocols', + 'category', 'type', 'org_id', 'specific' + ] + + +class _SimpleAccountSerializer(serializers.ModelSerializer): + """ Account """ + + class Meta: + model = Account + fields = ['name', 'username', 'secret_type', 'secret'] + + +class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): + """ Account """ + su_from = _SimpleAccountSerializer(required=False, label=_('Su from')) + + class Meta: + model = Account + fields = [ + 'name', 'username', 'secret_type', 'secret', 'su_from', + ] + + +class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer): + """ Gateway """ + + class Meta: + model = Asset + fields = [ + 'id', 'address', 'port', + # 'username', 'password', 'private_key' + ] + + +class _ConnectionTokenACLCmdGroupSerializer(serializers.ModelSerializer): + """ ACL command group""" + + class Meta: + model = CommandGroup + fields = [ + 'id', 'type', 'content', 'ignore_case', 'pattern' + ] + + +class _ConnectionTokenPlatformSerializer(PlatformSerializer): + class Meta(PlatformSerializer.Meta): + model = Platform + + def get_field_names(self, declared_fields, info): + names = super().get_field_names(declared_fields, info) + names = [n for n in names if n not in ['automation']] + return names + + +class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): + user = _ConnectionTokenUserSerializer(read_only=True) + asset = _ConnectionTokenAssetSerializer(read_only=True) + account = _ConnectionTokenAccountSerializer(read_only=True, source='account_object') + gateway = _ConnectionTokenGatewaySerializer(read_only=True) + platform = _ConnectionTokenPlatformSerializer(read_only=True) + acl_command_groups = _ConnectionTokenACLCmdGroupSerializer(read_only=True, many=True) + 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', 'acl_command_groups', 'protocol', + '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 diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 8689b2ba4..46d3f453a 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,194 +1,45 @@ +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 OrgResourceModelSerializerMixin from authentication.models import ConnectionToken -from common.utils import pretty_string -from common.utils.random import random_string -from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule -from users.models import User -from applications.models import Application -from assets.serializers import ProtocolsField -from perms.serializers.base import ActionsField - +from orgs.mixins.serializers import OrgResourceModelSerializerMixin __all__ = [ - 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', - 'SuperConnectionTokenSerializer', 'ConnectionTokenDisplaySerializer' + 'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer', ] class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display")) - is_valid = serializers.BooleanField(read_only=True, label=_('Validity')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) class Meta: model = ConnectionToken - fields_mini = ['id', 'type'] + fields_mini = ['id', 'value'] fields_small = fields_mini + [ - 'secret', 'date_expired', 'date_created', 'date_updated', - 'created_by', 'updated_by', 'org_id', 'org_name', - ] - fields_fk = [ - 'user', 'system_user', 'asset', 'application', + 'user', 'asset', 'account', 'input_username', + 'input_secret', 'connect_method', 'protocol', 'actions', + 'date_expired', 'date_created', 'date_updated', 'created_by', + 'updated_by', 'org_id', 'org_name', ] read_only_fields = [ # 普通 Token 不支持指定 user - 'user', 'is_valid', 'expire_time', - 'type_display', 'user_display', 'system_user_display', - 'asset_display', 'application_display', + 'user', 'expire_time', + 'user_display', 'asset_display', ] - fields = fields_small + fields_fk + read_only_fields - - def validate(self, attrs): - fields_attrs = self.construct_internal_fields_attrs(attrs) - attrs.update(fields_attrs) - return attrs - - @property - def request_user(self): - request = self.context.get('request') - if request: - return request.user + fields = fields_small + read_only_fields + extra_kwargs = { + 'value': {'read_only': True}, + } def get_user(self, attrs): - return self.request_user - - def construct_internal_fields_attrs(self, attrs): - user = self.get_user(attrs) - system_user = attrs.get('system_user') or '' - asset = attrs.get('asset') or '' - application = attrs.get('application') or '' - secret = attrs.get('secret') or random_string(16) - date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired() - - if isinstance(asset, Asset): - tp = ConnectionToken.Type.asset - org_id = asset.org_id - elif isinstance(application, Application): - tp = ConnectionToken.Type.application - org_id = application.org_id - else: - raise serializers.ValidationError(_('Asset or application required')) - - return { - 'type': tp, - 'user': user, - 'secret': secret, - 'date_expired': date_expired, - 'user_display': pretty_string(str(user), max_length=128), - 'system_user_display': pretty_string(str(system_user), max_length=128), - 'asset_display': pretty_string(str(asset), max_length=128), - 'application_display': pretty_string(str(application), max_length=128), - 'org_id': org_id, - } - - -class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer): - class Meta(ConnectionTokenSerializer.Meta): - extra_kwargs = { - 'secret': {'write_only': True}, - } - - -# -# SuperConnectionTokenSerializer -# + request = self.context.get('request') + user = request.user if request else None + return user class SuperConnectionTokenSerializer(ConnectionTokenSerializer): - class Meta(ConnectionTokenSerializer.Meta): - read_only_fields = [ - 'validity', 'user_display', 'system_user_display', - 'asset_display', 'application_display', - ] + read_only_fields = list(set(ConnectionTokenSerializer.Meta.read_only_fields) - {'user'}) def get_user(self, attrs): - return attrs.get('user') or self.request_user - - -# -# Connection Token Secret -# - - -class ConnectionTokenUserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ['id', 'name', 'username', 'email'] - - -class ConnectionTokenAssetSerializer(serializers.ModelSerializer): - protocols = ProtocolsField(label='Protocols', read_only=True) - - class Meta: - model = Asset - fields = ['id', 'hostname', 'ip', 'protocols', 'org_id'] - - -class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - fields = [ - 'id', 'name', 'username', 'password', 'private_key', - 'protocol', 'ad_domain', 'org_id' - ] - - -class ConnectionTokenGatewaySerializer(serializers.ModelSerializer): - class Meta: - model = Gateway - fields = ['id', 'ip', 'port', 'username', 'password', 'private_key'] - - -class ConnectionTokenRemoteAppSerializer(serializers.Serializer): - program = serializers.CharField(allow_null=True, allow_blank=True) - working_directory = serializers.CharField(allow_null=True, allow_blank=True) - parameters = serializers.CharField(allow_null=True, allow_blank=True) - - -class ConnectionTokenApplicationSerializer(serializers.ModelSerializer): - attrs = serializers.JSONField(read_only=True) - - class Meta: - model = Application - fields = ['id', 'name', 'category', 'type', 'attrs', 'org_id'] - - -class ConnectionTokenDomainSerializer(serializers.ModelSerializer): - gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True) - - class Meta: - model = Domain - fields = ['id', 'name', 'gateways'] - - -class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer): - class Meta: - model = CommandFilterRule - fields = [ - 'id', 'type', 'content', 'ignore_case', 'pattern', - 'priority', 'action', 'date_created', - ] - - -class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): - user = ConnectionTokenUserSerializer(read_only=True) - asset = ConnectionTokenAssetSerializer(read_only=True, source='asset_or_remote_app_asset') - application = ConnectionTokenApplicationSerializer(read_only=True) - remote_app = ConnectionTokenRemoteAppSerializer(read_only=True) - system_user = ConnectionTokenSystemUserSerializer(read_only=True) - gateway = ConnectionTokenGatewaySerializer(read_only=True) - domain = ConnectionTokenDomainSerializer(read_only=True) - cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) - actions = ActionsField() - expired_at = serializers.IntegerField() - - class Meta: - model = ConnectionToken - fields = [ - 'id', 'secret', 'type', 'user', 'asset', 'application', 'system_user', - 'remote_app', 'cmd_filter_rules', 'domain', 'gateway', 'actions', 'expired_at', - ] + return attrs.get('user') diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 0d19d3fcd..340ece19c 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -1,27 +1,28 @@ +from urllib.parse import urlencode + +from django.conf import settings +from django.db.utils import IntegrityError +from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ -from urllib.parse import urlencode from django.views import View -from django.conf import settings -from django.http.request import HttpRequest -from django.db.utils import IntegrityError -from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.exceptions import APIException +from rest_framework.permissions import AllowAny, IsAuthenticated +from authentication import errors +from authentication.const import ConfirmType +from authentication.mixins import AuthMixin +from authentication.notifications import OAuthBindMessage +from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin +from common.permissions import UserConfirmation +from common.sdk.im.dingtalk import URL, DingTalk +from common.utils import FlashMessageUtil, get_logger +from common.utils.common import get_request_ip +from common.utils.django import get_object_or_none, reverse +from common.utils.random import random_string from users.models import User from users.views import UserVerifyPasswordView -from common.utils import get_logger, FlashMessageUtil -from common.utils.random import random_string -from common.utils.django import reverse, get_object_or_none -from common.sdk.im.dingtalk import URL -from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin -from common.permissions import UserConfirmation -from authentication import errors -from authentication.mixins import AuthMixin -from authentication.const import ConfirmType -from common.sdk.im.dingtalk import DingTalk -from common.utils.common import get_request_ip -from authentication.notifications import OAuthBindMessage + from .mixins import METAMixin logger = get_logger(__file__) diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index da7999b95..4fdf6f846 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -1,26 +1,27 @@ +from urllib.parse import urlencode + +from django.conf import settings +from django.db.utils import IntegrityError +from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ -from urllib.parse import urlencode from django.views import View -from django.conf import settings -from django.http.request import HttpRequest -from django.db.utils import IntegrityError -from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.exceptions import APIException +from rest_framework.permissions import AllowAny, IsAuthenticated -from users.models import User -from users.views import UserVerifyPasswordView -from common.utils import get_logger, FlashMessageUtil -from common.utils.random import random_string -from common.utils.django import reverse, get_object_or_none -from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin -from common.permissions import UserConfirmation -from common.sdk.im.feishu import FeiShu, URL -from common.utils.common import get_request_ip from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage +from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin +from common.permissions import UserConfirmation +from common.sdk.im.feishu import URL, FeiShu +from common.utils import FlashMessageUtil, get_logger +from common.utils.common import get_request_ip +from common.utils.django import get_object_or_none, reverse +from common.utils.random import random_string +from users.models import User +from users.views import UserVerifyPasswordView logger = get_logger(__file__) diff --git a/apps/common/apps.py b/apps/common/apps.py index 2a5799d10..c55e4c6e3 100644 --- a/apps/common/apps.py +++ b/apps/common/apps.py @@ -10,6 +10,8 @@ class CommonConfig(AppConfig): def ready(self): from . import signal_handlers from .signals import django_ready - if 'migrate' in sys.argv or 'compilemessages' in sys.argv: - return + excludes = ['migrate', 'compilemessages', 'makemigrations'] + for i in excludes: + if i in sys.argv: + return django_ready.send(CommonConfig) diff --git a/apps/common/const/choices.py b/apps/common/const/choices.py index 6bff02254..9b6c817f3 100644 --- a/apps/common/const/choices.py +++ b/apps/common/const/choices.py @@ -1,4 +1,21 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ ADMIN = 'Admin' USER = 'User' AUDITOR = 'Auditor' + + +class Trigger(models.TextChoices): + manual = 'manual', _('Manual trigger') + timing = 'timing', _('Timing trigger') + + +class Status(models.TextChoices): + ready = 'ready', _('Ready') + pending = 'pending', _("Pending") + running = 'running', _("Running") + success = 'success', _("Success") + failed = 'failed', _("Failed") + error = 'error', _("Error") + canceled = 'canceled', _("Canceled") diff --git a/apps/common/const/crontab.py b/apps/common/const/crontab.py new file mode 100644 index 000000000..bd9809176 --- /dev/null +++ b/apps/common/const/crontab.py @@ -0,0 +1,5 @@ + +CRONTAB_AT_AM_TWO = '0 14 * * *' +CRONTAB_AT_AM_TEN = '0 10 * * *' +CRONTAB_AT_PM_TWO = '0 2 * * *' + diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index ed00a0931..7b2cccd7b 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -1,20 +1,33 @@ # -*- coding: utf-8 -*- # import json -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.utils.encoding import force_text + from django.core.validators import MinValueValidator, MaxValueValidator +from django.db import models +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _ + from common.utils import signer, crypto from common.local import add_encrypted_field_set - __all__ = [ - 'JsonMixin', 'JsonDictMixin', 'JsonListMixin', 'JsonTypeMixin', - 'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField', - 'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField', - 'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField', - 'EncryptJsonDictCharField', 'PortField' + "JsonMixin", + "JsonDictMixin", + "JsonListMixin", + "JsonTypeMixin", + "JsonCharField", + "JsonTextField", + "JsonListCharField", + "JsonListTextField", + "JsonDictCharField", + "JsonDictTextField", + "EncryptCharField", + "EncryptTextField", + "EncryptMixin", + "EncryptJsonDictTextField", + "EncryptJsonDictCharField", + "PortField", + "BitChoices", ] @@ -115,13 +128,13 @@ class EncryptMixin: """ def decrypt_from_signer(self, value): - return signer.unsign(value) or '' + return signer.unsign(value) or "" def from_db_value(self, value, expression, connection, context=None): - if value is None: + if not value: return value - value = force_text(value) + value = force_text(value) plain_value = crypto.decrypt(value) # 如果没有解开,使用原来的signer解密 @@ -130,17 +143,17 @@ class EncryptMixin: # 可能和Json mix,所以要先解密,再json sp = super() - if hasattr(sp, 'from_db_value'): + if hasattr(sp, "from_db_value"): plain_value = sp.from_db_value(plain_value, expression, connection, context) return plain_value def get_prep_value(self, value): - if value is None: + if not value: return value # 先 json 再解密 sp = super() - if hasattr(sp, 'get_prep_value'): + if hasattr(sp, "get_prep_value"): value = sp.get_prep_value(value) value = force_text(value) # 替换新的加密方式 @@ -158,12 +171,12 @@ class EncryptTextField(EncryptMixin, models.TextField): class EncryptCharField(EncryptMixin, models.CharField): @staticmethod def change_max_length(kwargs): - kwargs.setdefault('max_length', 1024) - max_length = kwargs.get('max_length') + kwargs.setdefault("max_length", 1024) + max_length = kwargs.get("max_length") if max_length < 129: max_length = 128 max_length = max_length * 2 - kwargs['max_length'] = max_length + kwargs["max_length"] = max_length def __init__(self, *args, **kwargs): self.change_max_length(kwargs) @@ -172,10 +185,10 @@ class EncryptCharField(EncryptMixin, models.CharField): def deconstruct(self): name, path, args, kwargs = super().deconstruct() - max_length = kwargs.pop('max_length') + max_length = kwargs.pop("max_length") if max_length > 255: max_length = max_length // 2 - kwargs['max_length'] = max_length + kwargs["max_length"] = max_length return name, path, args, kwargs @@ -193,10 +206,50 @@ class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField): class PortField(models.IntegerField): def __init__(self, *args, **kwargs): - kwargs.update({ - 'blank': False, - 'null': False, - 'validators': [MinValueValidator(0), MaxValueValidator(65535)] - }) + kwargs.update( + { + "blank": False, + "null": False, + "validators": [MinValueValidator(0), MaxValueValidator(65535)], + } + ) super().__init__(*args, **kwargs) + +class BitChoices(models.IntegerChoices): + @classmethod + def branches(cls): + return [i for i in cls] + + @classmethod + def is_tree(cls): + return False + + @classmethod + def tree(cls): + if not cls.is_tree(): + return [] + root = [_("All"), cls.branches()] + return [cls.render_node(root)] + + @classmethod + def render_node(cls, node): + if isinstance(node, BitChoices): + return { + "value": node.name, + "label": node.label, + } + else: + name, children = node + return { + "value": name, + "label": name, + "children": [cls.render_node(child) for child in children], + } + + @classmethod + def all(cls): + value = 0 + for c in cls: + value |= c.value + return value diff --git a/apps/common/db/models.py b/apps/common/db/models.py index c3b00c62c..0681d174b 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -13,8 +13,9 @@ import uuid from functools import reduce, partial import inspect +from django.db import models +from django.db.models import F, Value, ExpressionWrapper from django.db import transaction -from django.db.models import * from django.db.models import QuerySet from django.db.models.functions import Concat from django.utils.translation import ugettext_lazy as _ @@ -22,53 +23,6 @@ from django.utils.translation import ugettext_lazy as _ from ..const.signals import SKIP_SIGNAL -class Choice(str): - def __new__(cls, value, label=''): # `deepcopy` 的时候不会传 `label` - self = super().__new__(cls, value) - self.label = label - return self - - -class ChoiceSetType(type): - def __new__(cls, name, bases, attrs): - _choices = [] - collected = set() - new_attrs = {} - for k, v in attrs.items(): - if isinstance(v, tuple): - v = Choice(*v) - assert v not in collected, 'Cannot be defined repeatedly' - _choices.append(v) - collected.add(v) - new_attrs[k] = v - for base in bases: - if hasattr(base, '_choices'): - for c in base._choices: - if c not in collected: - _choices.append(c) - collected.add(c) - new_attrs['_choices'] = _choices - new_attrs['_choices_dict'] = {c: c.label for c in _choices} - return type.__new__(cls, name, bases, new_attrs) - - def __contains__(self, item): - return self._choices_dict.__contains__(item) - - def __getitem__(self, item): - return self._choices_dict.__getitem__(item) - - def get(self, item, default=None): - return self._choices_dict.get(item, default) - - @property - def choices(self): - return [(c, c.label) for c in self._choices] - - -class ChoiceSet(metaclass=ChoiceSetType): - choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 - - class BitOperationChoice: NONE = 0 NAME_MAP: dict @@ -110,18 +64,26 @@ class BitOperationChoice: return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] -class JMSBaseModel(Model): - created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) - updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by')) - date_created = DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) - date_updated = DateTimeField(auto_now=True, verbose_name=_('Date updated')) +class ChoicesMixin: + _value2label_map_: dict + + @classmethod + def get_label(cls, value: (str, int)): + return cls._value2label_map_[value] + + +class BaseCreateUpdateModel(models.Model): + created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) + updated_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by')) + date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated')) class Meta: abstract = True -class JMSModel(JMSBaseModel): - id = UUIDField(default=uuid.uuid4, primary_key=True) +class JMSBaseModel(BaseCreateUpdateModel): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) class Meta: abstract = True @@ -130,12 +92,8 @@ class JMSModel(JMSBaseModel): return str(self.id) -def concated_display(name1, name2): - return Concat(F(name1), Value('('), F(name2), Value(')')) - - def output_as_string(field_name): - return ExpressionWrapper(F(field_name), output_field=CharField()) + return ExpressionWrapper(F(field_name), output_field=models.CharField()) class UnionQuerySet(QuerySet): diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py index 073e9fd7e..23567aa32 100644 --- a/apps/common/drf/api.py +++ b/apps/common/drf/api.py @@ -2,31 +2,31 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelV from rest_framework_bulk import BulkModelViewSet from ..mixins.api import ( - RelationMixin, AllowBulkDestroyMixin, CommonMixin + RelationMixin, AllowBulkDestroyMixin, CommonApiMixin ) -class JMSGenericViewSet(CommonMixin, GenericViewSet): +class JMSGenericViewSet(CommonApiMixin, GenericViewSet): pass -class JMSViewSet(CommonMixin, ViewSet): +class JMSViewSet(CommonApiMixin, ViewSet): pass -class JMSModelViewSet(CommonMixin, ModelViewSet): +class JMSModelViewSet(CommonApiMixin, ModelViewSet): pass -class JMSReadOnlyModelViewSet(CommonMixin, ReadOnlyModelViewSet): +class JMSReadOnlyModelViewSet(CommonApiMixin, ReadOnlyModelViewSet): pass -class JMSBulkModelViewSet(CommonMixin, AllowBulkDestroyMixin, BulkModelViewSet): +class JMSBulkModelViewSet(CommonApiMixin, AllowBulkDestroyMixin, BulkModelViewSet): pass -class JMSBulkRelationModelViewSet(CommonMixin, +class JMSBulkRelationModelViewSet(CommonApiMixin, RelationMixin, AllowBulkDestroyMixin, BulkModelViewSet): diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py index 4370d0054..7c9f55eee 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -1,13 +1,22 @@ # -*- coding: utf-8 -*- # - +import six +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from rest_framework.fields import ChoiceField, empty +from common.db.fields import BitChoices from common.utils import decrypt_password from common.local import add_encrypted_field_set __all__ = [ - 'ReadableHiddenField', 'EncryptedField' + "ReadableHiddenField", + "EncryptedField", + "LabeledChoiceField", + "ObjectRelatedField", + "BitChoicesField", + "TreeChoicesMixin" ] @@ -16,14 +25,15 @@ __all__ = [ class ReadableHiddenField(serializers.HiddenField): - """ 可读的 HiddenField """ + """可读的 HiddenField""" + def __init__(self, **kwargs): super().__init__(**kwargs) self.write_only = False def to_representation(self, value): - if hasattr(value, 'id'): - return getattr(value, 'id') + if hasattr(value, "id"): + return getattr(value, "id") return value @@ -31,7 +41,7 @@ class EncryptedField(serializers.CharField): def __init__(self, write_only=None, **kwargs): if write_only is None: write_only = True - kwargs['write_only'] = write_only + kwargs["write_only"] = write_only encrypted_key = kwargs.pop('encrypted_key', None) super().__init__(**kwargs) add_encrypted_field_set(encrypted_key or self.label) @@ -39,3 +49,124 @@ class EncryptedField(serializers.CharField): def to_internal_value(self, value): value = super().to_internal_value(value) return decrypt_password(value) + + +class LabeledChoiceField(ChoiceField): + def __init__(self, *args, **kwargs): + super(LabeledChoiceField, self).__init__(*args, **kwargs) + self.choice_mapper = { + six.text_type(key): value for key, value in self.choices.items() + } + + def to_representation(self, value): + if value is None: + return value + return { + "value": value, + "label": self.choice_mapper.get(six.text_type(value), value), + } + + def to_internal_value(self, data): + if isinstance(data, dict): + return data.get("value") + return super(LabeledChoiceField, self).to_internal_value(data) + + +class ObjectRelatedField(serializers.RelatedField): + default_error_messages = { + "required": _("This field is required."), + "does_not_exist": _('Invalid pk "{pk_value}" - object does not exist.'), + "incorrect_type": _("Incorrect type. Expected pk value, received {data_type}."), + } + + def __init__(self, **kwargs): + self.attrs = kwargs.pop("attrs", None) or ("id", "name") + self.many = kwargs.get("many", False) + super().__init__(**kwargs) + + def to_representation(self, value): + data = {} + for attr in self.attrs: + data[attr] = getattr(value, attr) + return data + + def to_internal_value(self, data): + if not isinstance(data, dict): + pk = data + else: + pk = data.get("id") or data.get("pk") or data.get(self.attrs[0]) + queryset = self.get_queryset() + try: + if isinstance(data, bool): + raise TypeError + return queryset.get(pk=pk) + except ObjectDoesNotExist: + self.fail("does_not_exist", pk_value=pk) + except (TypeError, ValueError): + self.fail("incorrect_type", data_type=type(pk).__name__) + + +class TreeChoicesMixin: + tree = [] + + +class BitChoicesField(TreeChoicesMixin, serializers.MultipleChoiceField): + """ + 位字段 + """ + + def __init__(self, choice_cls, **kwargs): + assert issubclass(choice_cls, BitChoices) + choices = [(c.name, c.label) for c in choice_cls] + self.tree = choice_cls.tree() + self._choice_cls = choice_cls + super().__init__(choices=choices, **kwargs) + + def to_representation(self, value): + if isinstance(value, list) and len(value) == 1: + # Swagger 会使用 field.choices.keys() 迭代传递进来 + return [ + {"value": c.name, "label": c.label} + for c in self._choice_cls + if c.name == value[0] + ] + return [ + {"value": c.name, "label": c.label} + for c in self._choice_cls + if c.value & value == c.value + ] + + def to_internal_value(self, data): + if not isinstance(data, list): + raise serializers.ValidationError(_("Invalid data type, should be list")) + value = 0 + if not data: + return value + if isinstance(data[0], dict): + data = [d["value"] for d in data] + # 所有的 + if "all" in data: + for c in self._choice_cls: + value |= c.value + return value + + name_value_map = {c.name: c.value for c in self._choice_cls} + for name in data: + if name not in name_value_map: + raise serializers.ValidationError(_("Invalid choice: {}").format(name)) + value |= name_value_map[name] + return value + + def run_validation(self, data=empty): + """ + 备注: + 创建授权规则不包含 actions 字段时, 会使用默认值(AssetPermission 中设置), + 会直接使用 ['connect', '...'] 等字段保存到数据库,导致类型错误 + 这里将获取到的值再执行一下 to_internal_value 方法, 转化为内部值 + """ + data = super().run_validation(data) + if isinstance(data, int): + return data + value = self.to_internal_value(data) + self.run_validators(value) + return value diff --git a/apps/common/drf/filters.py b/apps/common/drf/filters.py index 1c89f49ac..80074edf9 100644 --- a/apps/common/drf/filters.py +++ b/apps/common/drf/filters.py @@ -85,7 +85,7 @@ class DatetimeRangeFilter(filters.BaseFilterBackend): lookup = "__gte" else: lookup = "__lte" - kwargs[attr+lookup] = value + kwargs[attr + lookup] = value except ValidationError as e: print(e) continue @@ -171,4 +171,13 @@ def current_user_filter(user_field='user'): class CurrentUserFilter(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): return queryset.filter(**{user_field: request.user}) + return CurrentUserFilter + + +class UUIDInFilter(drf_filters.BaseInFilter, drf_filters.UUIDFilter): + pass + + +class NumberInFilter(drf_filters.BaseInFilter, drf_filters.NumberFilter): + pass diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index 5abdf1d35..fc9ceb961 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -2,28 +2,33 @@ # from __future__ import unicode_literals -from collections import OrderedDict import datetime -from itertools import chain +from collections import OrderedDict from django.core.exceptions import PermissionDenied from django.http import Http404 from django.utils.encoding import force_text -from rest_framework.fields import empty - -from rest_framework.metadata import SimpleMetadata from rest_framework import exceptions, serializers +from rest_framework.fields import empty +from rest_framework.metadata import SimpleMetadata from rest_framework.request import clone_request +from common.drf.fields import TreeChoicesMixin + class SimpleMetadataWithFilters(SimpleMetadata): """Override SimpleMetadata, adding info about filters""" methods = {"PUT", "POST", "GET", "PATCH"} attrs = [ - 'read_only', 'label', 'help_text', - 'min_length', 'max_length', - 'min_value', 'max_value', "write_only", + "read_only", + "label", + "help_text", + "min_length", + "max_length", + "min_value", + "max_value", + "write_only", ] def determine_actions(self, request, view): @@ -32,18 +37,18 @@ class SimpleMetadataWithFilters(SimpleMetadata): the fields that are accepted for 'PUT' and 'POST' methods. """ actions = {} - view.raw_action = getattr(view, 'action', None) + view.raw_action = getattr(view, "action", None) for method in self.methods & set(view.allowed_methods): - if hasattr(view, 'action_map'): + if hasattr(view, "action_map"): view.action = view.action_map.get(method.lower(), view.action) view.request = clone_request(request, method) try: # Test global permissions - if hasattr(view, 'check_permissions'): + if hasattr(view, "check_permissions"): view.check_permissions(view.request) # Test object permissions - if method == 'PUT' and hasattr(view, 'get_object'): + if method == "PUT" and hasattr(view, "get_object"): view.get_object() except (exceptions.APIException, PermissionDenied, Http404): pass @@ -56,57 +61,86 @@ class SimpleMetadataWithFilters(SimpleMetadata): view.request = request return actions + def get_field_type(self, field): + """ + Given a field, return a string representing the type of the field. + """ + tp = self.label_lookup[field] + + class_name = field.__class__.__name__ + if class_name == "LabeledChoiceField": + tp = "labeled_choice" + elif class_name == "ObjectRelatedField": + tp = "object_related_field" + elif class_name == "ManyRelatedField": + child_relation_class_name = field.child_relation.__class__.__name__ + if child_relation_class_name == "ObjectRelatedField": + tp = "m2m_related_field" + return tp + + @staticmethod + def set_choices_field(field, field_info): + field_info["choices"] = [ + { + "value": choice_value, + "label": force_text(choice_label, strings_only=True), + } + for choice_value, choice_label in dict(field.choices).items() + ] + + @staticmethod + def set_tree_field(field, field_info): + field_info["tree"] = field.tree + field_info["type"] = "tree" + def get_field_info(self, field): """ Given an instance of a serializer field, return a dictionary of metadata about it. """ field_info = OrderedDict() - field_info['type'] = self.label_lookup[field] - field_info['required'] = getattr(field, 'required', False) + field_info["type"] = self.get_field_type(field) + field_info["required"] = getattr(field, "required", False) - default = getattr(field, 'default', None) + # Default value + default = getattr(field, "default", None) if default is not None and default != empty: if isinstance(default, (str, int, bool, float, datetime.datetime, list)): - field_info['default'] = default + field_info["default"] = default for attr in self.attrs: value = getattr(field, attr, None) - if value is not None and value != '': + if value is not None and value != "": field_info[attr] = force_text(value, strings_only=True) - if getattr(field, 'child', None): - field_info['child'] = self.get_field_info(field.child) - elif getattr(field, 'fields', None): - field_info['children'] = self.get_serializer_info(field) - - if not isinstance(field, (serializers.RelatedField, serializers.ManyRelatedField)) \ - and hasattr(field, 'choices'): - field_info['choices'] = [ - { - 'value': choice_value, - 'display_name': force_text(choice_name, strings_only=True) - } - for choice_value, choice_name in field.choices.items() - ] + if getattr(field, "child", None): + field_info["child"] = self.get_field_info(field.child) + elif getattr(field, "fields", None): + field_info["children"] = self.get_serializer_info(field) + if isinstance(field, TreeChoicesMixin): + self.set_tree_field(field, field_info) + elif isinstance(field, serializers.ChoiceField): + self.set_choices_field(field, field_info) return field_info - def get_filters_fields(self, request, view): + @staticmethod + def get_filters_fields(request, view): fields = [] - if hasattr(view, 'get_filter_fields'): + if hasattr(view, "get_filter_fields"): fields = view.get_filter_fields(request) - elif hasattr(view, 'filter_fields'): + elif hasattr(view, "filter_fields"): fields = view.filter_fields - elif hasattr(view, 'filterset_fields'): + elif hasattr(view, "filterset_fields"): fields = view.filterset_fields - elif hasattr(view, 'get_filterset_fields'): + elif hasattr(view, "get_filterset_fields"): fields = view.get_filterset_fields(request) - elif hasattr(view, 'filterset_class'): - fields = list(view.filterset_class.Meta.fields) + \ - list(view.filterset_class.declared_filters.keys()) + elif hasattr(view, "filterset_class"): + fields = list(view.filterset_class.Meta.fields) + list( + view.filterset_class.declared_filters.keys() + ) - if hasattr(view, 'custom_filter_fields'): + if hasattr(view, "custom_filter_fields"): # 不能写 fields += view.custom_filter_fields # 会改变 view 的 filter_fields fields = list(fields) + list(view.custom_filter_fields) @@ -115,16 +149,19 @@ class SimpleMetadataWithFilters(SimpleMetadata): fields = list(fields.keys()) return fields - def get_ordering_fields(self, request, view): + @staticmethod + def get_ordering_fields(request, view): fields = [] - if hasattr(view, 'get_ordering_fields'): + if hasattr(view, "get_ordering_fields"): fields = view.get_ordering_fields(request) - elif hasattr(view, 'ordering_fields'): + elif hasattr(view, "ordering_fields"): fields = view.ordering_fields return fields def determine_metadata(self, request, view): - metadata = super(SimpleMetadataWithFilters, self).determine_metadata(request, view) + metadata = super(SimpleMetadataWithFilters, self).determine_metadata( + request, view + ) filterset_fields = self.get_filters_fields(request, view) order_fields = self.get_ordering_fields(request, view) diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index 1f15ea72d..ac75ff645 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -39,7 +39,7 @@ class BaseFileParser(BaseParser): @abc.abstractmethod def generate_rows(self, stream_data): - raise NotImplemented + raise NotImplementedError def get_column_titles(self, rows): return next(rows) diff --git a/apps/common/drf/serializers/__init__.py b/apps/common/drf/serializers/__init__.py new file mode 100644 index 000000000..7ecadafc7 --- /dev/null +++ b/apps/common/drf/serializers/__init__.py @@ -0,0 +1,2 @@ +from .common import * +from .mixin import * diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers/common.py similarity index 60% rename from apps/common/drf/serializers.py rename to apps/common/drf/serializers/common.py index 1dde4a77e..805aaa453 100644 --- a/apps/common/drf/serializers.py +++ b/apps/common/drf/serializers/common.py @@ -1,28 +1,24 @@ -import copy + from rest_framework import serializers from rest_framework.serializers import Serializer from rest_framework.serializers import ModelSerializer from rest_framework_bulk.serializers import BulkListSerializer - -from common.mixins import BulkListSerializerMixin +from django.utils.translation import gettext_lazy as _ from django.utils.functional import cached_property -from rest_framework.utils.serializer_helpers import BindingDict -from common.mixins.serializers import BulkSerializerMixin -from common.drf.fields import EncryptedField +from drf_writable_nested.serializers import WritableNestedModelSerializer as NestedModelSerializer + +from .mixin import BulkListSerializerMixin, BulkSerializerMixin + __all__ = [ - 'MethodSerializer', - 'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer', - 'SecretReadableMixin' + 'MethodSerializer', 'EmptySerializer', 'BulkModelSerializer', + 'AdaptedBulkListSerializer', 'CeleryTaskExecutionSerializer', + 'WritableNestedModelSerializer', 'GroupedChoiceSerializer', + 'FileSerializer' ] -# MethodSerializer -# ---------------- - - class MethodSerializer(serializers.Serializer): - def __init__(self, method_name=None, **kwargs): self.method_name = method_name super().__init__(**kwargs) @@ -65,10 +61,6 @@ class MethodSerializer(serializers.Serializer): return self.serializer.get_initial() -# Other Serializer -# ---------------- - - class EmptySerializer(Serializer): pass @@ -81,24 +73,22 @@ class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): pass -class CeleryTaskSerializer(serializers.Serializer): +class CeleryTaskExecutionSerializer(serializers.Serializer): task = serializers.CharField(read_only=True) -class SecretReadableMixin(serializers.Serializer): - """ 加密字段 (EncryptedField) 可读性 """ +class ChoiceSerializer(serializers.Serializer): + label = serializers.CharField(label=_("Label")) + value = serializers.CharField(label=_("Value")) - def __init__(self, *args, **kwargs): - super(SecretReadableMixin, self).__init__(*args, **kwargs) - if not hasattr(self, 'Meta') or not hasattr(self.Meta, 'extra_kwargs'): - return - extra_kwargs = self.Meta.extra_kwargs - for field_name, serializer_field in self.fields.items(): - if not isinstance(serializer_field, EncryptedField): - continue - if field_name not in extra_kwargs: - continue - field_extra_kwargs = extra_kwargs[field_name] - if 'write_only' not in field_extra_kwargs: - continue - serializer_field.write_only = field_extra_kwargs['write_only'] + +class GroupedChoiceSerializer(ChoiceSerializer): + children = ChoiceSerializer(many=True, label=_("Children")) + + +class WritableNestedModelSerializer(NestedModelSerializer): + pass + + +class FileSerializer(serializers.Serializer): + file = serializers.FileField(label=_("File")) diff --git a/apps/common/mixins/serializers.py b/apps/common/drf/serializers/mixin.py similarity index 83% rename from apps/common/mixins/serializers.py rename to apps/common/drf/serializers/mixin.py index 72b7610d4..b0cbee27e 100644 --- a/apps/common/mixins/serializers.py +++ b/apps/common/drf/serializers/mixin.py @@ -1,14 +1,41 @@ -# -*- coding: utf-8 -*- -# from collections import Iterable -from django.db.models import Prefetch, F, NOT_PROVIDED +from django.db.models import NOT_PROVIDED from django.core.exceptions import ObjectDoesNotExist from rest_framework.utils import html +from rest_framework import serializers from rest_framework.settings import api_settings from rest_framework.exceptions import ValidationError from rest_framework.fields import SkipField, empty -__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin'] + +from common.drf.fields import EncryptedField +from common.utils import lazyproperty + + +__all__ = [ + 'BulkSerializerMixin', 'BulkListSerializerMixin', + 'CommonSerializerMixin', 'CommonBulkSerializerMixin', + 'SecretReadableMixin', +] + + +class SecretReadableMixin(serializers.Serializer): + """ 加密字段 (EncryptedField) 可读性 """ + + def __init__(self, *args, **kwargs): + super(SecretReadableMixin, self).__init__(*args, **kwargs) + if not hasattr(self, 'Meta') or not hasattr(self.Meta, 'extra_kwargs'): + return + extra_kwargs = self.Meta.extra_kwargs + for field_name, serializer_field in self.fields.items(): + if not isinstance(serializer_field, EncryptedField): + continue + if field_name not in extra_kwargs: + continue + field_extra_kwargs = extra_kwargs[field_name] + if 'write_only' not in field_extra_kwargs: + continue + serializer_field.write_only = field_extra_kwargs['write_only'] class BulkSerializerMixin(object): @@ -51,15 +78,15 @@ class BulkSerializerMixin(object): @classmethod def many_init(cls, *args, **kwargs): + from .common import AdaptedBulkListSerializer meta = getattr(cls, 'Meta', None) assert meta is not None, 'Must have `Meta`' if not hasattr(meta, 'list_serializer_class'): - from common.drf.serializers import AdaptedBulkListSerializer meta.list_serializer_class = AdaptedBulkListSerializer return super(BulkSerializerMixin, cls).many_init(*args, **kwargs) -class BulkListSerializerMixin(object): +class BulkListSerializerMixin: """ Become rest_framework_bulk doing bulk update raise Exception: 'QuerySet' object has no attribute 'pk' when doing bulk update @@ -281,20 +308,15 @@ class DynamicFieldsMixin: self.fields.pop(field, None) -class EagerLoadQuerySetFields: - def setup_eager_loading(self, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - Prefetch('nodes'), - Prefetch('labels'), - ).select_related('admin_user', 'domain', 'platform') \ - .annotate(platform_base=F('platform__base')) - return queryset - - class CommonSerializerMixin(DynamicFieldsMixin, DefaultValueFieldsMixin): instance: None initial_data: dict + common_fields = ( + 'comment', 'created_by', 'date_created', 'date_updated', + ) + secret_fields = ( + 'password', 'token', 'secret', 'key', 'private_key', 'public_key', + ) def get_initial_value(self, attr, default=None): value = self.initial_data.get(attr) @@ -305,6 +327,20 @@ class CommonSerializerMixin(DynamicFieldsMixin, DefaultValueFieldsMixin): return value return default + def get_fields(self): + fields = super().get_fields() + for name, field in fields.items(): + if name in self.secret_fields and \ + not isinstance(self, SecretReadableMixin): + field.write_only = True + return fields + + def get_field_names(self, declared_fields, info): + names = super().get_field_names(declared_fields, info) + common_names = [i for i in self.common_fields if i in names] + primary_names = [i for i in names if i not in self.common_fields] + return primary_names + common_names + class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): pass diff --git a/apps/common/hashers/sm3.py b/apps/common/hashers/sm3.py index 62f811e94..da360ea30 100644 --- a/apps/common/hashers/sm3.py +++ b/apps/common/hashers/sm3.py @@ -5,17 +5,28 @@ from django.contrib.auth.hashers import PBKDF2PasswordHasher class Hasher: name = 'sm3' + block_size = 64 + digest_size = 32 - def __init__(self, key): - self.key = key + def __init__(self, data): + self.__data = data def hexdigest(self): - return sm3.sm3_hash(func.bytes_to_list(self.key)) + return sm3.sm3_hash(func.bytes_to_list(self.__data)) + + def digest(self): + return bytes.fromhex(self.hexdigest()) @staticmethod - def hash(msg): + def hash(msg=b''): return Hasher(msg) + def update(self, data): + self.__data += data + + def copy(self): + return Hasher(self.__data) + class PBKDF2SM3PasswordHasher(PBKDF2PasswordHasher): algorithm = "pbkdf2_sm3" diff --git a/apps/common/management/commands/services/command.py b/apps/common/management/commands/services/command.py index dd2cd9cdb..fcaa8f1cd 100644 --- a/apps/common/management/commands/services/command.py +++ b/apps/common/management/commands/services/command.py @@ -1,3 +1,4 @@ +import multiprocessing from django.core.management.base import BaseCommand, CommandError from django.db.models import TextChoices from .utils import ServicesUtil @@ -6,7 +7,6 @@ from .hands import * class Services(TextChoices): gunicorn = 'gunicorn', 'gunicorn' - daphne = 'daphne', 'daphne' celery_ansible = 'celery_ansible', 'celery_ansible' celery_default = 'celery_default', 'celery_default' beat = 'beat', 'beat' @@ -22,7 +22,6 @@ class Services(TextChoices): from . import services services_map = { cls.gunicorn.value: services.GunicornService, - cls.daphne: services.DaphneService, cls.flower: services.FlowerService, cls.celery_default: services.CeleryDefaultService, cls.celery_ansible: services.CeleryAnsibleService, @@ -30,13 +29,9 @@ class Services(TextChoices): } return services_map.get(name) - @classmethod - def ws_services(cls): - return [cls.daphne] - @classmethod def web_services(cls): - return [cls.gunicorn, cls.daphne, cls.flower] + return [cls.gunicorn, cls.flower] @classmethod def celery_services(cls): @@ -97,11 +92,15 @@ class BaseActionCommand(BaseCommand): super().__init__(*args, **kwargs) def add_arguments(self, parser): + cores = 10 + if (multiprocessing.cpu_count() * 2 + 1) < cores: + cores = multiprocessing.cpu_count() * 2 + 1 + parser.add_argument( 'services', nargs='+', choices=Services.export_services_values(), help='Service', ) parser.add_argument('-d', '--daemon', nargs="?", const=True) - parser.add_argument('-w', '--worker', type=int, nargs="?", default=4) + parser.add_argument('-w', '--worker', type=int, nargs="?", default=cores) parser.add_argument('-f', '--force', nargs="?", const=True) def initial_util(self, *args, **options): diff --git a/apps/common/management/commands/services/services/__init__.py b/apps/common/management/commands/services/services/__init__.py index cceb9627c..35329a7d4 100644 --- a/apps/common/management/commands/services/services/__init__.py +++ b/apps/common/management/commands/services/services/__init__.py @@ -1,6 +1,5 @@ from .beat import * from .celery_ansible import * from .celery_default import * -from .daphne import * from .flower import * from .gunicorn import * diff --git a/apps/common/management/commands/services/services/base.py b/apps/common/management/commands/services/services/base.py index 7b36c9723..870014474 100644 --- a/apps/common/management/commands/services/services/base.py +++ b/apps/common/management/commands/services/services/base.py @@ -44,7 +44,9 @@ class BaseService(object): if self.is_running: msg = f'{self.name} is running: {self.pid}.' else: - msg = f'{self.name} is stopped.' + msg = '\033[31m{} is stopped.\033[0m\nYou can manual start it to find the error: \n' \ + ' $ cd {}\n' \ + ' $ {}'.format(self.name, self.cwd, ' '.join(self.cmd)) print(msg) # -- log -- diff --git a/apps/common/management/commands/services/services/daphne.py b/apps/common/management/commands/services/services/daphne.py deleted file mode 100644 index 09dd337a6..000000000 --- a/apps/common/management/commands/services/services/daphne.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..hands import * -from .base import BaseService - -__all__ = ['DaphneService'] - - -class DaphneService(BaseService): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - @property - def cmd(self): - print("\n- Start Daphne ASGI WS Server") - - cmd = [ - 'daphne', 'jumpserver.asgi:application', - '-b', HTTP_HOST, - '-p', str(WS_PORT), - ] - return cmd - - @property - def cwd(self): - return APPS_DIR diff --git a/apps/common/management/commands/services/services/gunicorn.py b/apps/common/management/commands/services/services/gunicorn.py index bfaeea8c4..5eab30ec3 100644 --- a/apps/common/management/commands/services/services/gunicorn.py +++ b/apps/common/management/commands/services/services/gunicorn.py @@ -16,11 +16,11 @@ class GunicornService(BaseService): log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' bind = f'{HTTP_HOST}:{HTTP_PORT}' + cmd = [ - 'gunicorn', 'jumpserver.wsgi', + 'gunicorn', 'jumpserver.asgi:application', '-b', bind, - '-k', 'gthread', - '--threads', '10', + '-k', 'uvicorn.workers.UvicornWorker', '-w', str(self.worker), '--max-requests', '4096', '--access-logformat', log_format, diff --git a/apps/common/management/commands/services/utils.py b/apps/common/management/commands/services/utils.py index a5c34d770..afa642a1a 100644 --- a/apps/common/management/commands/services/utils.py +++ b/apps/common/management/commands/services/utils.py @@ -76,7 +76,6 @@ class ServicesUtil(object): def clean_up(self): if not self.EXIT_EVENT.is_set(): self.EXIT_EVENT.set() - self.stop() def show_status(self): diff --git a/apps/common/mixins/__init__.py b/apps/common/mixins/__init__.py index 4249b3d64..b2a7ec7e4 100644 --- a/apps/common/mixins/__init__.py +++ b/apps/common/mixins/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # from .models import * -from .serializers import * from .api import * from .views import * diff --git a/apps/common/mixins/api/common.py b/apps/common/mixins/api/common.py index 8dbf4fb1e..20e2eabc7 100644 --- a/apps/common/mixins/api/common.py +++ b/apps/common/mixins/api/common.py @@ -13,7 +13,7 @@ from .queryset import QuerySetMixin __all__ = [ - 'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin', 'CommonMixin' + 'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin', ] @@ -82,15 +82,11 @@ class RelationMixin: self.send_m2m_changed_signal(instance, 'post_remove') -class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, RenderToJsonMixin): - pass - - -class CommonMixin(SerializerMixin, - QuerySetMixin, - ExtraFilterFieldsMixin, - RenderToJsonMixin): +class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, + QuerySetMixin, RenderToJsonMixin, + PaginatedResponseMixin): pass + diff --git a/apps/common/mixins/api/permission.py b/apps/common/mixins/api/permission.py index 64efbe7f5..3a57658c3 100644 --- a/apps/common/mixins/api/permission.py +++ b/apps/common/mixins/api/permission.py @@ -27,7 +27,6 @@ class RoleAdminMixin: user_id = self.kwargs.get(self.user_id_url_kwarg) if hasattr(self, 'swagger_fake_view') and not user_id: return self.request.user # NOQA - user_model = get_user_model() return user_model.objects.get(id=user_id) @@ -37,4 +36,4 @@ class RoleUserMixin: @lazyproperty def user(self): - return self.request.user \ No newline at end of file + return self.request.user diff --git a/apps/common/mixins/api/serializer.py b/apps/common/mixins/api/serializer.py index 52b0637df..84cad19ff 100644 --- a/apps/common/mixins/api/serializer.py +++ b/apps/common/mixins/api/serializer.py @@ -15,25 +15,30 @@ class SerializerMixin: serializer_classes = None single_actions = ['put', 'retrieve', 'patch'] - def get_serializer_class_by_view_action(self): - if not hasattr(self, 'serializer_classes'): - return None - if not isinstance(self.serializer_classes, dict): - return None + def get_serializer_classes(self): + classes = getattr(self, 'serializer_classes', None) or {} + return dict(classes) + def get_serializer_class_by_view_action(self): + serializer_classes = self.get_serializer_classes() + if serializer_classes is None: + return None + if not isinstance(serializer_classes, dict): + return None + serializer_classes = dict(serializer_classes) view_action = self.request.query_params.get('action') or self.action or 'list' - serializer_class = self.serializer_classes.get(view_action) + serializer_class = serializer_classes.get(view_action) if serializer_class is None: view_method = self.request.method.lower() - serializer_class = self.serializer_classes.get(view_method) + serializer_class = serializer_classes.get(view_method) if serializer_class is None and view_action in self.single_actions: - serializer_class = self.serializer_classes.get('single') + serializer_class = serializer_classes.get('single') if serializer_class is None: - serializer_class = self.serializer_classes.get('display') + serializer_class = serializer_classes.get('display') if serializer_class is None: - serializer_class = self.serializer_classes.get('default') + serializer_class = serializer_classes.get('default') return serializer_class def get_serializer_class(self): diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index 1965129bb..d1feb61d8 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -9,14 +9,20 @@ from rest_framework.request import Request from common.exceptions import UserConfirmRequired from audits.handler import create_or_update_operate_log from audits.models import OperateLog +from audits.const import ActionChoices -__all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"] +__all__ = [ + "PermissionsMixin", + "RecordViewLogMixin", + "UserConfirmRequiredExceptionMixin", +] class UserConfirmRequiredExceptionMixin: """ 异常处理 """ + def dispatch(self, request, *args, **kwargs): try: return super().dispatch(request, *args, **kwargs) @@ -40,23 +46,23 @@ class PermissionsMixin(UserPassesTestMixin): class RecordViewLogMixin: - ACTION = OperateLog.ACTION_VIEW + ACTION = ActionChoices.view @staticmethod def get_resource_display(request): query_params = dict(request.query_params) - if query_params.get('format'): - query_params.pop('format') - spm_filter = query_params.pop('spm') if query_params.get('spm') else None + if query_params.get("format"): + query_params.pop("format") + spm_filter = query_params.pop("spm") if query_params.get("spm") else None if not query_params and not spm_filter: - display_message = _('Export all') + display_message = _("Export all") elif spm_filter: - display_message = _('Export only selected items') + display_message = _("Export only selected items") else: - query = ','.join( - ['%s=%s' % (key, value) for key, value in query_params.items()] + query = ",".join( + ["%s=%s" % (key, value) for key, value in query_params.items()] ) - display_message = _('Export filtered: %s') % query + display_message = _("Export filtered: %s") % query return display_message def list(self, request, *args, **kwargs): diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 7ce39e1a4..87328c809 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -16,14 +16,13 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): """Allows access to valid user, is active and not expired""" def has_permission(self, request, view): - return super(IsValidUser, self).has_permission(request, view) \ + return super().has_permission(request, view) \ and request.user.is_valid class IsValidUserOrConnectionToken(IsValidUser): - def has_permission(self, request, view): - return super(IsValidUserOrConnectionToken, self).has_permission(request, view) \ + return super().has_permission(request, view) \ or self.is_valid_connection_token(request) @staticmethod @@ -42,6 +41,12 @@ class OnlySuperUser(IsValidUser): and request.user.is_superuser +class IsServiceAccount(IsValidUser): + def has_permission(self, request, view): + return super().has_permission(request, view) \ + and request.user.is_service_account + + class WithBootstrapToken(permissions.BasePermission): def has_permission(self, request, view): authorization = request.META.get('HTTP_AUTHORIZATION', '') diff --git a/apps/common/tasks.py b/apps/common/tasks.py index b9c7caf07..45828492b 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -1,5 +1,6 @@ import os +from django.utils.translation import ugettext_lazy as _ from django.core.mail import send_mail, EmailMultiAlternatives from django.conf import settings from celery import shared_task @@ -9,7 +10,7 @@ from .utils import get_logger logger = get_logger(__file__) -@shared_task +@shared_task(verbose_name=_("Send email")) def send_mail_async(*args, **kwargs): """ Using celery to send email async @@ -36,7 +37,7 @@ def send_mail_async(*args, **kwargs): logger.error("Sending mail error: {}".format(e)) -@shared_task +@shared_task(verbose_name=_("Send email attachment")) def send_mail_attachment_async(subject, message, recipient_list, attachment_list=None): if attachment_list is None: attachment_list = [] diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index a105d50c7..181b43cfb 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # import re -import socket from django.templatetags.static import static from collections import OrderedDict from itertools import chain @@ -14,6 +13,7 @@ import ipaddress import psutil import platform import os +import socket from django.conf import settings @@ -344,7 +344,7 @@ def get_file_by_arch(dir, filename): return file_path -def pretty_string(data: str, max_length=128, ellipsis_str='...'): +def pretty_string(data, max_length=128, ellipsis_str='...'): """ params: data: abcdefgh @@ -353,6 +353,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'): return: ab...gh """ + data = str(data) if len(data) < max_length: return data remain_length = max_length - len(ellipsis_str) diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py index 1f3f83282..2c4808d16 100644 --- a/apps/common/utils/django.py +++ b/apps/common/utils/django.py @@ -2,11 +2,11 @@ # import re -from django.shortcuts import reverse as dj_reverse from django.conf import settings -from django.utils import timezone from django.db import models from django.db.models.signals import post_save, pre_save +from django.shortcuts import reverse as dj_reverse +from django.utils import timezone UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') @@ -80,3 +80,18 @@ def bulk_create_with_signal(cls: models.Model, items, **kwargs): for i in items: post_save.send(sender=cls, instance=i, created=True) return result + + +def get_request_os(request): + """获取请求的操作系统""" + agent = request.META.get('HTTP_USER_AGENT', '').lower() + + if agent is None: + return 'unknown' + if 'windows' in agent.lower(): + return 'windows' + if 'mac' in agent.lower(): + return 'mac' + if 'linux' in agent.lower(): + return 'linux' + return 'unknown' diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index 2bf02ac4c..9eefde044 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -1,24 +1,23 @@ # -*- coding: utf-8 -*- # -import re -import json -from six import string_types import base64 -import os -import time import hashlib +import json +import os +import re +import time from io import StringIO -from itertools import chain import paramiko import sshpubkeys +from cryptography.hazmat.primitives import serialization +from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder from itsdangerous import ( TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, BadSignature, SignatureExpired ) -from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.db.models.fields.files import FileField +from six import string_types from .http import http_date @@ -69,22 +68,23 @@ class Signer(metaclass=Singleton): return None +_supported_paramiko_ssh_key_types = ( + paramiko.RSAKey, + paramiko.DSSKey, + paramiko.Ed25519Key, + paramiko.ECDSAKey,) + + def ssh_key_string_to_obj(text, password=None): key = None - try: - key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) - except paramiko.SSHException: - pass - else: - return key - - try: - key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) - except paramiko.SSHException: - pass - else: - return key - + for ssh_key_type in _supported_paramiko_ssh_key_types: + try: + key = ssh_key_type.from_private_key(StringIO(text), password=password) + return key + except paramiko.SSHException: + pass + if key is None: + raise ValueError('Invalid private key') return key @@ -137,17 +137,65 @@ def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', h def validate_ssh_private_key(text, password=None): - if isinstance(text, bytes): - try: - text = text.decode("utf-8") - except UnicodeDecodeError: - return False + key = parse_ssh_private_key_str(text, password=password) + return bool(key) - key = ssh_key_string_to_obj(text, password=password) - if key is None: - return False - else: - return True + +def parse_ssh_private_key_str(text: bytes, password=None) -> str: + private_key = _parse_ssh_private_key(text, password=password) + if private_key is None: + return "" + # 解析之后,转换成 openssh 格式的私钥 + private_key_bytes = private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.OpenSSH, + serialization.NoEncryption() + ) + return private_key_bytes.decode('utf-8') + + +def parse_ssh_public_key_str(text: bytes = "", password=None) -> str: + private_key = _parse_ssh_private_key(text, password=password) + if private_key is None: + return "" + public_key_bytes = private_key.public_key().public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH, + ) + return public_key_bytes.decode('utf-8') + + +def _parse_ssh_private_key(text, password=None): + """ + text: bytes + password: str + return:private key types: + ec.EllipticCurvePrivateKey, + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ed25519.Ed25519PrivateKey, + """ + if isinstance(text, str): + try: + text = text.encode("utf-8") + except UnicodeDecodeError: + return None + if isinstance(password, str): + try: + password = password.encode("utf-8") + except UnicodeDecodeError: + return None + + try: + if is_openssh_format_key(text): + return serialization.load_ssh_private_key(text, password=password) + return serialization.load_pem_private_key(text, password=password) + except (ValueError, TypeError) as e: + raise e + + +def is_openssh_format_key(text: bytes): + return text.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") def validate_ssh_public_key(text): diff --git a/apps/common/utils/geoip/GeoLite2-City.mmdb b/apps/common/utils/geoip/GeoLite2-City.mmdb new file mode 100644 index 000000000..c3b9d8bac --- /dev/null +++ b/apps/common/utils/geoip/GeoLite2-City.mmdb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638 +size 73906864 diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py index 185397881..ab39c4fa2 100644 --- a/apps/common/utils/http.py +++ b/apps/common/utils/http.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # -import time -from email.utils import formatdate import calendar import threading +import time +from email.utils import formatdate _STRPTIME_LOCK = threading.Lock() @@ -35,3 +35,6 @@ def http_to_unixtime(time_string): def iso8601_to_unixtime(time_string): """把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。""" return to_unixtime(time_string, _ISO8601_FORMAT) + + + diff --git a/apps/common/utils/integer.py b/apps/common/utils/integer.py new file mode 100644 index 000000000..9e657a6dd --- /dev/null +++ b/apps/common/utils/integer.py @@ -0,0 +1,4 @@ +def bit(x): + if x < 1: + raise ValueError("x must be greater than 1") + return 2 ** (x - 1) diff --git a/apps/common/utils/ip/utils.py b/apps/common/utils/ip/utils.py index d62cba00d..6a6fce26b 100644 --- a/apps/common/utils/ip/utils.py +++ b/apps/common/utils/ip/utils.py @@ -66,7 +66,7 @@ def contains_ip(ip, ip_group): if in_ip_segment(ip, _ip): return True else: - # is domain name + # address / host if ip == _ip: return True @@ -75,7 +75,7 @@ def contains_ip(ip, ip_group): def get_ip_city(ip): if not ip or not isinstance(ip, str): - return _("Invalid ip") + return _("Invalid address") if ':' in ip: return 'IPv6' diff --git a/apps/common/utils/lock.py b/apps/common/utils/lock.py index a14fe1184..773647725 100644 --- a/apps/common/utils/lock.py +++ b/apps/common/utils/lock.py @@ -76,7 +76,6 @@ class DistributedLock(RedisLock): # 要创建一个新的锁对象 with self.__class__(**self.kwargs_copy): return func(*args, **kwds) - return inner @classmethod @@ -105,22 +104,21 @@ class DistributedLock(RedisLock): if self._reentrant: if self.locked_by_current_thread(): self._acquired_reentrant_lock = True - logger.debug(f'Reentry lock ok: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') + logger.debug(f'Reentry lock ok: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}') return True - logger.debug(f'Attempt acquire reentrant-lock: lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Attempt acquire reentrant-lock: lock_id={self.id} lock={self.name}') acquired = super().acquire(blocking=blocking, timeout=timeout) if acquired: - logger.debug(f'Acquired reentrant-lock ok: lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Acquired reentrant-lock ok: lock_id={self.id} lock={self.name}') setattr(thread_local, self.name, self.id) else: - logger.debug( - f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name}') return acquired else: - logger.debug(f'Attempt acquire lock: lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Attempt acquire lock: lock_id={self.id} lock={self.name}') acquired = super().acquire(blocking=blocking, timeout=timeout) - logger.debug(f'Acquired lock: ok={acquired} lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Acquired lock: ok={acquired} lock_id={self.id} lock={self.name}') return acquired @property @@ -139,17 +137,17 @@ class DistributedLock(RedisLock): def _release_on_reentrant_locked_by_brother(self): if self._acquired_reentrant_lock: self._acquired_reentrant_lock = False - logger.debug(f'Released reentrant-lock: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') + logger.debug(f'Released reentrant-lock: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}') return else: - self._raise_exc_with_log(f'Reentrant-lock is not acquired: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') + self._raise_exc_with_log(f'Reentrant-lock is not acquired: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}') def _release_on_reentrant_locked_by_me(self): - logger.debug(f'Release reentrant-lock locked by me: lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Release reentrant-lock locked by me: lock_id={self.id} lock={self.name}') id = getattr(thread_local, self.name, None) if id != self.id: - raise PermissionError(f'Reentrant-lock is not locked by me: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') + raise PermissionError(f'Reentrant-lock is not locked by me: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}') try: # 这里要保证先删除 thread_local 的标记, delattr(thread_local, self.name) @@ -171,9 +169,9 @@ class DistributedLock(RedisLock): def _release(self): try: self._release_redis_lock() - logger.debug(f'Released lock: lock_id={self.id} lock={self.name} thread={self._thread_id}') + logger.debug(f'Released lock: lock_id={self.id} lock={self.name}') except NotAcquired as e: - logger.error(f'Release lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id} error: {e}') + logger.error(f'Release lock failed: lock_id={self.id} lock={self.name} error: {e}') self._raise_exc(e) def release(self): @@ -188,12 +186,12 @@ class DistributedLock(RedisLock): _release = self._release_on_reentrant_locked_by_brother else: self._raise_exc_with_log( - f'Reentrant-lock is not acquired: lock_id={self.id} lock={self.name} thread={self._thread_id}') + f'Reentrant-lock is not acquired: lock_id={self.id} lock={self.name}') # 处理是否在事务提交时才释放锁 if self._release_on_transaction_commit: logger.debug( - f'Release lock on transaction commit ... :lock_id={self.id} lock={self.name} thread={self._thread_id}') + f'Release lock on transaction commit ... :lock_id={self.id} lock={self.name}') transaction.on_commit(_release) else: _release() diff --git a/apps/common/utils/timezone.py b/apps/common/utils/timezone.py index 4b60008af..17e44b7bf 100644 --- a/apps/common/utils/timezone.py +++ b/apps/common/utils/timezone.py @@ -1,22 +1,21 @@ -import datetime - import pytz +from datetime import datetime, timedelta, timezone from django.utils import timezone as dj_timezone from rest_framework.fields import DateTimeField -max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc) +max = datetime.max.replace(tzinfo=timezone.utc) -def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo): +def astimezone(dt: datetime, tzinfo: pytz.tzinfo.DstTzInfo): assert dj_timezone.is_aware(dt) return tzinfo.normalize(dt.astimezone(tzinfo)) -def as_china_cst(dt: datetime.datetime): +def as_china_cst(dt: datetime): return astimezone(dt, pytz.timezone('Asia/Shanghai')) -def as_current_tz(dt: datetime.datetime): +def as_current_tz(dt: datetime): return astimezone(dt, dj_timezone.get_current_timezone()) @@ -36,6 +35,15 @@ def local_now_date_display(fmt='%Y-%m-%d'): return local_now().strftime(fmt) +def local_zero_hour(fmt='%Y-%m-%d'): + return datetime.strptime(local_now().strftime(fmt), fmt) + + +def local_monday(): + zero_hour_time = local_zero_hour() + return zero_hour_time - timedelta(zero_hour_time.weekday()) + + _rest_dt_field = DateTimeField() dt_parser = _rest_dt_field.to_internal_value dt_formatter = _rest_dt_field.to_representation diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index d5ff38593..b0b31f4c9 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -3,51 +3,131 @@ import time from django.core.cache import cache from django.utils import timezone from django.utils.timesince import timesince -from django.db.models import Count, Max +from django.db.models import Count, Max, F from django.http.response import JsonResponse, HttpResponse from rest_framework.views import APIView from rest_framework.permissions import AllowAny -from collections import Counter +from rest_framework.request import Request from rest_framework.response import Response from users.models import User from assets.models import Asset -from terminal.models import Session +from assets.const import AllTypes +from terminal.models import Session, Command from terminal.utils import ComponentsPrometheusMetricsUtil from orgs.utils import current_org from common.utils import lazyproperty +from audits.models import UserLoginLog, PasswordChangeLog, OperateLog +from audits.const import LoginStatusChoices +from common.utils.timezone import local_now, local_zero_hour from orgs.caches import OrgResourceStatisticsCache - __all__ = ['IndexApi'] -class DatesLoginMetricMixin: +class DateTimeMixin: + request: Request + + @property + def org(self): + return current_org + @lazyproperty def days(self): query_params = self.request.query_params - if query_params.get('monthly'): - return 30 - return 7 + count = query_params.get('days') + count = int(count) if count else 0 + return count + + @property + def days_to_datetime(self): + days = self.days + if days == 0: + t = local_zero_hour() + else: + t = local_now() - timezone.timedelta(days=days) + return t @lazyproperty - def sessions_queryset(self): - days = timezone.now() - timezone.timedelta(days=self.days) - sessions_queryset = Session.objects.filter(date_start__gt=days) - return sessions_queryset - - @lazyproperty - def session_dates_list(self): - now = timezone.now() + def dates_list(self): + now = local_now() dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)] dates.reverse() - # dates = self.sessions_queryset.dates('date_start', 'day') return dates def get_dates_metrics_date(self): - dates_metrics_date = [d.strftime('%m-%d') for d in self.session_dates_list] or ['0'] + dates_metrics_date = [d.strftime('%m-%d') for d in self.dates_list] or ['0'] return dates_metrics_date + @lazyproperty + def users(self): + return self.org.get_members() + + @lazyproperty + def sessions_queryset(self): + t = self.days_to_datetime + sessions_queryset = Session.objects.filter(date_start__gte=t) + return sessions_queryset + + def get_logs_queryset(self, queryset, query_params): + query = {} + if not self.org.is_root(): + if query_params == 'username': + query = { + f'{query_params}__in': self.users.values_list('username', flat=True) + } + else: + query = { + f'{query_params}__in': [str(user) for user in self.users] + } + queryset = queryset.filter(**query) + return queryset + + @lazyproperty + def login_logs_queryset(self): + t = self.days_to_datetime + queryset = UserLoginLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'username') + return queryset + + @lazyproperty + def password_change_logs_queryset(self): + t = self.days_to_datetime + queryset = PasswordChangeLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'user') + return queryset + + @lazyproperty + def operate_logs_queryset(self): + t = self.days_to_datetime + queryset = OperateLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'user') + return queryset + + @lazyproperty + def ftp_logs_queryset(self): + t = self.days_to_datetime + queryset = OperateLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'user') + return queryset + + @lazyproperty + def command_queryset(self): + t = self.days_to_datetime + t = t.timestamp() + queryset = Command.objects.filter(timestamp__gte=t) + return queryset + + +class DatesLoginMetricMixin: + dates_list: list + command_queryset: Command.objects + sessions_queryset: Session.objects + ftp_logs_queryset: OperateLog.objects + login_logs_queryset: UserLoginLog.objects + operate_logs_queryset: OperateLog.objects + password_change_logs_queryset: PasswordChangeLog.objects + @staticmethod def get_cache_key(date, tp): date_str = date.strftime("%Y%m%d") @@ -63,7 +143,7 @@ class DatesLoginMetricMixin: def __set_data_to_cache(self, date, tp, count): cache_key = self.get_cache_key(date, tp) - cache.set(cache_key, count, 3600*24*7) + cache.set(cache_key, count, 3600 * 24 * 7) @staticmethod def get_date_start_2_end(d): @@ -86,7 +166,7 @@ class DatesLoginMetricMixin: def get_dates_metrics_total_count_login(self): data = [] - for d in self.session_dates_list: + for d in self.dates_list: count = self.get_date_login_count(d) data.append(count) if len(data) == 0: @@ -105,7 +185,7 @@ class DatesLoginMetricMixin: def get_dates_metrics_total_count_active_users(self): data = [] - for d in self.session_dates_list: + for d in self.dates_list: count = self.get_date_user_count(d) data.append(count) return data @@ -122,80 +202,61 @@ class DatesLoginMetricMixin: def get_dates_metrics_total_count_active_assets(self): data = [] - for d in self.session_dates_list: + for d in self.dates_list: count = self.get_date_asset_count(d) data.append(count) return data - @lazyproperty - def dates_total_count_active_users(self): - count = len(set(self.sessions_queryset.values_list('user_id', flat=True))) + def get_date_session_count(self, date): + tp = "SESSION" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + ds, de = self.get_date_start_2_end(date) + count = Session.objects.filter(date_start__range=(ds, de)).count() + self.__set_data_to_cache(date, tp, count) return count - @lazyproperty - def dates_total_count_inactive_users(self): - total = current_org.get_members().count() - active = self.dates_total_count_active_users - count = total - active - if count < 0: - count = 0 - return count + def get_dates_metrics_total_count_sessions(self): + data = [] + for d in self.dates_list: + count = self.get_date_session_count(d) + data.append(count) + return data @lazyproperty - def dates_total_count_disabled_users(self): - return current_org.get_members().filter(is_active=False).count() + def get_type_to_assets(self): + result = Asset.objects.annotate(type=F('platform__type')). \ + values('type').order_by('type').annotate(total=Count(1)) + all_types_dict = dict(AllTypes.choices()) + result = list(result) + for i in result: + tp = i['type'] + i['label'] = all_types_dict[tp] + return result - @lazyproperty - def dates_total_count_active_assets(self): - return len(set(self.sessions_queryset.values_list('asset', flat=True))) - - @lazyproperty - def dates_total_count_inactive_assets(self): - total = Asset.objects.all().count() - active = self.dates_total_count_active_assets - count = total - active - if count < 0: - count = 0 - return count - - @lazyproperty - def dates_total_count_disabled_assets(self): - return Asset.objects.filter(is_active=False).count() - - # 以下是从week中而来 - def get_dates_login_times_top5_users(self): - users = self.sessions_queryset.values_list('user_id', flat=True) - users = [ - {'user': user, 'total': total} - for user, total in Counter(users).most_common(5) - ] - return users - - def get_dates_total_count_login_users(self): - return len(set(self.sessions_queryset.values_list('user_id', flat=True))) - - def get_dates_total_count_login_times(self): - return self.sessions_queryset.count() - - def get_dates_login_times_top10_assets(self): + def get_dates_login_times_assets(self): assets = self.sessions_queryset.values("asset") \ - .annotate(total=Count("asset")) \ - .annotate(last=Max("date_start")).order_by("-total")[:10] + .annotate(total=Count("asset")) \ + .annotate(last=Max("date_start")).order_by("-total") + assets = assets[:10] for asset in assets: asset['last'] = str(asset['last']) return list(assets) - def get_dates_login_times_top10_users(self): + def get_dates_login_times_users(self): users = self.sessions_queryset.values("user_id") \ - .annotate(total=Count("user_id")) \ - .annotate(user=Max('user')) \ - .annotate(last=Max("date_start")).order_by("-total")[:10] + .annotate(total=Count("user_id")) \ + .annotate(user=Max('user')) \ + .annotate(last=Max("date_start")).order_by("-total") + users = users[:10] for user in users: user['last'] = str(user['last']) return list(users) - def get_dates_login_record_top10_sessions(self): - sessions = self.sessions_queryset.order_by('-date_start')[:10] + def get_dates_login_record_sessions(self): + sessions = self.sessions_queryset.order_by('-date_start') + sessions = sessions[:10] for session in sessions: session.avatar_url = User.get_avatar_url("") sessions = [ @@ -210,19 +271,55 @@ class DatesLoginMetricMixin: ] return sessions + @lazyproperty + def user_login_logs_amount(self): + return self.login_logs_queryset.count() -class IndexApi(DatesLoginMetricMixin, APIView): + @lazyproperty + def user_login_success_logs_amount(self): + return self.login_logs_queryset.filter(status=LoginStatusChoices.success).count() + + @lazyproperty + def user_login_amount(self): + return self.login_logs_queryset.values('username').distinct().count() + + @lazyproperty + def operate_logs_amount(self): + return self.operate_logs_queryset.count() + + @lazyproperty + def change_password_logs_amount(self): + return self.password_change_logs_queryset.count() + + @lazyproperty + def commands_amount(self): + return self.command_queryset.count() + + @lazyproperty + def commands_danger_amount(self): + return self.command_queryset.filter(risk_level=Command.RISK_LEVEL_DANGEROUS).count() + + @lazyproperty + def sessions_amount(self): + return self.sessions_queryset.count() + + @lazyproperty + def ftp_logs_amount(self): + return self.ftp_logs_queryset.count() + + +class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView): http_method_names = ['get'] - rbac_perms = { - 'GET': 'rbac.view_audit | rbac.view_console' - } + + def check_permissions(self, request): + return request.user.has_perm(['rbac.view_audit', 'rbac.view_console']) def get(self, request, *args, **kwargs): data = {} query_params = self.request.query_params - caches = OrgResourceStatisticsCache(current_org) + caches = OrgResourceStatisticsCache(self.org) _all = query_params.get('all') @@ -236,6 +333,26 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'total_count_assets': caches.assets_amount, }) + if _all or query_params.get('total_count') or query_params.get('total_count_users_this_week'): + data.update({ + 'total_count_users_this_week': caches.new_users_amount_this_week, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_assets_this_week'): + data.update({ + 'total_count_assets_this_week': caches.new_assets_amount_this_week, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_login_users'): + data.update({ + 'total_count_login_users': self.user_login_amount + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'): + data.update({ + 'total_count_today_active_assets': caches.total_count_today_active_assets, + }) + if _all or query_params.get('total_count') or query_params.get('total_count_online_users'): data.update({ 'total_count_online_users': caches.total_count_online_users, @@ -246,6 +363,62 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'total_count_online_sessions': caches.total_count_online_sessions, }) + if _all or query_params.get('total_count') or query_params.get('total_count_today_failed_sessions'): + data.update({ + 'total_count_today_failed_sessions': caches.total_count_today_failed_sessions, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_user_login_logs'): + data.update({ + 'total_count_user_login_logs': self.user_login_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_user_login_success_logs'): + data.update({ + 'total_count_user_login_success_logs': self.user_login_success_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_operate_logs'): + data.update({ + 'total_count_operate_logs': self.operate_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_change_password_logs'): + data.update({ + 'total_count_change_password_logs': self.change_password_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_commands'): + data.update({ + 'total_count_commands': self.commands_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_commands_danger'): + data.update({ + 'total_count_commands_danger': self.commands_danger_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'): + data.update({ + 'total_count_history_sessions': self.sessions_amount - caches.total_count_online_sessions, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'): + data.update({ + 'total_count_ftp_logs': self.ftp_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'): + data.update({ + 'total_count_type_to_assets_amount': self.get_type_to_assets, + }) + + if _all or query_params.get('session_dates_metrics'): + data.update({ + 'dates_metrics_date': self.get_dates_metrics_date(), + 'dates_metrics_total_count_session': self.get_dates_metrics_total_count_sessions(), + }) + if _all or query_params.get('dates_metrics'): data.update({ 'dates_metrics_date': self.get_dates_metrics_date(), @@ -254,44 +427,19 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'dates_metrics_total_count_active_assets': self.get_dates_metrics_total_count_active_assets(), }) - if _all or query_params.get('dates_total_count_users'): - data.update({ - 'dates_total_count_active_users': self.dates_total_count_active_users, - 'dates_total_count_inactive_users': self.dates_total_count_inactive_users, - 'dates_total_count_disabled_users': self.dates_total_count_disabled_users, - }) - - if _all or query_params.get('dates_total_count_assets'): - data.update({ - 'dates_total_count_active_assets': self.dates_total_count_active_assets, - 'dates_total_count_inactive_assets': self.dates_total_count_inactive_assets, - 'dates_total_count_disabled_assets': self.dates_total_count_disabled_assets, - }) - - if _all or query_params.get('dates_total_count'): - data.update({ - 'dates_total_count_login_users': self.get_dates_total_count_login_users(), - 'dates_total_count_login_times': self.get_dates_total_count_login_times(), - }) - - if _all or query_params.get('dates_login_times_top5_users'): - data.update({ - 'dates_login_times_top5_users': self.get_dates_login_times_top5_users(), - }) - if _all or query_params.get('dates_login_times_top10_assets'): data.update({ - 'dates_login_times_top10_assets': self.get_dates_login_times_top10_assets(), + 'dates_login_times_top10_assets': self.get_dates_login_times_assets(), }) if _all or query_params.get('dates_login_times_top10_users'): data.update({ - 'dates_login_times_top10_users': self.get_dates_login_times_top10_users(), + 'dates_login_times_top10_users': self.get_dates_login_times_users(), }) if _all or query_params.get('dates_login_record_top10_sessions'): data.update({ - 'dates_login_record_top10_sessions': self.get_dates_login_record_top10_sessions() + 'dates_login_record_top10_sessions': self.get_dates_login_record_sessions() }) return JsonResponse(data, status=200) @@ -308,14 +456,14 @@ class HealthCheckView(HealthApiMixin): def get_db_status(): t1 = time.time() try: - User.objects.first() + ok = User.objects.first() is not None t2 = time.time() - return True, t2 - t1 - except: - t2 = time.time() - return False, t2 - t1 + return ok, t2 - t1 + except Exception as e: + return False, str(e) - def get_redis_status(self): + @staticmethod + def get_redis_status(): key = 'HEALTH_CHECK' t1 = time.time() @@ -324,12 +472,12 @@ class HealthCheckView(HealthApiMixin): cache.set(key, '1', 10) got = cache.get(key) t2 = time.time() + if value == got: - return True, t2 -t1 - return False, t2 -t1 - except: - t2 = time.time() - return False, t2 - t1 + return True, t2 - t1 + return False, 'Value not match' + except Exception as e: + return False, str(e) def get(self, request): redis_status, redis_time = self.get_redis_status() @@ -341,7 +489,7 @@ class HealthCheckView(HealthApiMixin): 'db_time': db_time, 'redis_status': redis_status, 'redis_time': redis_time, - 'time': int(time.time()) + 'time': int(time.time()), } return Response(data) @@ -353,4 +501,3 @@ class PrometheusMetricsApi(HealthApiMixin): util = ComponentsPrometheusMetricsUtil() metrics_text = util.get_prometheus_metrics_text() return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8') - diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d5857f90d..c2bfba805 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -7,23 +7,22 @@ 2. 程序需要, 用户不需要更改的写到settings中 3. 程序需要, 用户需要更改的写到本config中 """ +import base64 +import copy +import errno +import json +import logging import os import re import sys import types -import errno -import json -import yaml -import copy -import base64 -import logging from importlib import import_module from urllib.parse import urljoin, urlparse -from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT +import yaml from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ - +from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -124,7 +123,7 @@ class ConfigCrypto: if plaintext: value = plaintext except Exception as e: - logger.error('decrypt %s error: %s', item, e) + pass return value @classmethod @@ -134,7 +133,7 @@ class ConfigCrypto: secret_encrypt_key = os.environ.get('SECRET_ENCRYPT_KEY', '') if not secret_encrypt_key: return None - print('Info: Using SM4 to encrypt config secret value') + print('Info: try using SM4 to decrypt config secret value') return cls(secret_encrypt_key) @@ -423,7 +422,7 @@ class Config(dict): 'TERMINAL_PASSWORD_AUTH': True, 'TERMINAL_PUBLIC_KEY_AUTH': True, 'TERMINAL_HEARTBEAT_INTERVAL': 20, - 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', + 'TERMINAL_ASSET_LIST_SORT_BY': 'name', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', 'TERMINAL_SESSION_KEEP_DURATION': 200, 'TERMINAL_HOST_KEY': '', @@ -533,6 +532,9 @@ class Config(dict): 'FORGOT_PASSWORD_URL': '', 'HEALTH_CHECK_TOKEN': '', + + # Applet 等软件的下载地址 + 'APPLET_DOWNLOAD_HOST': '', } def __init__(self, *args): diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 414144d39..84c5a4bc2 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ default_interface = dict(( ('logo_logout', static('img/logo.png')), - ('logo_index', static('img/logo_text.png')), + ('logo_index', static('img/logo_text_white.png')), ('login_image', static('img/login_image.jpg')), ('favicon', static('img/facio.ico')), ('login_title', _('JumpServer Open Source Bastion Host')), diff --git a/apps/jumpserver/middleware.py b/apps/jumpserver/middleware.py index ed0c1bae6..bf7aa7945 100644 --- a/apps/jumpserver/middleware.py +++ b/apps/jumpserver/middleware.py @@ -3,6 +3,9 @@ import os import re import pytz +import time +import json + from django.utils import timezone from django.shortcuts import HttpResponse from django.conf import settings @@ -92,3 +95,37 @@ class RefererCheckMiddleware: return HttpResponseForbidden('CSRF CHECK ERROR') response = self.get_response(request) return response + + +class StartMiddleware: + def __init__(self, get_response): + self.get_response = get_response + if not settings.DEBUG_DEV: + raise MiddlewareNotUsed + + def __call__(self, request): + request._s_time_start = time.time() + response = self.get_response(request) + request._s_time_end = time.time() + if request.path == '/api/health/': + data = response.data + data['pre_middleware_time'] = request._e_time_start - request._s_time_start + data['api_time'] = request._e_time_end - request._e_time_start + data['post_middleware_time'] = request._s_time_end - request._e_time_end + response.content = json.dumps(data) + response.headers['Content-Length'] = str(len(response.content)) + return response + return response + + +class EndMiddleware: + def __init__(self, get_response): + self.get_response = get_response + if not settings.DEBUG_DEV: + raise MiddlewareNotUsed + + def __call__(self, request): + request._e_time_start = time.time() + response = self.get_response(request) + request._e_time_end = time.time() + return response diff --git a/apps/jumpserver/settings/_xpack.py b/apps/jumpserver/settings/_xpack.py index 322740201..9f4319a35 100644 --- a/apps/jumpserver/settings/_xpack.py +++ b/apps/jumpserver/settings/_xpack.py @@ -6,7 +6,6 @@ from .. import const from .base import INSTALLED_APPS, TEMPLATES XPACK_DIR = os.path.join(const.BASE_DIR, 'xpack') -# XPACK_ENABLED = False XPACK_ENABLED = os.path.isdir(XPACK_DIR) XPACK_TEMPLATES_DIR = [] XPACK_CONTEXT_PROCESSOR = [] diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 554ff50d0..aaedd2ddc 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -37,6 +37,7 @@ VERSION = const.VERSION BASE_DIR = const.BASE_DIR PROJECT_DIR = const.PROJECT_DIR DATA_DIR = os.path.join(PROJECT_DIR, 'data') +ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible') CERTS_DIR = os.path.join(DATA_DIR, 'certs') # Quick-start development settings - unsuitable for production @@ -56,6 +57,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV # Absolute url for some case, for example email link SITE_URL = CONFIG.SITE_URL +# Absolute url for downloading applet +APPLET_DOWNLOAD_HOST = CONFIG.APPLET_DOWNLOAD_HOST + # https://docs.djangoproject.com/en/4.1/ref/settings/ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -109,6 +113,7 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ + 'jumpserver.middleware.StartMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -128,6 +133,7 @@ MIDDLEWARE = [ 'authentication.middleware.ThirdPartyLoginMiddleware', 'authentication.middleware.SessionCookieMiddleware', 'simple_history.middleware.HistoryRequestMiddleware', + 'jumpserver.middleware.EndMiddleware', ] ROOT_URLCONF = 'jumpserver.urls' @@ -370,7 +376,6 @@ PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', ] - GMSSL_ENABLED = CONFIG.GMSSL_ENABLED GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher' if GMSSL_ENABLED: @@ -386,4 +391,3 @@ if os.environ.get('DEBUG_TOOLBAR', False): DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.profiling.ProfilingPanel', ] - diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index ef654fddf..730796b1d 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -21,7 +21,6 @@ api_v1 = [ path('settings/', include('settings.urls.api_urls', namespace='api-settings')), path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), path('common/', include('common.urls.api_urls', namespace='api-common')), - path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), path('acls/', include('acls.urls.api_urls', namespace='api-acls')), path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')), @@ -45,12 +44,6 @@ if settings.XPACK_ENABLED: ) -apps = [ - 'users', 'assets', 'perms', 'terminal', 'ops', 'audits', - 'orgs', 'auth', 'applications', 'tickets', 'settings', 'xpack', - 'flower', 'luna', 'koko', 'ws', 'docs', 'redocs', -] - urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('api/v1/', include(api_v1)), @@ -86,11 +79,5 @@ if os.environ.get('DEBUG_TOOLBAR', False): ] -# 兼容之前的 -old_app_pattern = '|'.join(apps) -old_app_pattern = r'^{}'.format(old_app_pattern) -urlpatterns += [re_path(old_app_pattern, views.redirect_old_apps_view)] - - handler404 = 'jumpserver.views.handler404' handler500 = 'jumpserver.views.handler500' diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index a922b4d2e..54a75e2ed 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7db985efdf818137dafe489f339955f4d71a245ffd6becc8f6efac539a625682 -size 133463 +oid sha256:0818af791dad7cd50e19c41de0bc8967f9d08f949f48d5c2020786153a743349 +size 116392 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po deleted file mode 100644 index 811755a88..000000000 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ /dev/null @@ -1,7073 +0,0 @@ -# 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. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-11-17 17:34+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: acls/apps.py:7 -msgid "Acls" -msgstr "Acls" - -#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 -#: applications/models/application.py:219 assets/models/asset.py:138 -#: assets/models/base.py:175 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:27 assets/models/domain.py:23 -#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 -#: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 -#: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/endpoint.py:14 terminal/models/endpoint.py:87 -#: terminal/models/storage.py:27 terminal/models/task.py:16 -#: terminal/models/terminal.py:101 users/forms/profile.py:33 -#: users/models/group.py:15 users/models/user.py:673 -#: xpack/plugins/cloud/models.py:27 -msgid "Name" -msgstr "名前" - -#: acls/models/base.py:27 assets/models/cmd_filter.py:88 -#: assets/models/user.py:252 terminal/models/endpoint.py:90 -msgid "Priority" -msgstr "優先順位" - -#: acls/models/base.py:28 assets/models/cmd_filter.py:88 -#: assets/models/user.py:252 terminal/models/endpoint.py:91 -msgid "1-100, the lower the value will be match first" -msgstr "1-100、低い値は最初に一致します" - -#: acls/models/base.py:31 authentication/models.py:21 -#: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/base.py:88 terminal/models/sharing.py:28 tickets/const.py:39 -msgid "Active" -msgstr "アクティブ" - -#: acls/models/base.py:32 applications/models/application.py:232 -#: assets/models/asset.py:143 assets/models/asset.py:231 -#: assets/models/backup.py:54 assets/models/base.py:180 -#: assets/models/cluster.py:29 assets/models/cmd_filter.py:52 -#: assets/models/cmd_filter.py:100 assets/models/domain.py:24 -#: assets/models/domain.py:65 assets/models/group.py:23 -#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 -#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/endpoint.py:22 terminal/models/endpoint.py:97 -#: terminal/models/storage.py:30 terminal/models/terminal.py:115 -#: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 -#: users/models/group.py:16 users/models/user.py:712 -#: xpack/plugins/change_auth_plan/models/base.py:44 -#: xpack/plugins/cloud/models.py:34 xpack/plugins/cloud/models.py:118 -#: xpack/plugins/gathered_user/models.py:26 -msgid "Comment" -msgstr "コメント" - -#: acls/models/login_acl.py:18 tickets/const.py:47 -#: tickets/templates/tickets/approve_check_password.html:49 -msgid "Reject" -msgstr "拒否" - -#: acls/models/login_acl.py:19 assets/models/cmd_filter.py:79 -msgid "Allow" -msgstr "許可" - -#: acls/models/login_acl.py:20 acls/models/login_acl.py:75 -#: acls/models/login_asset_acl.py:17 tickets/const.py:9 -msgid "Login confirm" -msgstr "ログイン確認" - -#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:38 -#: audits/models.py:63 audits/models.py:105 audits/serializers.py:106 -#: authentication/models.py:19 authentication/models.py:54 -#: authentication/models.py:78 notifications/models/notification.py:12 -#: orgs/models.py:220 perms/models/base.py:84 rbac/builtin.py:120 -#: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:13 terminal/models/session.py:45 -#: terminal/models/sharing.py:33 terminal/notifications.py:94 -#: terminal/notifications.py:142 tickets/models/comment.py:21 users/const.py:14 -#: users/models/user.py:905 users/models/user.py:936 -#: users/serializers/group.py:19 -msgid "User" -msgstr "ユーザー" - -#: acls/models/login_acl.py:28 -msgid "Rule" -msgstr "ルール" - -#: acls/models/login_acl.py:31 acls/models/login_asset_acl.py:26 -#: acls/serializers/login_acl.py:17 acls/serializers/login_asset_acl.py:75 -#: assets/models/cmd_filter.py:93 audits/models.py:64 audits/serializers.py:57 -#: authentication/templates/authentication/_access_key_modal.html:34 -msgid "Action" -msgstr "アクション" - -#: acls/models/login_acl.py:35 acls/models/login_asset_acl.py:32 -#: acls/serializers/login_acl.py:16 assets/models/cmd_filter.py:98 -msgid "Reviewers" -msgstr "レビュー担当者" - -#: acls/models/login_acl.py:42 -msgid "Login acl" -msgstr "ログインacl" - -#: acls/models/login_asset_acl.py:21 -#: applications/serializers/application.py:124 -#: applications/serializers/application.py:164 -msgid "System User" -msgstr "システムユーザー" - -#: acls/models/login_asset_acl.py:22 -#: applications/serializers/attrs/application_category/remote_app.py:36 -#: assets/models/asset.py:386 assets/models/authbook.py:19 -#: assets/models/backup.py:31 assets/models/cmd_filter.py:42 -#: assets/models/gathered_user.py:14 assets/serializers/label.py:30 -#: assets/serializers/system_user.py:268 audits/models.py:40 -#: authentication/models.py:66 authentication/models.py:90 -#: perms/models/asset_permission.py:23 terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:47 -#: terminal/notifications.py:93 -#: xpack/plugins/change_auth_plan/models/asset.py:199 -#: xpack/plugins/change_auth_plan/serializers/asset.py:181 -#: xpack/plugins/cloud/models.py:225 -msgid "Asset" -msgstr "資産" - -#: acls/models/login_asset_acl.py:40 -msgid "Login asset acl" -msgstr "ログインasset acl" - -#: acls/models/login_asset_acl.py:89 tickets/const.py:12 -msgid "Login asset confirm" -msgstr "ログイン資産の確認" - -#: acls/serializers/login_acl.py:11 acls/serializers/login_asset_acl.py:12 -msgid "Format for comma-delimited string, with * indicating a match all. " -msgstr "コンマ区切り文字列の形式。* はすべて一致することを示します。" - -#: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:17 -#: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176 -#: assets/models/gathered_user.py:15 audits/models.py:139 -#: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models.py:260 -#: authentication/templates/authentication/_msg_different_city.html:9 -#: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/forms/profile.py:112 -#: users/models/user.py:671 users/templates/users/_msg_user_created.html:12 -#: xpack/plugins/change_auth_plan/models/asset.py:34 -#: xpack/plugins/change_auth_plan/models/asset.py:195 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 -msgid "Username" -msgstr "ユーザー名" - -#: acls/serializers/login_asset_acl.py:24 -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/login_asset_acl.py:31 acls/serializers/rules/rules.py:33 -#: applications/serializers/attrs/application_type/mysql_workbench.py:18 -#: assets/models/asset.py:210 assets/models/domain.py:61 -#: assets/serializers/account.py:13 -#: 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/login_asset_acl.py:35 assets/models/asset.py:211 -#: assets/serializers/account.py:14 assets/serializers/gathered_user.py:23 -#: settings/serializers/terminal.py:9 -msgid "Hostname" -msgstr "ホスト名" - -#: acls/serializers/login_asset_acl.py:42 -msgid "" -"Format for comma-delimited string, with * indicating a match all. Protocol " -"options: {}" -msgstr "" -"コンマ区切り文字列の形式。* はすべて一致することを示します。プロトコルオプ" -"ション: {}" - -#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:213 -#: assets/models/domain.py:63 assets/models/user.py:253 -#: terminal/serializers/session.py:31 terminal/serializers/storage.py:68 -msgid "Protocol" -msgstr "プロトコル" - -#: acls/serializers/login_asset_acl.py:65 -msgid "Unsupported protocols: {}" -msgstr "サポートされていないプロトコル: {}" - -#: acls/serializers/login_asset_acl.py:98 -#: tickets/serializers/ticket/ticket.py:96 -msgid "The organization `{}` does not exist" -msgstr "組織 '{}'は存在しません" - -#: acls/serializers/login_asset_acl.py:103 -msgid "None of the reviewers belong to Organization `{}`" -msgstr "いずれのレビューアも組織 '{}' に属していません" - -#: acls/serializers/rules/rules.py:20 -#: xpack/plugins/cloud/serializers/task.py:24 -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:35 -msgid "Time Period" -msgstr "期間" - -#: applications/apps.py:9 applications/models/application.py:62 -msgid "Applications" -msgstr "アプリケーション" - -#: applications/const.py:8 -#: applications/serializers/attrs/application_category/db.py:14 -#: applications/serializers/attrs/application_type/mysql_workbench.py:26 -msgid "Database" -msgstr "データベース" - -#: applications/const.py:9 -msgid "Remote app" -msgstr "リモートアプリ" - -#: applications/const.py:36 -msgid "Custom" -msgstr "カスタム" - -#: applications/models/account.py:12 applications/models/application.py:236 -#: assets/models/backup.py:32 assets/models/cmd_filter.py:49 -#: authentication/models.py:67 authentication/models.py:95 -#: perms/models/application_permission.py:28 -#: xpack/plugins/change_auth_plan/models/app.py:32 -msgid "Application" -msgstr "アプリケーション" - -#: applications/models/account.py:15 assets/models/authbook.py:20 -#: assets/models/cmd_filter.py:46 assets/models/user.py:343 audits/models.py:41 -#: authentication/models.py:83 perms/models/application_permission.py:33 -#: perms/models/asset_permission.py:25 terminal/backends/command/models.py:22 -#: terminal/backends/command/serializers.py:36 terminal/models/session.py:49 -#: xpack/plugins/change_auth_plan/models/app.py:36 -#: xpack/plugins/change_auth_plan/models/app.py:147 -#: xpack/plugins/change_auth_plan/serializers/app.py:65 -msgid "System user" -msgstr "システムユーザー" - -#: applications/models/account.py:17 assets/models/authbook.py:21 -#: settings/serializers/auth/cas.py:20 -msgid "Version" -msgstr "バージョン" - -#: applications/models/account.py:23 -msgid "Application account" -msgstr "アプリケーションアカウント" - -#: applications/models/account.py:26 -msgid "Can view application account secret" -msgstr "アプリケーションアカウントの秘密を表示できます" - -#: applications/models/account.py:27 -msgid "Can change application account secret" -msgstr "アプリケーションアカウントの秘密を変更できます" - -#: applications/models/application.py:221 -#: applications/serializers/application.py:101 assets/models/label.py:21 -#: perms/models/application_permission.py:21 -#: perms/serializers/application/user_permission.py:33 settings/models.py:35 -#: tickets/models/ticket/apply_application.py:15 -#: xpack/plugins/change_auth_plan/models/app.py:25 -msgid "Category" -msgstr "カテゴリ" - -#: applications/models/application.py:224 -#: applications/serializers/application.py:103 assets/models/backup.py:49 -#: assets/models/cmd_filter.py:86 assets/models/user.py:251 -#: authentication/models.py:70 perms/models/application_permission.py:24 -#: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:59 terminal/models/storage.py:145 -#: tickets/models/comment.py:26 tickets/models/flow.py:57 -#: tickets/models/ticket/apply_application.py:18 -#: tickets/models/ticket/general.py:273 -#: xpack/plugins/change_auth_plan/models/app.py:28 -#: xpack/plugins/change_auth_plan/models/app.py:153 -msgid "Type" -msgstr "タイプ" - -#: applications/models/application.py:228 assets/models/asset.py:217 -#: assets/models/domain.py:29 assets/models/domain.py:64 -msgid "Domain" -msgstr "ドメイン" - -#: applications/models/application.py:230 xpack/plugins/cloud/models.py:32 -#: xpack/plugins/cloud/serializers/account.py:64 -msgid "Attrs" -msgstr "ツールバーの" - -#: applications/models/application.py:240 -msgid "Can match application" -msgstr "アプリケーションを一致させることができます" - -#: applications/models/application.py:310 -msgid "Application user" -msgstr "アプリケーションユーザー" - -#: applications/serializers/application.py:72 -#: applications/serializers/application.py:102 assets/serializers/label.py:13 -#: perms/serializers/application/permission.py:18 -msgid "Category display" -msgstr "カテゴリ表示" - -#: applications/serializers/application.py:73 -#: applications/serializers/application.py:104 -#: assets/serializers/cmd_filter.py:34 assets/serializers/system_user.py:34 -#: audits/serializers.py:29 authentication/serializers/connection_token.py:22 -#: perms/serializers/application/permission.py:19 -#: tickets/serializers/flow.py:49 tickets/serializers/ticket/ticket.py:18 -msgid "Type display" -msgstr "タイプ表示" - -#: applications/serializers/application.py:105 assets/models/asset.py:230 -#: assets/models/base.py:181 assets/models/cluster.py:26 -#: assets/models/cmd_filter.py:53 assets/models/domain.py:26 -#: assets/models/gathered_user.py:19 assets/models/group.py:22 -#: assets/models/label.py:25 assets/serializers/account.py:18 -#: assets/serializers/cmd_filter.py:28 assets/serializers/cmd_filter.py:48 -#: authentication/models.py:22 common/db/models.py:116 -#: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 -#: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92 -#: users/models/group.py:18 users/models/user.py:937 -#: xpack/plugins/change_auth_plan/models/base.py:45 -#: xpack/plugins/cloud/models.py:127 -msgid "Date created" -msgstr "作成された日付" - -#: applications/serializers/application.py:106 assets/models/base.py:182 -#: assets/models/cmd_filter.py:54 assets/models/gathered_user.py:20 -#: assets/serializers/account.py:21 assets/serializers/cmd_filter.py:29 -#: assets/serializers/cmd_filter.py:49 common/db/models.py:117 -#: common/mixins/models.py:51 ops/models/adhoc.py:40 orgs/models.py:224 -#: xpack/plugins/change_auth_plan/models/base.py:46 -msgid "Date updated" -msgstr "更新日" - -#: applications/serializers/application.py:123 -#: applications/serializers/application.py:163 authentication/models.py:99 -msgid "Application display" -msgstr "アプリケーション表示" - -#: applications/serializers/application.py:125 -msgid "account" -msgstr "アカウント" - -#: applications/serializers/attrs/application_category/cloud.py:8 -#: assets/models/cluster.py:40 -msgid "Cluster" -msgstr "クラスター" - -#: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:16 -#: settings/serializers/auth/sms.py:67 terminal/models/endpoint.py:15 -#: xpack/plugins/cloud/serializers/account_attrs.py:72 -msgid "Host" -msgstr "ホスト" - -#: applications/serializers/attrs/application_category/db.py:12 -#: applications/serializers/attrs/application_type/clickhouse.py:11 -#: applications/serializers/attrs/application_type/mongodb.py:10 -#: applications/serializers/attrs/application_type/mysql.py:10 -#: applications/serializers/attrs/application_type/mysql_workbench.py:22 -#: applications/serializers/attrs/application_type/oracle.py:10 -#: applications/serializers/attrs/application_type/pgsql.py:10 -#: applications/serializers/attrs/application_type/redis.py:10 -#: applications/serializers/attrs/application_type/sqlserver.py:10 -#: assets/models/asset.py:214 assets/models/domain.py:62 -#: 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_category/db.py:16 -#: settings/serializers/email.py:37 -msgid "Use SSL" -msgstr "SSLの使用" - -#: applications/serializers/attrs/application_category/db.py:18 -msgid "CA certificate" -msgstr "CA 証明書" - -#: applications/serializers/attrs/application_category/db.py:21 -msgid "Client certificate file" -msgstr "クライアント証明書" - -#: applications/serializers/attrs/application_category/db.py:24 -msgid "Certificate key file" -msgstr "証明書キー" - -#: applications/serializers/attrs/application_category/db.py:26 -msgid "Allow invalid cert" -msgstr "証明書チェックを無視" - -#: applications/serializers/attrs/application_category/remote_app.py:34 -msgid "Asset Info" -msgstr "資産情報" - -#: applications/serializers/attrs/application_category/remote_app.py:39 -#: applications/serializers/attrs/application_type/chrome.py:14 -#: applications/serializers/attrs/application_type/mysql_workbench.py:14 -#: applications/serializers/attrs/application_type/vmware_client.py:18 -msgid "Application path" -msgstr "アプリケーションパス" - -#: applications/serializers/attrs/application_category/remote_app.py:44 -#: assets/serializers/system_user.py:167 -#: tickets/serializers/ticket/apply_application.py:38 -#: tickets/serializers/ticket/common.py:59 -#: xpack/plugins/change_auth_plan/serializers/asset.py:67 -#: xpack/plugins/change_auth_plan/serializers/asset.py:70 -#: xpack/plugins/change_auth_plan/serializers/asset.py:73 -#: xpack/plugins/change_auth_plan/serializers/asset.py:104 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "このフィールドは必須です。" - -#: applications/serializers/attrs/application_type/chrome.py:18 -#: applications/serializers/attrs/application_type/vmware_client.py:22 -msgid "Target URL" -msgstr "ターゲットURL" - -#: applications/serializers/attrs/application_type/chrome.py:22 -msgid "Chrome username" -msgstr "Chromeユーザー名" - -#: applications/serializers/attrs/application_type/chrome.py:26 -#: applications/serializers/attrs/application_type/chrome.py:33 -msgid "Chrome password" -msgstr "Chromeパスワード" - -#: 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インタフェースとネイティブインタフェースは異な" -"るポートを使用する" - -#: applications/serializers/attrs/application_type/custom.py:12 -msgid "Operating parameter" -msgstr "操作パラメータ" - -#: applications/serializers/attrs/application_type/custom.py:16 -msgid "Target url" -msgstr "ターゲットURL" - -#: applications/serializers/attrs/application_type/custom.py:20 -msgid "Custom Username" -msgstr "カスタムユーザー名" - -#: applications/serializers/attrs/application_type/custom.py:25 -#: applications/serializers/attrs/application_type/custom.py:32 -#: xpack/plugins/change_auth_plan/models/base.py:27 -msgid "Custom password" -msgstr "カスタムパスワード" - -#: applications/serializers/attrs/application_type/mysql_workbench.py:30 -msgid "Mysql workbench username" -msgstr "Mysql workbench のユーザー名" - -#: applications/serializers/attrs/application_type/mysql_workbench.py:35 -#: applications/serializers/attrs/application_type/mysql_workbench.py:42 -msgid "Mysql workbench password" -msgstr "Mysql workbench パスワード" - -#: applications/serializers/attrs/application_type/vmware_client.py:26 -msgid "Vmware username" -msgstr "Vmware ユーザー名" - -#: applications/serializers/attrs/application_type/vmware_client.py:31 -#: applications/serializers/attrs/application_type/vmware_client.py:38 -msgid "Vmware password" -msgstr "Vmware パスワード" - -#: assets/api/domain.py:52 -msgid "Number required" -msgstr "必要な数" - -#: assets/api/node.py:61 -msgid "You can't update the root node name" -msgstr "ルートノード名を更新できません" - -#: assets/api/node.py:68 -msgid "You can't delete the root node ({})" -msgstr "ルートノード ({}) を削除できません。" - -#: assets/api/node.py:71 -msgid "Deletion failed and the node contains assets" -msgstr "削除に失敗し、ノードにアセットが含まれています。" - -#: assets/apps.py:9 -msgid "App assets" -msgstr "アプリ資産" - -#: assets/models/asset.py:139 -msgid "Base" -msgstr "ベース" - -#: assets/models/asset.py:140 -msgid "Charset" -msgstr "シャーセット" - -#: assets/models/asset.py:141 assets/serializers/asset.py:176 -#: tickets/models/ticket/general.py:298 -msgid "Meta" -msgstr "メタ" - -#: assets/models/asset.py:142 -msgid "Internal" -msgstr "内部" - -#: assets/models/asset.py:162 assets/models/asset.py:216 -#: assets/serializers/account.py:15 assets/serializers/asset.py:63 -#: perms/serializers/asset/user_permission.py:43 -#: xpack/plugins/cloud/serializers/account_attrs.py:179 -msgid "Platform" -msgstr "プラットフォーム" - -#: assets/models/asset.py:168 -msgid "Vendor" -msgstr "ベンダー" - -#: assets/models/asset.py:169 -msgid "Model" -msgstr "モデル" - -#: assets/models/asset.py:170 tickets/models/ticket/general.py:296 -msgid "Serial number" -msgstr "シリアル番号" - -#: assets/models/asset.py:172 -msgid "CPU model" -msgstr "CPU モデル" - -#: assets/models/asset.py:173 -msgid "CPU count" -msgstr "CPU カウント" - -#: assets/models/asset.py:174 -msgid "CPU cores" -msgstr "CPU カラー" - -#: assets/models/asset.py:175 -msgid "CPU vcpus" -msgstr "CPU 合計" - -#: assets/models/asset.py:176 -msgid "Memory" -msgstr "メモリ" - -#: assets/models/asset.py:177 -msgid "Disk total" -msgstr "ディスクの合計" - -#: assets/models/asset.py:178 -msgid "Disk info" -msgstr "ディスク情報" - -#: assets/models/asset.py:180 -msgid "OS" -msgstr "OS" - -#: assets/models/asset.py:181 -msgid "OS version" -msgstr "システムバージョン" - -#: assets/models/asset.py:182 -msgid "OS arch" -msgstr "システムアーキテクチャ" - -#: assets/models/asset.py:183 -msgid "Hostname raw" -msgstr "ホスト名生" - -#: assets/models/asset.py:215 assets/serializers/account.py:16 -#: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41 -#: xpack/plugins/cloud/models.py:106 xpack/plugins/cloud/serializers/task.py:44 -msgid "Protocols" -msgstr "プロトコル" - -#: assets/models/asset.py:218 assets/models/cmd_filter.py:38 -#: assets/models/user.py:243 perms/models/asset_permission.py:24 -#: xpack/plugins/change_auth_plan/models/asset.py:43 -#: xpack/plugins/gathered_user/models.py:24 -msgid "Nodes" -msgstr "ノード" - -#: assets/models/asset.py:219 assets/models/cmd_filter.py:51 -#: assets/models/domain.py:66 assets/models/label.py:22 -#: users/serializers/user.py:147 -msgid "Is active" -msgstr "アクティブです。" - -#: assets/models/asset.py:222 assets/models/cluster.py:19 -#: assets/models/user.py:240 assets/models/user.py:395 -msgid "Admin user" -msgstr "管理ユーザー" - -#: assets/models/asset.py:225 xpack/plugins/cloud/const.py:32 -msgid "Public IP" -msgstr "パブリックIP" - -#: assets/models/asset.py:226 -msgid "Asset number" -msgstr "資産番号" - -#: assets/models/asset.py:228 -msgid "Labels" -msgstr "ラベル" - -#: assets/models/asset.py:229 assets/models/base.py:183 -#: assets/models/cluster.py:28 assets/models/cmd_filter.py:56 -#: assets/models/cmd_filter.py:103 assets/models/group.py:21 -#: common/db/models.py:114 common/mixins/models.py:49 orgs/models.py:71 -#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:720 -#: users/serializers/group.py:33 -#: xpack/plugins/change_auth_plan/models/base.py:48 -#: xpack/plugins/cloud/models.py:124 xpack/plugins/gathered_user/models.py:30 -msgid "Created by" -msgstr "によって作成された" - -#: assets/models/asset.py:389 -msgid "Can refresh asset hardware info" -msgstr "資産ハードウェア情報を更新できます" - -#: assets/models/asset.py:390 -msgid "Can test asset connectivity" -msgstr "資産接続をテストできます" - -#: assets/models/asset.py:391 -msgid "Can push system user to asset" -msgstr "システムユーザーを資産にプッシュできます" - -#: assets/models/asset.py:392 -msgid "Can match asset" -msgstr "アセットを一致させることができます" - -#: assets/models/asset.py:393 -msgid "Add asset to node" -msgstr "ノードにアセットを追加する" - -#: assets/models/asset.py:394 -msgid "Move asset to node" -msgstr "アセットをノードに移動する" - -#: assets/models/authbook.py:27 -msgid "AuthBook" -msgstr "資産アカウント" - -#: assets/models/authbook.py:30 -msgid "Can test asset account connectivity" -msgstr "アセットアカウントの接続性をテストできます" - -#: assets/models/authbook.py:31 -msgid "Can view asset account secret" -msgstr "資産アカウントの秘密を表示できます" - -#: assets/models/authbook.py:32 -msgid "Can change asset account secret" -msgstr "資産口座の秘密を変更できます" - -#: assets/models/authbook.py:33 -msgid "Can view asset history account" -msgstr "資産履歴アカウントを表示できます" - -#: assets/models/authbook.py:34 -msgid "Can view asset history account secret" -msgstr "資産履歴アカウントパスワードを表示できます" - -#: assets/models/backup.py:30 perms/models/base.py:54 -#: settings/serializers/terminal.py:14 -msgid "All" -msgstr "すべて" - -#: assets/models/backup.py:52 assets/serializers/backup.py:32 -#: xpack/plugins/change_auth_plan/models/app.py:41 -#: xpack/plugins/change_auth_plan/models/asset.py:62 -#: xpack/plugins/change_auth_plan/serializers/base.py:45 -msgid "Recipient" -msgstr "受信者" - -#: assets/models/backup.py:62 assets/models/backup.py:124 -msgid "Account backup plan" -msgstr "アカウントバックアップ計画" - -#: assets/models/backup.py:100 -#: xpack/plugins/change_auth_plan/models/base.py:107 -msgid "Manual trigger" -msgstr "手動トリガー" - -#: assets/models/backup.py:101 -#: xpack/plugins/change_auth_plan/models/base.py:108 -msgid "Timing trigger" -msgstr "タイミングトリガー" - -#: assets/models/backup.py:105 audits/models.py:45 ops/models/command.py:31 -#: perms/models/base.py:89 terminal/models/session.py:59 -#: tickets/models/ticket/apply_application.py:29 -#: tickets/models/ticket/apply_asset.py:23 -#: xpack/plugins/change_auth_plan/models/base.py:112 -#: xpack/plugins/change_auth_plan/models/base.py:203 -#: xpack/plugins/gathered_user/models.py:76 -msgid "Date start" -msgstr "開始日" - -#: assets/models/backup.py:108 -#: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:187 ops/models/adhoc.py:258 -#: xpack/plugins/change_auth_plan/models/base.py:115 -#: xpack/plugins/change_auth_plan/models/base.py:204 -#: xpack/plugins/gathered_user/models.py:79 -msgid "Time" -msgstr "時間" - -#: assets/models/backup.py:112 -msgid "Account backup snapshot" -msgstr "アカウントのバックアップスナップショット" - -#: assets/models/backup.py:116 assets/serializers/backup.py:40 -#: xpack/plugins/change_auth_plan/models/base.py:125 -#: xpack/plugins/change_auth_plan/serializers/base.py:78 -msgid "Trigger mode" -msgstr "トリガーモード" - -#: assets/models/backup.py:119 audits/models.py:145 -#: terminal/models/sharing.py:108 -#: xpack/plugins/change_auth_plan/models/base.py:201 -#: xpack/plugins/change_auth_plan/serializers/app.py:66 -#: xpack/plugins/change_auth_plan/serializers/asset.py:180 -#: xpack/plugins/cloud/models.py:181 -msgid "Reason" -msgstr "理由" - -#: assets/models/backup.py:121 audits/serializers.py:88 -#: audits/serializers.py:103 ops/models/adhoc.py:260 -#: terminal/serializers/session.py:36 -#: xpack/plugins/change_auth_plan/models/base.py:202 -#: xpack/plugins/change_auth_plan/serializers/app.py:67 -#: xpack/plugins/change_auth_plan/serializers/asset.py:182 -msgid "Is success" -msgstr "成功は" - -#: assets/models/backup.py:128 -msgid "Account backup execution" -msgstr "アカウントバックアップの実行" - -#: assets/models/base.py:30 assets/tasks/const.py:51 audits/const.py:5 -#: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 -#: common/utils/ip/utils.py:84 -msgid "Unknown" -msgstr "不明" - -#: assets/models/base.py:31 -msgid "Ok" -msgstr "OK" - -#: assets/models/base.py:32 audits/models.py:136 -#: xpack/plugins/change_auth_plan/serializers/app.py:88 -#: xpack/plugins/change_auth_plan/serializers/asset.py:199 -#: xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失敗しました" - -#: assets/models/base.py:38 assets/serializers/domain.py:47 -msgid "Connectivity" -msgstr "接続性" - -#: assets/models/base.py:40 authentication/models.py:263 -msgid "Date verified" -msgstr "確認済みの日付" - -#: assets/models/base.py:177 assets/serializers/base.py:15 -#: assets/serializers/base.py:37 assets/serializers/system_user.py:29 -#: audits/signal_handlers.py:58 authentication/confirm/password.py:9 -#: authentication/forms.py:32 -#: authentication/templates/authentication/login.html:228 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:94 -#: users/templates/users/_msg_user_created.html:13 -#: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/change_auth_plan/models/base.py:42 -#: xpack/plugins/change_auth_plan/models/base.py:121 -#: xpack/plugins/change_auth_plan/models/base.py:196 -#: xpack/plugins/change_auth_plan/serializers/base.py:21 -#: xpack/plugins/change_auth_plan/serializers/base.py:73 -#: xpack/plugins/cloud/serializers/account_attrs.py:28 -msgid "Password" -msgstr "パスワード" - -#: assets/models/base.py:178 assets/serializers/base.py:41 -#: xpack/plugins/change_auth_plan/models/asset.py:53 -#: xpack/plugins/change_auth_plan/models/asset.py:130 -#: xpack/plugins/change_auth_plan/models/asset.py:206 -msgid "SSH private key" -msgstr "SSH秘密鍵" - -#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models/asset.py:56 -#: xpack/plugins/change_auth_plan/models/asset.py:126 -#: xpack/plugins/change_auth_plan/models/asset.py:202 -msgid "SSH public key" -msgstr "SSHパブリックキー" - -#: assets/models/cluster.py:20 -msgid "Bandwidth" -msgstr "帯域幅" - -#: assets/models/cluster.py:21 -msgid "Contact" -msgstr "連絡先" - -#: assets/models/cluster.py:22 users/models/user.py:693 -msgid "Phone" -msgstr "電話" - -#: assets/models/cluster.py:23 -msgid "Address" -msgstr "アドレス" - -#: assets/models/cluster.py:24 -msgid "Intranet" -msgstr "イントラネット" - -#: assets/models/cluster.py:25 -msgid "Extranet" -msgstr "エクストラネット" - -#: assets/models/cluster.py:27 -msgid "Operator" -msgstr "オペレーター" - -#: assets/models/cluster.py:36 assets/models/group.py:34 -#: xpack/plugins/cloud/providers/nutanix.py:30 -msgid "Default" -msgstr "デフォルト" - -#: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6 -#: users/models/user.py:922 -msgid "System" -msgstr "システム" - -#: assets/models/cluster.py:36 -msgid "Default Cluster" -msgstr "デフォルトクラスター" - -#: assets/models/cmd_filter.py:34 perms/models/base.py:86 -#: users/models/group.py:31 users/models/user.py:679 -msgid "User group" -msgstr "ユーザーグループ" - -#: assets/models/cmd_filter.py:64 assets/serializers/system_user.py:59 -msgid "Command filter" -msgstr "コマンドフィルター" - -#: assets/models/cmd_filter.py:71 -msgid "Regex" -msgstr "正規情報" - -#: assets/models/cmd_filter.py:72 ops/models/command.py:26 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:56 -#: terminal/templates/terminal/_msg_command_alert.html:12 -#: terminal/templates/terminal/_msg_command_execute_alert.html:10 -msgid "Command" -msgstr "コマンド" - -#: assets/models/cmd_filter.py:78 -msgid "Deny" -msgstr "拒否" - -#: assets/models/cmd_filter.py:80 -msgid "Reconfirm" -msgstr "再確認" - -#: assets/models/cmd_filter.py:84 -msgid "Filter" -msgstr "フィルター" - -#: assets/models/cmd_filter.py:91 settings/serializers/basic.py:10 -#: xpack/plugins/license/models.py:29 -msgid "Content" -msgstr "コンテンツ" - -#: assets/models/cmd_filter.py:91 -msgid "One line one command" -msgstr "1行1コマンド" - -#: assets/models/cmd_filter.py:92 -msgid "Ignore case" -msgstr "家を無視する" - -#: assets/models/cmd_filter.py:107 -msgid "Command filter rule" -msgstr "コマンドフィルタルール" - -#: assets/models/cmd_filter.py:151 -msgid "The generated regular expression is incorrect: {}" -msgstr "生成された正規表現が正しくありません: {}" - -#: assets/models/cmd_filter.py:177 tickets/const.py:13 -msgid "Command confirm" -msgstr "コマンドの確認" - -#: assets/models/domain.py:73 -msgid "Gateway" -msgstr "ゲートウェイ" - -#: assets/models/domain.py:75 -msgid "Test gateway" -msgstr "テストゲートウェイ" - -#: assets/models/domain.py:131 -#, python-brace-format -msgid "Unable to connect to port {port} on {ip}" -msgstr "{ip} でポート {port} に接続できません" - -#: assets/models/domain.py:134 authentication/middleware.py:75 -#: xpack/plugins/cloud/providers/fc.py:48 -msgid "Authentication failed" -msgstr "認証に失敗しました" - -#: assets/models/domain.py:136 assets/models/domain.py:158 -msgid "Connect failed" -msgstr "接続に失敗しました" - -#: assets/models/gathered_user.py:16 -msgid "Present" -msgstr "プレゼント" - -#: assets/models/gathered_user.py:17 -msgid "Date last login" -msgstr "最終ログイン日" - -#: assets/models/gathered_user.py:18 -msgid "IP last login" -msgstr "IP最終ログイン" - -#: assets/models/gathered_user.py:31 -msgid "GatherUser" -msgstr "収集ユーザー" - -#: assets/models/group.py:30 -msgid "Asset group" -msgstr "資産グループ" - -#: assets/models/group.py:34 -msgid "Default asset group" -msgstr "デフォルトアセットグループ" - -#: assets/models/label.py:19 assets/models/node.py:553 settings/models.py:34 -msgid "Value" -msgstr "値" - -#: assets/models/label.py:40 settings/serializers/sms.py:7 -msgid "Label" -msgstr "ラベル" - -#: assets/models/node.py:158 -msgid "New node" -msgstr "新しいノード" - -#: assets/models/node.py:481 -msgid "empty" -msgstr "空" - -#: assets/models/node.py:552 perms/models/asset_permission.py:101 -msgid "Key" -msgstr "キー" - -#: assets/models/node.py:554 assets/serializers/node.py:20 -msgid "Full value" -msgstr "フルバリュー" - -#: assets/models/node.py:557 perms/models/asset_permission.py:102 -msgid "Parent key" -msgstr "親キー" - -#: assets/models/node.py:566 assets/serializers/system_user.py:267 -#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers/task.py:75 -msgid "Node" -msgstr "ノード" - -#: assets/models/node.py:569 -msgid "Can match node" -msgstr "ノードを一致させることができます" - -#: assets/models/user.py:234 -msgid "Automatic managed" -msgstr "自動管理" - -#: assets/models/user.py:235 -msgid "Manually input" -msgstr "手動入力" - -#: assets/models/user.py:239 -msgid "Common user" -msgstr "共通ユーザー" - -#: assets/models/user.py:242 -msgid "Username same with user" -msgstr "ユーザーと同じユーザー名" - -#: assets/models/user.py:245 assets/serializers/domain.py:30 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -#: xpack/plugins/change_auth_plan/models/asset.py:39 -msgid "Assets" -msgstr "資産" - -#: assets/models/user.py:249 users/apps.py:9 -msgid "Users" -msgstr "ユーザー" - -#: assets/models/user.py:250 -msgid "User groups" -msgstr "ユーザーグループ" - -#: assets/models/user.py:254 -msgid "Auto push" -msgstr "オートプッシュ" - -#: assets/models/user.py:255 -msgid "Sudo" -msgstr "すど" - -#: assets/models/user.py:256 -msgid "Shell" -msgstr "シェル" - -#: assets/models/user.py:257 -msgid "Login mode" -msgstr "ログインモード" - -#: assets/models/user.py:258 -msgid "SFTP Root" -msgstr "SFTPルート" - -#: assets/models/user.py:259 assets/serializers/system_user.py:37 -#: authentication/models.py:52 -msgid "Token" -msgstr "トークン" - -#: assets/models/user.py:260 -msgid "Home" -msgstr "ホーム" - -#: assets/models/user.py:261 -msgid "System groups" -msgstr "システムグループ" - -#: assets/models/user.py:264 -msgid "User switch" -msgstr "ユーザースイッチ" - -#: assets/models/user.py:265 -msgid "Switch from" -msgstr "から切り替え" - -#: assets/models/user.py:345 -msgid "Can match system user" -msgstr "システムユーザーに一致できます" - -#: assets/models/utils.py:35 -#, python-format -msgid "%(value)s is not an even number" -msgstr "%(value)s は偶数ではありません" - -#: 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:19 -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/serializers/account.py:36 assets/serializers/account.py:83 -#: assets/serializers/account_history.py:10 authentication/models.py:87 -msgid "System user display" -msgstr "システムユーザー表示" - -#: assets/serializers/asset.py:20 -msgid "Protocol format should {}/{}" -msgstr "プロトコル形式は {}/{}" - -#: assets/serializers/asset.py:37 -msgid "Protocol duplicate: {}" -msgstr "プロトコル重複: {}" - -#: assets/serializers/asset.py:66 -msgid "Domain name" -msgstr "ドメイン名" - -#: assets/serializers/asset.py:68 -msgid "Nodes name" -msgstr "ノード名" - -#: assets/serializers/asset.py:71 -msgid "Labels name" -msgstr "ラベル名" - -#: assets/serializers/asset.py:105 -msgid "Hardware info" -msgstr "ハードウェア情報" - -#: assets/serializers/asset.py:106 -msgid "Admin user display" -msgstr "管理者ユーザー表示" - -#: assets/serializers/asset.py:107 -msgid "CPU info" -msgstr "CPU情報" - -#: assets/serializers/backup.py:20 perms/models/base.py:87 -#: perms/serializers/application/permission.py:17 -#: perms/serializers/application/permission.py:42 -#: perms/serializers/asset/permission.py:18 -#: perms/serializers/asset/permission.py:46 -#: tickets/models/ticket/apply_application.py:27 -#: tickets/models/ticket/apply_asset.py:21 -msgid "Actions" -msgstr "アクション" - -#: assets/serializers/backup.py:31 ops/mixin.py:26 ops/mixin.py:106 -#: ops/mixin.py:147 settings/serializers/auth/ldap.py:66 -#: xpack/plugins/change_auth_plan/serializers/base.py:43 -msgid "Periodic perform" -msgstr "定期的なパフォーマンス" - -#: assets/serializers/backup.py:33 -#: xpack/plugins/change_auth_plan/serializers/base.py:46 -msgid "Currently only mail sending is supported" -msgstr "現在、メール送信のみがサポートされています" - -#: assets/serializers/base.py:16 users/models/user.py:703 -msgid "Private key" -msgstr "ssh秘密鍵" - -#: assets/serializers/base.py:45 -msgid "Key password" -msgstr "キーパスワード" - -#: assets/serializers/base.py:58 -msgid "private key invalid or passphrase error" -msgstr "秘密鍵が無効またはpassphraseエラー" - -#: assets/serializers/cmd_filter.py:35 assets/serializers/cmd_filter.py:50 -msgid "Action display" -msgstr "アクション表示" - -#: assets/serializers/cmd_filter.py:51 ops/models/adhoc.py:155 -msgid "Pattern" -msgstr "パターン" - -#: assets/serializers/domain.py:14 assets/serializers/label.py:12 -#: assets/serializers/system_user.py:63 -#: perms/serializers/asset/permission.py:49 -msgid "Assets amount" -msgstr "資産額" - -#: assets/serializers/domain.py:15 -msgid "Applications amount" -msgstr "申し込み金額" - -#: assets/serializers/domain.py:16 -msgid "Gateways count" -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/system_user.py:35 -msgid "SSH key fingerprint" -msgstr "SSHキー指紋" - -#: assets/serializers/system_user.py:40 -#: perms/serializers/application/permission.py:46 -msgid "Apps amount" -msgstr "アプリの量" - -#: assets/serializers/system_user.py:62 -#: perms/serializers/asset/permission.py:50 -msgid "Nodes amount" -msgstr "ノード量" - -#: assets/serializers/system_user.py:64 assets/serializers/system_user.py:269 -msgid "Login mode display" -msgstr "ログインモード表示" - -#: assets/serializers/system_user.py:66 -msgid "Ad domain" -msgstr "広告ドメイン" - -#: assets/serializers/system_user.py:67 -msgid "Is asset protocol" -msgstr "資産プロトコルです" - -#: assets/serializers/system_user.py:68 -msgid "Only ssh and automatic login system users are supported" -msgstr "sshと自動ログインシステムのユーザーのみがサポートされています" - -#: assets/serializers/system_user.py:108 -msgid "Username same with user with protocol {} only allow 1" -msgstr "プロトコル {} のユーザーと同じユーザー名は1のみ許可します" - -#: assets/serializers/system_user.py:121 common/validators.py:14 -msgid "Special char not allowed" -msgstr "特別なcharは許可されていません" - -#: assets/serializers/system_user.py:131 -msgid "* Automatic login mode must fill in the username." -msgstr "* 自動ログインモードはユーザー名を入力する必要があります。" - -#: assets/serializers/system_user.py:146 -msgid "Path should starts with /" -msgstr "パスは/で始まる必要があります" - -#: assets/serializers/system_user.py:158 -msgid "Password or private key required" -msgstr "パスワードまたは秘密鍵が必要" - -#: assets/serializers/system_user.py:172 -msgid "Only ssh protocol system users are allowed" -msgstr "Sshプロトコルシステムユーザーのみが許可されています" - -#: assets/serializers/system_user.py:176 -msgid "The protocol must be consistent with the current user: {}" -msgstr "プロトコルは現在のユーザーと一致している必要があります: {}" - -#: assets/serializers/system_user.py:180 -msgid "Only system users with automatic login are allowed" -msgstr "自動ログインを持つシステムユーザーのみが許可されます" - -#: assets/serializers/system_user.py:288 -msgid "System user name" -msgstr "システムユーザー名" - -#: assets/serializers/system_user.py:289 orgs/mixins/serializers.py:26 -#: rbac/serializers/rolebinding.py:23 -msgid "Org name" -msgstr "組織名" - -#: assets/serializers/system_user.py:298 -msgid "Asset hostname" -msgstr "資産ホスト名" - -#: assets/serializers/utils.py:11 -msgid "Password can not contains `{{` " -msgstr "パスワードには '{{' を含まない" - -#: assets/serializers/utils.py:14 -msgid "Password can not contains `'` " -msgstr "パスワードには `'` を含まない" - -#: assets/serializers/utils.py:16 -msgid "Password can not contains `\"` " -msgstr "パスワードには `\"` を含まない" - -#: assets/tasks/account_connectivity.py:30 -msgid "The asset {} system platform {} does not support run Ansible tasks" -msgstr "" -"資産 {} システムプラットフォーム {} はAnsibleタスクの実行をサポートしていませ" -"ん。" - -#: assets/tasks/account_connectivity.py:107 -msgid "Test account connectivity: " -msgstr "テストアカウント接続:" - -#: assets/tasks/asset_connectivity.py:49 -msgid "Test assets connectivity. " -msgstr "資産の接続性をテストします。" - -#: assets/tasks/asset_connectivity.py:91 assets/tasks/asset_connectivity.py:102 -msgid "Test assets connectivity: " -msgstr "資産の接続性のテスト:" - -#: assets/tasks/asset_connectivity.py:113 -msgid "Test if the assets under the node are connectable: " -msgstr "ノードの下のアセットが接続可能かどうかをテストします。" - -#: assets/tasks/const.py:49 -msgid "Unreachable" -msgstr "達成できない" - -#: assets/tasks/const.py:50 -msgid "Reachable" -msgstr "接続可能" - -#: assets/tasks/gather_asset_hardware_info.py:46 -msgid "Get asset info failed: {}" -msgstr "資産情報の取得に失敗しました: {}" - -#: assets/tasks/gather_asset_hardware_info.py:97 -msgid "Update some assets hardware info. " -msgstr "一部の資産ハードウェア情報を更新します。" - -#: assets/tasks/gather_asset_hardware_info.py:114 -msgid "Update asset hardware info: " -msgstr "資産ハードウェア情報の更新:" - -#: assets/tasks/gather_asset_hardware_info.py:120 -msgid "Update assets hardware info: " -msgstr "資産のハードウェア情報を更新する:" - -#: assets/tasks/gather_asset_hardware_info.py:137 -msgid "Update node asset hardware information: " -msgstr "ノード資産のハードウェア情報を更新します。" - -#: assets/tasks/gather_asset_users.py:111 -msgid "Gather assets users" -msgstr "資産ユーザーの収集" - -#: assets/tasks/nodes_amount.py:27 -msgid "" -"The task of self-checking is already running and cannot be started repeatedly" -msgstr "" -"セルフチェックのタスクはすでに実行されており、繰り返し開始することはできませ" -"ん" - -#: assets/tasks/push_system_user.py:201 -msgid "System user is dynamic: {}" -msgstr "システムユーザーは動的です: {}" - -#: assets/tasks/push_system_user.py:242 -msgid "Start push system user for platform: [{}]" -msgstr "プラットフォームのプッシュシステムユーザーを開始: [{}]" - -#: assets/tasks/push_system_user.py:243 -#: assets/tasks/system_user_connectivity.py:106 -msgid "Hosts count: {}" -msgstr "ホスト数: {}" - -#: assets/tasks/push_system_user.py:264 assets/tasks/push_system_user.py:297 -msgid "Push system users to assets: " -msgstr "システムユーザーを資産にプッシュする:" - -#: assets/tasks/push_system_user.py:276 -msgid "Push system users to asset: " -msgstr "システムユーザーをアセットにプッシュする:" - -#: assets/tasks/system_user_connectivity.py:56 -msgid "Dynamic system user not support test" -msgstr "動的システムユーザーがテストをサポートしていない" - -#: assets/tasks/system_user_connectivity.py:105 -msgid "Start test system user connectivity for platform: [{}]" -msgstr "プラットフォームのテストシステムのユーザー接続を開始: [{}]" - -#: assets/tasks/system_user_connectivity.py:118 -#: assets/tasks/system_user_connectivity.py:129 -msgid "Test system user connectivity: " -msgstr "テストシステムユーザー接続:" - -#: assets/tasks/system_user_connectivity.py:148 -msgid "Test system user connectivity period: " -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 "資産はサポートできない場合があります。スキップ: {}" - -#: 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 "一致する資産がない、タスクを停止" - -#: 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/handler.py:134 -msgid "Yes" -msgstr "是" - -#: audits/handler.py:134 -msgid "No" -msgstr "否" - -#: audits/models.py:28 audits/models.py:60 -#: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:226 -msgid "Delete" -msgstr "削除" - -#: audits/models.py:29 -msgid "Upload" -msgstr "アップロード" - -#: audits/models.py:30 -msgid "Download" -msgstr "ダウンロード" - -#: audits/models.py:31 -msgid "Rmdir" -msgstr "Rmdir" - -#: audits/models.py:32 -msgid "Rename" -msgstr "名前の変更" - -#: audits/models.py:33 -msgid "Mkdir" -msgstr "Mkdir" - -#: audits/models.py:34 -msgid "Symlink" -msgstr "Symlink" - -#: audits/models.py:39 audits/models.py:67 audits/models.py:107 -#: terminal/models/session.py:52 terminal/models/sharing.py:96 -msgid "Remote addr" -msgstr "リモートaddr" - -#: audits/models.py:42 -msgid "Operate" -msgstr "操作" - -#: audits/models.py:43 -msgid "Filename" -msgstr "ファイル名" - -#: audits/models.py:44 audits/models.py:135 terminal/models/sharing.py:104 -#: tickets/views/approve.py:115 -#: xpack/plugins/change_auth_plan/serializers/app.py:87 -#: xpack/plugins/change_auth_plan/serializers/asset.py:198 -msgid "Success" -msgstr "成功" - -#: audits/models.py:48 -msgid "File transfer log" -msgstr "ファイル転送ログ" - -#: audits/models.py:57 -#: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:223 -msgid "Create" -msgstr "作成" - -#: audits/models.py:58 rbac/tree.py:224 -msgid "View" -msgstr "表示" - -#: audits/models.py:59 rbac/tree.py:225 templates/_csv_import_export.html:18 -#: templates/_csv_update_modal.html:6 -msgid "Update" -msgstr "更新" - -#: audits/models.py:65 audits/serializers.py:69 -msgid "Resource Type" -msgstr "リソースタイプ" - -#: audits/models.py:66 -msgid "Resource" -msgstr "リソース" - -#: audits/models.py:68 audits/models.py:108 -#: terminal/backends/command/serializers.py:40 -msgid "Datetime" -msgstr "時間" - -#: audits/models.py:100 -msgid "Operate log" -msgstr "ログの操作" - -#: audits/models.py:106 -msgid "Change by" -msgstr "による変更" - -#: audits/models.py:114 -msgid "Password change log" -msgstr "パスワード変更ログ" - -#: audits/models.py:129 -msgid "Disabled" -msgstr "無効" - -#: audits/models.py:130 settings/models.py:37 -msgid "Enabled" -msgstr "有効化" - -#: audits/models.py:131 -msgid "-" -msgstr "-" - -#: audits/models.py:140 -msgid "Login type" -msgstr "ログインタイプ" - -#: audits/models.py:141 tickets/models/ticket/login_confirm.py:10 -msgid "Login ip" -msgstr "ログインIP" - -#: audits/models.py:142 -#: authentication/templates/authentication/_msg_different_city.html:11 -#: tickets/models/ticket/login_confirm.py:11 -msgid "Login city" -msgstr "ログイン都市" - -#: audits/models.py:143 audits/serializers.py:44 -msgid "User agent" -msgstr "ユーザーエージェント" - -#: audits/models.py:144 -#: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:696 -#: users/serializers/profile.py:126 -msgid "MFA" -msgstr "MFA" - -#: audits/models.py:146 terminal/models/status.py:33 -#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:177 -#: xpack/plugins/cloud/models.py:229 -msgid "Status" -msgstr "ステータス" - -#: audits/models.py:147 -msgid "Date login" -msgstr "日付ログイン" - -#: audits/models.py:148 audits/serializers.py:46 -msgid "Authentication backend" -msgstr "認証バックエンド" - -#: audits/models.py:187 -msgid "User login log" -msgstr "ユーザーログインログ" - -#: audits/serializers.py:14 -msgid "Operate display" -msgstr "ディスプレイを操作する" - -#: audits/serializers.py:30 tickets/serializers/ticket/ticket.py:19 -msgid "Status display" -msgstr "ステータス表示" - -#: audits/serializers.py:31 -msgid "MFA display" -msgstr "MFAディスプレイ" - -#: audits/serializers.py:45 -msgid "Reason display" -msgstr "理由表示" - -#: audits/serializers.py:90 -msgid "Hosts display" -msgstr "ホスト表示" - -#: audits/serializers.py:102 ops/models/command.py:27 -#: xpack/plugins/cloud/models.py:175 -msgid "Result" -msgstr "結果" - -#: audits/serializers.py:104 terminal/serializers/storage.py:157 -msgid "Hosts" -msgstr "ホスト" - -#: audits/serializers.py:105 -msgid "Run as" -msgstr "として実行" - -#: audits/serializers.py:107 -msgid "Run as display" -msgstr "ディスプレイとして実行する" - -#: audits/serializers.py:108 authentication/models.py:81 -#: rbac/serializers/rolebinding.py:21 -msgid "User display" -msgstr "ユーザー表示" - -#: audits/signal_handlers.py:57 -msgid "SSH Key" -msgstr "SSHキー" - -#: audits/signal_handlers.py:59 settings/serializers/auth/sso.py:10 -msgid "SSO" -msgstr "SSO" - -#: audits/signal_handlers.py:60 -msgid "Auth Token" -msgstr "認証トークン" - -#: audits/signal_handlers.py:61 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:734 -msgid "WeCom" -msgstr "企業微信" - -#: audits/signal_handlers.py:62 authentication/views/feishu.py:144 -#: authentication/views/login.py:85 notifications/backends/__init__.py:14 -#: settings/serializers/auth/feishu.py:10 users/models/user.py:736 -msgid "FeiShu" -msgstr "本を飛ばす" - -#: audits/signal_handlers.py:63 authentication/views/dingtalk.py:179 -#: authentication/views/login.py:79 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:735 -msgid "DingTalk" -msgstr "DingTalk" - -#: audits/signal_handlers.py:64 authentication/models.py:267 -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.py:224 -#: 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:256 -#: 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 authentication/models.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 "署名ヘッダーが無効です。AccessKeyIdのような形式: Signature" - -#: 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: Date not provide or not" - -#: 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 "Authバックエンドが一致しない" - -#: 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 ip 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 "SMSコードを入力してください" - -#: 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 authは閉鎖されました" - -#: 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:242 -#: authentication/views/dingtalk.py:296 -msgid "DingTalk is not bound" -msgstr "DingTalkはバインドされていません" - -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 -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 "OTPコードが無効、またはサーバー時間エラー" - -#: authentication/mfa/otp.py:12 -msgid "OTP" -msgstr "OTP" - -#: authentication/mfa/otp.py:13 -msgid "OTP verification code" -msgstr "OTP検証コード" - -#: authentication/mfa/otp.py:48 -msgid "Virtual OTP based MFA" -msgstr "仮想OTPベースのMFA" - -#: authentication/mfa/radius.py:7 -msgid "Radius verify code invalid" -msgstr "Radius verifyコードが無効" - -#: authentication/mfa/radius.py:13 -msgid "Radius verification code" -msgstr "半径確認コード" - -#: authentication/mfa/radius.py:44 -msgid "Radius global enabled, cannot disable" -msgstr "Radius globalが有効になり、無効にできません" - -#: 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 "SMS確認コード" - -#: 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:76 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.py:37 -msgid "Access key" -msgstr "アクセスキー" - -#: authentication/models.py:44 -msgid "Private Token" -msgstr "プライベートトークン" - -#: authentication/models.py:53 -msgid "Expired" -msgstr "期限切れ" - -#: authentication/models.py:57 -msgid "SSO token" -msgstr "SSO token" - -#: authentication/models.py:72 authentication/models.py:261 -#: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:19 -msgid "Secret" -msgstr "ひみつ" - -#: authentication/models.py:74 authentication/models.py:264 -#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:717 -msgid "Date expired" -msgstr "期限切れの日付" - -#: authentication/models.py:93 -msgid "Asset display" -msgstr "アセット名" - -#: authentication/models.py:104 -msgid "Connection token" -msgstr "接続トークン" - -#: authentication/models.py:106 -msgid "Can view connection token secret" -msgstr "接続トークンの秘密を表示できます" - -#: authentication/models.py:149 -msgid "Connection token expired at: {}" -msgstr "接続トークンの有効期限: {}" - -#: authentication/models.py:154 -msgid "User not exists" -msgstr "ユーザーは存在しません" - -#: authentication/models.py:163 -msgid "System user not exists" -msgstr "システムユーザーが存在しません" - -#: authentication/models.py:169 -msgid "Asset not exists" -msgstr "アセットが存在しません" - -#: authentication/models.py:173 -msgid "Asset inactive" -msgstr "アセットがアクティブ化されていません" - -#: authentication/models.py:180 -msgid "User has no permission to access asset or permission expired" -msgstr "" -"ユーザーがアセットにアクセスする権限を持っていないか、権限の有効期限が切れて" -"います" - -#: authentication/models.py:188 -msgid "Application not exists" -msgstr "アプリが存在しません" - -#: authentication/models.py:195 -msgid "User has no permission to access application or permission expired" -msgstr "" -"ユーザーがアプリにアクセスする権限を持っていないか、権限の有効期限が切れてい" -"ます" - -#: authentication/models.py:262 -msgid "Verified" -msgstr "確認済み" - -#: authentication/models.py:283 -msgid "Super connection token" -msgstr "スーパー接続トークン" - -#: authentication/notifications.py:19 -msgid "Different city login reminder" -msgstr "異なる都市ログインのリマインダー" - -#: authentication/notifications.py:52 -msgid "binding reminder" -msgstr "バインディングリマインダー" - -#: authentication/serializers/connection_token.py:23 -#: xpack/plugins/cloud/models.py:33 -msgid "Validity" -msgstr "有効性" - -#: authentication/serializers/connection_token.py:24 -msgid "Expired time" -msgstr "期限切れ時間" - -#: authentication/serializers/connection_token.py:73 -msgid "Asset or application required" -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:675 -#: 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/application/permission.py:20 -#: perms/serializers/application/permission.py:41 -#: perms/serializers/asset/permission.py:19 -#: perms/serializers/asset/permission.py:45 users/serializers/user.py:148 -msgid "Is valid" -msgstr "有効です" - -#: authentication/templates/authentication/_access_key_modal.html:6 -msgid "API key list" -msgstr "APIキーリスト" - -#: authentication/templates/authentication/_access_key_modal.html:18 -msgid "Using api key sign api header, every requests header difference" -msgstr "APIキー記号APIヘッダーを使用すると、すべてのリクエストヘッダーの違い" - -#: authentication/templates/authentication/_access_key_modal.html:19 -msgid "docs" -msgstr "ドキュメント" - -#: authentication/templates/authentication/_access_key_modal.html:30 -#: users/serializers/group.py:35 -msgid "ID" -msgstr "ID" - -#: 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:557 -#: 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:558 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:45 -msgid "Close" -msgstr "閉じる" - -#: authentication/templates/authentication/_captcha_field.html:8 -msgid "Play CAPTCHA as audio file" -msgstr "CAPTCHAをオーディオファイルとして再生する" - -#: 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:413 ops/tasks.py:145 ops/tasks.py:148 -#: 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/sharing.py:26 terminal/models/sharing.py:80 -#: 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 "パスワードリセットページにcaptchaをコピーし、パスワードをリセットする" - -#: 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:20 -msgid "Cancel" -msgstr "キャンセル" - -#: authentication/templates/authentication/login.html:221 -msgid "Welcome back, please enter username and password to login" -msgstr "" -"おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" - -#: authentication/templates/authentication/login.html:264 -#: templates/_header_bar.html:89 -msgid "Login" -msgstr "ログイン" - -#: authentication/templates/authentication/login.html:271 -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 "セキュリティを提供できませんか? 管理者に連絡してください!" - -#: 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:41 -msgid "DingTalk Error, Please contact your system administrator" -msgstr "DingTalkエラー、システム管理者に連絡してください" - -#: authentication/views/dingtalk.py:44 -msgid "DingTalk Error" -msgstr "DingTalkエラー" - -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 -#: authentication/views/wecom.py:56 -msgid "" -"The system configuration is incorrect. Please contact your administrator" -msgstr "システム設定が正しくありません。管理者に連絡してください" - -#: authentication/views/dingtalk.py:80 -msgid "DingTalk is already bound" -msgstr "DingTalkはすでにバインドされています" - -#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:148 -msgid "Invalid user_id" -msgstr "無効なuser_id" - -#: authentication/views/dingtalk.py:164 -msgid "DingTalk query user failed" -msgstr "DingTalkクエリユーザーが失敗しました" - -#: authentication/views/dingtalk.py:173 -msgid "The DingTalk is already bound to another user" -msgstr "DingTalkはすでに別のユーザーにバインドされています" - -#: authentication/views/dingtalk.py:180 -msgid "Binding DingTalk successfully" -msgstr "DingTalkのバインドに成功" - -#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 -msgid "Failed to get user from DingTalk" -msgstr "DingTalkからユーザーを取得できませんでした" - -#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 -msgid "Please login with a password and then bind the DingTalk" -msgstr "パスワードでログインし、DingTalkをバインドしてください" - -#: authentication/views/feishu.py:39 -msgid "FeiShu Error" -msgstr "FeiShuエラー" - -#: authentication/views/feishu.py:87 -msgid "FeiShu is already bound" -msgstr "FeiShuはすでにバインドされています" - -#: authentication/views/feishu.py:129 -msgid "FeiShu query user failed" -msgstr "FeiShuクエリユーザーが失敗しました" - -#: authentication/views/feishu.py:138 -msgid "The FeiShu is already bound to another user" -msgstr "FeiShuはすでに別のユーザーにバインドされています" - -#: authentication/views/feishu.py:145 -msgid "Binding FeiShu successfully" -msgstr "本を飛ばすのバインドに成功" - -#: authentication/views/feishu.py:197 -msgid "Failed to get user from FeiShu" -msgstr "本を飛ばすからユーザーを取得できませんでした" - -#: authentication/views/feishu.py:204 -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 "クッキーを有効にして、もう一度お試しください。" - -#: 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 "パスワードでログインしてからWeComをバインドしてください" - -#: 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/db/encoder.py:11 -msgid "ugettext_lazy" -msgstr "ugettext_lazy" - -#: common/db/fields.py:81 -msgid "Marshal dict data to char field" -msgstr "チャーフィールドへのマーシャルディクトデータ" - -#: common/db/fields.py:85 -msgid "Marshal dict data to text field" -msgstr "テキストフィールドへのマーシャルディクトデータ" - -#: common/db/fields.py:97 -msgid "Marshal list data to char field" -msgstr "元帥リストデータをチャーフィールドに" - -#: common/db/fields.py:101 -msgid "Marshal list data to text field" -msgstr "マーシャルリストデータをテキストフィールドに" - -#: common/db/fields.py:105 -msgid "Marshal data to char field" -msgstr "チャーフィールドへのマーシャルデータ" - -#: common/db/fields.py:109 -msgid "Marshal data to text field" -msgstr "テキストフィールドへのマーシャルデータ" - -#: common/db/fields.py:151 -msgid "Encrypt field using Secret Key" -msgstr "Secret Keyを使用したフィールドの暗号化" - -#: common/db/models.py:115 -msgid "Updated by" -msgstr "によって更新" - -#: common/drf/exc_handlers.py:25 -msgid "Object" -msgstr "オブジェクト" - -#: 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/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 "M2Mリバースは許可されません" - -#: 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 "このアクションでは、MFAの確認が必要です。" - -#: 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:33 -msgid "is discard" -msgstr "は破棄されます" - -#: common/mixins/models.py:34 -msgid "discard time" -msgstr "時間を捨てる" - -#: common/mixins/views.py:52 -msgid "Export all" -msgstr "すべてエクスポート" - -#: common/mixins/views.py:54 -msgid "Export only selected items" -msgstr "選択項目のみエクスポート" - -#: common/mixins/views.py:59 -#, 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 "SMSプロバイダーはサポートしていません: {}" - -#: common/sdk/sms/endpoint.py:51 -msgid "SMS verification code signature or template invalid" -msgstr "SMS検証コードの署名またはテンプレートが無効" - -#: 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/utils/ip/geoip/utils.py:26 common/utils/ip/utils.py:78 -msgid "Invalid ip" -msgstr "無効なIP" - -#: 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:412 -msgid "Create account successfully" -msgstr "アカウントを正常に作成" - -#: jumpserver/conf.py:414 -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 "

フラワーサービス利用不可、チェック

" - -#: 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をデプロイする必要" -"があります。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:17 -msgid "User message" -msgstr "ユーザメッセージ" - -#: notifications/models/notification.py:20 -msgid "{} subscription" -msgstr "{} 購読" - -#: notifications/models/notification.py:32 -msgid "System message" -msgstr "システムメッセージ" - -#: ops/api/celery.py:61 ops/api/celery.py:76 -msgid "Waiting task start" -msgstr "タスク開始待ち" - -#: ops/api/command.py:56 -msgid "Not has host {} permission" -msgstr "ホスト {} 権限がありません" - -#: ops/apps.py:9 ops/notifications.py:16 -msgid "App ops" -msgstr "アプリ操作" - -#: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 -#: settings/serializers/auth/ldap.py:73 -msgid "Cycle perform" -msgstr "サイクル実行" - -#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150 -#: settings/serializers/auth/ldap.py:70 -msgid "Regularly perform" -msgstr "定期的に実行する" - -#: ops/mixin.py:112 -msgid "Interval" -msgstr "間隔" - -#: ops/mixin.py:122 -msgid "* Please enter a valid crontab expression" -msgstr "* 有効なcrontab式を入力してください" - -#: ops/mixin.py:129 -msgid "Range {} to {}" -msgstr "{} から {} までの範囲" - -#: ops/mixin.py:140 -msgid "Require periodic or regularly perform setting" -msgstr "定期的または定期的に設定を行う必要があります" - -#: ops/mixin.py:151 -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式<分時日月曜日>(オンラインワーク)
注" -"意:定期実行と周期実行を同時に設定した場合は、定期実行を優先します。" - -#: ops/mixin.py:162 -msgid "Unit: hour" -msgstr "単位: 時間" - -#: ops/models/adhoc.py:36 -msgid "Callback" -msgstr "コールバック" - -#: ops/models/adhoc.py:135 terminal/models/task.py:26 -#: xpack/plugins/gathered_user/models.py:73 -msgid "Task" -msgstr "タスク" - -#: ops/models/adhoc.py:138 -msgid "Can view task monitor" -msgstr "タスクモニターを表示できます" - -#: ops/models/adhoc.py:154 -msgid "Tasks" -msgstr "タスク" - -#: ops/models/adhoc.py:156 -msgid "Options" -msgstr "オプション" - -#: ops/models/adhoc.py:158 -msgid "Run as admin" -msgstr "再実行" - -#: ops/models/adhoc.py:161 -msgid "Become" -msgstr "になる" - -#: ops/models/adhoc.py:162 -msgid "Create by" -msgstr "による作成" - -#: ops/models/adhoc.py:243 -msgid "AdHoc" -msgstr "タスクの各バージョン" - -#: ops/models/adhoc.py:252 -msgid "Task display" -msgstr "タスク表示" - -#: ops/models/adhoc.py:254 -msgid "Host amount" -msgstr "ホスト量" - -#: ops/models/adhoc.py:256 -msgid "Start time" -msgstr "開始時間" - -#: ops/models/adhoc.py:257 -msgid "End time" -msgstr "終了時間" - -#: ops/models/adhoc.py:259 ops/models/command.py:29 -#: terminal/serializers/session.py:40 -msgid "Is finished" -msgstr "終了しました" - -#: ops/models/adhoc.py:261 -msgid "Adhoc raw result" -msgstr "アドホック生の結果" - -#: ops/models/adhoc.py:262 -msgid "Adhoc result summary" -msgstr "アドホック結果の概要" - -#: ops/models/adhoc.py:339 -msgid "AdHoc execution" -msgstr "アドホックエキューション" - -#: ops/models/command.py:32 -msgid "Date finished" -msgstr "終了日" - -#: ops/models/command.py:113 -msgid "Task start" -msgstr "タスクの開始" - -#: ops/models/command.py:147 -msgid "Command `{}` is forbidden ........" -msgstr "コマンド '{}' は禁止されています ........" - -#: ops/models/command.py:160 -msgid "Task end" -msgstr "タスク" - -#: ops/models/command.py:164 -msgid "Command execution" -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/tasks.py:72 -msgid "Clean task history period" -msgstr "クリーンなタスク履歴期間" - -#: ops/tasks.py:85 -msgid "Clean celery log period" -msgstr "きれいなセロリログ期間" - -#: ops/templates/ops/celery_task_log.html:4 -msgid "Task log" -msgstr "タスクログ" - -#: ops/utils.py:64 -msgid "Update task content: {}" -msgstr "タスク内容の更新: {}" - -#: orgs/api.py:69 -msgid "The current organization ({}) cannot be deleted" -msgstr "現在の組織 ({}) は削除できません" - -#: orgs/api.py:74 -msgid "" -"LDAP synchronization is set to the current organization. Please switch to " -"another organization before deleting" -msgstr "" -"LDAP 同期は現在の組織に設定されます。削除する前に別の組織に切り替えてください" - -#: orgs/api.py:83 -msgid "The organization have resource ({}) cannot be deleted" -msgstr "組織のリソース ({}) は削除できません" - -#: orgs/apps.py:7 rbac/tree.py:113 -msgid "App organizations" -msgstr "アプリ組織" - -#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:85 -#: orgs/models.py:217 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:300 tickets/serializers/ticket/ticket.py:72 -msgid "Organization" -msgstr "組織" - -#: orgs/models.py:79 -msgid "GLOBAL" -msgstr "グローバル組織" - -#: orgs/models.py:87 -msgid "Can view root org" -msgstr "グローバル組織を表示できます" - -#: orgs/models.py:88 -msgid "Can view all joined org" -msgstr "参加しているすべての組織を表示できます" - -#: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44 -#: users/models/user.py:683 -msgid "Role" -msgstr "ロール" - -#: perms/apps.py:9 -msgid "App permissions" -msgstr "アプリの権限" - -#: perms/exceptions.py:9 -msgid "The administrator is modifying permissions. Please wait" -msgstr "管理者は権限を変更しています。お待ちください" - -#: perms/exceptions.py:14 -msgid "The authorization cannot be revoked for the time being" -msgstr "当分の間、承認を取り消すことはできません。" - -#: perms/models/application_permission.py:38 -msgid "Application permission" -msgstr "申請許可" - -#: perms/models/application_permission.py:111 -msgid "Permed application" -msgstr "許可されたアプリケーション" - -#: perms/models/application_permission.py:114 -msgid "Can view my apps" -msgstr "自分のアプリを表示できます" - -#: perms/models/application_permission.py:115 -msgid "Can view user apps" -msgstr "ユーザーアプリを表示できます" - -#: perms/models/application_permission.py:116 -msgid "Can view usergroup apps" -msgstr "ユーザー・グループ認可の適用を表示できます" - -#: perms/models/asset_permission.py:29 -msgid "Asset permission" -msgstr "資産権限" - -#: perms/models/asset_permission.py:134 -msgid "Ungrouped" -msgstr "グループ化されていません" - -#: perms/models/asset_permission.py:136 -msgid "Favorite" -msgstr "お気に入り" - -#: perms/models/asset_permission.py:183 -msgid "Permed asset" -msgstr "許可された資産" - -#: perms/models/asset_permission.py:185 -msgid "Can view my assets" -msgstr "私の資産を見ることができます" - -#: perms/models/asset_permission.py:186 -msgid "Can view user assets" -msgstr "ユーザー資産を表示できます" - -#: perms/models/asset_permission.py:187 -msgid "Can view usergroup assets" -msgstr "ユーザーグループの資産を表示できます" - -#: perms/models/base.py:55 -msgid "Connect" -msgstr "接続" - -#: perms/models/base.py:56 -msgid "Upload file" -msgstr "ファイルのアップロード" - -#: perms/models/base.py:57 -msgid "Download file" -msgstr "ファイルのダウンロード" - -#: perms/models/base.py:58 -msgid "Upload download" -msgstr "ダウンロードのアップロード" - -#: perms/models/base.py:59 -msgid "Clipboard copy" -msgstr "クリップボードのコピー" - -#: perms/models/base.py:60 -msgid "Clipboard paste" -msgstr "クリップボードペースト" - -#: perms/models/base.py:61 -msgid "Clipboard copy paste" -msgstr "クリップボードコピーペースト" - -#: perms/models/base.py:94 -msgid "From ticket" -msgstr "チケットから" - -#: perms/notifications.py:12 perms/notifications.py:44 -#: perms/notifications.py:88 perms/notifications.py:119 -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/notifications.py:91 -msgid "Your permed applications is about to expire" -msgstr "パーマアプリケーションの有効期限が近づいています" - -#: perms/notifications.py:95 -msgid "permed applications" -msgstr "Permedアプリケーション" - -#: perms/notifications.py:134 -msgid "Application permissions is about to expire" -msgstr "アプリケーション権限の有効期限が近づいています" - -#: perms/notifications.py:138 -msgid "application permissions of organization {}" -msgstr "Organization {} のアプリケーション権限" - -#: perms/serializers/application/permission.py:21 -#: perms/serializers/application/permission.py:40 -#: perms/serializers/asset/permission.py:20 -#: perms/serializers/asset/permission.py:44 users/serializers/user.py:89 -#: users/serializers/user.py:150 -msgid "Is expired" -msgstr "期限切れです" - -#: perms/serializers/application/permission.py:43 -#: perms/serializers/asset/permission.py:47 rbac/serializers/role.py:26 -#: users/serializers/group.py:34 -msgid "Users amount" -msgstr "ユーザー数" - -#: perms/serializers/application/permission.py:44 -#: perms/serializers/asset/permission.py:48 -msgid "User groups amount" -msgstr "ユーザーグループの量" - -#: perms/serializers/application/permission.py:45 -#: perms/serializers/asset/permission.py:51 -msgid "System users amount" -msgstr "システムユーザー数" - -#: perms/serializers/application/permission.py:79 -msgid "" -"The application list contains applications that are different from the " -"permission type. ({})" -msgstr "" -"アプリケーションリストには、権限タイプとは異なるアプリケーションが含まれてい" -"ます。({})" - -#: perms/serializers/asset/permission.py:21 -msgid "Users display" -msgstr "ユーザー表示" - -#: perms/serializers/asset/permission.py:22 -msgid "User groups display" -msgstr "ユーザーグループの表示" - -#: perms/serializers/asset/permission.py:23 -msgid "Assets display" -msgstr "資産表示" - -#: perms/serializers/asset/permission.py:24 -msgid "Nodes display" -msgstr "ノード表示" - -#: perms/serializers/asset/permission.py:25 -msgid "System users display" -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 "質問があったら、管理者に連絡して下さい" - -#: perms/tree/app.py:24 -msgid "My applications" -msgstr "私のアプリケーション" - -#: perms/tree/app.py:41 -msgid "Empty" -msgstr "空" - -#: perms/utils/asset/user_permission.py:620 rbac/tree.py:57 -msgid "My assets" -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 -#: settings/serializers/auth/oauth2.py:37 -msgid "Scope" -msgstr "スコープ" - -#: rbac/models/role.py:36 -msgid "Built-in" -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:11 -msgid "Scope display" -msgstr "スコープ表示" - -#: rbac/serializers/role.py:27 -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:156 -msgid "System setting" -msgstr "システム設定" - -#: rbac/tree.py:29 -msgid "Other" -msgstr "その他" - -#: rbac/tree.py:37 -msgid "Accounts" -msgstr "アカウント" - -#: rbac/tree.py:41 -msgid "Session audits" -msgstr "セッション監査" - -#: rbac/tree.py:51 -msgid "Cloud import" -msgstr "クラウドインポート" - -#: rbac/tree.py:52 -msgid "Backup account" -msgstr "バックアップアカウント" - -#: rbac/tree.py:53 -msgid "Gather account" -msgstr "アカウントを集める" - -#: rbac/tree.py:54 -msgid "App change auth" -msgstr "応用改密" - -#: rbac/tree.py:55 -msgid "Asset change auth" -msgstr "資産の改ざん" - -#: rbac/tree.py:56 -msgid "Terminal setting" -msgstr "ターミナル設定" - -#: rbac/tree.py:58 -msgid "My apps" -msgstr "マイアプリ" - -#: rbac/tree.py:114 -msgid "Ticket comment" -msgstr "チケットコメント" - -#: rbac/tree.py:115 tickets/models/ticket/general.py:305 -msgid "Ticket" -msgstr "チケット" - -#: rbac/tree.py:116 -msgid "Common setting" -msgstr "共通設定" - -#: rbac/tree.py:117 -msgid "View permission tree" -msgstr "権限ツリーの表示" - -#: rbac/tree.py:118 -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:158 -msgid "Can change email setting" -msgstr "メール設定を変更できます" - -#: settings/models.py:159 -msgid "Can change auth setting" -msgstr "資格認定の設定" - -#: settings/models.py:160 -msgid "Can change system msg sub setting" -msgstr "システムmsgサブ设定を変更できます" - -#: settings/models.py:161 -msgid "Can change sms setting" -msgstr "Smsの設定を変えることができます" - -#: settings/models.py:162 -msgid "Can change security setting" -msgstr "セキュリティ設定を変更できます" - -#: settings/models.py:163 -msgid "Can change clean setting" -msgstr "きれいな設定を変えることができます" - -#: settings/models.py:164 -msgid "Can change interface setting" -msgstr "インターフェイスの設定を変えることができます" - -#: settings/models.py:165 -msgid "Can change license setting" -msgstr "ライセンス設定を変更できます" - -#: settings/models.py:166 -msgid "Can change terminal setting" -msgstr "ターミナルの設定を変えることができます" - -#: settings/models.py:167 -msgid "Can change other setting" -msgstr "他の設定を変えることができます" - -#: settings/serializers/auth/base.py:10 settings/serializers/basic.py:27 -msgid "Basic" -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:49 -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:63 -msgid "Provider auth endpoint" -msgstr "認証エンドポイントアドレス" - -#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:66 -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:72 -msgid "Provider userinfo endpoint" -msgstr "プロバイダーuserinfoエンドポイント" - -#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:75 -msgid "Provider end session endpoint" -msgstr "プロバイダーのセッション終了エンドポイント" - -#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:93 -#: 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:46 -msgid "Use Keycloak" -msgstr "Keycloakを使用する" - -#: settings/serializers/auth/oidc.py:52 -msgid "Realm name" -msgstr "レルム名" - -#: settings/serializers/auth/oidc.py:58 -msgid "Enable OPENID Auth" -msgstr "OIDC認証の有効化" - -#: settings/serializers/auth/oidc.py:60 -msgid "Provider endpoint" -msgstr "プロバイダーエンドポイント" - -#: settings/serializers/auth/oidc.py:69 -msgid "Provider jwks endpoint" -msgstr "プロバイダーjwksエンドポイント" - -#: settings/serializers/auth/oidc.py:78 -msgid "Provider sign alg" -msgstr "プロビダーサインalg" - -#: settings/serializers/auth/oidc.py:81 -msgid "Provider sign key" -msgstr "プロバイダ署名キー" - -#: settings/serializers/auth/oidc.py:83 -msgid "Scopes" -msgstr "スコープ" - -#: settings/serializers/auth/oidc.py:85 -msgid "Id token max age" -msgstr "IDトークンの最大年齢" - -#: settings/serializers/auth/oidc.py:88 -msgid "Id token include claims" -msgstr "IDトークンにはクレームが含まれます" - -#: settings/serializers/auth/oidc.py:90 -msgid "Use state" -msgstr "使用状態" - -#: settings/serializers/auth/oidc.py:91 -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/settings.py:61 -#: users/templates/users/reset_password.html:29 -msgid "Setting" -msgstr "設定" - -#: settings/serializers/terminal.py:6 terminal/models/terminal.py:185 -msgid "Terminal" -msgstr "ターミナル" - -#: 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/endpoint.py:34 -msgid "Not found protocol query params" -msgstr "プロトコルクエリパラメータが見つかりません" - -#: terminal/api/session.py:216 -msgid "Session does not exist: {}" -msgstr "セッションが存在しません: {}" - -#: terminal/api/session.py:219 -msgid "Session is finished or the protocol not supported" -msgstr "セッションが終了したか、プロトコルがサポートされていません" - -#: terminal/api/session.py:232 -msgid "User does not have permission" -msgstr "ユーザーに権限がありません" - -#: terminal/api/sharing.py:30 -msgid "Secure session sharing settings is disabled" -msgstr "安全なセッション共有設定が無効になっています" - -#: terminal/api/storage.py:28 -msgid "Deleting the default storage is not allowed" -msgstr "デフォルトのストレージの削除は許可されていません" - -#: terminal/api/storage.py:31 -msgid "Cannot delete storage that is being used" -msgstr "使用中のストレージを削除できません" - -#: terminal/api/storage.py:72 terminal/api/storage.py:73 -msgid "Command storages" -msgstr "コマンドストア" - -#: terminal/api/storage.py:79 -msgid "Invalid" -msgstr "無効" - -#: terminal/api/storage.py:119 -msgid "Test failure: {}" -msgstr "テスト失敗: {}" - -#: terminal/api/storage.py:122 -msgid "Test successful" -msgstr "テスト成功" - -#: terminal/api/storage.py:124 -msgid "Test failure: Account invalid" -msgstr "テスト失敗: アカウントが無効" - -#: terminal/api/terminal.py:39 -msgid "Have online sessions" -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:37 -msgid "Output" -msgstr "出力" - -#: terminal/backends/command/models.py:25 terminal/models/replay.py:9 -#: terminal/models/sharing.py:19 terminal/models/sharing.py:78 -#: terminal/templates/terminal/_msg_command_alert.html:10 -#: tickets/models/ticket/command_confirm.py:20 -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:38 -msgid "Risk level display" -msgstr "リスクレベル表示" - -#: terminal/backends/command/serializers.py:39 -msgid "Timestamp" -msgstr "タイムスタンプ" - -#: terminal/backends/command/serializers.py:41 terminal/models/terminal.py:106 -msgid "Remote Address" -msgstr "リモートアドレス" - -#: terminal/const.py:33 -msgid "Critical" -msgstr "クリティカル" - -#: terminal/const.py:34 -msgid "High" -msgstr "高い" - -#: terminal/const.py:35 users/templates/users/reset_password.html:50 -msgid "Normal" -msgstr "正常" - -#: terminal/const.py:36 -msgid "Offline" -msgstr "オフライン" - -#: terminal/exceptions.py:8 -msgid "Bulk create not support" -msgstr "一括作成非サポート" - -#: terminal/exceptions.py:13 -msgid "Storage is invalid" -msgstr "ストレージが無効です" - -#: terminal/models/command.py:66 -msgid "Command record" -msgstr "コマンドレコード" - -#: terminal/models/endpoint.py:17 -msgid "HTTPS Port" -msgstr "HTTPS ポート" - -#: terminal/models/endpoint.py:18 terminal/models/terminal.py:108 -msgid "HTTP Port" -msgstr "HTTP ポート" - -#: terminal/models/endpoint.py:19 terminal/models/terminal.py:107 -msgid "SSH Port" -msgstr "SSH ポート" - -#: terminal/models/endpoint.py:20 -msgid "RDP Port" -msgstr "RDP ポート" - -#: terminal/models/endpoint.py:27 terminal/models/endpoint.py:95 -#: 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/endpoint.py:88 -msgid "IP group" -msgstr "IP グループ" - -#: terminal/models/endpoint.py:100 -msgid "Endpoint rule" -msgstr "エンドポイントルール" - -#: terminal/models/replay.py:12 -msgid "Session replay" -msgstr "セッション再生" - -#: terminal/models/replay.py:14 -msgid "Can upload session replay" -msgstr "セッションのリプレイをアップロードできます" - -#: terminal/models/replay.py:15 -msgid "Can download session replay" -msgstr "セッション再生をダウンロードできます" - -#: terminal/models/session.py:51 terminal/models/sharing.py:101 -msgid "Login from" -msgstr "ログイン元" - -#: terminal/models/session.py:55 -msgid "Replay" -msgstr "リプレイ" - -#: terminal/models/session.py:60 -msgid "Date end" -msgstr "終了日" - -#: terminal/models/session.py:261 -msgid "Session record" -msgstr "セッション記録" - -#: terminal/models/session.py:263 -msgid "Can monitor session" -msgstr "セッションを監視できます" - -#: terminal/models/session.py:264 -msgid "Can share session" -msgstr "セッションを共有できます" - -#: terminal/models/session.py:265 -msgid "Can terminate session" -msgstr "セッションを終了できます" - -#: terminal/models/session.py:266 -msgid "Can validate session action perm" -msgstr "セッションアクションのパーマを検証できます" - -#: terminal/models/sharing.py:24 -msgid "Creator" -msgstr "作成者" - -#: terminal/models/sharing.py:31 -msgid "Expired time (min)" -msgstr "期限切れ時間 (分)" - -#: terminal/models/sharing.py:37 terminal/models/sharing.py:83 -msgid "Session sharing" -msgstr "セッション共有" - -#: terminal/models/sharing.py:39 -msgid "Can add super session sharing" -msgstr "スーパーセッション共有を追加できます" - -#: terminal/models/sharing.py:66 -msgid "Link not active" -msgstr "リンクがアクティブでない" - -#: terminal/models/sharing.py:68 -msgid "Link expired" -msgstr "リンク期限切れ" - -#: terminal/models/sharing.py:70 -msgid "User not allowed to join" -msgstr "ユーザーはセッションに参加できません" - -#: terminal/models/sharing.py:87 terminal/serializers/sharing.py:59 -msgid "Joiner" -msgstr "ジョイナー" - -#: terminal/models/sharing.py:90 -msgid "Date joined" -msgstr "参加日" - -#: terminal/models/sharing.py:93 -msgid "Date left" -msgstr "日付が残っています" - -#: terminal/models/sharing.py:111 tickets/const.py:26 -#: xpack/plugins/change_auth_plan/models/base.py:192 -msgid "Finished" -msgstr "終了" - -#: terminal/models/sharing.py:116 -msgid "Session join record" -msgstr "セッション参加記録" - -#: terminal/models/sharing.py:132 -msgid "Invalid verification code" -msgstr "検証コードが無効" - -#: terminal/models/status.py:18 -msgid "Session Online" -msgstr "セッションオンライン" - -#: terminal/models/status.py:19 -msgid "CPU Load" -msgstr "CPUロード" - -#: terminal/models/status.py:20 -msgid "Memory Used" -msgstr "使用メモリ" - -#: terminal/models/status.py:21 -msgid "Disk Used" -msgstr "使用済みディスク" - -#: terminal/models/status.py:22 -msgid "Connections" -msgstr "接続" - -#: terminal/models/status.py:23 -msgid "Threads" -msgstr "スレッド" - -#: terminal/models/status.py:24 -msgid "Boot Time" -msgstr "ブート時間" - -#: terminal/models/storage.py:29 -msgid "Default storage" -msgstr "デフォルトのストレージ" - -#: terminal/models/storage.py:139 terminal/models/terminal.py:109 -msgid "Command storage" -msgstr "コマンドストレージ" - -#: terminal/models/storage.py:199 terminal/models/terminal.py:110 -msgid "Replay storage" -msgstr "再生ストレージ" - -#: terminal/models/task.py:17 -msgid "Args" -msgstr "アルグ" - -#: terminal/models/task.py:18 -msgid "Kwargs" -msgstr "クワーグ" - -#: terminal/models/terminal.py:104 -msgid "type" -msgstr "タイプ" - -#: terminal/models/terminal.py:111 -msgid "Application User" -msgstr "ユーザーの適用" - -#: terminal/models/terminal.py:112 -msgid "Is Accepted" -msgstr "受け入れられる" - -#: terminal/models/terminal.py:187 -msgid "Can view terminal config" -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/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:15 terminal/serializers/session.py:42 -msgid "Terminal display" -msgstr "ターミナルディスプレイ" - -#: terminal/serializers/session.py:32 -msgid "User ID" -msgstr "ユーザーID" - -#: terminal/serializers/session.py:33 -msgid "Asset ID" -msgstr "資産ID" - -#: terminal/serializers/session.py:34 -msgid "System user ID" -msgstr "システムユーザーID" - -#: terminal/serializers/session.py:35 -msgid "Login from display" -msgstr "表示からのログイン" - -#: terminal/serializers/session.py:37 -msgid "Can replay" -msgstr "再生できます" - -#: terminal/serializers/session.py:38 -msgid "Can join" -msgstr "参加できます" - -#: terminal/serializers/session.py:39 -msgid "Terminal ID" -msgstr "ターミナル ID" - -#: terminal/serializers/session.py:41 -msgid "Can terminate" -msgstr "終了できます" - -#: terminal/serializers/session.py:47 -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:222 -msgid "Region" -msgstr "リージョン" - -#: terminal/serializers/storage.py:109 -msgid "Container name" -msgstr "コンテナー名" - -#: terminal/serializers/storage.py:111 -msgid "Account 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:44 -msgid "Load status" -msgstr "ロードステータス" - -#: terminal/serializers/terminal.py:81 terminal/serializers/terminal.py:89 -msgid "Not found" -msgstr "見つかりません" - -#: terminal/templates/terminal/_msg_command_alert.html:10 -msgid "view" -msgstr "表示" - -#: terminal/utils/db_port_mapper.py:65 -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:91 -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:93 -msgid "All available port count: {}, Already use port count: {}" -msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" - -#: tickets/apps.py:7 -msgid "Tickets" -msgstr "チケット" - -#: tickets/const.py:8 -msgid "General" -msgstr "一般" - -#: tickets/const.py:10 -msgid "Apply for asset" -msgstr "資産の申請" - -#: tickets/const.py:11 -msgid "Apply for application" -msgstr "申し込み" - -#: tickets/const.py:17 tickets/const.py:25 tickets/const.py:44 -msgid "Open" -msgstr "オープン" - -#: tickets/const.py:18 tickets/const.py:31 -msgid "Approved" -msgstr "承認済み" - -#: tickets/const.py:19 tickets/const.py:32 -msgid "Rejected" -msgstr "拒否" - -#: tickets/const.py:21 tickets/const.py:34 -msgid "Reopen" -msgstr "" - -#: tickets/const.py:30 tickets/const.py:38 -msgid "Pending" -msgstr "未定" - -#: tickets/const.py:33 tickets/const.py:40 -msgid "Closed" -msgstr "クローズ" - -#: tickets/const.py:46 -msgid "Approve" -msgstr "承認" - -#: tickets/const.py:51 -msgid "One level" -msgstr "1つのレベル" - -#: tickets/const.py:52 -msgid "Two level" -msgstr "2つのレベル" - -#: tickets/const.py:56 -msgid "Super admin" -msgstr "スーパー管理者" - -#: tickets/const.py:57 -msgid "Org admin" -msgstr "Org admin" - -#: tickets/const.py:58 -msgid "Super admin and org admin" -msgstr "スーパーadminとorg admin" - -#: tickets/const.py:59 -msgid "Custom user" -msgstr "カスタムユーザー" - -#: tickets/errors.py:9 -msgid "Ticket already closed" -msgstr "チケットはすでに閉じています" - -#: tickets/handlers/apply_application.py:38 -msgid "" -"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " -"processor: {}, ticket ID: {}" -msgstr "" -"チケットによって作成されたチケットタイトル: {}、チケット申請者: {}、チケット" -"処理者: {}、チケットID: {}" - -#: tickets/handlers/apply_asset.py:37 -msgid "" -"Created by the ticket ticket title: {} ticket applicant: {} ticket " -"processor: {} ticket ID: {}" -msgstr "" -"チケットのタイトル: {} チケット申請者: {} チケットプロセッサ: {} チケットID: " -"{}" - -#: tickets/handlers/base.py:84 -msgid "Change field" -msgstr "フィールドを変更" - -#: tickets/handlers/base.py:84 -msgid "Before change" -msgstr "変更前" - -#: tickets/handlers/base.py:84 -msgid "After change" -msgstr "変更後" - -#: tickets/handlers/base.py:96 -msgid "{} {} the ticket" -msgstr "{} {} チケット" - -#: tickets/handlers/login_confirm.py:18 -msgid "Applied login IP" -msgstr "応用ログインIP" - -#: tickets/handlers/login_confirm.py:19 -msgid "Applied login city" -msgstr "応用ログイン都市" - -#: tickets/handlers/login_confirm.py:20 -msgid "Applied login datetime" -msgstr "適用されたログインの日付時間" - -#: tickets/models/comment.py:13 tickets/models/ticket/general.py:41 -#: tickets/models/ticket/general.py:277 -msgid "State" -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:20 tickets/models/flow.py:62 -#: tickets/models/ticket/general.py:37 -msgid "Approve level" -msgstr "レベルを承認する" - -#: tickets/models/flow.py:25 tickets/serializers/flow.py:15 -msgid "Approve strategy" -msgstr "戦略を承認する" - -#: tickets/models/flow.py:30 tickets/serializers/flow.py:16 -msgid "Assignees" -msgstr "アシニーズ" - -#: tickets/models/flow.py:34 -msgid "Ticket flow approval rule" -msgstr "チケットフロー承認ルール" - -#: tickets/models/flow.py:67 -msgid "Ticket flow" -msgstr "チケットの流れ" - -#: tickets/models/relation.py:10 -msgid "Ticket session relation" -msgstr "チケットセッションの関係" - -#: tickets/models/ticket/apply_application.py:12 -#: tickets/models/ticket/apply_asset.py:13 -msgid "Permission name" -msgstr "認可ルール名" - -#: tickets/models/ticket/apply_application.py:21 -msgid "Apply applications" -msgstr "アプリケーションの適用" - -#: tickets/models/ticket/apply_application.py:24 -#: tickets/models/ticket/apply_asset.py:18 -msgid "Apply system users" -msgstr "システムユーザーの適用" - -#: tickets/models/ticket/apply_asset.py:9 -#: tickets/serializers/ticket/apply_asset.py:15 -msgid "Select at least one asset or node" -msgstr "少なくとも1つのアセットまたはノードを選択します。" - -#: tickets/models/ticket/apply_asset.py:14 -msgid "Apply nodes" -msgstr "ノードの適用" - -#: tickets/models/ticket/apply_asset.py:16 -msgid "Apply assets" -msgstr "資産の適用" - -#: tickets/models/ticket/command_confirm.py:10 -msgid "Run user" -msgstr "ユーザーの実行" - -#: tickets/models/ticket/command_confirm.py:12 -msgid "Run asset" -msgstr "アセットの実行" - -#: tickets/models/ticket/command_confirm.py:15 -msgid "Run system user" -msgstr "システムユーザーの実行" - -#: tickets/models/ticket/command_confirm.py:17 -msgid "Run command" -msgstr "実行コマンド" - -#: tickets/models/ticket/command_confirm.py:24 -msgid "From cmd filter" -msgstr "コマンドフィルタ規則から" - -#: tickets/models/ticket/command_confirm.py:28 -msgid "From cmd filter rule" -msgstr "コマンドフィルタ規則から" - -#: tickets/models/ticket/general.py:72 -msgid "Ticket step" -msgstr "チケットステップ" - -#: tickets/models/ticket/general.py:90 -msgid "Ticket assignee" -msgstr "割り当てられたチケット" - -#: tickets/models/ticket/general.py:270 -msgid "Title" -msgstr "タイトル" - -#: tickets/models/ticket/general.py:286 -msgid "Applicant" -msgstr "応募者" - -#: tickets/models/ticket/general.py:291 -msgid "TicketFlow" -msgstr "作業指示プロセス" - -#: tickets/models/ticket/general.py:294 -msgid "Approval step" -msgstr "承認ステップ" - -#: tickets/models/ticket/general.py:297 -msgid "Relation snapshot" -msgstr "製造オーダスナップショット" - -#: tickets/models/ticket/general.py:390 -msgid "Please try again" -msgstr "もう一度お試しください" - -#: tickets/models/ticket/general.py:421 -msgid "Super ticket" -msgstr "スーパーチケット" - -#: tickets/models/ticket/login_asset_confirm.py:12 -msgid "Login user" -msgstr "ログインユーザー" - -#: tickets/models/ticket/login_asset_confirm.py:16 -msgid "Login asset" -msgstr "ログイン資産" - -#: tickets/models/ticket/login_asset_confirm.py:20 -msgid "Login system user" -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:17 -msgid "Assignees display" -msgstr "受付者名" - -#: tickets/serializers/flow.py:43 -msgid "Please select the Assignees" -msgstr "受付をお選びください" - -#: tickets/serializers/flow.py:69 -msgid "The current organization type already exists" -msgstr "現在の組織タイプは既に存在します。" - -#: tickets/serializers/super_ticket.py:11 -msgid "Processor" -msgstr "プロセッサ" - -#: tickets/serializers/ticket/common.py:16 -#: tickets/serializers/ticket/common.py:79 -msgid "Created by ticket ({}-{})" -msgstr "チケットで作成 ({}-{})" - -#: tickets/serializers/ticket/common.py:69 -msgid "The expiration date should be greater than the start date" -msgstr "有効期限は開始日より大きくする必要があります" - -#: tickets/serializers/ticket/common.py:85 -msgid "Permission named `{}` already exists" -msgstr "'{}'という名前の権限は既に存在します" - -#: tickets/serializers/ticket/ticket.py:110 -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:39 -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:40 -msgid "" -"This ticket does not exist, the process has ended, or this link has expired" -msgstr "" -"このワークシートが存在しないか、ワークシートが終了したか、このリンクが無効に" -"なっています" - -#: tickets/views/approve.py:69 -msgid "Click the button below to approve or reject" -msgstr "下のボタンをクリックして同意または拒否。" - -#: tickets/views/approve.py:71 -msgid "After successful authentication, this ticket can be approved directly" -msgstr "認証に成功した後、作業指示書は直接承認することができる。" - -#: tickets/views/approve.py:93 -msgid "Illegal approval action" -msgstr "無効な承認アクション" - -#: tickets/views/approve.py:106 -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/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:706 -msgid "Public key" -msgstr "公開キー" - -#: users/models/user.py:559 -msgid "Force enable" -msgstr "強制有効" - -#: users/models/user.py:629 -msgid "Local" -msgstr "ローカル" - -#: users/models/user.py:685 users/serializers/user.py:149 -msgid "Is service account" -msgstr "サービスアカウントです" - -#: users/models/user.py:687 -msgid "Avatar" -msgstr "アバター" - -#: users/models/user.py:690 -msgid "Wechat" -msgstr "微信" - -#: users/models/user.py:699 -msgid "OTP secret key" -msgstr "OTP 秘密" - -#: users/models/user.py:709 -msgid "Secret key" -msgstr "秘密キー" - -#: users/models/user.py:714 users/serializers/profile.py:149 -#: users/serializers/user.py:146 -msgid "Is first login" -msgstr "最初のログインです" - -#: users/models/user.py:725 -msgid "Source" -msgstr "ソース" - -#: users/models/user.py:729 -msgid "Date password last updated" -msgstr "最終更新日パスワード" - -#: users/models/user.py:732 -msgid "Need update password" -msgstr "更新パスワードが必要" - -#: users/models/user.py:907 -msgid "Can invite user" -msgstr "ユーザーを招待できます" - -#: users/models/user.py:908 -msgid "Can remove user" -msgstr "ユーザーを削除できます" - -#: users/models/user.py:909 -msgid "Can match user" -msgstr "ユーザーに一致できます" - -#: users/models/user.py:918 -msgid "Administrator" -msgstr "管理者" - -#: users/models/user.py:921 -msgid "Administrator is the super user of system" -msgstr "管理者はシステムのスーパーユーザーです" - -#: users/models/user.py:946 -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:28 -msgid "System roles" -msgstr "システムの役割" - -#: users/serializers/user.py:33 -msgid "Org roles" -msgstr "組織ロール" - -#: users/serializers/user.py:35 -msgid "System roles display" -msgstr "システムロール表示" - -#: users/serializers/user.py:36 -msgid "Org roles display" -msgstr "組織ロール表示" - -#: users/serializers/user.py:81 -#: xpack/plugins/change_auth_plan/models/base.py:35 -#: xpack/plugins/change_auth_plan/serializers/base.py:27 -msgid "Password strategy" -msgstr "パスワード戦略" - -#: users/serializers/user.py:83 -msgid "MFA enabled" -msgstr "MFA有効化" - -#: users/serializers/user.py:84 -msgid "MFA force enabled" -msgstr "MFAフォース有効化" - -#: users/serializers/user.py:86 -msgid "MFA level display" -msgstr "MFAレベル表示" - -#: users/serializers/user.py:88 -msgid "Login blocked" -msgstr "ログインブロック" - -#: users/serializers/user.py:91 -msgid "Can public key authentication" -msgstr "公開鍵認証が可能" - -#: users/serializers/user.py:151 -msgid "Avatar url" -msgstr "アバターURL" - -#: users/serializers/user.py:153 -msgid "Groups name" -msgstr "グループ名" - -#: users/serializers/user.py:154 -msgid "Source name" -msgstr "ソース名" - -#: users/serializers/user.py:155 -msgid "Organization role name" -msgstr "組織の役割名" - -#: users/serializers/user.py:156 -msgid "Super role name" -msgstr "スーパーロール名" - -#: users/serializers/user.py:157 -msgid "Total role name" -msgstr "合計ロール名" - -#: users/serializers/user.py:159 -msgid "Is wecom bound" -msgstr "企業の微信をバインドしているかどうか" - -#: users/serializers/user.py:160 -msgid "Is dingtalk bound" -msgstr "ピンをバインドしているかどうか" - -#: users/serializers/user.py:161 -msgid "Is feishu bound" -msgstr "飛本を縛ったかどうか" - -#: users/serializers/user.py:162 -msgid "Is OTP bound" -msgstr "仮想MFAがバインドされているか" - -#: users/serializers/user.py:164 -msgid "System role name" -msgstr "システムロール名" - -#: users/serializers/user.py:263 -msgid "Select users" -msgstr "ユーザーの選択" - -#: users/serializers/user.py:264 -msgid "For security, only list several users" -msgstr "セキュリティのために、複数のユーザーのみをリストします" - -#: users/serializers/user.py:299 -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/change_auth_plan/api/app.py:112 -#: xpack/plugins/change_auth_plan/api/asset.py:95 -msgid "The parameter 'action' must be [{}]" -msgstr "パラメータ 'action' は [{}] でなければなりません。" - -#: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models/asset.py:123 -msgid "Change auth plan" -msgstr "密かな計画" - -#: xpack/plugins/change_auth_plan/models/app.py:46 -#: xpack/plugins/change_auth_plan/models/app.py:95 -msgid "Application change auth plan" -msgstr "改密計画の適用" - -#: xpack/plugins/change_auth_plan/models/app.py:99 -#: xpack/plugins/change_auth_plan/models/app.py:151 -msgid "Application change auth plan execution" -msgstr "改密計画実行の適用" - -#: xpack/plugins/change_auth_plan/models/app.py:144 -#: xpack/plugins/change_auth_plan/serializers/app.py:64 -msgid "App" -msgstr "適用" - -#: xpack/plugins/change_auth_plan/models/app.py:156 -msgid "Application change auth plan task" -msgstr "改密計画タスクの適用" - -#: xpack/plugins/change_auth_plan/models/app.py:180 -#: xpack/plugins/change_auth_plan/models/asset.py:263 -msgid "Password cannot be set to blank, exit. " -msgstr "パスワードを空白に設定することはできません。" - -#: xpack/plugins/change_auth_plan/models/asset.py:29 -msgid "Append SSH KEY" -msgstr "追加" - -#: xpack/plugins/change_auth_plan/models/asset.py:30 -msgid "Empty and append SSH KEY" -msgstr "すべてクリアして追加" - -#: xpack/plugins/change_auth_plan/models/asset.py:31 -msgid "Replace (The key generated by JumpServer) " -msgstr "置換(JumpServerによって生成された鍵)" - -#: xpack/plugins/change_auth_plan/models/asset.py:49 -#: xpack/plugins/change_auth_plan/serializers/asset.py:36 -msgid "SSH Key strategy" -msgstr "SSHキー戦略" - -#: xpack/plugins/change_auth_plan/models/asset.py:67 -msgid "Asset change auth plan" -msgstr "資産変更のオースプラン" - -#: xpack/plugins/change_auth_plan/models/asset.py:134 -msgid "Asset change auth plan execution" -msgstr "資産変更のオースプランの実行" - -#: xpack/plugins/change_auth_plan/models/asset.py:210 -msgid "Change auth plan execution" -msgstr "改密計画の実行" - -#: xpack/plugins/change_auth_plan/models/asset.py:217 -msgid "Asset change auth plan task" -msgstr "資産改密計画タスク" - -#: xpack/plugins/change_auth_plan/models/asset.py:252 -msgid "This asset does not have a privileged user set: " -msgstr "このアセットには特権ユーザーセットがありません。" - -#: xpack/plugins/change_auth_plan/models/asset.py:258 -msgid "" -"The password and key of the current asset privileged user cannot be changed: " -msgstr "現在のアセット特権ユーザーのパスワードとキーは変更できません。" - -#: xpack/plugins/change_auth_plan/models/asset.py:269 -msgid "Public key cannot be set to null, exit. " -msgstr "公開鍵をnull、exitに設定することはできません。" - -#: xpack/plugins/change_auth_plan/models/base.py:28 -msgid "All assets use the same random password" -msgstr "すべての資産は同じランダムパスワードを使用します" - -#: xpack/plugins/change_auth_plan/models/base.py:29 -msgid "All assets use different random password" -msgstr "すべての資産は異なるランダムパスワードを使用します" - -#: xpack/plugins/change_auth_plan/models/base.py:39 -msgid "Password rules" -msgstr "パスワードルール" - -#: xpack/plugins/change_auth_plan/models/base.py:118 -msgid "Change auth plan snapshot" -msgstr "計画スナップショットの暗号化" - -#: xpack/plugins/change_auth_plan/models/base.py:187 -msgid "Ready" -msgstr "の準備を" - -#: xpack/plugins/change_auth_plan/models/base.py:188 -msgid "Preflight check" -msgstr "プリフライトチェック" - -#: xpack/plugins/change_auth_plan/models/base.py:189 -msgid "Change auth" -msgstr "秘密を改める" - -#: xpack/plugins/change_auth_plan/models/base.py:190 -msgid "Verify auth" -msgstr "パスワード/キーの確認" - -#: xpack/plugins/change_auth_plan/models/base.py:191 -msgid "Keep auth" -msgstr "パスワード/キーの保存" - -#: xpack/plugins/change_auth_plan/models/base.py:199 -msgid "Step" -msgstr "ステップ" - -#: xpack/plugins/change_auth_plan/notifications.py:8 -msgid "Notification of implementation result of encryption change plan" -msgstr "暗号化変更プランの実装結果の通知" - -#: xpack/plugins/change_auth_plan/notifications.py:18 -msgid "" -"{} - The encryption change task has been completed. See the attachment for " -"details" -msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください" - -#: xpack/plugins/change_auth_plan/notifications.py:19 -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 "" -"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" -"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" - -#: xpack/plugins/change_auth_plan/serializers/asset.py:33 -msgid "Change Password" -msgstr "パスワードの変更" - -#: xpack/plugins/change_auth_plan/serializers/asset.py:34 -msgid "Change SSH Key" -msgstr "SSHキーの変更" - -#: xpack/plugins/change_auth_plan/serializers/base.py:44 -msgid "Run times" -msgstr "実行時間" - -#: xpack/plugins/change_auth_plan/serializers/base.py:58 -msgid "* Please enter the correct password length" -msgstr "* 正しいパスワードの長さを入力してください" - -#: xpack/plugins/change_auth_plan/serializers/base.py:61 -msgid "* Password length range 6-30 bits" -msgstr "* パスワードの長さの範囲6-30ビット" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:236 -msgid "After many attempts to change the secret, it still failed" -msgstr "秘密を変更しようとする多くの試みの後、それはまだ失敗しました" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:255 -msgid "Invalid/incorrect password" -msgstr "パスワードが無効/間違っている" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:257 -msgid "Failed to connect to the host" -msgstr "ホストへの接続に失敗しました" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:259 -msgid "Data could not be sent to remote" -msgstr "データをリモートに送信できませんでした" - -#: 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: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:29 -msgid "Provider" -msgstr "プロバイダー" - -#: xpack/plugins/cloud/models.py:38 -msgid "Cloud account" -msgstr "クラウドアカウント" - -#: xpack/plugins/cloud/models.py:40 -msgid "Test cloud account" -msgstr "クラウドアカウントのテスト" - -#: xpack/plugins/cloud/models.py:84 xpack/plugins/cloud/serializers/task.py:72 -msgid "Account" -msgstr "アカウント" - -#: xpack/plugins/cloud/models.py:87 xpack/plugins/cloud/serializers/task.py:39 -msgid "Regions" -msgstr "リージョン" - -#: xpack/plugins/cloud/models.py:90 -msgid "Hostname strategy" -msgstr "ホスト名戦略" - -#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:73 -msgid "Unix admin user" -msgstr "Unix adminユーザー" - -#: xpack/plugins/cloud/models.py:103 xpack/plugins/cloud/serializers/task.py:74 -msgid "Windows admin user" -msgstr "Windows管理者" - -#: xpack/plugins/cloud/models.py:109 xpack/plugins/cloud/serializers/task.py:47 -msgid "IP network segment group" -msgstr "IPネットワークセグメントグループ" - -#: xpack/plugins/cloud/models.py:112 xpack/plugins/cloud/serializers/task.py:52 -msgid "Sync IP type" -msgstr "同期IPタイプ" - -#: xpack/plugins/cloud/models.py:115 xpack/plugins/cloud/serializers/task.py:77 -msgid "Always update" -msgstr "常に更新" - -#: xpack/plugins/cloud/models.py:121 -msgid "Date last sync" -msgstr "最終同期日" - -#: xpack/plugins/cloud/models.py:132 xpack/plugins/cloud/models.py:173 -msgid "Sync instance task" -msgstr "インスタンスの同期タスク" - -#: xpack/plugins/cloud/models.py:184 xpack/plugins/cloud/models.py:232 -msgid "Date sync" -msgstr "日付の同期" - -#: xpack/plugins/cloud/models.py:188 -msgid "Sync instance task execution" -msgstr "インスタンスタスクの同期実行" - -#: xpack/plugins/cloud/models.py:212 -msgid "Sync task" -msgstr "同期タスク" - -#: xpack/plugins/cloud/models.py:216 -msgid "Sync instance task history" -msgstr "インスタンスタスク履歴の同期" - -#: xpack/plugins/cloud/models.py:219 -msgid "Instance" -msgstr "インスタンス" - -#: xpack/plugins/cloud/models.py:236 -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:65 -msgid "Validity display" -msgstr "有効表示" - -#: xpack/plugins/cloud/serializers/account.py:66 -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:30 -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:37 -msgid "History count" -msgstr "実行回数" - -#: xpack/plugins/cloud/serializers/task.py:38 -msgid "Instance count" -msgstr "インスタンス数" - -#: xpack/plugins/cloud/serializers/task.py:71 -msgid "Linux admin user" -msgstr "Linux管理者" - -#: xpack/plugins/cloud/serializers/task.py:76 -#: xpack/plugins/gathered_user/serializers.py:20 -msgid "Periodic display" -msgstr "定期的な表示" - -#: xpack/plugins/cloud/utils.py:69 -msgid "Account unavailable" -msgstr "利用できないアカウント" - -#: xpack/plugins/gathered_user/meta.py:11 -msgid "Gathered user" -msgstr "収集されたユーザー" - -#: xpack/plugins/gathered_user/models.py:39 -msgid "Gather user task" -msgstr "ユーザータスクの収集" - -#: xpack/plugins/gathered_user/models.py:85 -msgid "gather user task execution" -msgstr "ユーザータスクの実行を収集" - -#: xpack/plugins/gathered_user/models.py:91 -msgid "Assets is empty, please change nodes" -msgstr "資産は空です。ノードを変更してください" - -#: xpack/plugins/gathered_user/serializers.py:21 -msgid "Executed times" -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 "コミュニティ版" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a06c70b86..0ff05dd98 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:b143c62843946c3e18b623d05065f12e9d3c578efe5cd0d2016056d2b8448ae8 -size 109495 +oid sha256:1c09abdddb5699aeaf832e1162b58ea9b520c10df3f80390c0ec680da3e18f4d +size 103641 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po deleted file mode 100644 index 3670a0b59..000000000 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ /dev/null @@ -1,6963 +0,0 @@ -# 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-11-17 17:34+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:25 acls/serializers/login_asset_acl.py:47 -#: applications/models/application.py:219 assets/models/asset.py:138 -#: assets/models/base.py:175 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:27 assets/models/domain.py:23 -#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 -#: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 -#: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/endpoint.py:14 terminal/models/endpoint.py:87 -#: terminal/models/storage.py:27 terminal/models/task.py:16 -#: terminal/models/terminal.py:101 users/forms/profile.py:33 -#: users/models/group.py:15 users/models/user.py:673 -#: xpack/plugins/cloud/models.py:27 -msgid "Name" -msgstr "名称" - -#: acls/models/base.py:27 assets/models/cmd_filter.py:88 -#: assets/models/user.py:252 terminal/models/endpoint.py:90 -msgid "Priority" -msgstr "优先级" - -#: acls/models/base.py:28 assets/models/cmd_filter.py:88 -#: assets/models/user.py:252 terminal/models/endpoint.py:91 -msgid "1-100, the lower the value will be match first" -msgstr "优先级可选范围为 1-100 (数值越小越优先)" - -#: acls/models/base.py:31 authentication/models.py:21 -#: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/base.py:88 terminal/models/sharing.py:28 tickets/const.py:39 -msgid "Active" -msgstr "激活中" - -#: acls/models/base.py:32 applications/models/application.py:232 -#: assets/models/asset.py:143 assets/models/asset.py:231 -#: assets/models/backup.py:54 assets/models/base.py:180 -#: assets/models/cluster.py:29 assets/models/cmd_filter.py:52 -#: assets/models/cmd_filter.py:100 assets/models/domain.py:24 -#: assets/models/domain.py:65 assets/models/group.py:23 -#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 -#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/endpoint.py:22 terminal/models/endpoint.py:97 -#: terminal/models/storage.py:30 terminal/models/terminal.py:115 -#: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 -#: users/models/group.py:16 users/models/user.py:712 -#: xpack/plugins/change_auth_plan/models/base.py:44 -#: xpack/plugins/cloud/models.py:34 xpack/plugins/cloud/models.py:118 -#: xpack/plugins/gathered_user/models.py:26 -msgid "Comment" -msgstr "备注" - -#: acls/models/login_acl.py:18 tickets/const.py:47 -#: tickets/templates/tickets/approve_check_password.html:49 -msgid "Reject" -msgstr "拒绝" - -#: acls/models/login_acl.py:19 assets/models/cmd_filter.py:79 -msgid "Allow" -msgstr "允许" - -#: acls/models/login_acl.py:20 acls/models/login_acl.py:75 -#: acls/models/login_asset_acl.py:17 tickets/const.py:9 -msgid "Login confirm" -msgstr "登录复核" - -#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:38 -#: audits/models.py:63 audits/models.py:105 audits/serializers.py:106 -#: authentication/models.py:19 authentication/models.py:54 -#: authentication/models.py:78 notifications/models/notification.py:12 -#: orgs/models.py:220 perms/models/base.py:84 rbac/builtin.py:120 -#: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:13 terminal/models/session.py:45 -#: terminal/models/sharing.py:33 terminal/notifications.py:94 -#: terminal/notifications.py:142 tickets/models/comment.py:21 users/const.py:14 -#: users/models/user.py:905 users/models/user.py:936 -#: users/serializers/group.py:19 -msgid "User" -msgstr "用户" - -#: acls/models/login_acl.py:28 -msgid "Rule" -msgstr "规则" - -#: acls/models/login_acl.py:31 acls/models/login_asset_acl.py:26 -#: acls/serializers/login_acl.py:17 acls/serializers/login_asset_acl.py:75 -#: assets/models/cmd_filter.py:93 audits/models.py:64 audits/serializers.py:57 -#: authentication/templates/authentication/_access_key_modal.html:34 -msgid "Action" -msgstr "动作" - -#: acls/models/login_acl.py:35 acls/models/login_asset_acl.py:32 -#: acls/serializers/login_acl.py:16 assets/models/cmd_filter.py:98 -msgid "Reviewers" -msgstr "审批人" - -#: acls/models/login_acl.py:42 -msgid "Login acl" -msgstr "登录访问控制" - -#: acls/models/login_asset_acl.py:21 -#: applications/serializers/application.py:124 -#: applications/serializers/application.py:164 -msgid "System User" -msgstr "系统用户" - -#: acls/models/login_asset_acl.py:22 -#: applications/serializers/attrs/application_category/remote_app.py:36 -#: assets/models/asset.py:386 assets/models/authbook.py:19 -#: assets/models/backup.py:31 assets/models/cmd_filter.py:42 -#: assets/models/gathered_user.py:14 assets/serializers/label.py:30 -#: assets/serializers/system_user.py:268 audits/models.py:40 -#: authentication/models.py:66 authentication/models.py:90 -#: perms/models/asset_permission.py:23 terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:47 -#: terminal/notifications.py:93 -#: xpack/plugins/change_auth_plan/models/asset.py:199 -#: xpack/plugins/change_auth_plan/serializers/asset.py:181 -#: xpack/plugins/cloud/models.py:225 -msgid "Asset" -msgstr "资产" - -#: acls/models/login_asset_acl.py:40 -msgid "Login asset acl" -msgstr "登录资产访问控制" - -#: acls/models/login_asset_acl.py:89 tickets/const.py:12 -msgid "Login asset confirm" -msgstr "登录资产复核" - -#: acls/serializers/login_acl.py:11 acls/serializers/login_asset_acl.py:12 -msgid "Format for comma-delimited string, with * indicating a match all. " -msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " - -#: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:17 -#: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176 -#: assets/models/gathered_user.py:15 audits/models.py:139 -#: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models.py:260 -#: authentication/templates/authentication/_msg_different_city.html:9 -#: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/forms/profile.py:112 -#: users/models/user.py:671 users/templates/users/_msg_user_created.html:12 -#: xpack/plugins/change_auth_plan/models/asset.py:34 -#: xpack/plugins/change_auth_plan/models/asset.py:195 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 -msgid "Username" -msgstr "用户名" - -#: acls/serializers/login_asset_acl.py:24 -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/login_asset_acl.py:31 acls/serializers/rules/rules.py:33 -#: applications/serializers/attrs/application_type/mysql_workbench.py:18 -#: assets/models/asset.py:210 assets/models/domain.py:61 -#: assets/serializers/account.py:13 -#: 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/login_asset_acl.py:35 assets/models/asset.py:211 -#: assets/serializers/account.py:14 assets/serializers/gathered_user.py:23 -#: settings/serializers/terminal.py:9 -msgid "Hostname" -msgstr "主机名" - -#: acls/serializers/login_asset_acl.py:42 -msgid "" -"Format for comma-delimited string, with * indicating a match all. Protocol " -"options: {}" -msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" - -#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:213 -#: assets/models/domain.py:63 assets/models/user.py:253 -#: terminal/serializers/session.py:31 terminal/serializers/storage.py:68 -msgid "Protocol" -msgstr "协议" - -#: acls/serializers/login_asset_acl.py:65 -msgid "Unsupported protocols: {}" -msgstr "不支持的协议: {}" - -#: acls/serializers/login_asset_acl.py:98 -#: tickets/serializers/ticket/ticket.py:96 -msgid "The organization `{}` does not exist" -msgstr "组织 `{}` 不存在" - -#: acls/serializers/login_asset_acl.py:103 -msgid "None of the reviewers belong to Organization `{}`" -msgstr "所有复核人都不属于组织 `{}`" - -#: acls/serializers/rules/rules.py:20 -#: xpack/plugins/cloud/serializers/task.py:24 -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:35 -msgid "Time Period" -msgstr "时段" - -#: applications/apps.py:9 applications/models/application.py:62 -msgid "Applications" -msgstr "应用管理" - -#: applications/const.py:8 -#: applications/serializers/attrs/application_category/db.py:14 -#: applications/serializers/attrs/application_type/mysql_workbench.py:26 -msgid "Database" -msgstr "数据库" - -#: applications/const.py:9 -msgid "Remote app" -msgstr "远程应用" - -#: applications/const.py:36 -msgid "Custom" -msgstr "自定义" - -#: applications/models/account.py:12 applications/models/application.py:236 -#: assets/models/backup.py:32 assets/models/cmd_filter.py:49 -#: authentication/models.py:67 authentication/models.py:95 -#: perms/models/application_permission.py:28 -#: xpack/plugins/change_auth_plan/models/app.py:32 -msgid "Application" -msgstr "应用程序" - -#: applications/models/account.py:15 assets/models/authbook.py:20 -#: assets/models/cmd_filter.py:46 assets/models/user.py:343 audits/models.py:41 -#: authentication/models.py:83 perms/models/application_permission.py:33 -#: perms/models/asset_permission.py:25 terminal/backends/command/models.py:22 -#: terminal/backends/command/serializers.py:36 terminal/models/session.py:49 -#: xpack/plugins/change_auth_plan/models/app.py:36 -#: xpack/plugins/change_auth_plan/models/app.py:147 -#: xpack/plugins/change_auth_plan/serializers/app.py:65 -msgid "System user" -msgstr "系统用户" - -#: applications/models/account.py:17 assets/models/authbook.py:21 -#: settings/serializers/auth/cas.py:20 -msgid "Version" -msgstr "版本" - -#: applications/models/account.py:23 -msgid "Application account" -msgstr "应用账号" - -#: applications/models/account.py:26 -msgid "Can view application account secret" -msgstr "可以查看应用账号密码" - -#: applications/models/account.py:27 -msgid "Can change application account secret" -msgstr "可以查看应用账号密码" - -#: applications/models/application.py:221 -#: applications/serializers/application.py:101 assets/models/label.py:21 -#: perms/models/application_permission.py:21 -#: perms/serializers/application/user_permission.py:33 settings/models.py:35 -#: tickets/models/ticket/apply_application.py:15 -#: xpack/plugins/change_auth_plan/models/app.py:25 -msgid "Category" -msgstr "类别" - -#: applications/models/application.py:224 -#: applications/serializers/application.py:103 assets/models/backup.py:49 -#: assets/models/cmd_filter.py:86 assets/models/user.py:251 -#: authentication/models.py:70 perms/models/application_permission.py:24 -#: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:59 terminal/models/storage.py:145 -#: tickets/models/comment.py:26 tickets/models/flow.py:57 -#: tickets/models/ticket/apply_application.py:18 -#: tickets/models/ticket/general.py:273 -#: xpack/plugins/change_auth_plan/models/app.py:28 -#: xpack/plugins/change_auth_plan/models/app.py:153 -msgid "Type" -msgstr "类型" - -#: applications/models/application.py:228 assets/models/asset.py:217 -#: assets/models/domain.py:29 assets/models/domain.py:64 -msgid "Domain" -msgstr "网域" - -#: applications/models/application.py:230 xpack/plugins/cloud/models.py:32 -#: xpack/plugins/cloud/serializers/account.py:64 -msgid "Attrs" -msgstr "属性" - -#: applications/models/application.py:240 -msgid "Can match application" -msgstr "匹配应用" - -#: applications/models/application.py:310 -msgid "Application user" -msgstr "应用用户" - -#: applications/serializers/application.py:72 -#: applications/serializers/application.py:102 assets/serializers/label.py:13 -#: perms/serializers/application/permission.py:18 -msgid "Category display" -msgstr "类别名称" - -#: applications/serializers/application.py:73 -#: applications/serializers/application.py:104 -#: assets/serializers/cmd_filter.py:34 assets/serializers/system_user.py:34 -#: audits/serializers.py:29 authentication/serializers/connection_token.py:22 -#: perms/serializers/application/permission.py:19 -#: tickets/serializers/flow.py:49 tickets/serializers/ticket/ticket.py:18 -msgid "Type display" -msgstr "类型名称" - -#: applications/serializers/application.py:105 assets/models/asset.py:230 -#: assets/models/base.py:181 assets/models/cluster.py:26 -#: assets/models/cmd_filter.py:53 assets/models/domain.py:26 -#: assets/models/gathered_user.py:19 assets/models/group.py:22 -#: assets/models/label.py:25 assets/serializers/account.py:18 -#: assets/serializers/cmd_filter.py:28 assets/serializers/cmd_filter.py:48 -#: authentication/models.py:22 common/db/models.py:116 -#: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 -#: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92 -#: users/models/group.py:18 users/models/user.py:937 -#: xpack/plugins/change_auth_plan/models/base.py:45 -#: xpack/plugins/cloud/models.py:127 -msgid "Date created" -msgstr "创建日期" - -#: applications/serializers/application.py:106 assets/models/base.py:182 -#: assets/models/cmd_filter.py:54 assets/models/gathered_user.py:20 -#: assets/serializers/account.py:21 assets/serializers/cmd_filter.py:29 -#: assets/serializers/cmd_filter.py:49 common/db/models.py:117 -#: common/mixins/models.py:51 ops/models/adhoc.py:40 orgs/models.py:224 -#: xpack/plugins/change_auth_plan/models/base.py:46 -msgid "Date updated" -msgstr "更新日期" - -#: applications/serializers/application.py:123 -#: applications/serializers/application.py:163 authentication/models.py:99 -msgid "Application display" -msgstr "应用名称" - -#: applications/serializers/application.py:125 -msgid "account" -msgstr "账号" - -#: applications/serializers/attrs/application_category/cloud.py:8 -#: assets/models/cluster.py:40 -msgid "Cluster" -msgstr "集群" - -#: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:16 -#: settings/serializers/auth/sms.py:67 terminal/models/endpoint.py:15 -#: xpack/plugins/cloud/serializers/account_attrs.py:72 -msgid "Host" -msgstr "主机" - -#: applications/serializers/attrs/application_category/db.py:12 -#: applications/serializers/attrs/application_type/clickhouse.py:11 -#: applications/serializers/attrs/application_type/mongodb.py:10 -#: applications/serializers/attrs/application_type/mysql.py:10 -#: applications/serializers/attrs/application_type/mysql_workbench.py:22 -#: applications/serializers/attrs/application_type/oracle.py:10 -#: applications/serializers/attrs/application_type/pgsql.py:10 -#: applications/serializers/attrs/application_type/redis.py:10 -#: applications/serializers/attrs/application_type/sqlserver.py:10 -#: assets/models/asset.py:214 assets/models/domain.py:62 -#: 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_category/db.py:16 -#: settings/serializers/email.py:37 -msgid "Use SSL" -msgstr "使用 SSL" - -#: applications/serializers/attrs/application_category/db.py:18 -msgid "CA certificate" -msgstr "CA 证书" - -#: applications/serializers/attrs/application_category/db.py:21 -msgid "Client certificate file" -msgstr "客户端证书" - -#: applications/serializers/attrs/application_category/db.py:24 -msgid "Certificate key file" -msgstr "证书秘钥" - -#: applications/serializers/attrs/application_category/db.py:26 -msgid "Allow invalid cert" -msgstr "忽略证书校验" - -#: applications/serializers/attrs/application_category/remote_app.py:34 -msgid "Asset Info" -msgstr "资产信息" - -#: applications/serializers/attrs/application_category/remote_app.py:39 -#: applications/serializers/attrs/application_type/chrome.py:14 -#: applications/serializers/attrs/application_type/mysql_workbench.py:14 -#: applications/serializers/attrs/application_type/vmware_client.py:18 -msgid "Application path" -msgstr "应用路径" - -#: applications/serializers/attrs/application_category/remote_app.py:44 -#: assets/serializers/system_user.py:167 -#: tickets/serializers/ticket/apply_application.py:38 -#: tickets/serializers/ticket/common.py:59 -#: xpack/plugins/change_auth_plan/serializers/asset.py:67 -#: xpack/plugins/change_auth_plan/serializers/asset.py:70 -#: xpack/plugins/change_auth_plan/serializers/asset.py:73 -#: xpack/plugins/change_auth_plan/serializers/asset.py:104 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "该字段是必填项。" - -#: applications/serializers/attrs/application_type/chrome.py:18 -#: applications/serializers/attrs/application_type/vmware_client.py:22 -msgid "Target URL" -msgstr "目标URL" - -#: applications/serializers/attrs/application_type/chrome.py:22 -msgid "Chrome username" -msgstr "Chrome 用户名" - -#: applications/serializers/attrs/application_type/chrome.py:26 -#: applications/serializers/attrs/application_type/chrome.py:33 -msgid "Chrome password" -msgstr "Chrome 密码" - -#: 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接口和本机接口使用不同的端口" - -#: applications/serializers/attrs/application_type/custom.py:12 -msgid "Operating parameter" -msgstr "运行参数" - -#: applications/serializers/attrs/application_type/custom.py:16 -msgid "Target url" -msgstr "目标URL" - -#: applications/serializers/attrs/application_type/custom.py:20 -msgid "Custom Username" -msgstr "自定义用户名" - -#: applications/serializers/attrs/application_type/custom.py:25 -#: applications/serializers/attrs/application_type/custom.py:32 -#: xpack/plugins/change_auth_plan/models/base.py:27 -msgid "Custom password" -msgstr "自定义密码" - -#: applications/serializers/attrs/application_type/mysql_workbench.py:30 -msgid "Mysql workbench username" -msgstr "Mysql 工作台 用户名" - -#: applications/serializers/attrs/application_type/mysql_workbench.py:35 -#: applications/serializers/attrs/application_type/mysql_workbench.py:42 -msgid "Mysql workbench password" -msgstr "Mysql 工作台 密码" - -#: applications/serializers/attrs/application_type/vmware_client.py:26 -msgid "Vmware username" -msgstr "Vmware 用户名" - -#: applications/serializers/attrs/application_type/vmware_client.py:31 -#: applications/serializers/attrs/application_type/vmware_client.py:38 -msgid "Vmware password" -msgstr "Vmware 密码" - -#: assets/api/domain.py:52 -msgid "Number required" -msgstr "需要为数字" - -#: assets/api/node.py:61 -msgid "You can't update the root node name" -msgstr "不能修改根节点名称" - -#: assets/api/node.py:68 -msgid "You can't delete the root node ({})" -msgstr "不能删除根节点 ({})" - -#: assets/api/node.py:71 -msgid "Deletion failed and the node contains assets" -msgstr "删除失败,节点包含资产" - -#: assets/apps.py:9 -msgid "App assets" -msgstr "资产管理" - -#: assets/models/asset.py:139 -msgid "Base" -msgstr "基础" - -#: assets/models/asset.py:140 -msgid "Charset" -msgstr "编码" - -#: assets/models/asset.py:141 assets/serializers/asset.py:176 -#: tickets/models/ticket/general.py:298 -msgid "Meta" -msgstr "元数据" - -#: assets/models/asset.py:142 -msgid "Internal" -msgstr "内部的" - -#: assets/models/asset.py:162 assets/models/asset.py:216 -#: assets/serializers/account.py:15 assets/serializers/asset.py:63 -#: perms/serializers/asset/user_permission.py:43 -#: xpack/plugins/cloud/serializers/account_attrs.py:179 -msgid "Platform" -msgstr "系统平台" - -#: assets/models/asset.py:168 -msgid "Vendor" -msgstr "制造商" - -#: assets/models/asset.py:169 -msgid "Model" -msgstr "型号" - -#: assets/models/asset.py:170 tickets/models/ticket/general.py:296 -msgid "Serial number" -msgstr "序列号" - -#: assets/models/asset.py:172 -msgid "CPU model" -msgstr "CPU型号" - -#: assets/models/asset.py:173 -msgid "CPU count" -msgstr "CPU数量" - -#: assets/models/asset.py:174 -msgid "CPU cores" -msgstr "CPU核数" - -#: assets/models/asset.py:175 -msgid "CPU vcpus" -msgstr "CPU总数" - -#: assets/models/asset.py:176 -msgid "Memory" -msgstr "内存" - -#: assets/models/asset.py:177 -msgid "Disk total" -msgstr "硬盘大小" - -#: assets/models/asset.py:178 -msgid "Disk info" -msgstr "硬盘信息" - -#: assets/models/asset.py:180 -msgid "OS" -msgstr "操作系统" - -#: assets/models/asset.py:181 -msgid "OS version" -msgstr "系统版本" - -#: assets/models/asset.py:182 -msgid "OS arch" -msgstr "系统架构" - -#: assets/models/asset.py:183 -msgid "Hostname raw" -msgstr "主机名原始" - -#: assets/models/asset.py:215 assets/serializers/account.py:16 -#: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41 -#: xpack/plugins/cloud/models.py:106 xpack/plugins/cloud/serializers/task.py:44 -msgid "Protocols" -msgstr "协议组" - -#: assets/models/asset.py:218 assets/models/cmd_filter.py:38 -#: assets/models/user.py:243 perms/models/asset_permission.py:24 -#: xpack/plugins/change_auth_plan/models/asset.py:43 -#: xpack/plugins/gathered_user/models.py:24 -msgid "Nodes" -msgstr "节点" - -#: assets/models/asset.py:219 assets/models/cmd_filter.py:51 -#: assets/models/domain.py:66 assets/models/label.py:22 -#: users/serializers/user.py:147 -msgid "Is active" -msgstr "激活" - -#: assets/models/asset.py:222 assets/models/cluster.py:19 -#: assets/models/user.py:240 assets/models/user.py:395 -msgid "Admin user" -msgstr "特权用户" - -#: assets/models/asset.py:225 xpack/plugins/cloud/const.py:32 -msgid "Public IP" -msgstr "公网IP" - -#: assets/models/asset.py:226 -msgid "Asset number" -msgstr "资产编号" - -#: assets/models/asset.py:228 -msgid "Labels" -msgstr "标签管理" - -#: assets/models/asset.py:229 assets/models/base.py:183 -#: assets/models/cluster.py:28 assets/models/cmd_filter.py:56 -#: assets/models/cmd_filter.py:103 assets/models/group.py:21 -#: common/db/models.py:114 common/mixins/models.py:49 orgs/models.py:71 -#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:720 -#: users/serializers/group.py:33 -#: xpack/plugins/change_auth_plan/models/base.py:48 -#: xpack/plugins/cloud/models.py:124 xpack/plugins/gathered_user/models.py:30 -msgid "Created by" -msgstr "创建者" - -#: assets/models/asset.py:389 -msgid "Can refresh asset hardware info" -msgstr "可以更新资产硬件信息" - -#: assets/models/asset.py:390 -msgid "Can test asset connectivity" -msgstr "可以测试资产连接性" - -#: assets/models/asset.py:391 -msgid "Can push system user to asset" -msgstr "可以推送系统用户到资产" - -#: assets/models/asset.py:392 -msgid "Can match asset" -msgstr "可以匹配资产" - -#: assets/models/asset.py:393 -msgid "Add asset to node" -msgstr "添加资产到节点" - -#: assets/models/asset.py:394 -msgid "Move asset to node" -msgstr "移动资产到节点" - -#: assets/models/authbook.py:27 -msgid "AuthBook" -msgstr "资产账号" - -#: assets/models/authbook.py:30 -msgid "Can test asset account connectivity" -msgstr "可以测试资产账号连接性" - -#: assets/models/authbook.py:31 -msgid "Can view asset account secret" -msgstr "可以查看资产账号密码" - -#: assets/models/authbook.py:32 -msgid "Can change asset account secret" -msgstr "可以更改资产账号密码" - -#: assets/models/authbook.py:33 -msgid "Can view asset history account" -msgstr "可以查看资产历史账号" - -#: assets/models/authbook.py:34 -msgid "Can view asset history account secret" -msgstr "可以查看资产历史账号密码" - -#: assets/models/backup.py:30 perms/models/base.py:54 -#: settings/serializers/terminal.py:14 -msgid "All" -msgstr "全部" - -#: assets/models/backup.py:52 assets/serializers/backup.py:32 -#: xpack/plugins/change_auth_plan/models/app.py:41 -#: xpack/plugins/change_auth_plan/models/asset.py:62 -#: xpack/plugins/change_auth_plan/serializers/base.py:45 -msgid "Recipient" -msgstr "收件人" - -#: assets/models/backup.py:62 assets/models/backup.py:124 -msgid "Account backup plan" -msgstr "账号备份计划" - -#: assets/models/backup.py:100 -#: xpack/plugins/change_auth_plan/models/base.py:107 -msgid "Manual trigger" -msgstr "手动触发" - -#: assets/models/backup.py:101 -#: xpack/plugins/change_auth_plan/models/base.py:108 -msgid "Timing trigger" -msgstr "定时触发" - -#: assets/models/backup.py:105 audits/models.py:45 ops/models/command.py:31 -#: perms/models/base.py:89 terminal/models/session.py:59 -#: tickets/models/ticket/apply_application.py:29 -#: tickets/models/ticket/apply_asset.py:23 -#: xpack/plugins/change_auth_plan/models/base.py:112 -#: xpack/plugins/change_auth_plan/models/base.py:203 -#: xpack/plugins/gathered_user/models.py:76 -msgid "Date start" -msgstr "开始日期" - -#: assets/models/backup.py:108 -#: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:187 ops/models/adhoc.py:258 -#: xpack/plugins/change_auth_plan/models/base.py:115 -#: xpack/plugins/change_auth_plan/models/base.py:204 -#: xpack/plugins/gathered_user/models.py:79 -msgid "Time" -msgstr "时间" - -#: assets/models/backup.py:112 -msgid "Account backup snapshot" -msgstr "账号备份快照" - -#: assets/models/backup.py:116 assets/serializers/backup.py:40 -#: xpack/plugins/change_auth_plan/models/base.py:125 -#: xpack/plugins/change_auth_plan/serializers/base.py:78 -msgid "Trigger mode" -msgstr "触发模式" - -#: assets/models/backup.py:119 audits/models.py:145 -#: terminal/models/sharing.py:108 -#: xpack/plugins/change_auth_plan/models/base.py:201 -#: xpack/plugins/change_auth_plan/serializers/app.py:66 -#: xpack/plugins/change_auth_plan/serializers/asset.py:180 -#: xpack/plugins/cloud/models.py:181 -msgid "Reason" -msgstr "原因" - -#: assets/models/backup.py:121 audits/serializers.py:88 -#: audits/serializers.py:103 ops/models/adhoc.py:260 -#: terminal/serializers/session.py:36 -#: xpack/plugins/change_auth_plan/models/base.py:202 -#: xpack/plugins/change_auth_plan/serializers/app.py:67 -#: xpack/plugins/change_auth_plan/serializers/asset.py:182 -msgid "Is success" -msgstr "是否成功" - -#: assets/models/backup.py:128 -msgid "Account backup execution" -msgstr "账号备份执行" - -#: assets/models/base.py:30 assets/tasks/const.py:51 audits/const.py:5 -#: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 -#: common/utils/ip/utils.py:84 -msgid "Unknown" -msgstr "未知" - -#: assets/models/base.py:31 -msgid "Ok" -msgstr "成功" - -#: assets/models/base.py:32 audits/models.py:136 -#: xpack/plugins/change_auth_plan/serializers/app.py:88 -#: xpack/plugins/change_auth_plan/serializers/asset.py:199 -#: xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失败" - -#: assets/models/base.py:38 assets/serializers/domain.py:47 -msgid "Connectivity" -msgstr "可连接性" - -#: assets/models/base.py:40 authentication/models.py:263 -msgid "Date verified" -msgstr "校验日期" - -#: assets/models/base.py:177 assets/serializers/base.py:15 -#: assets/serializers/base.py:37 assets/serializers/system_user.py:29 -#: audits/signal_handlers.py:58 authentication/confirm/password.py:9 -#: authentication/forms.py:32 -#: authentication/templates/authentication/login.html:228 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:94 -#: users/templates/users/_msg_user_created.html:13 -#: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/change_auth_plan/models/base.py:42 -#: xpack/plugins/change_auth_plan/models/base.py:121 -#: xpack/plugins/change_auth_plan/models/base.py:196 -#: xpack/plugins/change_auth_plan/serializers/base.py:21 -#: xpack/plugins/change_auth_plan/serializers/base.py:73 -#: xpack/plugins/cloud/serializers/account_attrs.py:28 -msgid "Password" -msgstr "密码" - -#: assets/models/base.py:178 assets/serializers/base.py:41 -#: xpack/plugins/change_auth_plan/models/asset.py:53 -#: xpack/plugins/change_auth_plan/models/asset.py:130 -#: xpack/plugins/change_auth_plan/models/asset.py:206 -msgid "SSH private key" -msgstr "SSH密钥" - -#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models/asset.py:56 -#: xpack/plugins/change_auth_plan/models/asset.py:126 -#: xpack/plugins/change_auth_plan/models/asset.py:202 -msgid "SSH public key" -msgstr "SSH公钥" - -#: assets/models/cluster.py:20 -msgid "Bandwidth" -msgstr "带宽" - -#: assets/models/cluster.py:21 -msgid "Contact" -msgstr "联系人" - -#: assets/models/cluster.py:22 users/models/user.py:693 -msgid "Phone" -msgstr "手机" - -#: assets/models/cluster.py:23 -msgid "Address" -msgstr "地址" - -#: assets/models/cluster.py:24 -msgid "Intranet" -msgstr "内网" - -#: assets/models/cluster.py:25 -msgid "Extranet" -msgstr "外网" - -#: assets/models/cluster.py:27 -msgid "Operator" -msgstr "运营商" - -#: assets/models/cluster.py:36 assets/models/group.py:34 -#: xpack/plugins/cloud/providers/nutanix.py:30 -msgid "Default" -msgstr "默认" - -#: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6 -#: users/models/user.py:922 -msgid "System" -msgstr "系统" - -#: assets/models/cluster.py:36 -msgid "Default Cluster" -msgstr "默认Cluster" - -#: assets/models/cmd_filter.py:34 perms/models/base.py:86 -#: users/models/group.py:31 users/models/user.py:679 -msgid "User group" -msgstr "用户组" - -#: assets/models/cmd_filter.py:64 assets/serializers/system_user.py:59 -msgid "Command filter" -msgstr "命令过滤器" - -#: assets/models/cmd_filter.py:71 -msgid "Regex" -msgstr "正则表达式" - -#: assets/models/cmd_filter.py:72 ops/models/command.py:26 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:56 -#: terminal/templates/terminal/_msg_command_alert.html:12 -#: terminal/templates/terminal/_msg_command_execute_alert.html:10 -msgid "Command" -msgstr "命令" - -#: assets/models/cmd_filter.py:78 -msgid "Deny" -msgstr "拒绝" - -#: assets/models/cmd_filter.py:80 -msgid "Reconfirm" -msgstr "复核" - -#: assets/models/cmd_filter.py:84 -msgid "Filter" -msgstr "过滤器" - -#: assets/models/cmd_filter.py:91 settings/serializers/basic.py:10 -#: xpack/plugins/license/models.py:29 -msgid "Content" -msgstr "内容" - -#: assets/models/cmd_filter.py:91 -msgid "One line one command" -msgstr "每行一个命令" - -#: assets/models/cmd_filter.py:92 -msgid "Ignore case" -msgstr "忽略大小写" - -#: assets/models/cmd_filter.py:107 -msgid "Command filter rule" -msgstr "命令过滤规则" - -#: assets/models/cmd_filter.py:151 -msgid "The generated regular expression is incorrect: {}" -msgstr "生成的正则表达式有误" - -#: assets/models/cmd_filter.py:177 tickets/const.py:13 -msgid "Command confirm" -msgstr "命令复核" - -#: assets/models/domain.py:73 -msgid "Gateway" -msgstr "网关" - -#: assets/models/domain.py:75 -msgid "Test gateway" -msgstr "测试网关" - -#: assets/models/domain.py:131 -#, python-brace-format -msgid "Unable to connect to port {port} on {ip}" -msgstr "无法连接到 {ip} 上的端口 {port}" - -#: assets/models/domain.py:134 authentication/middleware.py:75 -#: xpack/plugins/cloud/providers/fc.py:48 -msgid "Authentication failed" -msgstr "认证失败" - -#: assets/models/domain.py:136 assets/models/domain.py:158 -msgid "Connect failed" -msgstr "连接失败" - -#: assets/models/gathered_user.py:16 -msgid "Present" -msgstr "存在" - -#: assets/models/gathered_user.py:17 -msgid "Date last login" -msgstr "最后登录日期" - -#: assets/models/gathered_user.py:18 -msgid "IP last login" -msgstr "最后登录IP" - -#: assets/models/gathered_user.py:31 -msgid "GatherUser" -msgstr "收集用户" - -#: assets/models/group.py:30 -msgid "Asset group" -msgstr "资产组" - -#: assets/models/group.py:34 -msgid "Default asset group" -msgstr "默认资产组" - -#: assets/models/label.py:19 assets/models/node.py:553 settings/models.py:34 -msgid "Value" -msgstr "值" - -#: assets/models/label.py:40 settings/serializers/sms.py:7 -msgid "Label" -msgstr "标签" - -#: assets/models/node.py:158 -msgid "New node" -msgstr "新节点" - -#: assets/models/node.py:481 -msgid "empty" -msgstr "空" - -#: assets/models/node.py:552 perms/models/asset_permission.py:101 -msgid "Key" -msgstr "键" - -#: assets/models/node.py:554 assets/serializers/node.py:20 -msgid "Full value" -msgstr "全称" - -#: assets/models/node.py:557 perms/models/asset_permission.py:102 -msgid "Parent key" -msgstr "ssh私钥" - -#: assets/models/node.py:566 assets/serializers/system_user.py:267 -#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers/task.py:75 -msgid "Node" -msgstr "节点" - -#: assets/models/node.py:569 -msgid "Can match node" -msgstr "可以匹配节点" - -#: assets/models/user.py:234 -msgid "Automatic managed" -msgstr "托管密码" - -#: assets/models/user.py:235 -msgid "Manually input" -msgstr "手动输入" - -#: assets/models/user.py:239 -msgid "Common user" -msgstr "普通用户" - -#: assets/models/user.py:242 -msgid "Username same with user" -msgstr "用户名与用户相同" - -#: assets/models/user.py:245 assets/serializers/domain.py:30 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -#: xpack/plugins/change_auth_plan/models/asset.py:39 -msgid "Assets" -msgstr "资产" - -#: assets/models/user.py:249 users/apps.py:9 -msgid "Users" -msgstr "用户管理" - -#: assets/models/user.py:250 -msgid "User groups" -msgstr "用户组" - -#: assets/models/user.py:254 -msgid "Auto push" -msgstr "自动推送" - -#: assets/models/user.py:255 -msgid "Sudo" -msgstr "Sudo" - -#: assets/models/user.py:256 -msgid "Shell" -msgstr "Shell" - -#: assets/models/user.py:257 -msgid "Login mode" -msgstr "认证方式" - -#: assets/models/user.py:258 -msgid "SFTP Root" -msgstr "SFTP根路径" - -#: assets/models/user.py:259 assets/serializers/system_user.py:37 -#: authentication/models.py:52 -msgid "Token" -msgstr "Token" - -#: assets/models/user.py:260 -msgid "Home" -msgstr "家目录" - -#: assets/models/user.py:261 -msgid "System groups" -msgstr "用户组" - -#: assets/models/user.py:264 -msgid "User switch" -msgstr "用户切换" - -#: assets/models/user.py:265 -msgid "Switch from" -msgstr "切换自" - -#: assets/models/user.py:345 -msgid "Can match system user" -msgstr "可以匹配系统用户" - -#: assets/models/utils.py:35 -#, 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:19 -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/serializers/account.py:36 assets/serializers/account.py:83 -#: assets/serializers/account_history.py:10 authentication/models.py:87 -msgid "System user display" -msgstr "系统用户名称" - -#: assets/serializers/asset.py:20 -msgid "Protocol format should {}/{}" -msgstr "协议格式 {}/{}" - -#: assets/serializers/asset.py:37 -msgid "Protocol duplicate: {}" -msgstr "协议重复: {}" - -#: assets/serializers/asset.py:66 -msgid "Domain name" -msgstr "网域名称" - -#: assets/serializers/asset.py:68 -msgid "Nodes name" -msgstr "节点名称" - -#: assets/serializers/asset.py:71 -msgid "Labels name" -msgstr "标签名称" - -#: assets/serializers/asset.py:105 -msgid "Hardware info" -msgstr "硬件信息" - -#: assets/serializers/asset.py:106 -msgid "Admin user display" -msgstr "特权用户名称" - -#: assets/serializers/asset.py:107 -msgid "CPU info" -msgstr "CPU信息" - -#: assets/serializers/backup.py:20 perms/models/base.py:87 -#: perms/serializers/application/permission.py:17 -#: perms/serializers/application/permission.py:42 -#: perms/serializers/asset/permission.py:18 -#: perms/serializers/asset/permission.py:46 -#: tickets/models/ticket/apply_application.py:27 -#: tickets/models/ticket/apply_asset.py:21 -msgid "Actions" -msgstr "动作" - -#: assets/serializers/backup.py:31 ops/mixin.py:26 ops/mixin.py:106 -#: ops/mixin.py:147 settings/serializers/auth/ldap.py:66 -#: xpack/plugins/change_auth_plan/serializers/base.py:43 -msgid "Periodic perform" -msgstr "定时执行" - -#: assets/serializers/backup.py:33 -#: xpack/plugins/change_auth_plan/serializers/base.py:46 -msgid "Currently only mail sending is supported" -msgstr "当前只支持邮件发送" - -#: assets/serializers/base.py:16 users/models/user.py:703 -msgid "Private key" -msgstr "ssh私钥" - -#: assets/serializers/base.py:45 -msgid "Key password" -msgstr "密钥密码" - -#: assets/serializers/base.py:58 -msgid "private key invalid or passphrase error" -msgstr "密钥不合法或密钥密码错误" - -#: assets/serializers/cmd_filter.py:35 assets/serializers/cmd_filter.py:50 -msgid "Action display" -msgstr "动作名称" - -#: assets/serializers/cmd_filter.py:51 ops/models/adhoc.py:155 -msgid "Pattern" -msgstr "模式" - -#: assets/serializers/domain.py:14 assets/serializers/label.py:12 -#: assets/serializers/system_user.py:63 -#: perms/serializers/asset/permission.py:49 -msgid "Assets amount" -msgstr "资产数量" - -#: assets/serializers/domain.py:15 -msgid "Applications amount" -msgstr "应用数量" - -#: assets/serializers/domain.py:16 -msgid "Gateways count" -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/system_user.py:35 -msgid "SSH key fingerprint" -msgstr "密钥指纹" - -#: assets/serializers/system_user.py:40 -#: perms/serializers/application/permission.py:46 -msgid "Apps amount" -msgstr "应用数量" - -#: assets/serializers/system_user.py:62 -#: perms/serializers/asset/permission.py:50 -msgid "Nodes amount" -msgstr "节点数量" - -#: assets/serializers/system_user.py:64 assets/serializers/system_user.py:269 -msgid "Login mode display" -msgstr "认证方式名称" - -#: assets/serializers/system_user.py:66 -msgid "Ad domain" -msgstr "Ad 网域" - -#: assets/serializers/system_user.py:67 -msgid "Is asset protocol" -msgstr "资产协议" - -#: assets/serializers/system_user.py:68 -msgid "Only ssh and automatic login system users are supported" -msgstr "仅支持ssh协议和自动登录的系统用户" - -#: assets/serializers/system_user.py:108 -msgid "Username same with user with protocol {} only allow 1" -msgstr "用户名和用户相同的一种协议只允许存在一个" - -#: assets/serializers/system_user.py:121 common/validators.py:14 -msgid "Special char not allowed" -msgstr "不能包含特殊字符" - -#: assets/serializers/system_user.py:131 -msgid "* Automatic login mode must fill in the username." -msgstr "自动登录模式,必须填写用户名" - -#: assets/serializers/system_user.py:146 -msgid "Path should starts with /" -msgstr "路径应该以 / 开头" - -#: assets/serializers/system_user.py:158 -msgid "Password or private key required" -msgstr "密码或密钥密码需要一个" - -#: assets/serializers/system_user.py:172 -msgid "Only ssh protocol system users are allowed" -msgstr "仅允许ssh协议的系统用户" - -#: assets/serializers/system_user.py:176 -msgid "The protocol must be consistent with the current user: {}" -msgstr "协议必须和当前用户保持一致: {}" - -#: assets/serializers/system_user.py:180 -msgid "Only system users with automatic login are allowed" -msgstr "仅允许自动登录的系统用户" - -#: assets/serializers/system_user.py:288 -msgid "System user name" -msgstr "系统用户名称" - -#: assets/serializers/system_user.py:289 orgs/mixins/serializers.py:26 -#: rbac/serializers/rolebinding.py:23 -msgid "Org name" -msgstr "组织名称" - -#: assets/serializers/system_user.py:298 -msgid "Asset hostname" -msgstr "资产主机名" - -#: assets/serializers/utils.py:11 -msgid "Password can not contains `{{` " -msgstr "密码不能包含 `{{` 字符" - -#: assets/serializers/utils.py:14 -msgid "Password can not contains `'` " -msgstr "密码不能包含 `'` 字符" - -#: assets/serializers/utils.py:16 -msgid "Password can not contains `\"` " -msgstr "密码不能包含 `\"` 字符" - -#: assets/tasks/account_connectivity.py:30 -msgid "The asset {} system platform {} does not support run Ansible tasks" -msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" - -#: assets/tasks/account_connectivity.py:107 -msgid "Test account connectivity: " -msgstr "测试账号可连接性: " - -#: assets/tasks/asset_connectivity.py:49 -msgid "Test assets connectivity. " -msgstr "测试资产可连接性. " - -#: assets/tasks/asset_connectivity.py:91 assets/tasks/asset_connectivity.py:102 -msgid "Test assets connectivity: " -msgstr "测试资产可连接性: " - -#: assets/tasks/asset_connectivity.py:113 -msgid "Test if the assets under the node are connectable: " -msgstr "测试节点下资产是否可连接: " - -#: assets/tasks/const.py:49 -msgid "Unreachable" -msgstr "不可达" - -#: assets/tasks/const.py:50 -msgid "Reachable" -msgstr "可连接" - -#: assets/tasks/gather_asset_hardware_info.py:46 -msgid "Get asset info failed: {}" -msgstr "获取资产信息失败:{}" - -#: assets/tasks/gather_asset_hardware_info.py:97 -msgid "Update some assets hardware info. " -msgstr "更新资产硬件信息. " - -#: assets/tasks/gather_asset_hardware_info.py:114 -msgid "Update asset hardware info: " -msgstr "更新资产硬件信息: " - -#: assets/tasks/gather_asset_hardware_info.py:120 -msgid "Update assets hardware info: " -msgstr "更新资产硬件信息: " - -#: assets/tasks/gather_asset_hardware_info.py:137 -msgid "Update node asset hardware information: " -msgstr "更新节点资产硬件信息: " - -#: assets/tasks/gather_asset_users.py:111 -msgid "Gather assets users" -msgstr "收集资产上的用户" - -#: assets/tasks/nodes_amount.py:27 -msgid "" -"The task of self-checking is already running and cannot be started repeatedly" -msgstr "自检程序已经在运行,不能重复启动" - -#: assets/tasks/push_system_user.py:201 -msgid "System user is dynamic: {}" -msgstr "系统用户是动态的: {}" - -#: assets/tasks/push_system_user.py:242 -msgid "Start push system user for platform: [{}]" -msgstr "推送系统用户到平台: [{}]" - -#: assets/tasks/push_system_user.py:243 -#: assets/tasks/system_user_connectivity.py:106 -msgid "Hosts count: {}" -msgstr "主机数量: {}" - -#: assets/tasks/push_system_user.py:264 assets/tasks/push_system_user.py:297 -msgid "Push system users to assets: " -msgstr "推送系统用户到入资产: " - -#: assets/tasks/push_system_user.py:276 -msgid "Push system users to asset: " -msgstr "推送系统用户到入资产: " - -#: assets/tasks/system_user_connectivity.py:56 -msgid "Dynamic system user not support test" -msgstr "动态系统用户不支持测试" - -#: assets/tasks/system_user_connectivity.py:105 -msgid "Start test system user connectivity for platform: [{}]" -msgstr "开始测试系统用户在该系统平台的可连接性: [{}]" - -#: assets/tasks/system_user_connectivity.py:118 -#: assets/tasks/system_user_connectivity.py:129 -msgid "Test system user connectivity: " -msgstr "测试系统用户可连接性: " - -#: assets/tasks/system_user_connectivity.py:148 -msgid "Test system user connectivity period: " -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 "没有匹配到资产,结束任务" - -#: 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/handler.py:134 -msgid "Yes" -msgstr "是" - -#: audits/handler.py:134 -msgid "No" -msgstr "否" - -#: audits/models.py:28 audits/models.py:60 -#: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:226 -msgid "Delete" -msgstr "删除" - -#: audits/models.py:29 -msgid "Upload" -msgstr "上传文件" - -#: audits/models.py:30 -msgid "Download" -msgstr "下载文件" - -#: audits/models.py:31 -msgid "Rmdir" -msgstr "删除目录" - -#: audits/models.py:32 -msgid "Rename" -msgstr "重命名" - -#: audits/models.py:33 -msgid "Mkdir" -msgstr "创建目录" - -#: audits/models.py:34 -msgid "Symlink" -msgstr "建立软链接" - -#: audits/models.py:39 audits/models.py:67 audits/models.py:107 -#: terminal/models/session.py:52 terminal/models/sharing.py:96 -msgid "Remote addr" -msgstr "远端地址" - -#: audits/models.py:42 -msgid "Operate" -msgstr "操作" - -#: audits/models.py:43 -msgid "Filename" -msgstr "文件名" - -#: audits/models.py:44 audits/models.py:135 terminal/models/sharing.py:104 -#: tickets/views/approve.py:115 -#: xpack/plugins/change_auth_plan/serializers/app.py:87 -#: xpack/plugins/change_auth_plan/serializers/asset.py:198 -msgid "Success" -msgstr "成功" - -#: audits/models.py:48 -msgid "File transfer log" -msgstr "文件管理" - -#: audits/models.py:57 -#: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:223 -msgid "Create" -msgstr "创建" - -#: audits/models.py:58 rbac/tree.py:224 -msgid "View" -msgstr "查看" - -#: audits/models.py:59 rbac/tree.py:225 templates/_csv_import_export.html:18 -#: templates/_csv_update_modal.html:6 -msgid "Update" -msgstr "更新" - -#: audits/models.py:65 audits/serializers.py:69 -msgid "Resource Type" -msgstr "资源类型" - -#: audits/models.py:66 -msgid "Resource" -msgstr "资源" - -#: audits/models.py:68 audits/models.py:108 -#: terminal/backends/command/serializers.py:40 -msgid "Datetime" -msgstr "日期" - -#: audits/models.py:100 -msgid "Operate log" -msgstr "操作日志" - -#: audits/models.py:106 -msgid "Change by" -msgstr "修改者" - -#: audits/models.py:114 -msgid "Password change log" -msgstr "改密日志" - -#: audits/models.py:129 -msgid "Disabled" -msgstr "禁用" - -#: audits/models.py:130 settings/models.py:37 -msgid "Enabled" -msgstr "启用" - -#: audits/models.py:131 -msgid "-" -msgstr "-" - -#: audits/models.py:140 -msgid "Login type" -msgstr "登录方式" - -#: audits/models.py:141 tickets/models/ticket/login_confirm.py:10 -msgid "Login ip" -msgstr "登录IP" - -#: audits/models.py:142 -#: authentication/templates/authentication/_msg_different_city.html:11 -#: tickets/models/ticket/login_confirm.py:11 -msgid "Login city" -msgstr "登录城市" - -#: audits/models.py:143 audits/serializers.py:44 -msgid "User agent" -msgstr "用户代理" - -#: audits/models.py:144 -#: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:696 -#: users/serializers/profile.py:126 -msgid "MFA" -msgstr "MFA" - -#: audits/models.py:146 terminal/models/status.py:33 -#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:177 -#: xpack/plugins/cloud/models.py:229 -msgid "Status" -msgstr "状态" - -#: audits/models.py:147 -msgid "Date login" -msgstr "登录日期" - -#: audits/models.py:148 audits/serializers.py:46 -msgid "Authentication backend" -msgstr "认证方式" - -#: audits/models.py:187 -msgid "User login log" -msgstr "用户登录日志" - -#: audits/serializers.py:14 -msgid "Operate display" -msgstr "操作名称" - -#: audits/serializers.py:30 tickets/serializers/ticket/ticket.py:19 -msgid "Status display" -msgstr "状态名称" - -#: audits/serializers.py:31 -msgid "MFA display" -msgstr "MFA名称" - -#: audits/serializers.py:45 -msgid "Reason display" -msgstr "原因描述" - -#: audits/serializers.py:90 -msgid "Hosts display" -msgstr "主机名称" - -#: audits/serializers.py:102 ops/models/command.py:27 -#: xpack/plugins/cloud/models.py:175 -msgid "Result" -msgstr "结果" - -#: audits/serializers.py:104 terminal/serializers/storage.py:157 -msgid "Hosts" -msgstr "主机" - -#: audits/serializers.py:105 -msgid "Run as" -msgstr "运行用户" - -#: audits/serializers.py:107 -msgid "Run as display" -msgstr "运行用户名称" - -#: audits/serializers.py:108 authentication/models.py:81 -#: rbac/serializers/rolebinding.py:21 -msgid "User display" -msgstr "用户名称" - -#: audits/signal_handlers.py:57 -msgid "SSH Key" -msgstr "SSH 密钥" - -#: audits/signal_handlers.py:59 settings/serializers/auth/sso.py:10 -msgid "SSO" -msgstr "SSO" - -#: audits/signal_handlers.py:60 -msgid "Auth Token" -msgstr "认证令牌" - -#: audits/signal_handlers.py:61 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:734 -msgid "WeCom" -msgstr "企业微信" - -#: audits/signal_handlers.py:62 authentication/views/feishu.py:144 -#: authentication/views/login.py:85 notifications/backends/__init__.py:14 -#: settings/serializers/auth/feishu.py:10 users/models/user.py:736 -msgid "FeiShu" -msgstr "飞书" - -#: audits/signal_handlers.py:63 authentication/views/dingtalk.py:179 -#: authentication/views/login.py:79 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:735 -msgid "DingTalk" -msgstr "钉钉" - -#: audits/signal_handlers.py:64 authentication/models.py:267 -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.py:224 -#: 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:256 -#: 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 authentication/models.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 ip 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:242 -#: authentication/views/dingtalk.py:296 -msgid "DingTalk is not bound" -msgstr "钉钉没有绑定" - -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 -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:76 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.py:37 -msgid "Access key" -msgstr "Access key" - -#: authentication/models.py:44 -msgid "Private Token" -msgstr "SSH密钥" - -#: authentication/models.py:53 -msgid "Expired" -msgstr "过期时间" - -#: authentication/models.py:57 -msgid "SSO token" -msgstr "SSO token" - -#: authentication/models.py:72 authentication/models.py:261 -#: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:19 -msgid "Secret" -msgstr "密钥" - -#: authentication/models.py:74 authentication/models.py:264 -#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:717 -msgid "Date expired" -msgstr "失效日期" - -#: authentication/models.py:93 -msgid "Asset display" -msgstr "资产名称" - -#: authentication/models.py:104 -msgid "Connection token" -msgstr "连接令牌" - -#: authentication/models.py:106 -msgid "Can view connection token secret" -msgstr "可以查看连接令牌密文" - -#: authentication/models.py:149 -msgid "Connection token expired at: {}" -msgstr "连接令牌过期: {}" - -#: authentication/models.py:154 -msgid "User not exists" -msgstr "用户不存在" - -#: authentication/models.py:163 -msgid "System user not exists" -msgstr "系统用户不存在" - -#: authentication/models.py:169 -msgid "Asset not exists" -msgstr "资产不存在" - -#: authentication/models.py:173 -msgid "Asset inactive" -msgstr "资产未激活" - -#: authentication/models.py:180 -msgid "User has no permission to access asset or permission expired" -msgstr "用户没有权限访问资产或权限已过期" - -#: authentication/models.py:188 -msgid "Application not exists" -msgstr "应用不存在" - -#: authentication/models.py:195 -msgid "User has no permission to access application or permission expired" -msgstr "用户没有权限访问应用或权限已过期" - -#: authentication/models.py:262 -msgid "Verified" -msgstr "已校验" - -#: authentication/models.py:283 -msgid "Super connection token" -msgstr "超级连接令牌" - -#: authentication/notifications.py:19 -msgid "Different city login reminder" -msgstr "异地登录提醒" - -#: authentication/notifications.py:52 -msgid "binding reminder" -msgstr "绑定提醒" - -#: authentication/serializers/connection_token.py:23 -#: xpack/plugins/cloud/models.py:33 -msgid "Validity" -msgstr "有效" - -#: authentication/serializers/connection_token.py:24 -msgid "Expired time" -msgstr "过期时间" - -#: authentication/serializers/connection_token.py:73 -msgid "Asset or application required" -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:675 -#: 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/application/permission.py:20 -#: perms/serializers/application/permission.py:41 -#: perms/serializers/asset/permission.py:19 -#: perms/serializers/asset/permission.py:45 users/serializers/user.py:148 -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:30 -#: users/serializers/group.py:35 -msgid "ID" -msgstr "ID" - -#: 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:557 -#: 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:558 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:45 -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:413 ops/tasks.py:145 ops/tasks.py:148 -#: 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/sharing.py:26 terminal/models/sharing.py:80 -#: 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:20 -msgid "Cancel" -msgstr "取消" - -#: authentication/templates/authentication/login.html:221 -msgid "Welcome back, please enter username and password to login" -msgstr "欢迎回来,请输入用户名和密码登录" - -#: authentication/templates/authentication/login.html:264 -#: templates/_header_bar.html:89 -msgid "Login" -msgstr "登录" - -#: authentication/templates/authentication/login.html:271 -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:41 -msgid "DingTalk Error, Please contact your system administrator" -msgstr "钉钉错误,请联系系统管理员" - -#: authentication/views/dingtalk.py:44 -msgid "DingTalk Error" -msgstr "钉钉错误" - -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 -#: authentication/views/wecom.py:56 -msgid "" -"The system configuration is incorrect. Please contact your administrator" -msgstr "企业配置错误,请联系系统管理员" - -#: authentication/views/dingtalk.py:80 -msgid "DingTalk is already bound" -msgstr "钉钉已经绑定" - -#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:148 -msgid "Invalid user_id" -msgstr "无效的 user_id" - -#: authentication/views/dingtalk.py:164 -msgid "DingTalk query user failed" -msgstr "钉钉查询用户失败" - -#: authentication/views/dingtalk.py:173 -msgid "The DingTalk is already bound to another user" -msgstr "该钉钉已经绑定其他用户" - -#: authentication/views/dingtalk.py:180 -msgid "Binding DingTalk successfully" -msgstr "绑定 钉钉 成功" - -#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 -msgid "Failed to get user from DingTalk" -msgstr "从钉钉获取用户失败" - -#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 -msgid "Please login with a password and then bind the DingTalk" -msgstr "请使用密码登录,然后绑定钉钉" - -#: authentication/views/feishu.py:39 -msgid "FeiShu Error" -msgstr "飞书错误" - -#: authentication/views/feishu.py:87 -msgid "FeiShu is already bound" -msgstr "飞书已经绑定" - -#: authentication/views/feishu.py:129 -msgid "FeiShu query user failed" -msgstr "飞书查询用户失败" - -#: authentication/views/feishu.py:138 -msgid "The FeiShu is already bound to another user" -msgstr "该飞书已经绑定其他用户" - -#: authentication/views/feishu.py:145 -msgid "Binding FeiShu successfully" -msgstr "绑定 飞书 成功" - -#: authentication/views/feishu.py:197 -msgid "Failed to get user from FeiShu" -msgstr "从飞书获取用户失败" - -#: authentication/views/feishu.py:204 -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/db/encoder.py:11 -msgid "ugettext_lazy" -msgstr "ugettext_lazy" - -#: common/db/fields.py:81 -msgid "Marshal dict data to char field" -msgstr "编码 dict 为 char" - -#: common/db/fields.py:85 -msgid "Marshal dict data to text field" -msgstr "编码 dict 为 text" - -#: common/db/fields.py:97 -msgid "Marshal list data to char field" -msgstr "编码 list 为 char" - -#: common/db/fields.py:101 -msgid "Marshal list data to text field" -msgstr "编码 list 为 text" - -#: common/db/fields.py:105 -msgid "Marshal data to char field" -msgstr "编码数据为 char" - -#: common/db/fields.py:109 -msgid "Marshal data to text field" -msgstr "编码数据为 text" - -#: common/db/fields.py:151 -msgid "Encrypt field using Secret Key" -msgstr "加密的字段" - -#: common/db/models.py:115 -msgid "Updated by" -msgstr "更新人" - -#: common/drf/exc_handlers.py:25 -msgid "Object" -msgstr "对象" - -#: 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/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:33 -msgid "is discard" -msgstr "忽略的" - -#: common/mixins/models.py:34 -msgid "discard time" -msgstr "忽略时间" - -#: common/mixins/views.py:52 -msgid "Export all" -msgstr "导出所有" - -#: common/mixins/views.py:54 -msgid "Export only selected items" -msgstr "仅导出选择项" - -#: common/mixins/views.py:59 -#, 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/utils/ip/geoip/utils.py:26 common/utils/ip/utils.py:78 -msgid "Invalid ip" -msgstr "无效IP" - -#: 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:412 -msgid "Create account successfully" -msgstr "创建账号成功" - -#: jumpserver/conf.py:414 -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:17 -msgid "User message" -msgstr "用户消息" - -#: notifications/models/notification.py:20 -msgid "{} subscription" -msgstr "{} 订阅" - -#: notifications/models/notification.py:32 -msgid "System message" -msgstr "系统信息" - -#: ops/api/celery.py:61 ops/api/celery.py:76 -msgid "Waiting task start" -msgstr "等待任务开始" - -#: ops/api/command.py:56 -msgid "Not has host {} permission" -msgstr "没有该主机 {} 权限" - -#: ops/apps.py:9 ops/notifications.py:16 -msgid "App ops" -msgstr "作业中心" - -#: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 -#: settings/serializers/auth/ldap.py:73 -msgid "Cycle perform" -msgstr "周期执行" - -#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150 -#: settings/serializers/auth/ldap.py:70 -msgid "Regularly perform" -msgstr "定期执行" - -#: ops/mixin.py:112 -msgid "Interval" -msgstr "间隔" - -#: ops/mixin.py:122 -msgid "* Please enter a valid crontab expression" -msgstr "* 请输入有效的 crontab 表达式" - -#: ops/mixin.py:129 -msgid "Range {} to {}" -msgstr "输入在 {} - {} 范围之间" - -#: ops/mixin.py:140 -msgid "Require periodic or regularly perform setting" -msgstr "需要周期或定期设置" - -#: ops/mixin.py:151 -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 表达式 <" -"分 时 日 月 星期> (在线工" -"具
注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" - -#: ops/mixin.py:162 -msgid "Unit: hour" -msgstr "单位: 时" - -#: ops/models/adhoc.py:36 -msgid "Callback" -msgstr "回调" - -#: ops/models/adhoc.py:135 terminal/models/task.py:26 -#: xpack/plugins/gathered_user/models.py:73 -msgid "Task" -msgstr "任务" - -#: ops/models/adhoc.py:138 -msgid "Can view task monitor" -msgstr "可以查看任务监控" - -#: ops/models/adhoc.py:154 -msgid "Tasks" -msgstr "任务" - -#: ops/models/adhoc.py:156 -msgid "Options" -msgstr "选项" - -#: ops/models/adhoc.py:158 -msgid "Run as admin" -msgstr "再次执行" - -#: ops/models/adhoc.py:161 -msgid "Become" -msgstr "Become" - -#: ops/models/adhoc.py:162 -msgid "Create by" -msgstr "创建者" - -#: ops/models/adhoc.py:243 -msgid "AdHoc" -msgstr "任务各版本" - -#: ops/models/adhoc.py:252 -msgid "Task display" -msgstr "任务名称" - -#: ops/models/adhoc.py:254 -msgid "Host amount" -msgstr "主机数量" - -#: ops/models/adhoc.py:256 -msgid "Start time" -msgstr "开始时间" - -#: ops/models/adhoc.py:257 -msgid "End time" -msgstr "完成时间" - -#: ops/models/adhoc.py:259 ops/models/command.py:29 -#: terminal/serializers/session.py:40 -msgid "Is finished" -msgstr "是否完成" - -#: ops/models/adhoc.py:261 -msgid "Adhoc raw result" -msgstr "结果" - -#: ops/models/adhoc.py:262 -msgid "Adhoc result summary" -msgstr "汇总" - -#: ops/models/adhoc.py:339 -msgid "AdHoc execution" -msgstr "任务执行" - -#: ops/models/command.py:32 -msgid "Date finished" -msgstr "结束日期" - -#: ops/models/command.py:113 -msgid "Task start" -msgstr "任务开始" - -#: ops/models/command.py:147 -msgid "Command `{}` is forbidden ........" -msgstr "命令 `{}` 不允许被执行 ......." - -#: ops/models/command.py:160 -msgid "Task end" -msgstr "任务结束" - -#: ops/models/command.py:164 -msgid "Command execution" -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/tasks.py:72 -msgid "Clean task history period" -msgstr "定期清除任务历史" - -#: ops/tasks.py:85 -msgid "Clean celery log period" -msgstr "定期清除Celery日志" - -#: ops/templates/ops/celery_task_log.html:4 -msgid "Task log" -msgstr "任务列表" - -#: ops/utils.py:64 -msgid "Update task content: {}" -msgstr "更新任务内容: {}" - -#: orgs/api.py:69 -msgid "The current organization ({}) cannot be deleted" -msgstr "当前组织 ({}) 不能被删除" - -#: orgs/api.py:74 -msgid "" -"LDAP synchronization is set to the current organization. Please switch to " -"another organization before deleting" -msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进行删除操作" - -#: orgs/api.py:83 -msgid "The organization have resource ({}) cannot be deleted" -msgstr "组织存在资源 ({}) 不能被删除" - -#: orgs/apps.py:7 rbac/tree.py:113 -msgid "App organizations" -msgstr "组织管理" - -#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:85 -#: orgs/models.py:217 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:300 tickets/serializers/ticket/ticket.py:72 -msgid "Organization" -msgstr "组织" - -#: orgs/models.py:79 -msgid "GLOBAL" -msgstr "全局组织" - -#: orgs/models.py:87 -msgid "Can view root org" -msgstr "可以查看全局组织" - -#: orgs/models.py:88 -msgid "Can view all joined org" -msgstr "可以查看所有加入的组织" - -#: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44 -#: users/models/user.py:683 -msgid "Role" -msgstr "角色" - -#: perms/apps.py:9 -msgid "App permissions" -msgstr "授权管理" - -#: perms/exceptions.py:9 -msgid "The administrator is modifying permissions. Please wait" -msgstr "管理员正在修改授权,请稍等" - -#: perms/exceptions.py:14 -msgid "The authorization cannot be revoked for the time being" -msgstr "该授权暂时不能撤销" - -#: perms/models/application_permission.py:38 -msgid "Application permission" -msgstr "应用授权" - -#: perms/models/application_permission.py:111 -msgid "Permed application" -msgstr "授权的应用" - -#: perms/models/application_permission.py:114 -msgid "Can view my apps" -msgstr "可以查看我的应用" - -#: perms/models/application_permission.py:115 -msgid "Can view user apps" -msgstr "可以查看用户授权的应用" - -#: perms/models/application_permission.py:116 -msgid "Can view usergroup apps" -msgstr "可以查看用户组授权的应用" - -#: perms/models/asset_permission.py:29 -msgid "Asset permission" -msgstr "资产授权" - -#: perms/models/asset_permission.py:134 -msgid "Ungrouped" -msgstr "未分组" - -#: perms/models/asset_permission.py:136 -msgid "Favorite" -msgstr "收藏夹" - -#: perms/models/asset_permission.py:183 -msgid "Permed asset" -msgstr "授权的资产" - -#: perms/models/asset_permission.py:185 -msgid "Can view my assets" -msgstr "可以查看我的资产" - -#: perms/models/asset_permission.py:186 -msgid "Can view user assets" -msgstr "可以查看用户授权的资产" - -#: perms/models/asset_permission.py:187 -msgid "Can view usergroup assets" -msgstr "可以查看用户组授权的资产" - -#: perms/models/base.py:55 -msgid "Connect" -msgstr "连接" - -#: perms/models/base.py:56 -msgid "Upload file" -msgstr "上传文件" - -#: perms/models/base.py:57 -msgid "Download file" -msgstr "下载文件" - -#: perms/models/base.py:58 -msgid "Upload download" -msgstr "上传下载" - -#: perms/models/base.py:59 -msgid "Clipboard copy" -msgstr "剪贴板复制" - -#: perms/models/base.py:60 -msgid "Clipboard paste" -msgstr "剪贴板粘贴" - -#: perms/models/base.py:61 -msgid "Clipboard copy paste" -msgstr "剪贴板复制粘贴" - -#: perms/models/base.py:94 -msgid "From ticket" -msgstr "来自工单" - -#: perms/notifications.py:12 perms/notifications.py:44 -#: perms/notifications.py:88 perms/notifications.py:119 -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/notifications.py:91 -msgid "Your permed applications is about to expire" -msgstr "你授权的应用即将过期" - -#: perms/notifications.py:95 -msgid "permed applications" -msgstr "授权的应用" - -#: perms/notifications.py:134 -msgid "Application permissions is about to expire" -msgstr "应用授权规则即将过期" - -#: perms/notifications.py:138 -msgid "application permissions of organization {}" -msgstr "组织 ({}) 的应用授权" - -#: perms/serializers/application/permission.py:21 -#: perms/serializers/application/permission.py:40 -#: perms/serializers/asset/permission.py:20 -#: perms/serializers/asset/permission.py:44 users/serializers/user.py:89 -#: users/serializers/user.py:150 -msgid "Is expired" -msgstr "已过期" - -#: perms/serializers/application/permission.py:43 -#: perms/serializers/asset/permission.py:47 rbac/serializers/role.py:26 -#: users/serializers/group.py:34 -msgid "Users amount" -msgstr "用户数量" - -#: perms/serializers/application/permission.py:44 -#: perms/serializers/asset/permission.py:48 -msgid "User groups amount" -msgstr "用户组数量" - -#: perms/serializers/application/permission.py:45 -#: perms/serializers/asset/permission.py:51 -msgid "System users amount" -msgstr "系统用户数量" - -#: perms/serializers/application/permission.py:79 -msgid "" -"The application list contains applications that are different from the " -"permission type. ({})" -msgstr "应用列表中包含与授权类型不同的应用。({})" - -#: perms/serializers/asset/permission.py:21 -msgid "Users display" -msgstr "用户名称" - -#: perms/serializers/asset/permission.py:22 -msgid "User groups display" -msgstr "用户组名称" - -#: perms/serializers/asset/permission.py:23 -msgid "Assets display" -msgstr "资产名称" - -#: perms/serializers/asset/permission.py:24 -msgid "Nodes display" -msgstr "节点名称" - -#: perms/serializers/asset/permission.py:25 -msgid "System users display" -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 "如果有疑问或需求,请联系系统管理员" - -#: perms/tree/app.py:24 -msgid "My applications" -msgstr "我的应用" - -#: perms/tree/app.py:41 -msgid "Empty" -msgstr "空" - -#: perms/utils/asset/user_permission.py:620 rbac/tree.py:57 -msgid "My assets" -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 -#: settings/serializers/auth/oauth2.py:37 -msgid "Scope" -msgstr "范围" - -#: rbac/models/role.py:36 -msgid "Built-in" -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:11 -msgid "Scope display" -msgstr "范围名称" - -#: rbac/serializers/role.py:27 -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:156 -msgid "System setting" -msgstr "系统设置" - -#: rbac/tree.py:29 -msgid "Other" -msgstr "其它" - -#: rbac/tree.py:37 -msgid "Accounts" -msgstr "账号管理" - -#: rbac/tree.py:41 -msgid "Session audits" -msgstr "会话审计" - -#: rbac/tree.py:51 -msgid "Cloud import" -msgstr "云同步" - -#: rbac/tree.py:52 -msgid "Backup account" -msgstr "备份账号" - -#: rbac/tree.py:53 -msgid "Gather account" -msgstr "收集账号" - -#: rbac/tree.py:54 -msgid "App change auth" -msgstr "应用改密" - -#: rbac/tree.py:55 -msgid "Asset change auth" -msgstr "资产改密" - -#: rbac/tree.py:56 -msgid "Terminal setting" -msgstr "终端设置" - -#: rbac/tree.py:58 -msgid "My apps" -msgstr "我的应用" - -#: rbac/tree.py:114 -msgid "Ticket comment" -msgstr "工单评论" - -#: rbac/tree.py:115 tickets/models/ticket/general.py:305 -msgid "Ticket" -msgstr "工单管理" - -#: rbac/tree.py:116 -msgid "Common setting" -msgstr "一般设置" - -#: rbac/tree.py:117 -msgid "View permission tree" -msgstr "查看授权树" - -#: rbac/tree.py:118 -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:158 -msgid "Can change email setting" -msgstr "邮件设置" - -#: settings/models.py:159 -msgid "Can change auth setting" -msgstr "认证设置" - -#: settings/models.py:160 -msgid "Can change system msg sub setting" -msgstr "消息订阅设置" - -#: settings/models.py:161 -msgid "Can change sms setting" -msgstr "短信设置" - -#: settings/models.py:162 -msgid "Can change security setting" -msgstr "安全设置" - -#: settings/models.py:163 -msgid "Can change clean setting" -msgstr "定期清理" - -#: settings/models.py:164 -msgid "Can change interface setting" -msgstr "界面设置" - -#: settings/models.py:165 -msgid "Can change license setting" -msgstr "许可证设置" - -#: settings/models.py:166 -msgid "Can change terminal setting" -msgstr "终端设置" - -#: settings/models.py:167 -msgid "Can change other setting" -msgstr "其它设置" - -#: settings/serializers/auth/base.py:10 settings/serializers/basic.py:27 -msgid "Basic" -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 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 "启用登录跳转提示" - -#: 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:49 -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:63 -msgid "Provider auth endpoint" -msgstr "授权端点地址" - -#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:66 -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:72 -msgid "Provider userinfo endpoint" -msgstr "用户信息端点地址" - -#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:75 -msgid "Provider end session endpoint" -msgstr "注销会话端点地址" - -#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:93 -#: 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:46 -msgid "Use Keycloak" -msgstr "使用 Keycloak" - -#: settings/serializers/auth/oidc.py:52 -msgid "Realm name" -msgstr "域" - -#: settings/serializers/auth/oidc.py:58 -msgid "Enable OPENID Auth" -msgstr "启用 OIDC 认证" - -#: settings/serializers/auth/oidc.py:60 -msgid "Provider endpoint" -msgstr "端点地址" - -#: settings/serializers/auth/oidc.py:69 -msgid "Provider jwks endpoint" -msgstr "jwks 端点地址" - -#: settings/serializers/auth/oidc.py:78 -msgid "Provider sign alg" -msgstr "签名算法" - -#: settings/serializers/auth/oidc.py:81 -msgid "Provider sign key" -msgstr "签名 Key" - -#: settings/serializers/auth/oidc.py:83 -msgid "Scopes" -msgstr "连接范围" - -#: settings/serializers/auth/oidc.py:85 -msgid "Id token max age" -msgstr "令牌有效时间" - -#: settings/serializers/auth/oidc.py:88 -msgid "Id token include claims" -msgstr "声明" - -#: settings/serializers/auth/oidc.py:90 -msgid "Use state" -msgstr "使用状态" - -#: settings/serializers/auth/oidc.py:91 -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 Token 认证" - -#: 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 "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 "用户第一次登录,修改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/settings.py:61 -#: users/templates/users/reset_password.html:29 -msgid "Setting" -msgstr "设置" - -#: settings/serializers/terminal.py:6 terminal/models/terminal.py:185 -msgid "Terminal" -msgstr "终端" - -#: 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/endpoint.py:34 -msgid "Not found protocol query params" -msgstr "" - -#: terminal/api/session.py:216 -msgid "Session does not exist: {}" -msgstr "会话不存在: {}" - -#: terminal/api/session.py:219 -msgid "Session is finished or the protocol not supported" -msgstr "会话已经完成或协议不支持" - -#: terminal/api/session.py:232 -msgid "User does not have permission" -msgstr "用户没有权限" - -#: terminal/api/sharing.py:30 -msgid "Secure session sharing settings is disabled" -msgstr "未开启会话共享" - -#: terminal/api/storage.py:28 -msgid "Deleting the default storage is not allowed" -msgstr "不允许删除默认存储配置" - -#: terminal/api/storage.py:31 -msgid "Cannot delete storage that is being used" -msgstr "不允许删除正在使用的存储配置" - -#: terminal/api/storage.py:72 terminal/api/storage.py:73 -msgid "Command storages" -msgstr "命令存储" - -#: terminal/api/storage.py:79 -msgid "Invalid" -msgstr "无效" - -#: terminal/api/storage.py:119 -msgid "Test failure: {}" -msgstr "测试失败: {}" - -#: terminal/api/storage.py:122 -msgid "Test successful" -msgstr "测试成功" - -#: terminal/api/storage.py:124 -msgid "Test failure: Account invalid" -msgstr "测试失败: 账号无效" - -#: terminal/api/terminal.py:39 -msgid "Have online sessions" -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:37 -msgid "Output" -msgstr "输出" - -#: terminal/backends/command/models.py:25 terminal/models/replay.py:9 -#: terminal/models/sharing.py:19 terminal/models/sharing.py:78 -#: terminal/templates/terminal/_msg_command_alert.html:10 -#: tickets/models/ticket/command_confirm.py:20 -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:38 -msgid "Risk level display" -msgstr "风险等级名称" - -#: terminal/backends/command/serializers.py:39 -msgid "Timestamp" -msgstr "时间戳" - -#: terminal/backends/command/serializers.py:41 terminal/models/terminal.py:106 -msgid "Remote Address" -msgstr "远端地址" - -#: terminal/const.py:33 -msgid "Critical" -msgstr "严重" - -#: terminal/const.py:34 -msgid "High" -msgstr "较高" - -#: terminal/const.py:35 users/templates/users/reset_password.html:50 -msgid "Normal" -msgstr "正常" - -#: terminal/const.py:36 -msgid "Offline" -msgstr "离线" - -#: terminal/exceptions.py:8 -msgid "Bulk create not support" -msgstr "不支持批量创建" - -#: terminal/exceptions.py:13 -msgid "Storage is invalid" -msgstr "存储无效" - -#: terminal/models/command.py:66 -msgid "Command record" -msgstr "命令记录" - -#: terminal/models/endpoint.py:17 -msgid "HTTPS Port" -msgstr "HTTPS 端口" - -#: terminal/models/endpoint.py:18 terminal/models/terminal.py:108 -msgid "HTTP Port" -msgstr "HTTP 端口" - -#: terminal/models/endpoint.py:19 terminal/models/terminal.py:107 -msgid "SSH Port" -msgstr "SSH 端口" - -#: terminal/models/endpoint.py:20 -msgid "RDP Port" -msgstr "RDP 端口" - -#: terminal/models/endpoint.py:27 terminal/models/endpoint.py:95 -#: 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/endpoint.py:88 -msgid "IP group" -msgstr "IP 组" - -#: terminal/models/endpoint.py:100 -msgid "Endpoint rule" -msgstr "端点规则" - -#: terminal/models/replay.py:12 -msgid "Session replay" -msgstr "会话录像" - -#: terminal/models/replay.py:14 -msgid "Can upload session replay" -msgstr "可以上传会话录像" - -#: terminal/models/replay.py:15 -msgid "Can download session replay" -msgstr "可以下载会话录像" - -#: terminal/models/session.py:51 terminal/models/sharing.py:101 -msgid "Login from" -msgstr "登录来源" - -#: terminal/models/session.py:55 -msgid "Replay" -msgstr "回放" - -#: terminal/models/session.py:60 -msgid "Date end" -msgstr "结束日期" - -#: terminal/models/session.py:261 -msgid "Session record" -msgstr "会话记录" - -#: terminal/models/session.py:263 -msgid "Can monitor session" -msgstr "可以监控会话" - -#: terminal/models/session.py:264 -msgid "Can share session" -msgstr "可以分享会话" - -#: terminal/models/session.py:265 -msgid "Can terminate session" -msgstr "可以终断会话" - -#: terminal/models/session.py:266 -msgid "Can validate session action perm" -msgstr "可以验证会话动作权限" - -#: terminal/models/sharing.py:24 -msgid "Creator" -msgstr "创建者" - -#: terminal/models/sharing.py:31 -msgid "Expired time (min)" -msgstr "过期时间 (分)" - -#: terminal/models/sharing.py:37 terminal/models/sharing.py:83 -msgid "Session sharing" -msgstr "会话分享" - -#: terminal/models/sharing.py:39 -msgid "Can add super session sharing" -msgstr "可以创建超级会话分享" - -#: terminal/models/sharing.py:66 -msgid "Link not active" -msgstr "链接失效" - -#: terminal/models/sharing.py:68 -msgid "Link expired" -msgstr "链接过期" - -#: terminal/models/sharing.py:70 -msgid "User not allowed to join" -msgstr "该用户无权加入会话" - -#: terminal/models/sharing.py:87 terminal/serializers/sharing.py:59 -msgid "Joiner" -msgstr "加入者" - -#: terminal/models/sharing.py:90 -msgid "Date joined" -msgstr "加入日期" - -#: terminal/models/sharing.py:93 -msgid "Date left" -msgstr "结束日期" - -#: terminal/models/sharing.py:111 tickets/const.py:26 -#: xpack/plugins/change_auth_plan/models/base.py:192 -msgid "Finished" -msgstr "结束" - -#: terminal/models/sharing.py:116 -msgid "Session join record" -msgstr "会话加入记录" - -#: terminal/models/sharing.py:132 -msgid "Invalid verification code" -msgstr "验证码不正确" - -#: terminal/models/status.py:18 -msgid "Session Online" -msgstr "在线会话" - -#: terminal/models/status.py:19 -msgid "CPU Load" -msgstr "CPU负载" - -#: terminal/models/status.py:20 -msgid "Memory Used" -msgstr "内存使用" - -#: terminal/models/status.py:21 -msgid "Disk Used" -msgstr "磁盘使用" - -#: terminal/models/status.py:22 -msgid "Connections" -msgstr "连接数" - -#: terminal/models/status.py:23 -msgid "Threads" -msgstr "线程数" - -#: terminal/models/status.py:24 -msgid "Boot Time" -msgstr "运行时间" - -#: terminal/models/storage.py:29 -msgid "Default storage" -msgstr "默认存储" - -#: terminal/models/storage.py:139 terminal/models/terminal.py:109 -msgid "Command storage" -msgstr "命令存储" - -#: terminal/models/storage.py:199 terminal/models/terminal.py:110 -msgid "Replay storage" -msgstr "录像存储" - -#: terminal/models/task.py:17 -msgid "Args" -msgstr "参数" - -#: terminal/models/task.py:18 -msgid "Kwargs" -msgstr "其它参数" - -#: terminal/models/terminal.py:104 -msgid "type" -msgstr "类型" - -#: terminal/models/terminal.py:111 -msgid "Application User" -msgstr "应用用户" - -#: terminal/models/terminal.py:112 -msgid "Is Accepted" -msgstr "被接受" - -#: terminal/models/terminal.py:187 -msgid "Can view terminal config" -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/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:15 terminal/serializers/session.py:42 -msgid "Terminal display" -msgstr "终端显示" - -#: terminal/serializers/session.py:32 -msgid "User ID" -msgstr "用户 ID" - -#: terminal/serializers/session.py:33 -msgid "Asset ID" -msgstr "资产 ID" - -#: terminal/serializers/session.py:34 -msgid "System user ID" -msgstr "系统用户 ID" - -#: terminal/serializers/session.py:35 -msgid "Login from display" -msgstr "登录来源名称" - -#: terminal/serializers/session.py:37 -msgid "Can replay" -msgstr "是否可重放" - -#: terminal/serializers/session.py:38 -msgid "Can join" -msgstr "是否可加入" - -#: terminal/serializers/session.py:39 -msgid "Terminal ID" -msgstr "终端 ID" - -#: terminal/serializers/session.py:41 -msgid "Can terminate" -msgstr "是否可中断" - -#: terminal/serializers/session.py:47 -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:222 -msgid "Region" -msgstr "地域" - -#: terminal/serializers/storage.py:109 -msgid "Container name" -msgstr "容器名称" - -#: terminal/serializers/storage.py:111 -msgid "Account 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:44 -msgid "Load status" -msgstr "负载状态" - -#: terminal/serializers/terminal.py:81 terminal/serializers/terminal.py:89 -msgid "Not found" -msgstr "没有发现" - -#: terminal/templates/terminal/_msg_command_alert.html:10 -msgid "view" -msgstr "查看" - -#: terminal/utils/db_port_mapper.py:65 -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:91 -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:93 -msgid "All available port count: {}, Already use port count: {}" -msgstr "所有可用端口数量:{},已使用端口数量:{}" - -#: tickets/apps.py:7 -msgid "Tickets" -msgstr "工单管理" - -#: tickets/const.py:8 -msgid "General" -msgstr "一般" - -#: tickets/const.py:10 -msgid "Apply for asset" -msgstr "申请资产" - -#: tickets/const.py:11 -msgid "Apply for application" -msgstr "申请应用" - -#: tickets/const.py:17 tickets/const.py:25 tickets/const.py:44 -msgid "Open" -msgstr "打开" - -#: tickets/const.py:18 tickets/const.py:31 -msgid "Approved" -msgstr "已同意" - -#: tickets/const.py:19 tickets/const.py:32 -msgid "Rejected" -msgstr "已拒绝" - -#: tickets/const.py:21 tickets/const.py:34 -msgid "Reopen" -msgstr "" - -#: tickets/const.py:30 tickets/const.py:38 -msgid "Pending" -msgstr "待定的" - -#: tickets/const.py:33 tickets/const.py:40 -msgid "Closed" -msgstr "关闭的" - -#: tickets/const.py:46 -msgid "Approve" -msgstr "同意" - -#: tickets/const.py:51 -msgid "One level" -msgstr "1 级" - -#: tickets/const.py:52 -msgid "Two level" -msgstr "2 级" - -#: tickets/const.py:56 -msgid "Super admin" -msgstr "超级管理员" - -#: tickets/const.py:57 -msgid "Org admin" -msgstr "组织管理员" - -#: tickets/const.py:58 -msgid "Super admin and org admin" -msgstr "组织管理员或超级管理员" - -#: tickets/const.py:59 -msgid "Custom user" -msgstr "自定义用户" - -#: tickets/errors.py:9 -msgid "Ticket already closed" -msgstr "工单已经关闭" - -#: tickets/handlers/apply_application.py:38 -msgid "" -"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket " -"processor: {}, ticket ID: {}" -msgstr "" -"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" - -#: tickets/handlers/apply_asset.py:37 -msgid "" -"Created by the ticket ticket title: {} ticket applicant: {} ticket " -"processor: {} ticket ID: {}" -msgstr "" -"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" - -#: tickets/handlers/base.py:84 -msgid "Change field" -msgstr "变更字段" - -#: tickets/handlers/base.py:84 -msgid "Before change" -msgstr "变更前" - -#: tickets/handlers/base.py:84 -msgid "After change" -msgstr "变更后" - -#: tickets/handlers/base.py:96 -msgid "{} {} the ticket" -msgstr "{} {} 工单" - -#: tickets/handlers/login_confirm.py:18 -msgid "Applied login IP" -msgstr "申请登录的IP" - -#: tickets/handlers/login_confirm.py:19 -msgid "Applied login city" -msgstr "申请登录的城市" - -#: tickets/handlers/login_confirm.py:20 -msgid "Applied login datetime" -msgstr "申请登录的日期" - -#: tickets/models/comment.py:13 tickets/models/ticket/general.py:41 -#: tickets/models/ticket/general.py:277 -msgid "State" -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:20 tickets/models/flow.py:62 -#: tickets/models/ticket/general.py:37 -msgid "Approve level" -msgstr "审批级别" - -#: tickets/models/flow.py:25 tickets/serializers/flow.py:15 -msgid "Approve strategy" -msgstr "审批策略" - -#: tickets/models/flow.py:30 tickets/serializers/flow.py:16 -msgid "Assignees" -msgstr "受理人" - -#: tickets/models/flow.py:34 -msgid "Ticket flow approval rule" -msgstr "工单批准信息" - -#: tickets/models/flow.py:67 -msgid "Ticket flow" -msgstr "工单流程" - -#: tickets/models/relation.py:10 -msgid "Ticket session relation" -msgstr "工单会话" - -#: tickets/models/ticket/apply_application.py:12 -#: tickets/models/ticket/apply_asset.py:13 -msgid "Permission name" -msgstr "授权规则名称" - -#: tickets/models/ticket/apply_application.py:21 -msgid "Apply applications" -msgstr "申请应用" - -#: tickets/models/ticket/apply_application.py:24 -#: tickets/models/ticket/apply_asset.py:18 -msgid "Apply system users" -msgstr "申请的系统用户" - -#: tickets/models/ticket/apply_asset.py:9 -#: tickets/serializers/ticket/apply_asset.py:15 -msgid "Select at least one asset or node" -msgstr "资产或者节点至少选择一项" - -#: tickets/models/ticket/apply_asset.py:14 -msgid "Apply nodes" -msgstr "申请节点" - -#: tickets/models/ticket/apply_asset.py:16 -msgid "Apply assets" -msgstr "申请资产" - -#: tickets/models/ticket/command_confirm.py:10 -msgid "Run user" -msgstr "运行的用户" - -#: tickets/models/ticket/command_confirm.py:12 -msgid "Run asset" -msgstr "运行的资产" - -#: tickets/models/ticket/command_confirm.py:15 -msgid "Run system user" -msgstr "运行的系统用户" - -#: tickets/models/ticket/command_confirm.py:17 -msgid "Run command" -msgstr "运行的命令" - -#: tickets/models/ticket/command_confirm.py:24 -msgid "From cmd filter" -msgstr "来自命令过滤规则" - -#: tickets/models/ticket/command_confirm.py:28 -msgid "From cmd filter rule" -msgstr "来自命令过滤规则" - -#: tickets/models/ticket/general.py:72 -msgid "Ticket step" -msgstr "工单步骤" - -#: tickets/models/ticket/general.py:90 -msgid "Ticket assignee" -msgstr "工单受理人" - -#: tickets/models/ticket/general.py:270 -msgid "Title" -msgstr "标题" - -#: tickets/models/ticket/general.py:286 -msgid "Applicant" -msgstr "申请人" - -#: tickets/models/ticket/general.py:291 -msgid "TicketFlow" -msgstr "工单流程" - -#: tickets/models/ticket/general.py:294 -msgid "Approval step" -msgstr "审批步骤" - -#: tickets/models/ticket/general.py:297 -msgid "Relation snapshot" -msgstr "工单快照" - -#: tickets/models/ticket/general.py:390 -msgid "Please try again" -msgstr "请再次尝试" - -#: tickets/models/ticket/general.py:421 -msgid "Super ticket" -msgstr "超级工单" - -#: tickets/models/ticket/login_asset_confirm.py:12 -msgid "Login user" -msgstr "登录用户" - -#: tickets/models/ticket/login_asset_confirm.py:16 -msgid "Login asset" -msgstr "登录资产" - -#: tickets/models/ticket/login_asset_confirm.py:20 -msgid "Login system user" -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:17 -msgid "Assignees display" -msgstr "受理人名称" - -#: tickets/serializers/flow.py:43 -msgid "Please select the Assignees" -msgstr "请选择受理人" - -#: tickets/serializers/flow.py:69 -msgid "The current organization type already exists" -msgstr "当前组织已存在该类型" - -#: tickets/serializers/super_ticket.py:11 -msgid "Processor" -msgstr "处理人" - -#: tickets/serializers/ticket/common.py:16 -#: tickets/serializers/ticket/common.py:79 -msgid "Created by ticket ({}-{})" -msgstr "通过工单创建 ({}-{})" - -#: tickets/serializers/ticket/common.py:69 -msgid "The expiration date should be greater than the start date" -msgstr "过期时间要大于开始时间" - -#: tickets/serializers/ticket/common.py:85 -msgid "Permission named `{}` already exists" -msgstr "授权名称 `{}` 已存在" - -#: tickets/serializers/ticket/ticket.py:110 -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:39 -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:40 -msgid "" -"This ticket does not exist, the process has ended, or this link has expired" -msgstr "工单不存在,或者工单流程已经结束,或者此链接已经过期" - -#: tickets/views/approve.py:69 -msgid "Click the button below to approve or reject" -msgstr "点击下方按钮同意或者拒绝" - -#: tickets/views/approve.py:71 -msgid "After successful authentication, this ticket can be approved directly" -msgstr "认证成功后,工单可直接审批" - -#: tickets/views/approve.py:93 -msgid "Illegal approval action" -msgstr "无效的审批动作" - -#: tickets/views/approve.py:106 -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/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:706 -msgid "Public key" -msgstr "SSH公钥" - -#: users/models/user.py:559 -msgid "Force enable" -msgstr "强制启用" - -#: users/models/user.py:629 -msgid "Local" -msgstr "数据库" - -#: users/models/user.py:685 users/serializers/user.py:149 -msgid "Is service account" -msgstr "服务账号" - -#: users/models/user.py:687 -msgid "Avatar" -msgstr "头像" - -#: users/models/user.py:690 -msgid "Wechat" -msgstr "微信" - -#: users/models/user.py:699 -msgid "OTP secret key" -msgstr "OTP 秘钥" - -#: users/models/user.py:709 -msgid "Secret key" -msgstr "Secret key" - -#: users/models/user.py:714 users/serializers/profile.py:149 -#: users/serializers/user.py:146 -msgid "Is first login" -msgstr "首次登录" - -#: users/models/user.py:725 -msgid "Source" -msgstr "来源" - -#: users/models/user.py:729 -msgid "Date password last updated" -msgstr "最后更新密码日期" - -#: users/models/user.py:732 -msgid "Need update password" -msgstr "需要更新密码" - -#: users/models/user.py:907 -msgid "Can invite user" -msgstr "可以邀请用户" - -#: users/models/user.py:908 -msgid "Can remove user" -msgstr "可以移除用户" - -#: users/models/user.py:909 -msgid "Can match user" -msgstr "可以匹配用户" - -#: users/models/user.py:918 -msgid "Administrator" -msgstr "管理员" - -#: users/models/user.py:921 -msgid "Administrator is the super user of system" -msgstr "Administrator是初始的超级管理员" - -#: users/models/user.py:946 -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:28 -msgid "System roles" -msgstr "系统角色" - -#: users/serializers/user.py:33 -msgid "Org roles" -msgstr "组织角色" - -#: users/serializers/user.py:35 -msgid "System roles display" -msgstr "系统角色显示" - -#: users/serializers/user.py:36 -msgid "Org roles display" -msgstr "组织角色显示" - -#: users/serializers/user.py:81 -#: xpack/plugins/change_auth_plan/models/base.py:35 -#: xpack/plugins/change_auth_plan/serializers/base.py:27 -msgid "Password strategy" -msgstr "密码策略" - -#: users/serializers/user.py:83 -msgid "MFA enabled" -msgstr "MFA 已启用" - -#: users/serializers/user.py:84 -msgid "MFA force enabled" -msgstr "强制 MFA" - -#: users/serializers/user.py:86 -msgid "MFA level display" -msgstr "MFA 等级名称" - -#: users/serializers/user.py:88 -msgid "Login blocked" -msgstr "登录被阻塞" - -#: users/serializers/user.py:91 -msgid "Can public key authentication" -msgstr "能否公钥认证" - -#: users/serializers/user.py:151 -msgid "Avatar url" -msgstr "头像路径" - -#: users/serializers/user.py:153 -msgid "Groups name" -msgstr "用户组名" - -#: users/serializers/user.py:154 -msgid "Source name" -msgstr "用户来源名" - -#: users/serializers/user.py:155 -msgid "Organization role name" -msgstr "组织角色名称" - -#: users/serializers/user.py:156 -msgid "Super role name" -msgstr "超级角色名称" - -#: users/serializers/user.py:157 -msgid "Total role name" -msgstr "汇总角色名称" - -#: users/serializers/user.py:159 -msgid "Is wecom bound" -msgstr "是否绑定了企业微信" - -#: users/serializers/user.py:160 -msgid "Is dingtalk bound" -msgstr "是否绑定了钉钉" - -#: users/serializers/user.py:161 -msgid "Is feishu bound" -msgstr "是否绑定了飞书" - -#: users/serializers/user.py:162 -msgid "Is OTP bound" -msgstr "是否绑定了虚拟 MFA" - -#: users/serializers/user.py:164 -msgid "System role name" -msgstr "系统角色名称" - -#: users/serializers/user.py:263 -msgid "Select users" -msgstr "选择用户" - -#: users/serializers/user.py:264 -msgid "For security, only list several users" -msgstr "为了安全,仅列出几个用户" - -#: users/serializers/user.py:299 -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 "Token错误或失效" - -#: 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/change_auth_plan/api/app.py:112 -#: xpack/plugins/change_auth_plan/api/asset.py:95 -msgid "The parameter 'action' must be [{}]" -msgstr "参数 'action' 必须是 [{}]" - -#: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models/asset.py:123 -msgid "Change auth plan" -msgstr "改密计划" - -#: xpack/plugins/change_auth_plan/models/app.py:46 -#: xpack/plugins/change_auth_plan/models/app.py:95 -msgid "Application change auth plan" -msgstr "应用改密计划" - -#: xpack/plugins/change_auth_plan/models/app.py:99 -#: xpack/plugins/change_auth_plan/models/app.py:151 -msgid "Application change auth plan execution" -msgstr "应用改密计划执行" - -#: xpack/plugins/change_auth_plan/models/app.py:144 -#: xpack/plugins/change_auth_plan/serializers/app.py:64 -msgid "App" -msgstr "应用" - -#: xpack/plugins/change_auth_plan/models/app.py:156 -msgid "Application change auth plan task" -msgstr "应用改密计划任务" - -#: xpack/plugins/change_auth_plan/models/app.py:180 -#: xpack/plugins/change_auth_plan/models/asset.py:263 -msgid "Password cannot be set to blank, exit. " -msgstr "密码不能设置为空, 退出. " - -#: xpack/plugins/change_auth_plan/models/asset.py:29 -msgid "Append SSH KEY" -msgstr "追加" - -#: xpack/plugins/change_auth_plan/models/asset.py:30 -msgid "Empty and append SSH KEY" -msgstr "清空所有并添加" - -#: xpack/plugins/change_auth_plan/models/asset.py:31 -msgid "Replace (The key generated by JumpServer) " -msgstr "替换 (由 JumpServer 生成的密钥)" - -#: xpack/plugins/change_auth_plan/models/asset.py:49 -#: xpack/plugins/change_auth_plan/serializers/asset.py:36 -msgid "SSH Key strategy" -msgstr "SSH 密钥策略" - -#: xpack/plugins/change_auth_plan/models/asset.py:67 -msgid "Asset change auth plan" -msgstr "资产改密计划" - -#: xpack/plugins/change_auth_plan/models/asset.py:134 -msgid "Asset change auth plan execution" -msgstr "资产改密计划执行" - -#: xpack/plugins/change_auth_plan/models/asset.py:210 -msgid "Change auth plan execution" -msgstr "改密计划执行" - -#: xpack/plugins/change_auth_plan/models/asset.py:217 -msgid "Asset change auth plan task" -msgstr "资产改密计划任务" - -#: xpack/plugins/change_auth_plan/models/asset.py:252 -msgid "This asset does not have a privileged user set: " -msgstr "该资产没有设置特权用户: " - -#: xpack/plugins/change_auth_plan/models/asset.py:258 -msgid "" -"The password and key of the current asset privileged user cannot be changed: " -msgstr "不能更改当前资产特权用户的密码及密钥: " - -#: xpack/plugins/change_auth_plan/models/asset.py:269 -msgid "Public key cannot be set to null, exit. " -msgstr "公钥不能设置为空, 退出. " - -#: xpack/plugins/change_auth_plan/models/base.py:28 -msgid "All assets use the same random password" -msgstr "使用相同的随机密码" - -#: xpack/plugins/change_auth_plan/models/base.py:29 -msgid "All assets use different random password" -msgstr "使用不同的随机密码" - -#: xpack/plugins/change_auth_plan/models/base.py:39 -msgid "Password rules" -msgstr "密码规则" - -#: xpack/plugins/change_auth_plan/models/base.py:118 -msgid "Change auth plan snapshot" -msgstr "改密计划快照" - -#: xpack/plugins/change_auth_plan/models/base.py:187 -msgid "Ready" -msgstr "准备" - -#: xpack/plugins/change_auth_plan/models/base.py:188 -msgid "Preflight check" -msgstr "改密前的校验" - -#: xpack/plugins/change_auth_plan/models/base.py:189 -msgid "Change auth" -msgstr "执行改密" - -#: xpack/plugins/change_auth_plan/models/base.py:190 -msgid "Verify auth" -msgstr "验证密码/密钥" - -#: xpack/plugins/change_auth_plan/models/base.py:191 -msgid "Keep auth" -msgstr "保存密码/密钥" - -#: xpack/plugins/change_auth_plan/models/base.py:199 -msgid "Step" -msgstr "步骤" - -#: xpack/plugins/change_auth_plan/notifications.py:8 -msgid "Notification of implementation result of encryption change plan" -msgstr "改密计划任务结果通知" - -#: xpack/plugins/change_auth_plan/notifications.py:18 -msgid "" -"{} - The encryption change task has been completed. See the attachment for " -"details" -msgstr "{} - 改密任务已完成, 详情见附件" - -#: xpack/plugins/change_auth_plan/notifications.py:19 -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 "" -"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" -"密密码" - -#: xpack/plugins/change_auth_plan/serializers/asset.py:33 -msgid "Change Password" -msgstr "更改密码" - -#: xpack/plugins/change_auth_plan/serializers/asset.py:34 -msgid "Change SSH Key" -msgstr "修改 SSH Key" - -#: xpack/plugins/change_auth_plan/serializers/base.py:44 -msgid "Run times" -msgstr "执行次数" - -#: xpack/plugins/change_auth_plan/serializers/base.py:58 -msgid "* Please enter the correct password length" -msgstr "* 请输入正确的密码长度" - -#: xpack/plugins/change_auth_plan/serializers/base.py:61 -msgid "* Password length range 6-30 bits" -msgstr "* 密码长度范围 6-30 位" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:236 -msgid "After many attempts to change the secret, it still failed" -msgstr "多次尝试改密后, 依然失败" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:255 -msgid "Invalid/incorrect password" -msgstr "无效/错误 密码" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:257 -msgid "Failed to connect to the host" -msgstr "连接主机失败" - -#: xpack/plugins/change_auth_plan/task_handlers/base/handler.py:259 -msgid "Data could not be sent to remote" -msgstr "无法将数据发送到远程" - -#: 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: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:29 -msgid "Provider" -msgstr "云服务商" - -#: xpack/plugins/cloud/models.py:38 -msgid "Cloud account" -msgstr "云账号" - -#: xpack/plugins/cloud/models.py:40 -msgid "Test cloud account" -msgstr "测试云账号" - -#: xpack/plugins/cloud/models.py:84 xpack/plugins/cloud/serializers/task.py:72 -msgid "Account" -msgstr "账号" - -#: xpack/plugins/cloud/models.py:87 xpack/plugins/cloud/serializers/task.py:39 -msgid "Regions" -msgstr "地域" - -#: xpack/plugins/cloud/models.py:90 -msgid "Hostname strategy" -msgstr "主机名策略" - -#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:73 -msgid "Unix admin user" -msgstr "Unix 管理员" - -#: xpack/plugins/cloud/models.py:103 xpack/plugins/cloud/serializers/task.py:74 -msgid "Windows admin user" -msgstr "Windows 管理员" - -#: xpack/plugins/cloud/models.py:109 xpack/plugins/cloud/serializers/task.py:47 -msgid "IP network segment group" -msgstr "IP网段组" - -#: xpack/plugins/cloud/models.py:112 xpack/plugins/cloud/serializers/task.py:52 -msgid "Sync IP type" -msgstr "同步IP类型" - -#: xpack/plugins/cloud/models.py:115 xpack/plugins/cloud/serializers/task.py:77 -msgid "Always update" -msgstr "总是更新" - -#: xpack/plugins/cloud/models.py:121 -msgid "Date last sync" -msgstr "最后同步日期" - -#: xpack/plugins/cloud/models.py:132 xpack/plugins/cloud/models.py:173 -msgid "Sync instance task" -msgstr "同步实例任务" - -#: xpack/plugins/cloud/models.py:184 xpack/plugins/cloud/models.py:232 -msgid "Date sync" -msgstr "同步日期" - -#: xpack/plugins/cloud/models.py:188 -msgid "Sync instance task execution" -msgstr "同步实例任务执行" - -#: xpack/plugins/cloud/models.py:212 -msgid "Sync task" -msgstr "同步任务" - -#: xpack/plugins/cloud/models.py:216 -msgid "Sync instance task history" -msgstr "同步实例任务历史" - -#: xpack/plugins/cloud/models.py:219 -msgid "Instance" -msgstr "实例" - -#: xpack/plugins/cloud/models.py:236 -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:65 -msgid "Validity display" -msgstr "有效性显示" - -#: xpack/plugins/cloud/serializers/account.py:66 -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:30 -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:37 -msgid "History count" -msgstr "执行次数" - -#: xpack/plugins/cloud/serializers/task.py:38 -msgid "Instance count" -msgstr "实例个数" - -#: xpack/plugins/cloud/serializers/task.py:71 -msgid "Linux admin user" -msgstr "Linux 管理员" - -#: xpack/plugins/cloud/serializers/task.py:76 -#: xpack/plugins/gathered_user/serializers.py:20 -msgid "Periodic display" -msgstr "定时执行" - -#: xpack/plugins/cloud/utils.py:69 -msgid "Account unavailable" -msgstr "账号无效" - -#: xpack/plugins/gathered_user/meta.py:11 -msgid "Gathered user" -msgstr "收集用户" - -#: xpack/plugins/gathered_user/models.py:39 -msgid "Gather user task" -msgstr "收集用户任务" - -#: xpack/plugins/gathered_user/models.py:85 -msgid "gather user task execution" -msgstr "收集用户执行" - -#: xpack/plugins/gathered_user/models.py:91 -msgid "Assets is empty, please change nodes" -msgstr "资产为空,请更改节点" - -#: xpack/plugins/gathered_user/serializers.py:21 -msgid "Executed times" -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 "社区版" diff --git a/apps/notifications/migrations/0002_auto_20210909_1946.py b/apps/notifications/migrations/0002_auto_20210909_1946.py index fb48ced63..ef198dc89 100644 --- a/apps/notifications/migrations/0002_auto_20210909_1946.py +++ b/apps/notifications/migrations/0002_auto_20210909_1946.py @@ -30,7 +30,7 @@ def init_user_msg_subscription(apps, schema_editor): to_create.append(UserMsgSubscription(user=user, receive_backends=receive_backends)) UserMsgSubscription.objects.bulk_create(to_create) - print(f'\n Init user message subscription: {len(to_create)}') + print(f'\n\tInit user message subscription: {len(to_create)}') class Migration(migrations.Migration): diff --git a/apps/notifications/models/notification.py b/apps/notifications/models/notification.py index 0d6a8b8a4..fc6f8aa96 100644 --- a/apps/notifications/models/notification.py +++ b/apps/notifications/models/notification.py @@ -1,26 +1,23 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.db.models import JMSModel, CASCADE_SIGNAL_SKIP +from common.db.models import JMSBaseModel, CASCADE_SIGNAL_SKIP __all__ = ('SystemMsgSubscription', 'UserMsgSubscription') -class UserMsgSubscription(JMSModel): +class UserMsgSubscription(JMSBaseModel): user = models.OneToOneField( 'users.User', related_name='user_msg_subscription', on_delete=CASCADE_SIGNAL_SKIP, verbose_name=_('User') ) receive_backends = models.JSONField(default=list, verbose_name=_('receive backend')) - class Meta: - verbose_name = _('User message') - def __str__(self): return _('{} subscription').format(self.user) -class SystemMsgSubscription(JMSModel): +class SystemMsgSubscription(JMSBaseModel): message_type = models.CharField(max_length=128, unique=True) users = models.ManyToManyField('users.User', related_name='system_msg_subscriptions') groups = models.ManyToManyField('users.UserGroup', related_name='system_msg_subscriptions') diff --git a/apps/notifications/models/site_msg.py b/apps/notifications/models/site_msg.py index 3e3c09baa..e08cd5c71 100644 --- a/apps/notifications/models/site_msg.py +++ b/apps/notifications/models/site_msg.py @@ -1,18 +1,18 @@ from django.db import models -from common.db.models import JMSModel +from common.db.models import JMSBaseModel __all__ = ('SiteMessageUsers', 'SiteMessage') -class SiteMessageUsers(JMSModel): +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') has_read = models.BooleanField(default=False) read_at = models.DateTimeField(default=None, null=True) -class SiteMessage(JMSModel): +class SiteMessage(JMSBaseModel): subject = models.CharField(max_length=1024) message = models.TextField() users = models.ManyToManyField( diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py index 481a4bc08..55f1cdbad 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -16,7 +16,6 @@ from .models import SystemMsgSubscription, UserMsgSubscription __all__ = ('SystemMessage', 'UserMessage', 'system_msgs', 'Message') - system_msgs = [] user_msgs = [] @@ -44,7 +43,7 @@ class MessageType(type): return clz -@shared_task +@shared_task(verbose_name=_('Publish the station message')) def publish_task(msg): msg.publish() diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 3fe1933ac..7d2b1f39d 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -1,325 +1,144 @@ -# ~*~ coding: utf-8 ~*~ - -import datetime -import json -import os from collections import defaultdict -import ansible.constants as C -from ansible.plugins.callback import CallbackBase -from ansible.plugins.callback.default import CallbackModule -from ansible.plugins.callback.minimal import CallbackModule as CMDCallBackModule -from common.utils.strings import safe_str - - -class CallbackMixin: - def __init__(self, display=None): - # result_raw example: { - # "ok": {"hostname": {"task_name": {},...},..}, - # "failed": {"hostname": {"task_name": {}..}, ..}, - # "unreachable: {"hostname": {"task_name": {}, ..}}, - # "skipped": {"hostname": {"task_name": {}, ..}, ..}, - # } - # results_summary example: { - # "contacted": {"hostname": {"task_name": {}}, "hostname": {}}, - # "dark": {"hostname": {"task_name": {}, "task_name": {}},...,}, - # "success": True - # } - self.results_raw = dict( - ok=defaultdict(dict), - failed=defaultdict(dict), - unreachable=defaultdict(dict), - skippe=defaultdict(dict), - ) - self.results_summary = dict( - contacted=defaultdict(dict), - dark=defaultdict(dict), - success=True - ) - self.results = { - 'raw': self.results_raw, - 'summary': self.results_summary, - } - super().__init__() - if display: - self._display = display - - cols = os.environ.get("TERM_COLS", None) - self._display.columns = 79 - if cols and cols.isdigit(): - self._display.columns = int(cols) - 1 - - def display(self, msg): - self._display.display(msg) - - def gather_result(self, t, result): - self._clean_results(result._result, result._task.action) - host = result._host.get_name() - task_name = result.task_name - task_result = result._result - - self.results_raw[t][host][task_name] = task_result - self.clean_result(t, host, task_name, task_result) - - def close(self): - if hasattr(self._display, 'close'): - self._display.close() - - -class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule): - """ - Task result Callback - """ - context = None - - def clean_result(self, t, host, task_name, task_result): - contacted = self.results_summary["contacted"] - dark = self.results_summary["dark"] - - if task_result.get('rc') is not None: - cmd = task_result.get('cmd') - if isinstance(cmd, list): - cmd = " ".join(cmd) - else: - cmd = str(cmd) - detail = { - 'cmd': cmd, - 'stderr': task_result.get('stderr'), - 'stdout': safe_str(str(task_result.get('stdout', ''))), - 'rc': task_result.get('rc'), - 'delta': task_result.get('delta'), - 'msg': task_result.get('msg', '') - } - else: - detail = { - "changed": task_result.get('changed', False), - "msg": task_result.get('msg', '') - } - - if t in ("ok", "skipped"): - contacted[host][task_name] = detail - else: - dark[host][task_name] = detail - - def v2_runner_on_failed(self, result, ignore_errors=False): - self.results_summary['success'] = False - self.gather_result("failed", result) - - if result._task.action in C.MODULE_NO_JSON: - CMDCallBackModule.v2_runner_on_failed(self, - result, ignore_errors=ignore_errors - ) - else: - super().v2_runner_on_failed( - result, ignore_errors=ignore_errors - ) - - def v2_runner_on_ok(self, result): - self.gather_result("ok", result) - if result._task.action in C.MODULE_NO_JSON: - CMDCallBackModule.v2_runner_on_ok(self, result) - else: - super().v2_runner_on_ok(result) - - def v2_runner_on_skipped(self, result): - self.gather_result("skipped", result) - super().v2_runner_on_skipped(result) - - def v2_runner_on_unreachable(self, result): - self.results_summary['success'] = False - self.gather_result("unreachable", result) - super().v2_runner_on_unreachable(result) - - def v2_runner_on_start(self, *args, **kwargs): - pass - - def display_skipped_hosts(self): - pass - - def display_ok_hosts(self): - pass - - def display_failed_stderr(self): - pass - - def set_play_context(self, context): - # for k, v in context._attributes.items(): - # print("{} ==> {}".format(k, v)) - if self.context and isinstance(self.context, dict): - for k, v in self.context.items(): - setattr(context, k, v) - - -class CommandResultCallback(AdHocResultCallback): - """ - Command result callback - - results_command: { - "cmd": "", - "stderr": "", - "stdout": "", - "rc": 0, - "delta": 0:0:0.123 +class DefaultCallback: + STATUS_MAPPER = { + 'successful': 'success', + 'failure': 'failed', + 'failed': 'failed', + 'running': 'running', + 'pending': 'pending', + 'unknown': 'unknown' } - """ - def __init__(self, display=None, **kwargs): - self.results_command = dict() - super().__init__(display) + def __init__(self): + self.result = dict( + ok=defaultdict(dict), + failures=defaultdict(dict), + dark=defaultdict(dict), + skipped=defaultdict(dict), + ) + self.summary = dict( + ok=[], + failures={}, + dark={}, + skipped=[], + ) + self.status = 'running' + self.finished = False - def gather_result(self, t, res): - super().gather_result(t, res) - self.gather_cmd(t, res) + @property + def host_results(self): + results = {} + for state, hosts in self.result.items(): + for host, items in hosts.items(): + results[host] = items + return results - def v2_playbook_on_play_start(self, play): - now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - msg = '$ {} ({})'.format(play.name, now) - self._play = play - self._display.banner(msg) + def is_success(self): + return self.status != 'success' - def v2_runner_on_unreachable(self, result): - self.results_summary['success'] = False - self.gather_result("unreachable", result) - msg = result._result.get("msg") - if not msg: - msg = json.dumps(result._result, indent=4) - self._display.display("%s | FAILED! => \n%s" % ( - result._host.get_name(), - msg, - ), color=C.COLOR_ERROR) + def event_handler(self, data, **kwargs): + event = data.get('event', None) + if not event: + return + event_data = data.get('event_data', {}) + host = event_data.get('remote_addr', '') + task = event_data.get('task', '') + res = event_data.get('res', {}) + handler = getattr(self, event, self.on_any) + handler(event_data, host=host, task=task, res=res) - def v2_runner_on_failed(self, result, ignore_errors=False): - self.results_summary['success'] = False - self.gather_result("failed", result) - msg = result._result.get("msg", '') - stderr = result._result.get("stderr") - if stderr: - msg += '\n' + stderr - module_stdout = result._result.get("module_stdout") - if module_stdout: - msg += '\n' + module_stdout - if not msg: - msg = json.dumps(result._result, indent=4) - self._display.display("%s | FAILED! => \n%s" % ( - result._host.get_name(), - msg, - ), color=C.COLOR_ERROR) - - def v2_playbook_on_stats(self, stats): - pass - - def _print_task_banner(self, task): - pass - - def gather_cmd(self, t, res): - host = res._host.get_name() - cmd = {} - if t == "ok": - cmd['cmd'] = res._result.get('cmd') - cmd['stderr'] = res._result.get('stderr') - cmd['stdout'] = safe_str(str(res._result.get('stdout', ''))) - cmd['rc'] = res._result.get('rc') - cmd['delta'] = res._result.get('delta') - else: - cmd['err'] = "Error: {}".format(res) - self.results_command[host] = cmd - - -class PlaybookResultCallBack(CallbackBase): - """ - Custom callback model for handlering the output data of - execute playbook file, - Base on the build-in callback plugins of ansible which named `json`. - """ - - CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'stdout' - CALLBACK_NAME = 'Dict' - - def __init__(self, display=None): - super(PlaybookResultCallBack, self).__init__(display) - self.results = [] - self.output = "" - self.item_results = {} # {"host": []} - - def _new_play(self, play): - return { - 'play': { - 'name': play.name, - 'id': str(play._uuid) - }, - 'tasks': [] + def runner_on_ok(self, event_data, host=None, task=None, res=None): + detail = { + 'action': event_data.get('task_action', ''), + 'res': res, + 'rc': res.get('rc', 0), + 'stdout': res.get('stdout', ''), } + self.result['ok'][host][task] = detail - def _new_task(self, task): - return { - 'task': { - 'name': task.get_name(), - }, - 'hosts': {} + def runner_on_failed(self, event_data, host=None, task=None, res=None, **kwargs): + detail = { + 'action': event_data.get('task_action', ''), + 'res': res, + 'rc': res.get('rc', 0), + 'stdout': res.get('stdout', ''), + 'stderr': ';'.join([res.get('stderr', ''), res.get('msg', '')]).strip(';') } + self.result['failures'][host][task] = detail - def v2_playbook_on_no_hosts_matched(self): - self.output = "skipping: No match hosts." + def runner_on_skipped(self, event_data, host=None, task=None, **kwargs): + detail = { + 'action': event_data.get('task_action', ''), + 'res': {}, + 'rc': 0, + } + self.result['skipped'][host][task] = detail - def v2_playbook_on_no_hosts_remaining(self): + def runner_on_unreachable(self, event_data, host=None, task=None, res=None, **kwargs): + detail = { + 'action': event_data.get('task_action', ''), + 'res': res, + 'rc': 255, + 'stderr': ';'.join([res.get('stderr', ''), res.get('msg', '')]).strip(';') + } + self.result['dark'][host][task] = detail + + def runner_on_start(self, event_data, **kwargs): pass - def v2_playbook_on_task_start(self, task, is_conditional): - self.results[-1]['tasks'].append(self._new_task(task)) + def runner_retry(self, event_data, **kwargs): + pass - def v2_playbook_on_play_start(self, play): - self.results.append(self._new_play(play)) + def runner_on_file_diff(self, event_data, **kwargs): + pass - def v2_playbook_on_stats(self, stats): - hosts = sorted(stats.processed.keys()) - summary = {} - for h in hosts: - s = stats.summarize(h) - summary[h] = s + def runner_item_on_failed(self, event_data, **kwargs): + pass - if self.output: - pass - else: - self.output = { - 'plays': self.results, - 'stats': summary - } + def runner_item_on_skipped(self, event_data, **kwargs): + pass - def gather_result(self, res): - if res._task.loop and "results" in res._result and res._host.name in self.item_results: - res._result.update({"results": self.item_results[res._host.name]}) - del self.item_results[res._host.name] + def playbook_on_play_start(self, event_data, **kwargs): + pass - self.results[-1]['tasks'][-1]['hosts'][res._host.name] = res._result + def playbook_on_stats(self, event_data, **kwargs): + failed = [] + for i in ['dark', 'failures']: + for host, tasks in self.result[i].items(): + failed.append(host) + error = '' + for task, detail in tasks.items(): + error += f'{task}: {detail["stderr"]};' + self.summary[i][host] = error.strip(';') + self.summary['ok'] = list(set(self.result['ok'].keys()) - set(failed)) + self.summary['skipped'] = list(set(self.result['skipped'].keys()) - set(failed)) - def v2_runner_on_ok(self, res, **kwargs): - if "ansible_facts" in res._result: - del res._result["ansible_facts"] + def playbook_on_include(self, event_data, **kwargs): + pass - self.gather_result(res) + def playbook_on_notify(self, event_data, **kwargs): + pass - def v2_runner_on_failed(self, res, **kwargs): - self.gather_result(res) + def playbook_on_vars_prompt(self, event_data, **kwargs): + pass - def v2_runner_on_unreachable(self, res, **kwargs): - self.gather_result(res) + def playbook_on_handler_task_start(self, event_data, **kwargs): + pass - def v2_runner_on_skipped(self, res, **kwargs): - self.gather_result(res) + def playbook_on_no_hosts_matched(self, event_data, **kwargs): + pass - def gather_item_result(self, res): - self.item_results.setdefault(res._host.name, []).append(res._result) - - def v2_runner_item_on_ok(self, res): - self.gather_item_result(res) - - def v2_runner_item_on_failed(self, res): - self.gather_item_result(res) - - def v2_runner_item_on_skipped(self, res): - self.gather_item_result(res) + def playbook_on_no_hosts_remaining(self, event_data, **kwargs): + pass + def warning(self, event_data, **kwargs): + pass + def on_any(self, event_data, **kwargs): + pass + def status_handler(self, data, **kwargs): + status = data.get('status', '') + self.status = self.STATUS_MAPPER.get(status, 'unknown') diff --git a/apps/ops/ansible/display.py b/apps/ops/ansible/display.py deleted file mode 100644 index ab93892b2..000000000 --- a/apps/ops/ansible/display.py +++ /dev/null @@ -1,69 +0,0 @@ -import errno -import sys -import os - -from ansible.utils.display import Display -from ansible.utils.color import stringc -from ansible.utils.singleton import Singleton - -from .utils import get_ansible_task_log_path - - -class UnSingleton(Singleton): - def __init__(cls, name, bases, dct): - type.__init__(cls, name, bases, dct) - - def __call__(cls, *args, **kwargs): - return type.__call__(cls, *args, **kwargs) - - -class AdHocDisplay(Display, metaclass=UnSingleton): - def __init__(self, execution_id, verbosity=0): - super().__init__(verbosity=verbosity) - if execution_id: - log_path = get_ansible_task_log_path(execution_id) - else: - log_path = os.devnull - self.log_file = open(log_path, mode='a') - - def close(self): - self.log_file.close() - - def set_cowsay_info(self): - # 中断 cowsay 的测试,会频繁开启子进程 - return - - def _write_to_screen(self, msg, stderr): - if not stderr: - screen = sys.stdout - else: - screen = sys.stderr - - screen.write(msg) - - try: - screen.flush() - except IOError as e: - # Ignore EPIPE in case fileobj has been prematurely closed, eg. - # when piping to "head -n1" - if e.errno != errno.EPIPE: - raise - - def _write_to_log_file(self, msg): - # 这里先不 flush,log 文件不需要那么及时。 - self.log_file.write(msg) - - def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False, newline=True): - if log_only: - return - - if color: - msg = stringc(msg, color) - - if not msg.endswith(u'\n'): - msg2 = msg + u'\n' - else: - msg2 = msg - - self._write_to_log_file(msg2) - self._write_to_screen(msg2, stderr) diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index df20b06d6..1258742e0 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -1,156 +1,211 @@ # ~*~ coding: utf-8 ~*~ -from ansible.inventory.host import Host -from ansible.vars.manager import VariableManager -from ansible.inventory.manager import InventoryManager -from ansible.parsing.dataloader import DataLoader +import json +import os +from collections import defaultdict + +from django.utils.translation import gettext as _ + +__all__ = ['JMSInventory'] -__all__ = [ - 'BaseHost', 'BaseInventory' -] - - -class BaseHost(Host): - def __init__(self, host_data): +class JMSInventory: + def __init__(self, assets, account_policy='privileged_first', + account_prefer='root,Administrator', host_callback=None): """ - 初始化 - :param host_data: { - "hostname": "", - "ip": "", - "port": "", - # behind is not must be required - "username": "", - "password": "", - "private_key": "", - "become": { - "method": "", - "user": "", - "pass": "", - } - "groups": [], - "vars": {}, + :param assets: + :param account_prefer: account username name if not set use account_policy + :param account_policy: privileged_only, privileged_first, skip + """ + self.assets = self.clean_assets(assets) + self.account_prefer = account_prefer + self.account_policy = account_policy + self.host_callback = host_callback + + @staticmethod + def clean_assets(assets): + from assets.models import Asset + asset_ids = [asset.id for asset in assets] + assets = Asset.objects.filter(id__in=asset_ids, is_active=True) \ + .prefetch_related('platform', 'domain', 'accounts') + return assets + + @staticmethod + def group_by_platform(assets): + groups = defaultdict(list) + for asset in assets: + groups[asset.platform].append(asset) + return groups + + @staticmethod + def make_proxy_command(gateway): + proxy_command_list = [ + "ssh", "-o", "Port={}".format(gateway.port), + "-o", "StrictHostKeyChecking=no", + "{}@{}".format(gateway.username, gateway.address), + "-W", "%h:%p", "-q", + ] + + if gateway.password: + proxy_command_list.insert( + 0, "sshpass -p '{}'".format(gateway.password) + ) + if gateway.private_key: + proxy_command_list.append("-i {}".format(gateway.private_key_path)) + + proxy_command = "'-o ProxyCommand={}'".format( + " ".join(proxy_command_list) + ) + return {"ansible_ssh_common_args": proxy_command} + + @staticmethod + def make_account_ansible_vars(account): + var = { + 'ansible_user': account.username, } - """ - self.host_data = host_data - hostname = host_data.get('hostname') or host_data.get('ip') - port = host_data.get('port') or 22 - super().__init__(hostname, port) - self.__set_required_variables() - self.__set_extra_variables() + if not account.secret: + return var + if account.secret_type == 'password': + var['ansible_password'] = account.secret + elif account.secret_type == 'ssh_key': + var['ansible_ssh_private_key_file'] = account.private_key_path + return var - def __set_required_variables(self): - host_data = self.host_data - self.set_variable('ansible_host', host_data['ip']) - self.set_variable('ansible_port', host_data['port']) + def make_ssh_account_vars(self, host, asset, account, automation, protocols, platform, gateway): + if not account: + host['error'] = _("No account available") + return host - if host_data.get('username'): - self.set_variable('ansible_user', host_data['username']) + ssh_protocol_matched = list(filter(lambda x: x.name == 'ssh', protocols)) + ssh_protocol = ssh_protocol_matched[0] if ssh_protocol_matched else None + host['ansible_host'] = asset.address + host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22 - # 添加密码和密钥 - if host_data.get('password'): - self.set_variable('ansible_ssh_pass', host_data['password']) - if host_data.get('private_key'): - self.set_variable('ansible_ssh_private_key_file', host_data['private_key']) - - # 添加become支持 - become = host_data.get("become", False) - if become: - self.set_variable("ansible_become", True) - self.set_variable("ansible_become_method", become.get('method', 'sudo')) - self.set_variable("ansible_become_user", become.get('user', 'root')) - self.set_variable("ansible_become_pass", become.get('pass', '')) - else: - self.set_variable("ansible_become", False) - - def __set_extra_variables(self): - for k, v in self.host_data.get('vars', {}).items(): - self.set_variable(k, v) - - def __repr__(self): - return self.name - - -class BaseInventory(InventoryManager): - """ - 提供生成Ansible inventory对象的方法 - """ - loader_class = DataLoader - variable_manager_class = VariableManager - host_manager_class = BaseHost - - def __init__(self, host_list=None, group_list=None): - """ - 用于生成动态构建Ansible Inventory. super().__init__ 会自动调用 - host_list: [{ - "hostname": "", - "ip": "", - "port": "", - "username": "", - "password": "", - "private_key": "", - "become": { - "method": "", - "user": "", - "pass": "", - }, - "groups": [], - "vars": {}, - }, - ] - group_list: [ - {"name: "", children: [""]}, - ] - :param host_list: - :param group_list - """ - self.host_list = host_list or [] - self.group_list = group_list or [] - assert isinstance(host_list, list) - self.loader = self.loader_class() - self.variable_manager = self.variable_manager_class() - super().__init__(self.loader) - - def get_groups(self): - return self._inventory.groups - - def get_group(self, name): - return self._inventory.groups.get(name, None) - - def get_or_create_group(self, name): - group = self.get_group(name) - if not group: - self.add_group(name) - return self.get_or_create_group(name) - else: - return group - - def parse_groups(self): - for g in self.group_list: - parent = self.get_or_create_group(g.get("name")) - children = [self.get_or_create_group(n) for n in g.get('children', [])] - for child in children: - parent.add_child_group(child) - - def parse_hosts(self): - group_all = self.get_or_create_group('all') - ungrouped = self.get_or_create_group('ungrouped') - for host_data in self.host_list: - host = self.host_manager_class(host_data=host_data) - self.hosts[host_data['hostname']] = host - groups_data = host_data.get('groups') - if groups_data: - for group_name in groups_data: - group = self.get_or_create_group(group_name) - group.add_host(host) + su_from = account.su_from + if platform.su_enabled and su_from: + host.update(self.make_account_ansible_vars(su_from)) + become_method = 'sudo' if platform.su_method != 'su' else 'su' + host['ansible_become'] = True + host['ansible_become_method'] = 'sudo' + host['ansible_become_user'] = account.username + if become_method == 'sudo': + host['ansible_become_password'] = su_from.secret else: - ungrouped.add_host(host) - group_all.add_host(host) + host['ansible_become_password'] = account.secret + else: + host.update(self.make_account_ansible_vars(account)) - def parse_sources(self, cache=False): - self.parse_groups() - self.parse_hosts() + if gateway: + host.update(self.make_proxy_command(gateway)) - def get_matched_hosts(self, pattern): - return self.get_hosts(pattern) + def asset_to_host(self, asset, account, automation, protocols, platform): + host = { + 'name': '{}'.format(asset.name), + 'jms_asset': { + 'id': str(asset.id), 'name': asset.name, 'address': asset.address, + 'type': asset.type, 'category': asset.category, + 'protocol': asset.protocol, 'port': asset.port, + 'specific': asset.specific, + 'protocols': [{'name': p.name, 'port': p.port} for p in protocols], + }, + 'jms_account': { + 'id': str(account.id), 'username': account.username, + '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 + ansible_config = dict(automation.ansible_config) + ansible_connection = ansible_config.get('ansible_connection', 'ssh') + host.update(ansible_config) + gateway = None + if asset.domain: + gateway = asset.domain.select_gateway() + + if ansible_connection == 'local': + if gateway: + host['ansible_host'] = gateway.address + host['ansible_port'] = gateway.port + host['ansible_user'] = gateway.username + host['ansible_password'] = gateway.password + host['ansible_connection'] = 'smart' + else: + host['ansible_connection'] = 'local' + else: + self.make_ssh_account_vars(host, asset, account, automation, protocols, platform, gateway) + return host + + def select_account(self, asset): + accounts = list(asset.accounts.all()) + account_selected = None + account_usernames = self.account_prefer + + if isinstance(self.account_prefer, str): + account_usernames = self.account_prefer.split(',') + + # 优先使用提供的名称 + if account_usernames: + account_matched = list(filter(lambda account: account.username in account_usernames, accounts)) + account_selected = account_matched[0] if account_matched else None + + if account_selected or self.account_policy == 'skip': + return account_selected + + if self.account_policy in ['privileged_only', 'privileged_first']: + account_matched = list(filter(lambda account: account.privileged, accounts)) + account_selected = account_matched[0] if account_matched else None + + if account_selected: + return account_selected + + if self.account_policy == 'privileged_first': + account_selected = accounts[0] if accounts else None + return account_selected + + def generate(self, path_dir): + hosts = [] + platform_assets = self.group_by_platform(self.assets) + for platform, assets in platform_assets.items(): + automation = platform.automation + + for asset in assets: + protocols = asset.protocols.all() + account = self.select_account(asset) + host = self.asset_to_host(asset, account, automation, protocols, platform) + + if not automation.ansible_enabled: + host['error'] = _('Ansible disabled') + + if self.host_callback is not None: + host = self.host_callback( + host, asset=asset, account=account, + platform=platform, automation=automation, + path_dir=path_dir + ) + + if isinstance(host, list): + hosts.extend(host) + else: + hosts.append(host) + + exclude_hosts = list(filter(lambda x: x.get('error'), hosts)) + if exclude_hosts: + print(_("Skip hosts below:")) + for i, host in enumerate(exclude_hosts, start=1): + print("{}: [{}] \t{}".format(i, host['name'], host['error'])) + + hosts = list(filter(lambda x: not x.get('error'), hosts)) + data = {'all': {'hosts': {}}} + for host in hosts: + name = host.pop('name') + data['all']['hosts'][name] = host + return data + + def write_to_file(self, path): + path_dir = os.path.dirname(path) + if not os.path.exists(path_dir): + os.makedirs(path_dir, 0o700, True) + data = self.generate(path_dir) + with open(path, 'w') as f: + f.write(json.dumps(data, indent=4)) diff --git a/apps/ops/ansible/modules/__init__.py b/apps/ops/ansible/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ops/ansible/modules/mongodb_ping.py b/apps/ops/ansible/modules/mongodb_ping.py new file mode 100644 index 000000000..d018cd852 --- /dev/null +++ b/apps/ops/ansible/modules/mongodb_ping.py @@ -0,0 +1,126 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: mongodb_ping +short_description: Check remote MongoDB server availability +description: +- Simple module to check remote MongoDB server availability. + +requirements: + - "pymongo" +''' + +EXAMPLES = ''' +- name: > + Ping MongoDB server using non-default credentials and SSL + registering the return values into the result variable for future use + mongodb_ping: + login_db: test_db + login_host: jumpserver + login_user: jms + login_password: secret_pass + ssl: True + ssl_ca_certs: "/tmp/ca.crt" + ssl_certfile: "/tmp/tls.key" #cert and key in one file + connection_options: + - "tlsAllowInvalidHostnames=true" +''' + +RETURN = ''' +is_available: + description: MongoDB server availability. + returned: always + type: bool + sample: true +server_version: + description: MongoDB server version. + returned: always + type: str + sample: '4.0.0' +conn_err_msg: + description: Connection error message. + returned: always + type: str + sample: '' +''' + + +from pymongo.errors import PyMongoError +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import ( + mongodb_common_argument_spec, + mongo_auth, + get_mongodb_client, +) + + +class MongoDBPing(object): + def __init__(self, module, client): + self.module = module + self.client = client + self.is_available = False + self.conn_err_msg = '' + self.version = '' + + def do(self): + self.get_mongodb_version() + return self.is_available, self.version + + def get_err(self): + return self.conn_err_msg + + def get_mongodb_version(self): + try: + server_info = self.client.server_info() + self.is_available = True + self.version = server_info.get('version', '') + except PyMongoError as err: + self.is_available = False + self.version = '' + self.conn_err_msg = err + + +# ========================================= +# Module execution. +# + + +def main(): + argument_spec = mongodb_common_argument_spec() + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + client = None + result = { + 'changed': False, 'is_available': False, 'server_version': '' + } + try: + client = get_mongodb_client(module, directConnection=True) + client = mongo_auth(module, client, directConnection=True) + except Exception as e: + module.fail_json(msg='Unable to connect to database: %s' % to_native(e)) + + mongodb_ping = MongoDBPing(module, client) + result["is_available"], result["server_version"] = mongodb_ping.do() + conn_err_msg = mongodb_ping.get_err() + if conn_err_msg: + module.fail_json(msg='Unable to connect to database: %s' % conn_err_msg) + + try: + client.close() + except Exception: + pass + + return module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/mongodb_user.py b/apps/ops/ansible/modules/mongodb_user.py new file mode 100644 index 000000000..93493fd58 --- /dev/null +++ b/apps/ops/ansible/modules/mongodb_user.py @@ -0,0 +1,426 @@ +#!/usr/bin/python + +# Modified from ansible_collections.community.mongodb.plugins.modules.mongodb_user + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: mongodb_user +short_description: Adds or removes a user from a MongoDB database +description: + - Adds or removes a user from a MongoDB database. +version_added: "1.0.0" + +extends_documentation_fragment: + - community.mongodb.login_options + - community.mongodb.ssl_options + +options: + replica_set: + description: + - Replica set to connect to (automatically connects to primary for writes). + type: str + database: + description: + - The name of the database to add/remove the user from. + required: true + type: str + aliases: [db] + name: + description: + - The name of the user to add or remove. + required: true + aliases: [user] + type: str + password: + description: + - The password to use for the user. + type: str + aliases: [pass] + roles: + type: list + elements: raw + description: + - > + The database user roles valid values could either be one or more of the following strings: + 'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase', + 'dbAdminAnyDatabase' + - "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'." + - "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required." + state: + description: + - The database user state. + default: present + choices: [absent, present] + type: str + update_password: + default: always + choices: [always, on_create] + description: + - C(always) will always update passwords and cause the module to return changed. + - C(on_create) will only set the password for newly created users. + - This must be C(always) to use the localhost exception when adding the first admin user. + - This option is effectively ignored when using x.509 certs. It is defaulted to 'on_create' to maintain a \ + a specific module behaviour when the login_database is '$external'. + type: str + create_for_localhost_exception: + type: path + description: + - This is parmeter is only useful for handling special treatment around the localhost exception. + - If C(login_user) is defined, then the localhost exception is not active and this parameter has no effect. + - If this file is NOT present (and C(login_user) is not defined), then touch this file after successfully adding the user. + - If this file is present (and C(login_user) is not defined), then skip this task. + +notes: + - Requires the pymongo Python package on the remote host, version 2.4.2+. This + can be installed using pip or the OS package manager. Newer mongo server versions require newer + pymongo versions. @see http://api.mongodb.org/python/current/installation.html +requirements: + - "pymongo" +author: + - "Elliott Foster (@elliotttf)" + - "Julien Thebault (@Lujeni)" +''' + +EXAMPLES = ''' +- name: Create 'burgers' database user with name 'bob' and password '12345'. + community.mongodb.mongodb_user: + database: burgers + name: bob + password: 12345 + state: present + +- name: Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly) + community.mongodb.mongodb_user: + database: burgers + name: bob + password: 12345 + state: present + ssl: True + +- name: Delete 'burgers' database user with name 'bob'. + community.mongodb.mongodb_user: + database: burgers + name: bob + state: absent + +- name: Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style) + community.mongodb.mongodb_user: + database: burgers + name: ben + password: 12345 + roles: read + state: present + +- name: Define roles + community.mongodb.mongodb_user: + database: burgers + name: jim + password: 12345 + roles: readWrite,dbAdmin,userAdmin + state: present + +- name: Define roles + community.mongodb.mongodb_user: + database: burgers + name: joe + password: 12345 + roles: readWriteAnyDatabase + state: present + +- name: Add a user to database in a replica set, the primary server is automatically discovered and written to + community.mongodb.mongodb_user: + database: burgers + name: bob + replica_set: belcher + password: 12345 + roles: readWriteAnyDatabase + state: present + +# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is useful for oplog access (MONGO_OPLOG_URL). +# please notice the credentials must be added to the 'admin' database because the 'local' database is not synchronized and can't receive user credentials +# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin" +# This syntax requires mongodb 2.6+ and pymongo 2.5+ +- name: Roles as a dictionary + community.mongodb.mongodb_user: + login_user: root + login_password: root_password + database: admin + user: oplog_reader + password: oplog_reader_password + state: present + replica_set: belcher + roles: + - db: local + role: read + +- name: Adding a user with X.509 Member Authentication + community.mongodb.mongodb_user: + login_host: "mongodb-host.test" + login_port: 27001 + login_database: "$external" + database: "admin" + name: "admin" + password: "test" + roles: + - dbAdminAnyDatabase + ssl: true + ssl_ca_certs: "/tmp/ca.crt" + ssl_certfile: "/tmp/tls.key" #cert and key in one file + state: present + auth_mechanism: "MONGODB-X509" + connection_options: + - "tlsAllowInvalidHostnames=true" +''' + +RETURN = ''' +user: + description: The name of the user to add or remove. + returned: success + type: str +''' + +import os +import traceback +from operator import itemgetter + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import binary_type, text_type +from ansible.module_utils._text import to_native, to_bytes +from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import ( + missing_required_lib, + mongodb_common_argument_spec, + mongo_auth, + PYMONGO_IMP_ERR, + pymongo_found, + get_mongodb_client, +) + + +def user_find(client, user, db_name): + """Check if the user exists. + + Args: + client (cursor): Mongodb cursor on admin database. + user (str): User to check. + db_name (str): User's database. + + Returns: + dict: when user exists, False otherwise. + """ + try: + for mongo_user in client[db_name].command('usersInfo')['users']: + if mongo_user['user'] == user: + # NOTE: there is no 'db' field in mongo 2.4. + if 'db' not in mongo_user: + return mongo_user + # Workaround to make the condition works with AWS DocumentDB, + # since all users are in the admin database. + if mongo_user["db"] in [db_name, "admin"]: + return mongo_user + except Exception as excep: + if hasattr(excep, 'code') and excep.code == 11: # 11=UserNotFound + pass # Allow return False + else: + raise + return False + + +def user_add(module, client, db_name, user, password, roles): + # pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated + # without reproducing a lot of the logic in database.py of pymongo + db = client[db_name] + + try: + exists = user_find(client, user, db_name) + except Exception as excep: + # We get this exception: "not authorized on admin to execute command" + # when auth is enabled on a new instance. The loalhost exception should + # allow us to create the first user. If the localhost exception does not apply, + # then user creation will also fail with unauthorized. So, ignore Unauthorized here. + if hasattr(excep, 'code') and excep.code == 13: # 13=Unauthorized + exists = False + else: + raise + + if exists: + user_add_db_command = 'updateUser' + if not roles: + roles = None + else: + user_add_db_command = 'createUser' + + user_dict = {} + + if password is not None: + user_dict["pwd"] = password + if roles is not None: + user_dict["roles"] = roles + + db.command(user_add_db_command, user, **user_dict) + + +def user_remove(module, client, db_name, user): + exists = user_find(client, user, db_name) + if exists: + if module.check_mode: + module.exit_json(changed=True, user=user) + db = client[db_name] + db.command("dropUser", user) + else: + module.exit_json(changed=False, user=user) + + +def check_if_roles_changed(uinfo, roles, db_name): + # We must be aware of users which can read the oplog on a replicaset + # Such users must have access to the local DB, but since this DB does not store users credentials + # and is not synchronized among replica sets, the user must be stored on the admin db + # Therefore their structure is the following : + # { + # "_id" : "admin.oplog_reader", + # "user" : "oplog_reader", + # "db" : "admin", # <-- admin DB + # "roles" : [ + # { + # "role" : "read", + # "db" : "local" # <-- local DB + # } + # ] + # } + + def make_sure_roles_are_a_list_of_dict(roles, db_name): + output = list() + for role in roles: + if isinstance(role, (binary_type, text_type)): + new_role = {"role": role, "db": db_name} + output.append(new_role) + else: + output.append(role) + return output + + roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name) + uinfo_roles = uinfo.get('roles', []) + + if sorted(roles_as_list_of_dict, key=itemgetter('db')) == sorted(uinfo_roles, key=itemgetter('db')): + return False + return True + + +# ========================================= +# Module execution. +# + +def main(): + argument_spec = mongodb_common_argument_spec() + argument_spec.update( + database=dict(required=True, aliases=['db']), + name=dict(required=True, aliases=['user']), + password=dict(aliases=['pass'], no_log=True), + replica_set=dict(default=None), + roles=dict(default=None, type='list', elements='raw'), + state=dict(default='present', choices=['absent', 'present']), + update_password=dict(default="always", choices=["always", "on_create"], no_log=False), + create_for_localhost_exception=dict(default=None, type='path'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + login_user = module.params['login_user'] + + # Certs don't have a password but we want this module behaviour + if module.params['login_database'] == '$external': + module.params['update_password'] = 'on_create' + + if not pymongo_found: + module.fail_json(msg=missing_required_lib('pymongo'), + exception=PYMONGO_IMP_ERR) + + create_for_localhost_exception = module.params['create_for_localhost_exception'] + b_create_for_localhost_exception = ( + to_bytes(create_for_localhost_exception, errors='surrogate_or_strict') + if create_for_localhost_exception is not None else None + ) + + db_name = module.params['database'] + user = module.params['name'] + password = module.params['password'] + roles = module.params['roles'] or [] + state = module.params['state'] + update_password = module.params['update_password'] + + try: + directConnection = False + if module.params['replica_set'] is None: + directConnection = True + client = get_mongodb_client(module, directConnection=directConnection) + client = mongo_auth(module, client, directConnection=directConnection) + except Exception as e: + module.fail_json(msg='Unable to connect to database: %s' % to_native(e)) + + if state == 'present': + if password is None and update_password == 'always': + module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') + + if login_user is None and create_for_localhost_exception is not None: + if os.path.exists(b_create_for_localhost_exception): + try: + client.close() + except Exception: + pass + module.exit_json(changed=False, user=user, skipped=True, msg="The path in create_for_localhost_exception exists.") + + try: + if update_password != 'always': + uinfo = user_find(client, user, db_name) + if uinfo: + password = None + if not check_if_roles_changed(uinfo, roles, db_name): + module.exit_json(changed=False, user=user) + + if module.check_mode: + module.exit_json(changed=True, user=user) + user_add(module, client, db_name, user, password, roles) + except Exception as e: + module.fail_json(msg='Unable to add or update user: %s' % to_native(e), exception=traceback.format_exc()) + finally: + try: + client.close() + except Exception: + pass + # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848 + # newuinfo = user_find(client, user, db_name) + # if uinfo['role'] == newuinfo['role'] and CheckPasswordHere: + # module.exit_json(changed=False, user=user) + + if login_user is None and create_for_localhost_exception is not None: + # localhost exception applied. + try: + # touch the file + open(b_create_for_localhost_exception, 'wb').close() + except Exception as e: + module.fail_json( + changed=True, + msg='Added user but unable to touch create_for_localhost_exception file %s: %s' % (create_for_localhost_exception, to_native(e)), + exception=traceback.format_exc() + ) + + elif state == 'absent': + try: + user_remove(module, client, db_name, user) + except Exception as e: + module.fail_json(msg='Unable to remove user: %s' % to_native(e), exception=traceback.format_exc()) + finally: + try: + client.close() + except Exception: + pass + module.exit_json(changed=True, user=user) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/oracle_info.py b/apps/ops/ansible/modules/oracle_info.py new file mode 100644 index 000000000..005206be9 --- /dev/null +++ b/apps/ops/ansible/modules/oracle_info.py @@ -0,0 +1,261 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: oracle_info +short_description: Gather information about Oracle servers +description: +- Gathers information about Oracle servers. + +options: + filter: + description: + - Limit the collected information by comma separated string or YAML list. + - Allowable values are C(version), C(databases), C(settings), C(users). + - By default, collects all subsets. + - You can use '!' before value (for example, C(!users)) to exclude it from the information. + - If you pass including and excluding values to the filter, for example, I(filter=!settings,version), + the excluding values, C(!settings) in this case, will be ignored. + type: list + elements: str + login_db: + description: + - Database name to connect to. + - It makes sense if I(login_user) is allowed to connect to a specific database only. + type: str + exclude_fields: + description: + - List of fields which are not needed to collect. + - "Supports elements: C(db_size). Unsupported elements will be ignored." + type: list + elements: str +''' + +EXAMPLES = r''' +- name: Get Oracle version with non-default credentials + oracle_info: + login_user: mysuperuser + login_password: mysuperpass + login_database: service_name + filter: version + +- name: Collect all info except settings and users by sys + oracle_info: + login_user: sys + login_password: sys_pass + login_database: service_name + filter: "!settings,!users" + exclude_fields: db_size +''' + +RETURN = r''' +version: + description: Database server version. + returned: if not excluded by filter + type: dict + sample: { "version": {"full": "11.2.0.1.0"} } + contains: + full: + description: Full server version. + returned: if not excluded by filter + type: str + sample: "11.2.0.1.0" +databases: + description: Information about databases. + returned: if not excluded by filter + type: dict + sample: + - { "USERS": { "size": 5242880 }, "EXAMPLE": { "size": 104857600 } } + contains: + size: + description: Database size in bytes. + returned: if not excluded by filter + type: dict + sample: { 'size': 656594 } +settings: + description: Global settings (variables) information. + returned: if not excluded by filter + type: dict + sample: + - { "result_cache_mode": "MANUAL", "instance_type": "RDBMS" } +users: + description: Users information. + returned: if not excluded by filter + type: dict + sample: + - { "USERS": { "TEST": { "USERNAME": "TEST", "ACCOUNT_STATUS": "OPEN" } } } +''' + +from ansible.module_utils.basic import AnsibleModule + +from ops.ansible.modules_utils.oracle_common import ( + OracleClient, oracle_common_argument_spec +) + + +class OracleInfo(object): + def __init__(self, module, oracle_client): + self.module = module + self.oracle_client = oracle_client + self.info = { + 'version': {}, 'databases': {}, + 'settings': {}, 'users': {}, + } + + def get_info(self, filter_, exclude_fields): + include_list = [] + exclude_list = [] + + if filter_: + partial_info = {} + + for fi in filter_: + if fi.lstrip('!') not in self.info: + self.module.warn('filter element: %s is not allowable, ignored' % fi) + continue + + if fi[0] == '!': + exclude_list.append(fi.lstrip('!')) + else: + include_list.append(fi) + + if include_list: + self.__collect(exclude_fields, set(include_list)) + + for i in self.info: + if i in include_list: + partial_info[i] = self.info[i] + else: + not_in_exclude_list = list(set(self.info) - set(exclude_list)) + self.__collect(exclude_fields, set(not_in_exclude_list)) + + for i in self.info: + if i not in exclude_list: + partial_info[i] = self.info[i] + return partial_info + else: + self.__collect(exclude_fields, set(self.info)) + return self.info + + def __collect(self, exclude_fields, wanted): + """Collect all possible subsets.""" + if 'version' in wanted: + self.__get_version() + + if 'settings' in wanted: + self.__get_settings() + + if 'databases' in wanted: + self.__get_databases(exclude_fields) + # + if 'users' in wanted: + self.__get_users() + + def __get_version(self): + version_sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION where ROWNUM=1' + rtn, err = self.oracle_client.execute(version_sql, exception_to_fail=True) + self.info['version'] = {'full': rtn.get('version')} + + def __get_settings(self): + """Get global variables (instance settings).""" + def _set_settings_value(item_dict): + try: + self.info['settings'][item_dict['name']] = item_dict['value'] + except KeyError: + pass + + settings_sql = "SELECT name, value FROM V$PARAMETER" + rtn, err = self.oracle_client.execute(settings_sql, exception_to_fail=True) + + if isinstance(rtn, dict): + _set_settings_value(rtn) + elif isinstance(rtn, list): + for i in rtn: + _set_settings_value(i) + + def __get_users(self): + """Get user info.""" + def _set_users_value(item_dict): + try: + tablespace = item_dict.pop('default_tablespace') + username = item_dict.pop('username') + partial_users = self.info['users'].get(tablespace, {}) + partial_users[username] = item_dict + self.info['users'][tablespace] = partial_users + except KeyError: + pass + + users_sql = "SELECT * FROM dba_users" + rtn, err = self.oracle_client.execute(users_sql, exception_to_fail=True) + if isinstance(rtn, dict): + _set_users_value(rtn) + elif isinstance(rtn, list): + for i in rtn: + _set_users_value(i) + + def __get_databases(self, exclude_fields): + """Get info about databases.""" + def _set_databases_value(item_dict): + try: + tablespace_name = item_dict.pop('tablespace_name') + size = item_dict.get('size') + partial_params = {} + if size: + partial_params['size'] = size + self.info['databases'][tablespace_name] = partial_params + except KeyError: + pass + + database_sql = 'SELECT ' \ + ' tablespace_name, sum(bytes) as "size"' \ + 'FROM dba_data_files GROUP BY tablespace_name' + if exclude_fields and 'db_size' in exclude_fields: + database_sql = "SELECT " \ + " tablespace_name " \ + "FROM dba_data_files GROUP BY tablespace_name" + + rtn, err = self.oracle_client.execute(database_sql, exception_to_fail=True) + if isinstance(rtn, dict): + _set_databases_value(rtn) + elif isinstance(rtn, list): + for i in rtn: + _set_databases_value(i) + + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = oracle_common_argument_spec() + argument_spec.update( + filter=dict(type='list'), + exclude_fields=dict(type='list'), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + filter_ = module.params['filter'] + exclude_fields = module.params['exclude_fields'] + + if filter_: + filter_ = [f.strip() for f in filter_] + + if exclude_fields: + exclude_fields = set([f.strip() for f in exclude_fields]) + + oracle_client = OracleClient(module) + oracle = OracleInfo(module, oracle_client) + + module.exit_json(changed=False, **oracle.get_info(filter_, exclude_fields)) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/oracle_ping.py b/apps/ops/ansible/modules/oracle_ping.py new file mode 100644 index 000000000..df1069d11 --- /dev/null +++ b/apps/ops/ansible/modules/oracle_ping.py @@ -0,0 +1,107 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: oracle_ping +short_description: Check remote Oracle server availability +description: +- Simple module to check remote Oracle server availability. + +requirements: + - "oracledb" +''' + +EXAMPLES = ''' +- name: > + Ping Oracle server using non-default credentials and SSL + registering the return values into the result variable for future use + oracle_ping: + login_host: jumpserver + login_port: 1521 + login_user: jms + login_password: secret_pass + login_database: test_db +''' + +RETURN = ''' +is_available: + description: Oracle server availability. + returned: always + type: bool + sample: true +server_version: + description: Oracle server version. + returned: always + type: str + sample: '4.0.0' +conn_err_msg: + description: Connection error message. + returned: always + type: str + sample: '' +''' + +from ansible.module_utils.basic import AnsibleModule +from ops.ansible.modules_utils.oracle_common import ( + OracleClient, oracle_common_argument_spec +) + + +class OracleDBPing(object): + def __init__(self, module, oracle_client): + self.module = module + self.oracle_client = oracle_client + self.is_available = False + self.conn_err_msg = '' + self.version = '' + + def do(self): + self.get_oracle_version() + return self.is_available, self.version + + def get_err(self): + return self.conn_err_msg + + def get_oracle_version(self): + version_sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION where ROWNUM=1' + rtn, err = self.oracle_client.execute(version_sql) + if err: + self.conn_err_msg = err + else: + self.version = rtn.get('version') + self.is_available = True + + +# ========================================= +# Module execution. +# + + +def main(): + argument_spec = oracle_common_argument_spec() + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + result = { + 'changed': False, 'is_available': False, 'server_version': '' + } + oracle_client = OracleClient(module) + + oracle_ping = OracleDBPing(module, oracle_client) + result["is_available"], result["server_version"] = oracle_ping.do() + conn_err_msg = oracle_ping.get_err() + oracle_client.close() + if conn_err_msg: + module.fail_json(msg='Unable to connect to database: %s' % conn_err_msg) + + return module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/oracle_user.py b/apps/ops/ansible/modules/oracle_user.py new file mode 100644 index 000000000..9e23fe70c --- /dev/null +++ b/apps/ops/ansible/modules/oracle_user.py @@ -0,0 +1,215 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: oracle_user +short_description: Adds or removes a user from a Oracle database +description: + - Adds or removes a user from a Oracle database. + +options: + authentication_type: + description: + - Authentication type of the user(default password) + required: false + type: str + choices: ['external', 'global', 'no_authentication', 'password'] + default_tablespace: + description: + - The default tablespace for the user + - If not provided, the default is used + required: false + type: str + oracle_home: + description: + - Define the directory into which all Oracle software is installed. + - Define ORACLE_HOME environment variable if set. + type: str + state: + description: + - The database user state. + default: present + choices: [absent, present] + type: str + update_password: + default: always + choices: [always, on_create] + description: + - C(always) will always update passwords and cause the module to return changed. + - C(on_create) will only set the password for newly created users. + type: str + temporary_tablespace: + description: + - The default temporary tablespace for the user + - If not provided, the default is used + required: false + type: str + name: + description: + - The name of the user to add or remove. + required: true + aliases: [user] + type: str + password: + description: + - The password to use for the user. + type: str + aliases: [pass] + +requirements: + - "oracledb" +''' + +EXAMPLES = ''' +- name: Create default tablespace user with name 'jms' and password '123456'. + oracle_user: + hostname: "remote server" + login_database: "helowin" + login_username: "system" + login_password: "123456" + name: "jms" + password: "123456" + +- name: Delete user with name 'jms'. + oracle_user: + hostname: "remote server" + login_database: "helowin" + login_username: "system" + login_password: "123456" + name: "jms" + state: "absent" +''' + +RETURN = ''' +name: + description: The name of the user to add or remove. + returned: success + type: str +''' + +from ansible.module_utils.basic import AnsibleModule + +from ops.ansible.modules_utils.oracle_common import ( + OracleClient, oracle_common_argument_spec +) + + +def user_find(oracle_client, username): + user = None + username = username.upper() + user_find_sql = "select username, " \ + " authentication_type, " \ + " default_tablespace, " \ + " temporary_tablespace " \ + "from dba_users where username='%s'" % username + rtn, err = oracle_client.execute(user_find_sql) + if isinstance(rtn, dict): + user = rtn + return user + + +def user_add( + module, oracle_client, username, password, auth_type, + default_tablespace, temporary_tablespace +): + username = username.upper() + extend_sql = None + user = user_find(oracle_client, username) + auth_type = auth_type.lower() + identified_suffix_map = { + 'external': 'identified externally ', + 'global': 'identified globally ', + 'password': 'identified by "%s" ', + } + if user: + user_sql = "alter user %s " % username + user_sql += identified_suffix_map.get(auth_type, 'no authentication ') % password + + if default_tablespace and default_tablespace.lower() != user['default_tablespace'].lower(): + user_sql += 'default tablespace %s quota unlimited on %s ' % (default_tablespace, default_tablespace) + if temporary_tablespace and temporary_tablespace.lower() != user['temporary_tablespace'].lower(): + user_sql += 'temporary tablespace %s ' % temporary_tablespace + else: + user_sql = "create user %s " % username + user_sql += identified_suffix_map.get(auth_type, 'no authentication ') % password + if default_tablespace: + user_sql += 'default tablespace %s quota unlimited on %s ' % (default_tablespace, default_tablespace) + if temporary_tablespace: + user_sql += 'temporary tablespace %s ' % temporary_tablespace + extend_sql = 'grant connect to %s' % username + + rtn, err = oracle_client.execute(user_sql) + if err: + module.fail_json(msg='Cannot add/edit user %s: %s' % (username, err), changed=False) + else: + if extend_sql: + oracle_client.execute(extend_sql) + module.exit_json(msg='User %s has been created.' % username, changed=True, name=username) + + +def user_remove(module, oracle_client, username): + user = user_find(oracle_client, username) + + if user: + rtn, err = oracle_client.execute('drop user %s cascade' % username) + if err: + module.fail_json(msg='Cannot drop user %s: %s' % (username, err), changed=False) + else: + module.exit_json(msg='User %s dropped.' % username, changed=True, name=username) + else: + module.exit_json(msg="User %s doesn't exist." % username, changed=False, name=username) + + +# ========================================= +# Module execution. +# + +def main(): + argument_spec = oracle_common_argument_spec() + argument_spec.update( + authentication_type=dict( + type='str', required=False, + choices=['external', 'global', 'no_authentication', 'password'] + ), + default_tablespace=dict(required=False, aliases=['db']), + name=dict(required=True, aliases=['user']), + password=dict(aliases=['pass'], no_log=True), + state=dict(type='str', default='present', choices=['absent', 'present']), + update_password=dict(default="always", choices=["always", "on_create"], no_log=False), + temporary_tablespace=dict(type='str', default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + authentication_type = module.params['authentication_type'] or 'password' + default_tablespace = module.params['default_tablespace'] + user = module.params['name'] + password = module.params['password'] + state = module.params['state'] + update_password = module.params['update_password'] + temporary_tablespace = module.params['temporary_tablespace'] + + oracle_client = OracleClient(module) + if state == 'present': + if password is None and update_password == 'always': + module.fail_json( + msg='password parameter required when adding a user unless update_password is set to on_create' + ) + user_add( + module, oracle_client, username=user, password=password, + auth_type=authentication_type, default_tablespace=default_tablespace, + temporary_tablespace=temporary_tablespace + ) + elif state == 'absent': + user_remove(oracle_client) + module.exit_json(changed=True, user=user) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules_utils/__init__.py b/apps/ops/ansible/modules_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ops/ansible/modules_utils/oracle_common.py b/apps/ops/ansible/modules_utils/oracle_common.py new file mode 100644 index 000000000..c88f04373 --- /dev/null +++ b/apps/ops/ansible/modules_utils/oracle_common.py @@ -0,0 +1,94 @@ +import os + +import oracledb + +from oracledb.exceptions import DatabaseError +from ansible.module_utils._text import to_native + + +def oracle_common_argument_spec(): + """ + Returns a dict containing common options shared across the Oracle modules. + """ + options = dict( + login_user=dict(type='str', required=False), + login_password=dict(type='str', required=False, no_log=True), + login_database=dict(type='str', required=False, default='test'), + login_host=dict(type='str', required=False, default='localhost'), + login_port=dict(type='int', required=False, default=1521), + oracle_home=dict(type='str', required=False), + mode=dict(type='str', required=False), + ) + return options + + +class OracleClient(object): + def __init__(self, module): + self.module = module + self._conn = None + self._cursor = None + self.connect_params = {} + + self.init_params() + + def init_params(self): + params = self.module.params + hostname = params['login_host'] + port = params['login_port'] + service_name = params['login_database'] + username = params['login_user'] + password = params['login_password'] + oracle_home = params['oracle_home'] + mode = params['mode'] + + if oracle_home: + os.environ.setdefault('ORACLE_HOME', oracle_home) + if mode == 'sysdba': + self.connect_params['mode'] = oracledb.SYSDBA + + self.connect_params['host'] = hostname + self.connect_params['port'] = port + self.connect_params['user'] = username + self.connect_params['password'] = password + self.connect_params['service_name'] = service_name + + @property + def cursor(self): + if self._cursor is None: + try: + oracledb.init_oracle_client(lib_dir='/Users/jiangweidong/Downloads/instantclient_19_8') + self._conn = oracledb.connect(**self.connect_params) + self._cursor = self._conn.cursor() + except DatabaseError as err: + self.module.fail_json( + msg="Unable to connect to database: %s, %s" % (to_native(err), self.connect_params) + ) + return self._cursor + + def execute(self, sql, exception_to_fail=False): + sql = sql[:-1] if sql.endswith(';') else sql + result, error = None, None + try: + self.cursor.execute(sql) + sql_header = self.cursor.description or [] + column_names = [description[0].lower() for description in sql_header] + if column_names: + result = [dict(zip(column_names, row)) for row in self.cursor] + result = result[0] if len(result) == 1 else result + else: + result = None + except DatabaseError as err: + error = err + if exception_to_fail and error: + self.module.fail_json(msg='Cannot execute sql: %s' % to_native(error)) + return result, error + + def close(self): + try: + if self._cursor: + self._cursor.close() + if self._conn: + self._conn.close() + except: + pass + diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index a25d681b9..13d56bd00 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -1,261 +1,87 @@ -# ~*~ coding: utf-8 ~*~ - +import uuid import os -import shutil -from collections import namedtuple +import ansible_runner +from django.conf import settings -from ansible import context -from ansible.playbook import Playbook -from ansible.module_utils.common.collections import ImmutableDict -from ansible.executor.task_queue_manager import TaskQueueManager -from ansible.vars.manager import VariableManager -from ansible.parsing.dataloader import DataLoader -from ansible.executor.playbook_executor import PlaybookExecutor -from ansible.playbook.play import Play -import ansible.constants as C - -from .callback import ( - AdHocResultCallback, PlaybookResultCallBack, CommandResultCallback -) -from common.utils import get_logger -from .exceptions import AnsibleError -from .display import AdHocDisplay - - -__all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"] -C.HOST_KEY_CHECKING = False -logger = get_logger(__name__) - - -Options = namedtuple('Options', [ - 'listtags', 'listtasks', 'listhosts', 'syntax', 'connection', - 'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout', - 'ssh_common_args', 'ssh_extra_args', 'sftp_extra_args', - 'scp_extra_args', 'become', 'become_method', 'become_user', - 'verbosity', 'check', 'extra_vars', 'playbook_path', 'passwords', - 'diff', 'gathering', 'remote_tmp', -]) - - -def get_default_options(): - options = dict( - syntax=False, - timeout=30, - connection='ssh', - forks=10, - remote_user='root', - private_key_file=None, - become=None, - become_method=None, - become_user=None, - verbosity=1, - check=False, - diff=False, - gathering='implicit', - remote_tmp='/tmp/.ansible' - ) - return options - - -# JumpServer not use playbook -class PlayBookRunner: - """ - 用于执行AnsiblePlaybook的接口.简化Playbook对象的使用. - """ - - # Default results callback - results_callback_class = PlaybookResultCallBack - loader_class = DataLoader - variable_manager_class = VariableManager - options = get_default_options() - - def __init__(self, inventory=None, options=None): - """ - :param options: Ansible options like ansible.cfg - :param inventory: Ansible inventory - """ - if options: - self.options = options - C.RETRY_FILES_ENABLED = False - self.inventory = inventory - self.loader = self.loader_class() - self.results_callback = self.results_callback_class() - self.playbook_path = options.playbook_path - self.variable_manager = self.variable_manager_class( - loader=self.loader, inventory=self.inventory - ) - self.passwords = options.passwords - self.__check() - - def __check(self): - if self.options.playbook_path is None or \ - not os.path.exists(self.options.playbook_path): - raise AnsibleError( - "Not Found the playbook file: {}.".format(self.options.playbook_path) - ) - if not self.inventory.list_hosts('all'): - raise AnsibleError('Inventory is empty') - - def run(self): - executor = PlaybookExecutor( - playbooks=[self.playbook_path], - inventory=self.inventory, - variable_manager=self.variable_manager, - loader=self.loader, - passwords={"conn_pass": self.passwords} - ) - context.CLIARGS = ImmutableDict(self.options) - - if executor._tqm: - executor._tqm._stdout_callback = self.results_callback - executor.run() - executor._tqm.cleanup() - return self.results_callback.output +from .callback import DefaultCallback class AdHocRunner: - """ - ADHoc Runner接口 - """ - results_callback_class = AdHocResultCallback - results_callback = None - loader_class = DataLoader - variable_manager_class = VariableManager - default_options = get_default_options() - command_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell') + cmd_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell') + cmd_blacklist = [ + "reboot", 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' + ] - def __init__(self, inventory, options=None): - self.options = self.update_options(options) + def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars={}): + self.id = uuid.uuid4() self.inventory = inventory - self.loader = DataLoader() - self.variable_manager = VariableManager( - loader=self.loader, inventory=self.inventory - ) + self.pattern = pattern + self.module = module + self.module_args = module_args + self.project_dir = project_dir + self.cb = DefaultCallback() + self.runner = None + self.extra_vars = extra_vars - def get_result_callback(self, execution_id=None): - return self.__class__.results_callback_class(display=AdHocDisplay(execution_id)) + def check_module(self): + if self.module not in self.cmd_modules_choices: + return + if self.module_args and self.module_args.split()[0] in self.cmd_blacklist: + raise Exception("command not allowed: {}".format(self.module_args[0])) - @staticmethod - def check_module_args(module_name, module_args=''): - if module_name in C.MODULE_REQUIRE_ARGS and not module_args: - err = "No argument passed to '%s' module." % module_name - raise AnsibleError(err) + def run(self, verbosity=0, **kwargs): + self.check_module() + if verbosity is None and settings.DEBUG: + verbosity = 1 - def check_pattern(self, pattern): - if not pattern: - raise AnsibleError("Pattern `{}` is not valid!".format(pattern)) - if not self.inventory.list_hosts("all"): - raise AnsibleError("Inventory is empty.") - if not self.inventory.list_hosts(pattern): - raise AnsibleError( - "pattern: %s dose not match any hosts." % pattern - ) + if not os.path.exists(self.project_dir): + os.mkdir(self.project_dir, 0o755) - def clean_args(self, module, args): - if not args: - return '' - if module not in self.command_modules_choices: - return args - if isinstance(args, str): - if args.startswith('executable='): - _args = args.split(' ') - executable, command = _args[0].split('=')[1], ' '.join(_args[1:]) - args = {'executable': executable, '_raw_params': command} - else: - args = {'_raw_params': args} - return args - else: - return args - - def clean_tasks(self, tasks): - cleaned_tasks = [] - for task in tasks: - module = task['action']['module'] - args = task['action'].get('args') - cleaned_args = self.clean_args(module, args) - task['action']['args'] = cleaned_args - self.check_module_args(module, cleaned_args) - cleaned_tasks.append(task) - return cleaned_tasks - - def update_options(self, options): - _options = {k: v for k, v in self.default_options.items()} - if options and isinstance(options, dict): - _options.update(options) - return _options - - def set_control_master_if_need(self, cleaned_tasks): - modules = [task.get('action', {}).get('module') for task in cleaned_tasks] - if {'ping', 'win_ping'} & set(modules): - self.results_callback.context = { - 'ssh_args': '-C -o ControlMaster=no' - } - - def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', execution_id=None): - """ - :param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ] - :param pattern: all, *, or others - :param play_name: The play name - :param gather_facts: - :return: - """ - self.check_pattern(pattern) - self.results_callback = self.get_result_callback(execution_id) - cleaned_tasks = self.clean_tasks(tasks) - self.set_control_master_if_need(cleaned_tasks) - context.CLIARGS = ImmutableDict(self.options) - - play_source = dict( - name=play_name, - hosts=pattern, - gather_facts=gather_facts, - tasks=cleaned_tasks - ) - - play = Play().load( - play_source, - variable_manager=self.variable_manager, - loader=self.loader, - ) - loader = DataLoader() - # used in start callback - playbook = Playbook(loader) - playbook._entries.append(play) - playbook._file_name = '__adhoc_playbook__' - - tqm = TaskQueueManager( + ansible_runner.run( + extravars=self.extra_vars, + host_pattern=self.pattern, + private_data_dir=self.project_dir, inventory=self.inventory, - variable_manager=self.variable_manager, - loader=self.loader, - stdout_callback=self.results_callback, - passwords={"conn_pass": self.options.get("password", "")} + module=self.module, + module_args=self.module_args, + verbosity=verbosity, + event_handler=self.cb.event_handler, + status_handler=self.cb.status_handler, + **kwargs ) - try: - tqm.send_callback('v2_playbook_on_start', playbook) - tqm.run(play) - tqm.send_callback('v2_playbook_on_stats', tqm._stats) - return self.results_callback - except Exception as e: - raise AnsibleError(e) - finally: - if tqm is not None: - tqm.cleanup() - shutil.rmtree(C.DEFAULT_LOCAL_TMP, True) + return self.cb - self.results_callback.close() + +class PlaybookRunner: + def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None): + self.id = uuid.uuid4() + self.inventory = inventory + self.playbook = playbook + self.project_dir = project_dir + if not callback: + callback = DefaultCallback() + self.cb = callback + + def run(self, verbosity=0, **kwargs): + if verbosity is None and settings.DEBUG: + verbosity = 1 + + ansible_runner.run( + private_data_dir=self.project_dir, + inventory=self.inventory, + playbook=self.playbook, + verbosity=verbosity, + event_handler=self.cb.event_handler, + status_handler=self.cb.status_handler, + **kwargs + ) + return self.cb class CommandRunner(AdHocRunner): - results_callback_class = CommandResultCallback - modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell') - - def execute(self, cmd, pattern, module='shell'): - if module and module not in self.modules_choices: - raise AnsibleError("Module should in {}".format(self.modules_choices)) - - tasks = [ - {"action": {"module": module, "args": cmd}} - ] - return self.run(tasks, pattern, play_name=cmd) + def __init__(self, inventory, command, pattern='*', project_dir='/tmp/'): + super().__init__(inventory, 'shell', command, pattern, project_dir) + def run(self, verbosity=0, **kwargs): + return super().run(verbosity, **kwargs) diff --git a/apps/ops/ansible/test_inventory.py b/apps/ops/ansible/test_inventory.py deleted file mode 100644 index 00c7fa459..000000000 --- a/apps/ops/ansible/test_inventory.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import sys -import unittest - - -sys.path.insert(0, '../..') -from ops.ansible.inventory import BaseInventory - - -class TestJMSInventory(unittest.TestCase): - def setUp(self): - host_list = [{ - "hostname": "testserver1", - "ip": "102.1.1.1", - "port": 22, - "username": "root", - "password": "password", - "private_key": "/tmp/private_key", - "become": { - "method": "sudo", - "user": "root", - "pass": None, - }, - "groups": ["group1", "group2"], - "vars": {"sexy": "yes"}, - }, { - "hostname": "testserver2", - "ip": "8.8.8.8", - "port": 2222, - "username": "root", - "password": "password", - "private_key": "/tmp/private_key", - "become": { - "method": "su", - "user": "root", - "pass": "123", - }, - "groups": ["group3", "group4"], - "vars": {"love": "yes"}, - }] - - self.inventory = BaseInventory(host_list=host_list) - - def test_hosts(self): - print("#"*10 + "Hosts" + "#"*10) - for host in self.inventory.hosts: - print(host) - - def test_groups(self): - print("#" * 10 + "Groups" + "#" * 10) - for group in self.inventory.groups: - print(group) - - def test_group_all(self): - print("#" * 10 + "all group hosts" + "#" * 10) - group = self.inventory.get_group('all') - print(group.hosts) - - -if __name__ == '__main__': - unittest.main() diff --git a/apps/ops/ansible/test_runner.py b/apps/ops/ansible/test_runner.py deleted file mode 100644 index e38168a6c..000000000 --- a/apps/ops/ansible/test_runner.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import unittest -import sys - -sys.path.insert(0, "../..") - -from ops.ansible.runner import AdHocRunner, CommandRunner -from ops.ansible.inventory import BaseInventory - - -class TestAdHocRunner(unittest.TestCase): - def setUp(self): - host_data = [ - { - "hostname": "testserver", - "ip": "192.168.244.185", - "port": 22, - "username": "root", - "password": "redhat", - }, - ] - inventory = BaseInventory(host_data) - self.runner = AdHocRunner(inventory) - - def test_run(self): - tasks = [ - {"action": {"module": "shell", "args": "ls"}, "name": "run_cmd"}, - {"action": {"module": "shell", "args": "whoami"}, "name": "run_whoami"}, - ] - ret = self.runner.run(tasks, "all") - print(ret.results_summary) - print(ret.results_raw) - - -class TestCommandRunner(unittest.TestCase): - def setUp(self): - host_data = [ - { - "hostname": "testserver", - "ip": "192.168.244.168", - "port": 22, - "username": "root", - "password": "redhat", - }, - ] - inventory = BaseInventory(host_data) - self.runner = CommandRunner(inventory) - - def test_execute(self): - res = self.runner.execute('ls', 'all') - print(res.results_command) - print(res.results_raw) - - -if __name__ == "__main__": - unittest.main() diff --git a/apps/ops/ansible/utils.py b/apps/ops/ansible/utils.py index 478badc56..daeb98b14 100644 --- a/apps/ops/ansible/utils.py +++ b/apps/ops/ansible/utils.py @@ -3,4 +3,4 @@ from django.conf import settings def get_ansible_task_log_path(task_id): from ops.utils import get_task_log_path - return get_task_log_path(settings.ANSIBLE_LOG_DIR, task_id, level=3) + return get_task_log_path(settings.CELERY_LOG_DIR, task_id, level=2) diff --git a/apps/ops/api/__init__.py b/apps/ops/api/__init__.py index e59889cd2..e82f3fb2e 100644 --- a/apps/ops/api/__init__.py +++ b/apps/ops/api/__init__.py @@ -2,4 +2,5 @@ # from .adhoc import * from .celery import * -from .command import * +from .job import * +from .playbook import * diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 0cc7b6d55..aca7047f1 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -1,92 +1,20 @@ # -*- coding: utf-8 -*- # -from django.shortcuts import get_object_or_404 -from rest_framework import viewsets, generics -from rest_framework.views import Response +from rest_framework import viewsets -from common.drf.serializers import CeleryTaskSerializer -from ..models import Task, AdHoc, AdHocExecution -from ..serializers import ( - TaskSerializer, - AdHocSerializer, - AdHocExecutionSerializer, - TaskDetailSerializer, - AdHocDetailSerializer, -) -from ..tasks import run_ansible_task from orgs.mixins.api import OrgBulkModelViewSet +from ..models import AdHoc +from ..serializers import ( + AdHocSerializer +) __all__ = [ - 'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet' + 'AdHocViewSet' ] -class TaskViewSet(OrgBulkModelViewSet): - model = Task - filterset_fields = ("name",) - search_fields = filterset_fields - serializer_class = TaskSerializer - - def get_serializer_class(self): - if self.action == 'retrieve': - return TaskDetailSerializer - return super().get_serializer_class() - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.select_related('latest_execution') - return queryset - - -class TaskRun(generics.RetrieveAPIView): - queryset = Task.objects.all() - serializer_class = CeleryTaskSerializer - rbac_perms = { - 'retrieve': 'ops.add_adhoc' - } - - def retrieve(self, request, *args, **kwargs): - task = self.get_object() - t = run_ansible_task.delay(str(task.id)) - return Response({"task": t.id}) - - -class AdHocViewSet(viewsets.ModelViewSet): - queryset = AdHoc.objects.all() +class AdHocViewSet(OrgBulkModelViewSet): serializer_class = AdHocSerializer - - def get_serializer_class(self): - if self.action == 'retrieve': - return AdHocDetailSerializer - return super().get_serializer_class() - - def get_queryset(self): - task_id = self.request.query_params.get('task') - if task_id: - task = get_object_or_404(Task, id=task_id) - self.queryset = self.queryset.filter(task=task) - return self.queryset - - -class AdHocRunHistoryViewSet(viewsets.ModelViewSet): - queryset = AdHocExecution.objects.all() - serializer_class = AdHocExecutionSerializer - - def get_queryset(self): - task_id = self.request.query_params.get('task') - adhoc_id = self.request.query_params.get('adhoc') - if task_id: - task = get_object_or_404(Task, id=task_id) - adhocs = task.adhoc.all() - self.queryset = self.queryset.filter(adhoc__in=adhocs) - - if adhoc_id: - adhoc = get_object_or_404(AdHoc, id=adhoc_id) - self.queryset = self.queryset.filter(adhoc=adhoc) - return self.queryset - - - - - + permission_classes = () + model = AdHoc diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index cd452c471..8d58c1981 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -4,6 +4,7 @@ import os import re +from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ from rest_framework import viewsets from celery.result import AsyncResult @@ -12,20 +13,21 @@ from django_celery_beat.models import PeriodicTask from common.permissions import IsValidUser from common.api import LogTailApi -from ..models import CeleryTask +from ..models import CeleryTaskExecution, CeleryTask from ..serializers import CeleryResultSerializer, CeleryPeriodTaskSerializer from ..celery.utils import get_celery_task_log_path from ..ansible.utils import get_ansible_task_log_path from common.mixins.api import CommonApiMixin - __all__ = [ - 'CeleryTaskLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet', - 'AnsibleTaskLogApi', + 'CeleryTaskExecutionLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet', + 'AnsibleTaskLogApi', 'CeleryTaskViewSet', 'CeleryTaskExecutionViewSet' ] +from ..serializers.celery import CeleryTaskSerializer, CeleryTaskExecutionSerializer -class CeleryTaskLogApi(LogTailApi): + +class CeleryTaskExecutionLogApi(LogTailApi): permission_classes = (IsValidUser,) task = None task_id = '' @@ -46,8 +48,8 @@ class CeleryTaskLogApi(LogTailApi): if new_path and os.path.isfile(new_path): return new_path try: - task = CeleryTask.objects.get(id=self.task_id) - except CeleryTask.DoesNotExist: + task = CeleryTaskExecution.objects.get(id=self.task_id) + except CeleryTaskExecution.DoesNotExist: return None return task.full_log_path @@ -94,3 +96,29 @@ class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet): queryset = super().get_queryset() queryset = queryset.exclude(description='') return queryset + + +class CelerySummaryAPIView(generics.RetrieveAPIView): + def get(self, request, *args, **kwargs): + pass + + +class CeleryTaskViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet): + serializer_class = CeleryTaskSerializer + http_method_names = ('get', 'head', 'options',) + + def get_queryset(self): + return CeleryTask.objects.exclude(name__startswith='celery') + + +class CeleryTaskExecutionViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet): + serializer_class = CeleryTaskExecutionSerializer + http_method_names = ('get', 'head', 'options',) + queryset = CeleryTaskExecution.objects.all() + + def get_queryset(self): + task_id = self.request.query_params.get('task_id') + if task_id: + task = get_object_or_404(CeleryTask, id=task_id) + self.queryset = self.queryset.filter(name=task.name) + return self.queryset diff --git a/apps/ops/api/command.py b/apps/ops/api/command.py deleted file mode 100644 index 0d513cd9e..000000000 --- a/apps/ops/api/command.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework import viewsets -from rest_framework.exceptions import ValidationError -from django.db import transaction -from django.db.models import Q -from django.utils.translation import ugettext as _ -from django.conf import settings - -from assets.models import Asset, Node -from orgs.mixins.api import RootOrgViewMixin -from common.permissions import IsValidUser -from rbac.permissions import RBACPermission -from ..models import CommandExecution -from ..serializers import CommandExecutionSerializer -from ..tasks import run_command_execution - - -class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet): - serializer_class = CommandExecutionSerializer - permission_classes = (RBACPermission,) - - def get_queryset(self): - return CommandExecution.objects.filter(user_id=str(self.request.user.id)) - - def check_hosts(self, serializer): - data = serializer.validated_data - assets = data["hosts"] - system_user = data["run_as"] - user = self.request.user - - q = Q(granted_by_permissions__system_users__id=system_user.id) & ( - Q(granted_by_permissions__users=user) | - Q(granted_by_permissions__user_groups__users=user) - ) - - permed_assets = set() - permed_assets.update(Asset.objects.filter(id__in=[a.id for a in assets]).filter(q).distinct()) - node_keys = Node.objects.filter(q).distinct().values_list('key', flat=True) - - nodes_assets_q = Q() - for _key in node_keys: - nodes_assets_q |= Q(nodes__key__startswith=f'{_key}:') - nodes_assets_q |= Q(nodes__key=_key) - - permed_assets.update( - Asset.objects.filter( - id__in=[a.id for a in assets] - ).filter( - nodes_assets_q - ).distinct() - ) - - invalid_assets = set(assets) - set(permed_assets) - if invalid_assets: - msg = _("Not has host {} permission").format( - [str(a.id) for a in invalid_assets] - ) - raise ValidationError({"hosts": msg}) - - def check_permissions(self, request): - if not settings.SECURITY_COMMAND_EXECUTION: - return self.permission_denied(request, "Command execution disabled") - return super().check_permissions(request) - - def perform_create(self, serializer): - self.check_hosts(serializer) - instance = serializer.save() - instance.user = self.request.user - instance.save() - cols = self.request.query_params.get("cols", '80') - rows = self.request.query_params.get("rows", '24') - transaction.on_commit(lambda: run_command_execution.apply_async( - args=(instance.id,), kwargs={"cols": cols, "rows": rows}, - task_id=str(instance.id) - )) diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py new file mode 100644 index 000000000..c5dfbc1e1 --- /dev/null +++ b/apps/ops/api/job.py @@ -0,0 +1,64 @@ +from rest_framework import viewsets + +from ops.models import Job, JobExecution +from ops.serializers.job import JobSerializer, JobExecutionSerializer + +__all__ = ['JobViewSet', 'JobExecutionViewSet'] + +from ops.tasks import run_ops_job_execution +from orgs.mixins.api import OrgBulkModelViewSet + + +def set_task_to_serializer_data(serializer, task): + data = getattr(serializer, "_data", {}) + data["task_id"] = task.id + setattr(serializer, "_data", data) + + +class JobViewSet(OrgBulkModelViewSet): + serializer_class = JobSerializer + model = Job + permission_classes = () + + def get_queryset(self): + query_set = super().get_queryset() + if self.action != 'retrieve': + return query_set.filter(instant=False) + return query_set + + def perform_create(self, serializer): + 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): + 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): + execution = job.create_execution() + task = run_ops_job_execution.delay(execution.id) + set_task_to_serializer_data(serializer, task) + + +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() + task = run_ops_job_execution.delay(instance.id) + set_task_to_serializer_data(serializer, task) + + def get_queryset(self): + query_set = super().get_queryset() + job_id = self.request.query_params.get('job_id') + if job_id: + query_set = query_set.filter(job_id=job_id) + return query_set diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py new file mode 100644 index 000000000..e8cf1ff16 --- /dev/null +++ b/apps/ops/api/playbook.py @@ -0,0 +1,35 @@ +import os +import zipfile + +from django.conf import settings + +from orgs.mixins.api import OrgBulkModelViewSet +from ..exception import PlaybookNoValidEntry +from ..models import Playbook +from ..serializers.playbook import PlaybookSerializer + +__all__ = ["PlaybookViewSet"] + + +def unzip_playbook(src, dist): + fz = zipfile.ZipFile(src, 'r') + for file in fz.namelist(): + fz.extract(file, dist) + + +class PlaybookViewSet(OrgBulkModelViewSet): + serializer_class = PlaybookSerializer + permission_classes = () + model = Playbook + + def perform_create(self, serializer): + instance = serializer.save() + src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name) + dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) + unzip_playbook(src_path, dest_path) + valid_entry = ('main.yml', 'main.yaml', 'main') + for f in os.listdir(dest_path): + if f in valid_entry: + return + os.remove(dest_path) + raise PlaybookNoValidEntry diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 819a23002..7956a5dc1 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -15,4 +15,5 @@ class OpsConfig(AppConfig): from .celery import signal_handler from . import signal_handlers from . import notifications + from . import tasks super().ready() diff --git a/apps/ops/const.py b/apps/ops/const.py new file mode 100644 index 000000000..2f68efd3b --- /dev/null +++ b/apps/ops/const.py @@ -0,0 +1,29 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class StrategyChoice(models.TextChoices): + push = 'push', _('Push') + verify = 'verify', _('Verify') + collect = 'collect', _('Collect') + change_secret = 'change_secret', _('Change password') + + +class SSHKeyStrategy(models.TextChoices): + add = 'add', _('Append SSH KEY') + set = 'set', _('Empty and append SSH KEY') + set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ') + + +class PasswordStrategy(models.TextChoices): + custom = 'custom', _('Custom password') + random_one = 'random_one', _('All assets use the same random password') + random_all = 'random_all', _('All assets use different random password') + + +string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' +DEFAULT_PASSWORD_LENGTH = 30 +DEFAULT_PASSWORD_RULES = { + 'length': DEFAULT_PASSWORD_LENGTH, + 'symbol_set': string_punctuation +} diff --git a/apps/ops/exception.py b/apps/ops/exception.py new file mode 100644 index 000000000..9cd98a94d --- /dev/null +++ b/apps/ops/exception.py @@ -0,0 +1,6 @@ +from common.exceptions import JMSException +from django.utils.translation import gettext_lazy as _ + + +class PlaybookNoValidEntry(JMSException): + default_detail = _('no valid program entry found.') diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py deleted file mode 100644 index 9ad69b9ac..000000000 --- a/apps/ops/inventory.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.conf import settings -from .ansible.inventory import BaseInventory - -from common.utils import get_logger - -__all__ = [ - 'JMSInventory', 'JMSCustomInventory', -] - - -logger = get_logger(__file__) - - -class JMSBaseInventory(BaseInventory): - def convert_to_ansible(self, asset, run_as_admin=False): - info = { - 'id': asset.id, - 'hostname': asset.hostname, - 'ip': asset.ip, - 'port': asset.ssh_port, - 'vars': dict(), - 'groups': [], - } - if asset.domain and asset.domain.has_gateway(): - info["vars"].update(self.make_proxy_command(asset)) - if run_as_admin: - info.update(asset.get_auth_info(with_become=True)) - if asset.is_windows(): - info["vars"].update({ - "ansible_connection": "ssh", - "ansible_shell_type": settings.WINDOWS_SSH_DEFAULT_SHELL, - }) - for label in asset.labels.all(): - info["vars"].update({ - label.name: label.value - }) - if asset.domain: - info["vars"].update({ - "domain": asset.domain.name, - }) - return info - - @staticmethod - def make_proxy_command(asset): - gateway = asset.domain.random_gateway() - proxy_command_list = [ - "ssh", "-o", "Port={}".format(gateway.port), - "-o", "StrictHostKeyChecking=no", - "{}@{}".format(gateway.username, gateway.ip), - "-W", "%h:%p", "-q", - ] - - if gateway.password: - proxy_command_list.insert( - 0, "sshpass -p '{}'".format(gateway.password) - ) - if gateway.private_key: - proxy_command_list.append("-i {}".format(gateway.private_key_file)) - - proxy_command = "'-o ProxyCommand={}'".format( - " ".join(proxy_command_list) - ) - return {"ansible_ssh_common_args": proxy_command} - - -class JMSInventory(JMSBaseInventory): - """ - JMS Inventory is the inventory with jumpserver assets, so you can - write you own inventory, construct you inventory, - user_info is obtained from admin_user or asset_user - """ - def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None, system_user=None): - """ - :param assets: assets - :param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同 - :param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username) - :param become_info: 是否become成某个用户去执行 - """ - self.assets = assets - self.using_admin = run_as_admin - self.run_as = run_as - self.system_user = system_user - self.become_info = become_info - - host_list = [] - - for asset in assets: - host = self.convert_to_ansible(asset, run_as_admin=run_as_admin) - if run_as is not None: - run_user_info = self.get_run_user_info(host) - host.update(run_user_info) - if become_info and asset.is_unixlike(): - host.update(become_info) - host_list.append(host) - - super().__init__(host_list=host_list) - - def get_run_user_info(self, host): - if not self.run_as and not self.system_user: - return {} - - asset_id = host.get('id', '') - asset = self.assets.filter(id=asset_id).first() - if not asset: - logger.error('Host not found: ', asset_id) - return {} - - if self.system_user: - self.system_user.load_asset_special_auth(asset=asset, username=self.run_as) - return self.system_user._to_secret_json() - else: - return {} - - -class JMSCustomInventory(JMSBaseInventory): - """ - JMS Custom Inventory is the inventory with jumpserver assets, - user_info is obtained from custom parameter - """ - - def __init__(self, assets, username, password=None, public_key=None, private_key=None): - """ - """ - self.assets = assets - self.username = username - self.password = password - self.public_key = public_key - self.private_key = private_key - - host_list = [] - - for asset in assets: - host = self.convert_to_ansible(asset) - run_user_info = self.get_run_user_info() - host.update(run_user_info) - host_list.append(host) - - super().__init__(host_list=host_list) - - def get_run_user_info(self): - return { - 'username': self.username, - 'password': self.password, - 'public_key': self.public_key, - 'private_key': self.private_key - } diff --git a/apps/ops/migrations/0022_auto_20220817_1346.py b/apps/ops/migrations/0022_auto_20220817_1346.py new file mode 100644 index 000000000..b06c85a18 --- /dev/null +++ b/apps/ops/migrations/0022_auto_20220817_1346.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.14 on 2022-08-17 05:46 + +from django.db import migrations, models + + +def migrate_run_system_user_to_account(apps, schema_editor): + execution_model = apps.get_model('ops', 'CommandExecution') + count = 0 + bulk_size = 1000 + + while True: + executions = execution_model.objects.all().prefetch_related('run_as')[count:bulk_size] + if not executions: + break + count += len(executions) + updated = [] + for obj in executions: + run_as = obj.run_as + if not run_as: + continue + obj.account = run_as.username + updated.append(obj) + execution_model.objects.bulk_update(updated, ['account']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0021_auto_20211130_1037'), + ] + + operations = [ + migrations.RemoveField( + model_name='adhoc', + name='run_system_user', + ), + migrations.AddField( + model_name='commandexecution', + name='account', + field=models.CharField(default='', max_length=128, verbose_name='account'), + ), + migrations.RunPython(migrate_run_system_user_to_account), + migrations.RemoveField( + model_name='commandexecution', + name='run_as', + ), + ] diff --git a/apps/ops/migrations/0023_auto_20220929_2025.py b/apps/ops/migrations/0023_auto_20220929_2025.py new file mode 100644 index 000000000..b5c7475f4 --- /dev/null +++ b/apps/ops/migrations/0023_auto_20220929_2025.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.14 on 2022-09-29 12:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0022_auto_20220817_1346'), + ] + + operations = [ + migrations.RemoveField( + model_name='celerytask', + name='log_path', + ), + migrations.RemoveField( + model_name='celerytask', + name='status', + ), + migrations.AddField( + model_name='celerytask', + name='args', + field=models.JSONField(default=[], verbose_name='Args'), + preserve_default=False, + ), + migrations.AddField( + model_name='celerytask', + name='is_finished', + field=models.BooleanField(default=False, verbose_name='Finished'), + ), + migrations.AddField( + model_name='celerytask', + name='kwargs', + field=models.JSONField(default={}, verbose_name='Kwargs'), + preserve_default=False, + ), + migrations.AddField( + model_name='celerytask', + name='state', + field=models.CharField(default='SUCCESS', max_length=16, verbose_name='State'), + preserve_default=False, + ), + ] diff --git a/apps/ops/migrations/0024_auto_20221008_1514.py b/apps/ops/migrations/0024_auto_20221008_1514.py new file mode 100644 index 000000000..e208af96e --- /dev/null +++ b/apps/ops/migrations/0024_auto_20221008_1514.py @@ -0,0 +1,58 @@ +# Generated by Django 3.2.14 on 2022-10-08 07:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0106_auto_20220916_1556'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0023_auto_20220929_2025'), + ] + + operations = [ + migrations.RemoveField( + model_name='adhocexecution', + name='adhoc', + ), + migrations.RemoveField( + model_name='adhocexecution', + name='task', + ), + migrations.RemoveField( + model_name='commandexecution', + name='hosts', + ), + migrations.RemoveField( + model_name='commandexecution', + name='user', + ), + migrations.AlterUniqueTogether( + name='task', + unique_together=None, + ), + migrations.RemoveField( + model_name='task', + name='latest_adhoc', + ), + migrations.RemoveField( + model_name='task', + name='latest_execution', + ), + migrations.DeleteModel( + name='AdHoc', + ), + migrations.DeleteModel( + name='AdHocExecution', + ), + migrations.DeleteModel( + name='CommandExecution', + ), + migrations.DeleteModel( + name='Task', + ), + ] diff --git a/apps/ops/migrations/0025_auto_20221008_1631.py b/apps/ops/migrations/0025_auto_20221008_1631.py new file mode 100644 index 000000000..7e814c3d1 --- /dev/null +++ b/apps/ops/migrations/0025_auto_20221008_1631.py @@ -0,0 +1,72 @@ +# Generated by Django 3.2.14 on 2022-10-08 08:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0106_auto_20220916_1556'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0024_auto_20221008_1514'), + ] + + operations = [ + migrations.CreateModel( + name='AdHoc', + 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(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')), + ('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')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('account', models.CharField(default='root', max_length=128, verbose_name='Account')), + ('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')), + ('pattern', models.CharField(default='all', max_length=1024, verbose_name='Pattern')), + ('module', models.CharField(default='shell', max_length=128, verbose_name='Module')), + ('args', models.CharField(default='', max_length=1024, verbose_name='Args')), + ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AdHocExecution', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), + ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), + ('summary', models.JSONField(default=dict, verbose_name='Summary')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True)), + ('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, related_name='executions', to='ops.adhoc', verbose_name='Adhoc')), + ], + options={ + 'verbose_name': 'AdHoc execution', + 'db_table': 'ops_adhoc_execution', + 'get_latest_by': 'date_start', + }, + ), + migrations.AddField( + model_name='adhoc', + name='last_execution', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.adhocexecution', verbose_name='Last execution'), + ), + migrations.AddField( + model_name='adhoc', + name='owner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + ] diff --git a/apps/ops/migrations/0026_auto_20221009_2050.py b/apps/ops/migrations/0026_auto_20221009_2050.py new file mode 100644 index 000000000..699246531 --- /dev/null +++ b/apps/ops/migrations/0026_auto_20221009_2050.py @@ -0,0 +1,100 @@ +# Generated by Django 3.2.14 on 2022-10-09 12:50 + +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'), + ('ops', '0025_auto_20221008_1631'), + ] + + operations = [ + migrations.CreateModel( + name='Playbook', + 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(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')), + ('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')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('account', models.CharField(default='root', max_length=128, verbose_name='Account')), + ('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')), + ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='adhocexecution', + name='date_finished', + field=models.DateTimeField(null=True, verbose_name='Date finished'), + ), + migrations.CreateModel( + name='PlaybookTemplate', + 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(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')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('path', models.FilePathField(verbose_name='Path')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ], + options={ + 'verbose_name': 'Playbook template', + 'ordering': ['name'], + 'unique_together': {('org_id', 'name')}, + }, + ), + migrations.CreateModel( + name='PlaybookExecution', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), + ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), + ('summary', models.JSONField(default=dict, verbose_name='Summary')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('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')), + ], + options={ + 'ordering': ['-date_start'], + 'abstract': False, + }, + ), + 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'), + ), + 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'), + ), + 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'), + ), + ] diff --git a/apps/ops/migrations/0027_auto_20221024_1709.py b/apps/ops/migrations/0027_auto_20221024_1709.py new file mode 100644 index 000000000..08411d42b --- /dev/null +++ b/apps/ops/migrations/0027_auto_20221024_1709.py @@ -0,0 +1,273 @@ +# Generated by Django 3.2.14 on 2022-12-05 03:23 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0112_gateway_to_asset'), + ('ops', '0026_auto_20221009_2050'), + ] + + operations = [ + migrations.CreateModel( + name='CeleryTaskExecution', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=1024)), + ('args', models.JSONField(verbose_name='Args')), + ('kwargs', models.JSONField(verbose_name='Kwargs')), + ('state', models.CharField(max_length=16, verbose_name='State')), + ('is_finished', models.BooleanField(default=False, verbose_name='Finished')), + ('date_published', models.DateTimeField(auto_now_add=True, verbose_name='Date published')), + ('date_start', models.DateTimeField(null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ], + ), + migrations.CreateModel( + name='Job', + 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(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, 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)), + ('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(default=uuid.uuid4, primary_key=True, serialize=False)), + ('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')), + ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), + ('owner', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'ordering': ['date_created'], + }, + ), + migrations.CreateModel( + name='JobExecution', + 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_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('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)), + ('task_id', models.UUIDField(null=True)), + ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), + ('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')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ('job', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', + to='ops.job')), + ], + options={ + 'ordering': ['-date_created'], + }, + ), + migrations.RemoveField( + model_name='playbookexecution', + name='creator', + ), + migrations.RemoveField( + model_name='playbookexecution', + name='task', + ), + migrations.AlterUniqueTogether( + name='playbooktemplate', + unique_together=None, + ), + migrations.AlterModelOptions( + name='celerytask', + options={'ordering': ('name',)}, + ), + migrations.RenameField( + model_name='adhoc', + old_name='owner', + new_name='creator', + ), + migrations.RenameField( + model_name='celerytask', + old_name='date_finished', + new_name='last_published_time', + ), + migrations.RemoveField( + model_name='adhoc', + name='account', + ), + migrations.RemoveField( + model_name='adhoc', + name='account_policy', + ), + migrations.RemoveField( + model_name='adhoc', + name='assets', + ), + migrations.RemoveField( + model_name='adhoc', + name='crontab', + ), + migrations.RemoveField( + model_name='adhoc', + name='date_last_run', + ), + migrations.RemoveField( + model_name='adhoc', + name='interval', + ), + migrations.RemoveField( + model_name='adhoc', + name='is_periodic', + ), + migrations.RemoveField( + model_name='adhoc', + name='last_execution', + ), + migrations.RemoveField( + model_name='celerytask', + name='args', + ), + migrations.RemoveField( + model_name='celerytask', + name='date_published', + ), + migrations.RemoveField( + model_name='celerytask', + name='date_start', + ), + migrations.RemoveField( + model_name='celerytask', + name='is_finished', + ), + migrations.RemoveField( + model_name='celerytask', + name='kwargs', + ), + migrations.RemoveField( + model_name='celerytask', + name='state', + ), + migrations.RemoveField( + model_name='playbook', + name='account', + ), + migrations.RemoveField( + model_name='playbook', + name='account_policy', + ), + migrations.RemoveField( + model_name='playbook', + name='assets', + ), + migrations.RemoveField( + model_name='playbook', + name='crontab', + ), + migrations.RemoveField( + model_name='playbook', + name='date_last_run', + ), + migrations.RemoveField( + model_name='playbook', + name='interval', + ), + migrations.RemoveField( + model_name='playbook', + name='is_periodic', + ), + migrations.RemoveField( + model_name='playbook', + name='last_execution', + ), + migrations.RemoveField( + model_name='playbook', + name='owner', + ), + migrations.RemoveField( + model_name='playbook', + name='template', + ), + migrations.AddField( + model_name='adhoc', + name='comment', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), + ), + migrations.AddField( + model_name='playbook', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AlterField( + model_name='adhoc', + name='module', + field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', + max_length=128, verbose_name='Module'), + ), + migrations.AlterField( + model_name='celerytask', + name='name', + field=models.CharField(max_length=1024, verbose_name='Name'), + ), + migrations.AlterField( + model_name='playbook', + name='comment', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), + ), + migrations.AlterField( + model_name='playbook', + name='name', + field=models.CharField(max_length=128, null=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='playbook', + name='path', + field=models.FileField(upload_to='playbooks/'), + ), + migrations.DeleteModel( + name='AdHocExecution', + ), + migrations.DeleteModel( + name='PlaybookExecution', + ), + migrations.DeleteModel( + name='PlaybookTemplate', + ), + migrations.AddField( + model_name='job', + name='playbook', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbook', + verbose_name='Playbook'), + ), + ] diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index da77bacae..5b706101b 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # import abc -import uuid from django.utils.translation import ugettext_lazy as _ from django.db import models -from django import forms from rest_framework import serializers from .celery.utils import ( @@ -14,12 +12,10 @@ from .celery.utils import ( __all__ = [ 'PeriodTaskModelMixin', 'PeriodTaskSerializerMixin', - 'PeriodTaskFormMixin', ] class PeriodTaskModelMixin(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField( max_length=128, unique=False, verbose_name=_("Name") ) @@ -73,7 +69,7 @@ class PeriodTaskModelMixin(models.Model): } create_or_update_celery_periodic_tasks(tasks) - def save(self, **kwargs): + def save(self, *args, **kwargs): instance = super().save(**kwargs) self.set_period_schedule() return instance @@ -140,42 +136,3 @@ class PeriodTaskSerializerMixin(serializers.Serializer): msg = _("Require periodic or regularly perform setting") raise serializers.ValidationError(msg) return ok - - -class PeriodTaskFormMixin(forms.Form): - is_periodic = forms.BooleanField( - initial=True, required=False, label=_('Periodic perform') - ) - crontab = forms.CharField( - max_length=128, required=False, label=_('Regularly perform'), - help_text=_("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"), - ) - interval = forms.IntegerField( - required=False, initial=24, - help_text=_('Unit: hour'), label=_("Cycle perform"), - ) - - def get_initial_for_field(self, field, field_name): - """ - Return initial data for field on form. Use initial data from the form - or the field, in that order. Evaluate callable values. - """ - if field_name not in ['is_periodic', 'crontab', 'interval']: - return super().get_initial_for_field(field, field_name) - instance = getattr(self, 'instance', None) - if instance is None: - return super().get_initial_for_field(field, field_name) - init_attr_name = field_name + '_initial' - value = getattr(self, init_attr_name, None) - if value is None: - return super().get_initial_for_field(field, field_name) - return value - - diff --git a/apps/ops/models/__init__.py b/apps/ops/models/__init__.py index 0a9ed463c..b6edb768d 100644 --- a/apps/ops/models/__init__.py +++ b/apps/ops/models/__init__.py @@ -3,4 +3,5 @@ from .adhoc import * from .celery import * -from .command import * +from .playbook import * +from .job import * diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index cd088b7f6..890a47b91 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -1,339 +1,42 @@ # ~*~ coding: utf-8 ~*~ - import uuid -import os -import time -import datetime -from celery import current_task from django.db import models -from django.conf import settings -from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, lazyproperty, make_dirs -from common.utils.translate import translate_value -from common.db.fields import ( - JsonListTextField, JsonDictCharField, EncryptJsonDictCharField, - JsonDictTextField, -) -from orgs.mixins.models import OrgModelMixin -from ..ansible import AdHocRunner, AnsibleError -from ..inventory import JMSInventory -from ..mixin import PeriodTaskModelMixin - -__all__ = ["Task", "AdHoc", "AdHocExecution"] +from common.utils import get_logger +from orgs.mixins.models import JMSOrgBaseModel +__all__ = ["AdHoc"] logger = get_logger(__file__) -class Task(PeriodTaskModelMixin, OrgModelMixin): - """ - This task is different ansible task, Task like 'push system user', 'get asset info' .. - One task can have some versions of adhoc, run a task only run the latest version adhoc - """ - callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task - is_deleted = models.BooleanField(default=False) - comment = models.TextField(blank=True, verbose_name=_("Comment")) - date_created = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_("Date created")) - date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) - latest_adhoc = models.ForeignKey('ops.AdHoc', on_delete=models.SET_NULL, - null=True, related_name='task_latest') - latest_execution = models.ForeignKey('ops.AdHocExecution', on_delete=models.SET_NULL, null=True, related_name='task_latest') - total_run_amount = models.IntegerField(default=0) - success_run_amount = models.IntegerField(default=0) - _ignore_auto_created_by = True +class AdHoc(JMSOrgBaseModel): + class Modules(models.TextChoices): + shell = 'shell', _('Shell') + winshell = 'win_shell', _('Powershell') + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all') + module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell, + verbose_name=_('Module')) + args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) + @property - def short_id(self): - return str(self.id).split('-')[-1] - - @lazyproperty - def versions(self): - return self.adhoc.all().count() - - @property - def is_success(self): - if self.latest_execution: - return self.latest_execution.is_success - else: - return False - - @lazyproperty - def display_name(self): - value = translate_value(self.name) - return value - - @property - def timedelta(self): - if self.latest_execution: - return self.latest_execution.timedelta - else: + def row_count(self): + if len(self.args) == 0: return 0 + count = str(self.args).count('\n') + return count + 1 @property - def date_start(self): - if self.latest_execution: - return self.latest_execution.date_start - else: - return None - - @property - def assets_amount(self): - if self.latest_execution: - return self.latest_execution.hosts_amount - return 0 - - def get_latest_adhoc(self): - if self.latest_adhoc: - return self.latest_adhoc - try: - adhoc = self.adhoc.all().latest() - self.latest_adhoc = adhoc - self.save() - return adhoc - except AdHoc.DoesNotExist: - return None - - @property - def history_summary(self): - total = self.total_run_amount - success = self.success_run_amount - failed = total - success - return {'total': total, 'success': success, 'failed': failed} - - def get_run_execution(self): - return self.execution.all() - - def run(self): - latest_adhoc = self.get_latest_adhoc() - if latest_adhoc: - return latest_adhoc.run() - else: - return {'error': 'No adhoc'} - - @property - def period_key(self): - return self.__str__() - - def get_register_task(self): - from ..tasks import run_ansible_task - name = self.__str__() - task = run_ansible_task.name - args = (str(self.id),) - kwargs = {"callback": self.callback} - return name, task, args, kwargs + def size(self): + return len(self.args) def __str__(self): - return self.name + '@' + str(self.org_id) - - class Meta: - db_table = 'ops_task' - unique_together = ('name', 'org_id') - ordering = ('-date_updated',) - verbose_name = _("Task") - get_latest_by = 'date_created' - permissions = [ - ('view_taskmonitor', _('Can view task monitor')) - ] - - -class AdHoc(OrgModelMixin): - """ - task: A task reference - _tasks: [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ] - _options: ansible options, more see ops.ansible.runner.Options - run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level - run_as: username(Add the uniform AssetUserManager and change it to username) - _become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"] - pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts - """ - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - task = models.ForeignKey(Task, related_name='adhoc', on_delete=models.CASCADE) - tasks = JsonListTextField(verbose_name=_('Tasks')) - pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern')) - options = JsonDictCharField(max_length=1024, default='', verbose_name=_('Options')) - hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host")) - run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) - run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username')) - run_system_user = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE) - become = EncryptJsonDictCharField(max_length=1024, default='', blank=True, null=True, verbose_name=_("Become")) - created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by')) - date_created = models.DateTimeField(auto_now_add=True, db_index=True) - - @lazyproperty - def run_times(self): - return self.execution.count() - - @property - def inventory(self): - if self.become: - become_info = { - 'become': { - self.become - } - } - else: - become_info = None - - inventory = JMSInventory( - self.hosts.all(), run_as_admin=self.run_as_admin, - run_as=self.run_as, become_info=become_info, system_user=self.run_system_user - ) - return inventory - - @property - def become_display(self): - if self.become: - return self.become.get("user", "") - return "" - - def run(self): - try: - celery_task_id = current_task.request.id - except AttributeError: - celery_task_id = None - - execution = AdHocExecution( - celery_task_id=celery_task_id, - adhoc=self, task=self.task, - task_display=str(self.task)[:128], - date_start=timezone.now(), - hosts_amount=self.hosts.count(), - ) - execution.save() - return execution.start() - - @property - def short_id(self): - return str(self.id).split('-')[-1] - - @property - def latest_execution(self): - try: - return self.execution.all().latest() - except AdHocExecution.DoesNotExist: - return None - - def save(self, **kwargs): - instance = super().save(**kwargs) - self.task.latest_adhoc = instance - self.task.save() - return instance - - def __str__(self): - return "{} of {}".format(self.task.name, self.short_id) - - def same_with(self, other): - if not isinstance(other, self.__class__): - return False - fields_check = [] - for field in self.__class__._meta.fields: - if field.name not in ['id', 'date_created']: - fields_check.append(field) - for field in fields_check: - if getattr(self, field.name) != getattr(other, field.name): - return False - return True - - class Meta: - db_table = "ops_adhoc" - get_latest_by = 'date_created' - verbose_name = _('AdHoc') - - -class AdHocExecution(OrgModelMixin): - """ - AdHoc running history. - """ - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - task = models.ForeignKey(Task, related_name='execution', on_delete=models.SET_NULL, null=True) - task_display = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Task display")) - celery_task_id = models.UUIDField(default=None, null=True) - hosts_amount = models.IntegerField(default=0, verbose_name=_("Host amount")) - adhoc = models.ForeignKey(AdHoc, related_name='execution', on_delete=models.SET_NULL, null=True) - date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time')) - date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time')) - timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True) - is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) - is_success = models.BooleanField(default=False, verbose_name=_('Is success')) - result = JsonDictTextField(blank=True, null=True, verbose_name=_('Adhoc raw result')) - summary = JsonDictTextField(blank=True, null=True, verbose_name=_('Adhoc result summary')) - - @property - def short_id(self): - return str(self.id).split('-')[-1] - - @property - def adhoc_short_id(self): - return str(self.adhoc_id).split('-')[-1] - - @property - def log_path(self): - dt = datetime.datetime.now().strftime('%Y-%m-%d') - log_dir = os.path.join(settings.PROJECT_DIR, 'data', 'ansible', dt) - if not os.path.exists(log_dir): - make_dirs(log_dir) - return os.path.join(log_dir, str(self.id) + '.log') - - def start_runner(self): - runner = AdHocRunner(self.adhoc.inventory, options=self.adhoc.options) - try: - result = runner.run( - self.adhoc.tasks, - self.adhoc.pattern, - self.task.name, - execution_id=self.id - ) - return result.results_raw, result.results_summary - except AnsibleError as e: - logger.warn("Failed run adhoc {}, {}".format(self.task.name, e)) - return {}, {} - - def start(self): - self.task.latest_execution = self - self.task.save() - time_start = time.time() - summary = {} - raw = '' - - try: - raw, summary = self.start_runner() - except Exception as e: - logger.error(e, exc_info=True) - raw = {"dark": {"all": str(e)}, "contacted": []} - finally: - self.clean_up(summary, time_start) - return raw, summary - - def clean_up(self, summary, time_start): - is_success = summary.get('success', False) - task = Task.objects.get(id=self.task_id) - task.total_run_amount = models.F('total_run_amount') + 1 - if is_success: - task.success_run_amount = models.F('success_run_amount') + 1 - task.save() - AdHocExecution.objects.filter(id=self.id).update( - is_finished=True, - is_success=is_success, - date_finished=timezone.now(), - timedelta=time.time() - time_start, - summary=summary - ) - - @property - def success_hosts(self): - return self.summary.get('contacted', []) - - @property - def failed_hosts(self): - return self.summary.get('dark', {}) - - def __str__(self): - return self.short_id - - class Meta: - db_table = "ops_adhoc_execution" - get_latest_by = 'date_start' - verbose_name = _("AdHoc execution") + return "{}: {}".format(self.module, self.args) diff --git a/apps/ops/models/base.py b/apps/ops/models/base.py new file mode 100644 index 000000000..3d6d1438d --- /dev/null +++ b/apps/ops/models/base.py @@ -0,0 +1,136 @@ +import os.path +import uuid +import logging + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from django.conf import settings + +from orgs.mixins.models import JMSOrgBaseModel +from ..ansible.inventory import JMSInventory +from ..mixin import PeriodTaskModelMixin + + +class BaseAnsibleJob(PeriodTaskModelMixin, JMSOrgBaseModel): + owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) + account = models.CharField(max_length=128, default='root', verbose_name=_('Account')) + account_policy = models.CharField(max_length=128, default='root', verbose_name=_('Account policy')) + last_execution = models.ForeignKey('BaseAnsibleExecution', verbose_name=_("Last execution"), + on_delete=models.SET_NULL, null=True) + date_last_run = models.DateTimeField(null=True, verbose_name=_('Date last run')) + + class Meta: + abstract = True + + @property + def inventory(self): + inv = JMSInventory(self.assets.all(), self.account, self.account_policy) + return inv + + def get_register_task(self): + raise NotImplementedError + + def to_json(self): + raise NotImplementedError + + def create_execution(self): + execution = self.executions.create() + return execution + + def run(self, *args, **kwargs): + execution = self.create_execution() + return execution.start() + + +class BaseAnsibleExecution(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') + task = models.ForeignKey(BaseAnsibleJob, on_delete=models.CASCADE, related_name='executions', null=True) + result = models.JSONField(blank=True, null=True, verbose_name=_('Result')) + summary = models.JSONField(default=dict, verbose_name=_('Summary')) + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) + date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) + + class Meta: + abstract = True + ordering = ["-date_start"] + + def __str__(self): + return str(self.id) + + @property + def private_dir(self): + uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id + return os.path.join(settings.ANSIBLE_DIR, self.task.name, uniq) + + @property + def inventory_path(self): + return os.path.join(self.private_dir, 'inventory', 'hosts') + + def get_runner(self): + raise NotImplementedError + + def finish_task(self): + self.date_finished = timezone.now() + self.save(update_fields=['result', 'status', 'summary', 'date_finished']) + self.update_task() + + def set_error(self, error): + this = self.__class__.objects.get(id=self.id) # 重新获取一次,避免数据库超时连接超时 + this.status = 'failed' + this.summary['error'] = str(error) + this.finish_task() + + def set_result(self, cb): + status_mapper = { + 'successful': '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.finish_task() + print("Finished") + + def update_task(self): + self.task.last_execution = self + self.task.date_last_run = timezone.now() + self.task.save(update_fields=['last_execution', 'date_last_run']) + + def start(self, **kwargs): + runner = self.get_runner() + try: + cb = runner.run(**kwargs) + self.set_result(cb) + return cb + except Exception as e: + logging.error(e, exc_info=True) + self.set_error(e) + + @property + def is_finished(self): + return self.status in ['success', 'failed'] + + @property + def is_success(self): + return self.status == 'success' + + @property + def short_id(self): + return str(self.id).split('-')[-1] + + @property + def timedelta(self): + if self.date_start and self.date_finished: + return self.date_finished - self.date_start + return None + + @property + def time_cost(self): + if self.date_finished and self.date_start: + return (self.date_finished - self.date_start).total_seconds() + return None diff --git a/apps/ops/models/celery.py b/apps/ops/models/celery.py index 9ab5f49e1..7b98dc3ab 100644 --- a/apps/ops/models/celery.py +++ b/apps/ops/models/celery.py @@ -7,34 +7,73 @@ from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.db import models +from ops.celery import app + class CeleryTask(models.Model): - WAITING = "waiting" - RUNNING = "running" - FINISHED = "finished" - LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery') + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + name = models.CharField(max_length=1024, verbose_name=_('Name')) + last_published_time = models.DateTimeField(null=True) - STATUS_CHOICES = ( - (WAITING, WAITING), - (RUNNING, RUNNING), - (FINISHED, FINISHED), - ) + @property + def meta(self): + task = app.tasks.get(self.name, None) + return { + "comment": getattr(task, 'verbose_name', None), + "queue": getattr(task, 'queue', 'default') + } + + @property + def summary(self): + executions = CeleryTaskExecution.objects.filter(name=self.name) + total = executions.count() + success = executions.filter(state='SUCCESS').count() + return {'total': total, 'success': success} + + @property + def state(self): + last_five_executions = CeleryTaskExecution.objects.filter(name=self.name).order_by('-date_published')[:5] + + if len(last_five_executions) > 0: + if last_five_executions[0].state == 'FAILURE': + return "red" + + for execution in last_five_executions: + if execution.state == 'FAILURE': + return "yellow" + return "green" + + class Meta: + ordering = ('name',) + + +class CeleryTaskExecution(models.Model): + LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery') id = models.UUIDField(primary_key=True, default=uuid.uuid4) name = models.CharField(max_length=1024) - status = models.CharField(max_length=128, choices=STATUS_CHOICES, db_index=True) - log_path = models.CharField(max_length=256, blank=True, null=True) - date_published = models.DateTimeField(auto_now_add=True) - date_start = models.DateTimeField(null=True) - date_finished = models.DateTimeField(null=True) + args = models.JSONField(verbose_name=_("Args")) + kwargs = models.JSONField(verbose_name=_("Kwargs")) + state = models.CharField(max_length=16, verbose_name=_("State")) + is_finished = models.BooleanField(default=False, verbose_name=_("Finished")) + date_published = models.DateTimeField(auto_now_add=True, verbose_name=_('Date published')) + date_start = models.DateTimeField(null=True, verbose_name=_('Date start')) + date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) + + @property + def time_cost(self): + if self.date_finished and self.date_start: + return (self.date_finished - self.date_start).total_seconds() + return None + + @property + def timedelta(self): + if self.date_start and self.date_finished: + return self.date_finished - self.date_start + return None + + @property + def is_success(self): + return self.state == 'SUCCESS' def __str__(self): return "{}: {}".format(self.name, self.id) - - def is_finished(self): - return self.status == self.FINISHED - - @property - def full_log_path(self): - if not self.log_path: - return None - return os.path.join(self.LOG_DIR, self.log_path) diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py deleted file mode 100644 index b5a1f2292..000000000 --- a/apps/ops/models/command.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# -import uuid -import json - -from celery.exceptions import SoftTimeLimitExceeded -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import ugettext -from django.db import models - -from terminal.notifications import CommandExecutionAlert -from assets.models import Asset -from common.utils import lazyproperty -from orgs.models import Organization -from orgs.mixins.models import OrgModelMixin -from orgs.utils import tmp_to_org -from ..ansible.runner import CommandRunner -from ..inventory import JMSInventory - - -class CommandExecution(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - hosts = models.ManyToManyField('assets.Asset') - run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE) - command = models.TextField(verbose_name=_("Command")) - _result = models.TextField(blank=True, null=True, verbose_name=_('Result')) - user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True) - is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) - date_start = models.DateTimeField(null=True, verbose_name=_('Date start')) - date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) - - def __str__(self): - return self.command[:10] - - def save(self, *args, **kwargs): - with tmp_to_org(self.run_as.org_id): - super().save(*args, **kwargs) - - @property - def inventory(self): - if self.run_as.username_same_with_user: - username = self.user.username - else: - username = self.run_as.username - inv = JMSInventory(self.allow_assets, run_as=username, system_user=self.run_as) - return inv - - @lazyproperty - def run_as_display(self): - return str(self.run_as) - - @lazyproperty - def user_display(self): - return str(self.user) - - @lazyproperty - def hosts_display(self): - return ','.join(self.hosts.all().values_list('hostname', flat=True)) - - @property - def result(self): - if self._result: - return json.loads(self._result) - else: - return {} - - @result.setter - def result(self, item): - self._result = json.dumps(item) - - @property - def is_success(self): - if 'error' in self.result: - return False - return True - - def get_hosts_names(self): - return ','.join(self.hosts.all().values_list('hostname', flat=True)) - - def cmd_filter_rules(self, asset_id=None): - from assets.models import CommandFilterRule - user_id = self.user.id - system_user_id = self.run_as.id - rules = CommandFilterRule.get_queryset( - user_id=user_id, - system_user_id=system_user_id, - asset_id=asset_id, - ) - return rules - - def is_command_can_run(self, command, asset_id=None): - for rule in self.cmd_filter_rules(asset_id=asset_id): - action, matched_cmd = rule.match(command) - if action == rule.ActionChoices.allow: - return True, None - elif action == rule.ActionChoices.deny: - return False, matched_cmd - return True, None - - @property - def allow_assets(self): - allow_asset_ids = [] - for asset in self.hosts.all(): - ok, __ = self.is_command_can_run(self.command, asset_id=asset.id) - if ok: - allow_asset_ids.append(asset.id) - allow_assets = Asset.objects.filter(id__in=allow_asset_ids) - return allow_assets - - def run(self): - print('-' * 10 + ' ' + ugettext('Task start') + ' ' + '-' * 10) - org = Organization.get_instance(self.run_as.org_id) - org.change_to() - self.date_start = timezone.now() - ok, msg = self.is_command_can_run(self.command) - if ok: - allow_assets = self.allow_assets - deny_assets = set(list(self.hosts.all())) - set(list(allow_assets)) - for asset in deny_assets: - print(f'资产{asset}: 命令{self.command}不允许执行') - if not allow_assets: - self.result = { - "error": 'There are currently no assets that can be executed' - } - self.save() - return self.result - runner = CommandRunner(self.inventory) - try: - host = allow_assets.first() - if host and host.is_windows(): - shell = 'win_shell' - elif host and host.is_unixlike(): - shell = 'shell' - else: - shell = 'raw' - result = runner.execute(self.command, 'all', module=shell) - self.result = result.results_command - except SoftTimeLimitExceeded as e: - print("Run timeout than 60s") - self.result = {"error": str(e)} - except Exception as e: - print("Error occur: {}".format(e)) - self.result = {"error": str(e)} - else: - msg = _("Command `{}` is forbidden ........").format(self.command) - print('\033[31m' + msg + '\033[0m') - CommandExecutionAlert({ - 'input': self.command, - 'assets': self.hosts.all(), - 'user': str(self.user), - 'risk_level': 5, - }).publish_async() - self.result = {"error": msg} - self.org_id = self.run_as.org_id - self.is_finished = True - self.date_finished = timezone.now() - self.save() - print('-' * 10 + ' ' + ugettext('Task end') + ' ' + '-' * 10) - return self.result - - class Meta: - verbose_name = _("Command execution") diff --git a/apps/ops/models/common.py b/apps/ops/models/common.py new file mode 100644 index 000000000..9df754798 --- /dev/null +++ b/apps/ops/models/common.py @@ -0,0 +1,4 @@ +# 内置环境变量 +BUILTIN_VARIABLES = { + +} diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py new file mode 100644 index 000000000..ff858a4fb --- /dev/null +++ b/apps/ops/models/job.py @@ -0,0 +1,219 @@ +import json +import os +import uuid +import logging + +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 + +__all__ = ["Job", "JobExecution"] + +from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner +from ops.mixin import PeriodTaskModelMixin +from orgs.mixins.models import JMSOrgBaseModel + + +class Job(JMSOrgBaseModel, 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) + 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)')) + 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")) + owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) + runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas')) + runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip, + 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(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) + + @property + def last_execution(self): + return self.executions.last() + + @property + def date_last_run(self): + return self.last_execution.date_created if self.last_execution else None + + @property + def summary(self): + summary = { + "total": 0, + "success": 0, + } + for execution in self.executions.all(): + summary["total"] += 1 + if execution.is_success: + summary["success"] += 1 + return summary + + @property + def average_time_cost(self): + total_cost = 0 + finished_count = self.executions.filter(status__in=['success', 'failed']).count() + for execution in self.executions.filter(status__in=['success', 'failed']).all(): + total_cost += execution.time_cost + return total_cost / finished_count if finished_count else 0 + + def get_register_task(self): + from ..tasks import run_ops_job_execution + name = "run_ops_job_period_{}".format(str(self.id)[:8]) + task = run_ops_job_execution.name + args = (str(self.id),) + kwargs = {} + return name, task, args, kwargs + + @property + def inventory(self): + return JMSInventory(self.assets.all(), self.runas_policy, self.runas) + + def create_execution(self): + return self.executions.create() + + class Meta: + ordering = ['date_created'] + + +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') + job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='executions', null=True) + 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')) + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + 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 job_type(self): + return self.job.type + + def compile_shell(self): + if self.job.type != 'adhoc': + return + result = "{}{}{} ".format('\'', self.job.args, '\'') + result += "chdir={}".format(self.job.chdir) + return result + + def get_runner(self): + inv = self.job.inventory + inv.write_to_file(self.inventory_path) + if isinstance(self.parameters, str): + extra_vars = json.loads(self.parameters) + else: + extra_vars = {} + + if self.job.type == 'adhoc': + args = self.compile_shell() + runner = AdHocRunner( + self.inventory_path, self.job.module, module_args=args, + pattern="all", project_dir=self.private_dir, extra_vars=extra_vars, + ) + elif self.job.type == 'playbook': + runner = PlaybookRunner( + self.inventory_path, self.job.playbook.entry + ) + else: + raise Exception("unsupported job type") + return runner + + @property + def short_id(self): + return str(self.id).split('-')[-1] + + @property + def time_cost(self): + if self.date_finished and self.date_start: + return (self.date_finished - self.date_start).total_seconds() + return None + + @property + def timedelta(self): + if self.date_start and self.date_finished: + return self.date_finished - self.date_start + return None + + @property + def is_finished(self): + return self.status in ['success', 'failed'] + + @property + def is_success(self): + return self.status == 'success' + + @property + def inventory_path(self): + return os.path.join(self.private_dir, 'inventory', 'hosts') + + @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' + 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.finish_task() + + def set_result(self, cb): + status_mapper = { + 'successful': '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.finish_task() + + def finish_task(self): + self.date_finished = timezone.now() + self.save(update_fields=['result', 'status', 'summary', 'date_finished']) + + def set_celery_id(self): + if not current_task: + return + task_id = current_task.request.root_id + self.task_id = task_id + + def start(self, **kwargs): + self.date_start = timezone.now() + self.set_celery_id() + self.save() + runner = self.get_runner() + try: + cb = runner.run(**kwargs) + self.set_result(cb) + return cb + except Exception as e: + logging.error(e, exc_info=True) + self.set_error(e) + + class Meta: + ordering = ['-date_created'] diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py new file mode 100644 index 000000000..f92968762 --- /dev/null +++ b/apps/ops/models/playbook.py @@ -0,0 +1,26 @@ +import os.path +import uuid + +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from ops.exception import PlaybookNoValidEntry +from orgs.mixins.models import JMSOrgBaseModel + + +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/') + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) + + @property + def entry(self): + work_dir = os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__()) + valid_entry = ('main.yml', 'main.yaml', 'main') + for f in os.listdir(work_dir): + if f in valid_entry: + return os.path.join(work_dir, f) + raise PlaybookNoValidEntry diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 094abf958..08d583be1 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -1,153 +1,19 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals + from rest_framework import serializers -from django.shortcuts import reverse +from common.drf.fields import ReadableHiddenField from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import Task, AdHoc, AdHocExecution, CommandExecution +from ..models import AdHoc -class AdHocExecutionSerializer(serializers.ModelSerializer): - stat = serializers.SerializerMethodField() - last_success = serializers.ListField(source='success_hosts') - last_failure = serializers.DictField(source='failed_hosts') - - class Meta: - model = AdHocExecution - fields_mini = ['id'] - fields_small = fields_mini + [ - 'hosts_amount', 'timedelta', 'result', 'summary', 'short_id', - 'is_finished', 'is_success', - 'date_start', 'date_finished', - ] - fields_fk = ['task', 'task_display', 'adhoc', 'adhoc_short_id',] - fields_custom = ['stat', 'last_success', 'last_failure'] - fields = fields_small + fields_fk + fields_custom - - @staticmethod - def get_task(obj): - return obj.task.id - - @staticmethod - def get_stat(obj): - count_failed_hosts = len(obj.failed_hosts) - count_success_hosts = len(obj.success_hosts) - count_total = count_success_hosts + count_failed_hosts - return { - "total": count_total, - "success": count_success_hosts, - "failed": count_failed_hosts - } - - -class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer): - class Meta: - model = AdHocExecution - fields = [ - 'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat', - 'date_finished', 'timedelta', 'is_finished', 'is_success', - 'short_id', 'adhoc_short_id', 'last_success', 'last_failure' - ] - - -class TaskSerializer(BulkOrgResourceModelSerializer): - summary = serializers.ReadOnlyField(source='history_summary') - latest_execution = AdHocExecutionExcludeResultSerializer(read_only=True) - - class Meta: - model = Task - fields_mini = ['id', 'name', 'display_name'] - fields_small = fields_mini + [ - 'interval', 'crontab', - 'is_periodic', 'is_deleted', - 'date_created', 'date_updated', - 'comment', - ] - fields_fk = ['latest_execution'] - fields_custom = ['summary'] - fields = fields_small + fields_fk + fields_custom - read_only_fields = [ - 'is_deleted', 'date_created', 'date_updated', - 'latest_adhoc', 'latest_execution', 'total_run_amount', - 'success_run_amount', 'summary', - ] - - -class TaskDetailSerializer(TaskSerializer): - contents = serializers.ListField(source='latest_adhoc.tasks') - - class Meta(TaskSerializer.Meta): - fields = TaskSerializer.Meta.fields + ['contents'] - - -class AdHocSerializer(serializers.ModelSerializer): - become_display = serializers.ReadOnlyField() - tasks = serializers.ListField() +class AdHocSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer): + creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) + row_count = serializers.IntegerField(read_only=True) + size = serializers.IntegerField(read_only=True) class Meta: model = AdHoc - fields_mini = ['id'] - fields_small = fields_mini + [ - 'tasks', "pattern", "options", "run_as", - "become", "become_display", "short_id", - "run_as_admin", - "date_created", - ] - fields_fk = ["task"] - fields_m2m = ["hosts"] - fields = fields_small + fields_fk + fields_m2m - read_only_fields = [ - 'date_created' - ] - extra_kwargs = { - "become": {'write_only': True} - } - - -class AdHocExecutionNestSerializer(serializers.ModelSerializer): - last_success = serializers.ListField(source='success_hosts') - last_failure = serializers.DictField(source='failed_hosts') - last_run = serializers.CharField(source='short_id') - - class Meta: - model = AdHocExecution - fields = ( - 'last_success', 'last_failure', 'last_run', 'timedelta', - 'is_finished', 'is_success' - ) - - -class AdHocDetailSerializer(AdHocSerializer): - latest_execution = AdHocExecutionNestSerializer(allow_null=True) - task_name = serializers.CharField(source='task.name') - - class Meta(AdHocSerializer.Meta): - fields = AdHocSerializer.Meta.fields + [ - 'latest_execution', 'created_by', 'run_times', 'task_name' - ] - - -class CommandExecutionSerializer(serializers.ModelSerializer): - result = serializers.JSONField(read_only=True) - log_url = serializers.SerializerMethodField() - - class Meta: - model = CommandExecution - fields_mini = ['id'] - fields_small = fields_mini + [ - 'command', 'result', 'log_url', - 'is_finished', 'date_created', 'date_finished' - ] - fields_fk = ['run_as'] - fields_m2m = ['hosts'] - fields = fields_small + fields_fk + fields_m2m - read_only_fields = [ - 'result', 'is_finished', 'log_url', 'date_created', - 'date_finished' - ] - ref_name = 'OpsCommandExecution' - - @staticmethod - def get_log_url(obj): - return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id}) - + read_only_field = ["id", "row_count", "size", "creator", "date_created", "date_updated"] + fields = read_only_field + ["id", "name", "module", "args", "comment"] diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py index 8015c2482..776b1fd5b 100644 --- a/apps/ops/serializers/celery.py +++ b/apps/ops/serializers/celery.py @@ -1,14 +1,17 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import PeriodicTask __all__ = [ - 'CeleryResultSerializer', 'CeleryTaskSerializer', - 'CeleryPeriodTaskSerializer' + 'CeleryResultSerializer', 'CeleryTaskExecutionSerializer', + 'CeleryPeriodTaskSerializer', 'CeleryTaskSerializer' ] +from ops.models import CeleryTask, CeleryTaskExecution + class CeleryResultSerializer(serializers.Serializer): id = serializers.UUIDField() @@ -16,10 +19,6 @@ class CeleryResultSerializer(serializers.Serializer): state = serializers.CharField(max_length=16) -class CeleryTaskSerializer(serializers.Serializer): - pass - - class CeleryPeriodTaskSerializer(serializers.ModelSerializer): class Meta: model = PeriodicTask @@ -27,3 +26,22 @@ class CeleryPeriodTaskSerializer(serializers.ModelSerializer): 'name', 'task', 'enabled', 'description', 'last_run_at', 'total_run_count' ] + + +class CeleryTaskSerializer(serializers.ModelSerializer): + class Meta: + model = CeleryTask + read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'last_published_time'] + fields = read_only_fields + + +class CeleryTaskExecutionSerializer(serializers.ModelSerializer): + is_success = serializers.BooleanField(required=False, read_only=True, label=_('Success')) + + class Meta: + model = CeleryTaskExecution + fields = [ + "id", "name", "args", "kwargs", "time_cost", "timedelta", "is_success", "is_finished", "date_published", + "date_start", + "date_finished" + ] diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py new file mode 100644 index 000000000..1851cdfd8 --- /dev/null +++ b/apps/ops/serializers/job.py @@ -0,0 +1,36 @@ +from django.utils.translation import ugettext as _ +from rest_framework import serializers +from common.drf.fields import ReadableHiddenField +from ops.mixin import PeriodTaskSerializerMixin +from ops.models import Job, JobExecution +from orgs.mixins.serializers import BulkOrgResourceModelSerializer + + +class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): + owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) + run_after_save = serializers.BooleanField(label=_("Run after save"), read_only=True, default=False, required=False) + + class Meta: + model = Job + read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost", + "run_after_save"] + fields = read_only_fields + [ + "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner", + "use_parameter_define", + "parameters_define", + "timeout", + "chdir", + "comment", + "summary", + "is_periodic", "interval", "crontab" + ] + + +class JobExecutionSerializer(serializers.ModelSerializer): + 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'] + fields = read_only_fields + [ + "job", "parameters" + ] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py new file mode 100644 index 000000000..1633688be --- /dev/null +++ b/apps/ops/serializers/playbook.py @@ -0,0 +1,31 @@ +import os + +from rest_framework import serializers + +from common.drf.fields import ReadableHiddenField +from ops.models import Playbook +from orgs.mixins.serializers import BulkOrgResourceModelSerializer + + +def parse_playbook_name(path): + file_name = os.path.split(path)[-1] + return file_name.split(".")[-2] + + +class PlaybookSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer): + creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) + path = serializers.FileField(required=False) + + def create(self, validated_data): + name = validated_data.get('name') + if not name: + path = validated_data.get('path').name + validated_data['name'] = parse_playbook_name(path) + return super().create(validated_data) + + class Meta: + model = Playbook + read_only_fields = ["id", "date_created", "date_updated"] + fields = read_only_fields + [ + "id", 'path', "name", "comment", "creator", + ] diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py index dfd364845..965bd494c 100644 --- a/apps/ops/signal_handlers.py +++ b/apps/ops/signal_handlers.py @@ -1,15 +1,42 @@ -from django.utils import translation +import ast +from celery import signals + +from django.db import transaction from django.core.cache import cache -from celery.signals import task_prerun, task_postrun, before_task_publish +from django.dispatch import receiver +from django.db.utils import ProgrammingError +from django.utils import translation, timezone +from django.utils.translation import gettext as _ -from common.db.utils import close_old_connections +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 + +logger = get_logger(__name__) TASK_LANG_CACHE_KEY = 'TASK_LANG_{}' TASK_LANG_CACHE_TTL = 1800 -@before_task_publish.connect() +@receiver(django_ready) +def sync_registered_tasks(*args, **kwargs): + with transaction.atomic(): + try: + db_tasks = CeleryTask.objects.all() + celery_task_names = [key for key in app.tasks] + db_task_names = db_tasks.values_list('name', flat=True) + + db_tasks.exclude(name__in=celery_task_names).delete() + not_in_db_tasks = set(celery_task_names) - set(db_task_names) + tasks_to_create = [CeleryTask(name=name) for name in not_in_db_tasks] + CeleryTask.objects.bulk_create(tasks_to_create) + except ProgrammingError: + pass + + +@signals.before_task_publish.connect def before_task_publish(headers=None, **kwargs): task_id = headers.get('id') current_lang = translation.get_language() @@ -17,8 +44,11 @@ def before_task_publish(headers=None, **kwargs): cache.set(key, current_lang, 1800) -@task_prerun.connect() +@signals.task_prerun.connect def on_celery_task_pre_run(task_id='', **kwargs): + # 更新状态 + CeleryTaskExecution.objects.filter(id=task_id) \ + .update(state='RUNNING', date_start=timezone.now()) # 关闭之前的数据库连接 close_old_connections() @@ -29,6 +59,41 @@ def on_celery_task_pre_run(task_id='', **kwargs): translation.activate(task_lang) -@task_postrun.connect() -def on_celery_task_post_run(**kwargs): +@signals.task_postrun.connect +def on_celery_task_post_run(task_id='', state='', **kwargs): close_old_connections() + print(_("Task") + ": {} {}".format(task_id, state)) + + CeleryTaskExecution.objects.filter(id=task_id).update( + state=state, date_finished=timezone.now(), is_finished=True + ) + + +@signals.after_task_publish.connect +def task_sent_handler(headers=None, body=None, **kwargs): + info = headers if 'task' in headers else body + task = info.get('task') + i = info.get('id') + if not i or not task: + logger.error("Not found task id or name: {}".format(info)) + return + + args = info.get('argsrepr', '()') + kwargs = info.get('kwargsrepr', '{}') + try: + args = list(ast.literal_eval(args)) + kwargs = ast.literal_eval(kwargs) + except (ValueError, SyntaxError): + args = [] + kwargs = {} + + data = { + 'id': i, + 'name': task, + 'state': 'PENDING', + 'is_finished': False, + 'args': args, + 'kwargs': kwargs + } + CeleryTaskExecution.objects.create(**data) + CeleryTask.objects.filter(name=task).update(last_published_time=timezone.now()) diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index e68b4c55c..bd4d5d448 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -1,17 +1,16 @@ # coding: utf-8 import os import subprocess -import time from django.conf import settings -from celery import shared_task, subtask +from celery import shared_task from celery.exceptions import SoftTimeLimitExceeded from django.utils import timezone -from django.utils.translation import ugettext_lazy as _, gettext +from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger, get_object_or_none, get_log_keep_day -from orgs.utils import tmp_to_root_org, tmp_to_org +from orgs.utils import tmp_to_org from .celery.decorator import ( register_as_period_task, after_app_shutdown_clean_periodic, after_app_ready_start @@ -20,76 +19,44 @@ from .celery.utils import ( create_or_update_celery_periodic_tasks, get_celery_periodic_task, disable_celery_periodic_task, delete_celery_periodic_task ) -from .models import Task, CommandExecution, CeleryTask +from .models import CeleryTaskExecution, Job, JobExecution from .notifications import ServerPerformanceCheckUtil logger = get_logger(__file__) -def rerun_task(): - pass +@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) -@shared_task(queue="ansible") -def run_ansible_task(tid, callback=None, **kwargs): - """ - :param tid: is the tasks serialized data - :param callback: callback function name - :return: - """ - with tmp_to_root_org(): - task = get_object_or_none(Task, id=tid) - if not task: - logger.error("No task found") - return - with tmp_to_org(task.org): - result = task.run() - if callback is not None: - subtask(callback).delay(result, task_name=task.name) - return result - - -@shared_task(soft_time_limit=60, queue="ansible") -def run_command_execution(cid, **kwargs): - with tmp_to_root_org(): - execution = get_object_or_none(CommandExecution, id=cid) - if not execution: - logger.error("Not found the execution id: {}".format(cid)) - return - with tmp_to_org(execution.run_as.org): +@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: - os.environ.update({ - "TERM_ROWS": kwargs.get("rows", ""), - "TERM_COLS": kwargs.get("cols", ""), - }) - execution.run() + execution.start() except SoftTimeLimitExceeded: - logger.error("Run time out") + 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 +@shared_task(verbose_name=_('Periodic clear celery tasks')) @after_app_shutdown_clean_periodic -@register_as_period_task(interval=3600*24, description=_("Clean task history period")) -def clean_tasks_adhoc_period(): - logger.debug("Start clean task adhoc and run history") - tasks = Task.objects.all() - for task in tasks: - adhoc = task.adhoc.all().order_by('-date_created')[5:] - for ad in adhoc: - ad.execution.all().delete() - ad.delete() - - -@shared_task -@after_app_shutdown_clean_periodic -@register_as_period_task(interval=3600*24, description=_("Clean celery log period")) +@register_as_period_task(interval=3600 * 24, description=_("Clean celery log period")) def clean_celery_tasks_period(): logger.debug("Start clean celery task history") expire_days = get_log_keep_day('TASK_LOG_KEEP_DAYS') days_ago = timezone.now() - timezone.timedelta(days=expire_days) - tasks = CeleryTask.objects.filter(date_start__lt=days_ago) + tasks = CeleryTaskExecution.objects.filter(date_start__lt=days_ago) tasks.delete() - tasks = CeleryTask.objects.filter(date_start__isnull=True) + tasks = CeleryTaskExecution.objects.filter(date_start__isnull=True) tasks.delete() command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;" % ( settings.CELERY_LOG_DIR, expire_days @@ -99,7 +66,7 @@ def clean_celery_tasks_period(): subprocess.call(command, shell=True) -@shared_task +@shared_task(verbose_name=_('Clear celery periodic tasks')) @after_app_ready_start def clean_celery_periodic_tasks(): """清除celery定时任务""" @@ -122,7 +89,7 @@ def clean_celery_periodic_tasks(): logger.info('Clean task failure: {}'.format(task)) -@shared_task +@shared_task(verbose_name=_('Create or update periodic tasks')) @after_app_ready_start def create_or_update_registered_periodic_tasks(): from .celery.decorator import get_register_period_tasks @@ -130,52 +97,7 @@ def create_or_update_registered_periodic_tasks(): create_or_update_celery_periodic_tasks(task) -@shared_task +@shared_task(verbose_name=_("Periodic check service performance")) @register_as_period_task(interval=3600) def check_server_performance_period(): ServerPerformanceCheckUtil().check_and_publish() - - -@shared_task(queue="ansible") -def hello(name, callback=None): - from users.models import User - import time - - count = User.objects.count() - print(gettext("Hello") + ': ' + name) - print("Count: ", count) - time.sleep(1) - return gettext("Hello") - - -@shared_task -# @after_app_shutdown_clean_periodic -# @register_as_period_task(interval=30) -def hello123(): - return None - - -@shared_task -def hello_callback(result): - print(result) - print("Hello callback") - - -@shared_task -def add(a, b): - time.sleep(5) - return a + b - - -@shared_task -def add_m(x): - from celery import chain - a = range(x) - b = [a[i:i + 10] for i in range(0, len(a), 10)] - s = list() - s.append(add.s(b[0], b[1])) - for i in b[1:]: - s.append(add.s(i)) - res = chain(*tuple(s))() - return res - diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index a5838073f..a8b71734f 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -4,27 +4,33 @@ from __future__ import unicode_literals from django.urls import path from rest_framework.routers import DefaultRouter from rest_framework_bulk.routes import BulkRouter -from .. import api +from .. import api app_name = "ops" router = DefaultRouter() bulk_router = BulkRouter() -bulk_router.register(r'tasks', api.TaskViewSet, 'task') -router.register(r'adhoc', api.AdHocViewSet, 'adhoc') -router.register(r'adhoc-executions', api.AdHocRunHistoryViewSet, 'execution') -router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') +router.register(r'adhocs', api.AdHocViewSet, 'adhoc') +router.register(r'playbooks', api.PlaybookViewSet, 'playbook') +router.register(r'jobs', api.JobViewSet, 'job') +router.register(r'job-executions', api.JobExecutionViewSet, 'job-execution') + router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') -urlpatterns = [ - path('tasks//run/', api.TaskRun.as_view(), name='task-run'), - path('celery/task//log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'), - path('celery/task//result/', api.CeleryResultApi.as_view(), name='celery-result'), +router.register(r'tasks', api.CeleryTaskViewSet, 'task') +router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-executions') + +urlpatterns = [ + + path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'), + + path('celery/task//task-execution//log/', api.CeleryTaskExecutionLogApi.as_view(), + name='celery-task-execution-log'), + path('celery/task//task-execution//result/', api.CeleryResultApi.as_view(), + name='celery-task-execution-result'), - path('ansible/task//log/', api.AnsibleTaskLogApi.as_view(), name='ansible-task-log'), ] -urlpatterns += router.urls -urlpatterns += bulk_router.urls +urlpatterns += (router.urls + bulk_router.urls) diff --git a/apps/ops/utils.py b/apps/ops/utils.py index c86a6aa08..456e73d00 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -4,12 +4,12 @@ import uuid from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, get_object_or_none, make_dirs -from common.tasks import send_mail_async +from common.utils import get_logger, get_object_or_none from orgs.utils import org_aware_func from jumpserver.const import PROJECT_DIR -from .models import Task, AdHoc +from .models import AdHoc, CeleryTask +from .const import DEFAULT_PASSWORD_RULES logger = get_logger(__file__) @@ -29,7 +29,7 @@ def update_or_create_ansible_task( interval=None, crontab=None, is_periodic=False, callback=None, pattern='all', options=None, run_as_admin=False, run_as=None, system_user=None, become_info=None, - ): +): if not hosts or not tasks or not task_name: return None, None if options is None: @@ -80,3 +80,15 @@ def get_task_log_path(base_path, task_id, level=2): path = os.path.join(base_path, rel_path) make_dirs(os.path.dirname(path), exist_ok=True) return path + + +def generate_random_password(**kwargs): + import random + import string + length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) + symbol_set = kwargs.get('symbol_set') + if symbol_set is None: + symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] + chars = string.ascii_letters + string.digits + symbol_set + password = ''.join([random.choice(chars) for _ in range(length)]) + return password diff --git a/apps/ops/ws.py b/apps/ops/ws.py index 94d71d90d..24023a33e 100644 --- a/apps/ops/ws.py +++ b/apps/ops/ws.py @@ -1,18 +1,18 @@ -import time +import asyncio import os -import threading -import json -from channels.generic.websocket import JsonWebsocketConsumer -from common.utils import get_logger +import aiofiles +from channels.generic.websocket import AsyncJsonWebsocketConsumer + from common.db.utils import close_old_connections -from .celery.utils import get_celery_task_log_path +from common.utils import get_logger from .ansible.utils import get_ansible_task_log_path +from .celery.utils import get_celery_task_log_path logger = get_logger(__name__) -class TaskLogWebsocket(JsonWebsocketConsumer): +class TaskLogWebsocket(AsyncJsonWebsocketConsumer): disconnected = False log_types = { @@ -20,70 +20,59 @@ class TaskLogWebsocket(JsonWebsocketConsumer): 'ansible': get_ansible_task_log_path } - def connect(self): + async def connect(self): user = self.scope["user"] if user.is_authenticated: - self.accept() + await self.accept() else: - self.close() + await self.close() - def get_log_path(self, task_id): - func = self.log_types.get(self.log_type) + def get_log_path(self, task_id, log_type): + func = self.log_types.get(log_type) if func: return func(task_id) - def receive(self, text_data=None, bytes_data=None, **kwargs): - data = json.loads(text_data) - task_id = data.get('task') - self.log_type = data.get('type', 'celery') - if task_id: - self.handle_task(task_id) + async def receive_json(self, content, **kwargs): + task_id = content.get('task') + task_typ = content.get('type', 'celery') + log_path = self.get_log_path(task_id, task_typ) + await self.async_handle_task(task_id, log_path) - def wait_util_log_path_exist(self, task_id): - log_path = self.get_log_path(task_id) + async def async_handle_task(self, task_id, log_path): + logger.info("Task id: {}".format(task_id)) while not self.disconnected: if not os.path.exists(log_path): - self.send_json({'message': '.', 'task': task_id}) - time.sleep(0.5) - continue - self.send_json({'message': '\r\n'}) - try: - logger.debug('Task log path: {}'.format(log_path)) - task_log_f = open(log_path, 'rb') - return task_log_f - except OSError: - return None - - def read_log_file(self, task_id): - task_log_f = self.wait_util_log_path_exist(task_id) - if not task_log_f: - logger.debug('Task log file is None: {}'.format(task_id)) - return - - task_end_mark = [] - while not self.disconnected: - data = task_log_f.read(4096) - if data: - data = data.replace(b'\n', b'\r\n') - self.send_json( - {'message': data.decode(errors='ignore'), 'task': task_id} - ) - if data.find(b'succeeded in') != -1: - task_end_mark.append(1) - if data.find(bytes(task_id, 'utf8')) != -1: - task_end_mark.append(1) - elif len(task_end_mark) == 2: - logger.debug('Task log end: {}'.format(task_id)) + await self.send_json({'message': '.', 'task': task_id}) + await asyncio.sleep(0.5) + else: + await self.send_task_log(task_id, log_path) break - time.sleep(0.2) - task_log_f.close() - def handle_task(self, task_id): - logger.info("Task id: {}".format(task_id)) - thread = threading.Thread(target=self.read_log_file, args=(task_id,)) - thread.start() + async def send_task_log(self, task_id, log_path): + await self.send_json({'message': '\r\n'}) + try: + logger.debug('Task log path: {}'.format(log_path)) + task_end_mark = [] + async with aiofiles.open(log_path, 'rb') as task_log_f: + while not self.disconnected: + data = await task_log_f.read(4096) + if data: + data = data.replace(b'\n', b'\r\n') + await self.send_json( + {'message': data.decode(errors='ignore'), 'task': task_id} + ) + if data.find(b'succeeded in') != -1: + task_end_mark.append(1) + if data.find(bytes(task_id, 'utf8')) != -1: + task_end_mark.append(1) + elif len(task_end_mark) == 2: + logger.debug('Task log end: {}'.format(task_id)) + break + await asyncio.sleep(0.2) + except OSError as e: + logger.warn('Task log path open failed: {}'.format(e)) + await self.close() - def disconnect(self, close_code): + async def disconnect(self, close_code): self.disconnected = True - self.close() close_old_connections() diff --git a/apps/orgs/api.py b/apps/orgs/api.py index 7a3271c50..da9b1530f 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -14,11 +14,10 @@ from .serializers import ( ) from users.models import User, UserGroup from assets.models import ( - Asset, Domain, SystemUser, Label, Node, Gateway, + Asset, Domain, Label, Node, CommandFilter, CommandFilterRule, GatheredUser ) -from applications.models import Application -from perms.models import AssetPermission, ApplicationPermission +from perms.models import AssetPermission from orgs.utils import current_org, tmp_to_root_org from common.utils import get_logger @@ -28,10 +27,9 @@ logger = get_logger(__file__) # 部分 org 相关的 model,需要清空这些数据之后才能删除该组织 org_related_models = [ - User, UserGroup, Asset, Label, Domain, Gateway, Node, SystemUser, Label, + User, UserGroup, Asset, Label, Domain, Node, Label, CommandFilter, CommandFilterRule, GatheredUser, - AssetPermission, ApplicationPermission, - Application, + AssetPermission, ] diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index e8b0bdcba..cd67984a3 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -1,15 +1,15 @@ from django.db.transaction import on_commit + from orgs.models import Organization from orgs.tasks import refresh_org_cache_task from orgs.utils import current_org, tmp_to_org - from common.cache import Cache, IntegerField from common.utils import get_logger +from common.utils.timezone import local_zero_hour, local_monday from users.models import UserGroup, User -from assets.models import Node, SystemUser, Domain, Gateway, Asset +from assets.models import Node, Domain, Asset, Account from terminal.models import Session -from applications.models import Application -from perms.models import AssetPermission, ApplicationPermission +from perms.models import AssetPermission logger = get_logger(__file__) @@ -36,35 +36,34 @@ class OrgRelatedCache(Cache): """ 在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据 """ + def func(): logger.debug(f'CACHE: Send refresh task {self}.{fields}') refresh_org_cache_task.delay(self, *fields) + on_commit(func) def expire(self, *fields): def func(): super(OrgRelatedCache, self).expire(*fields) + on_commit(func) class OrgResourceStatisticsCache(OrgRelatedCache): users_amount = IntegerField() - groups_amount = IntegerField(queryset=UserGroup.objects) - assets_amount = IntegerField() + new_users_amount_this_week = IntegerField() + new_assets_amount_this_week = IntegerField() nodes_amount = IntegerField(queryset=Node.objects) - admin_users_amount = IntegerField() - system_users_amount = IntegerField() domains_amount = IntegerField(queryset=Domain.objects) - gateways_amount = IntegerField(queryset=Gateway.objects) - - applications_amount = IntegerField(queryset=Application.objects) - + groups_amount = IntegerField(queryset=UserGroup.objects) + accounts_amount = IntegerField(queryset=Account.objects) asset_perms_amount = IntegerField(queryset=AssetPermission.objects) - app_perms_amount = IntegerField(queryset=ApplicationPermission.objects) - total_count_online_users = IntegerField() total_count_online_sessions = IntegerField() + total_count_today_active_assets = IntegerField() + total_count_today_failed_sessions = IntegerField() def __init__(self, org): super().__init__() @@ -76,24 +75,49 @@ class OrgResourceStatisticsCache(OrgRelatedCache): def get_current_org(self): return self.org - def compute_admin_users_amount(self): - return SystemUser.objects.filter(type=SystemUser.Type.admin).count() + def get_users(self): + return User.get_org_users(self.org) - def compute_system_users_amount(self): - return SystemUser.objects.filter(type=SystemUser.Type.common).count() + @staticmethod + def get_assets(): + return Asset.objects.all() def compute_users_amount(self): - amount = User.get_org_users(self.org).count() - return amount + users = self.get_users() + return users.count() + + def compute_new_users_amount_this_week(self): + monday_time = local_monday() + users = self.get_users().filter(date_joined__gte=monday_time) + return users.count() def compute_assets_amount(self): - if self.org.is_root(): - return Asset.objects.all().count() - node = Node.org_root() - return node.assets_amount + assets = self.get_assets() + return assets.count() - def compute_total_count_online_users(self): - return Session.objects.filter(is_finished=False).values_list('user_id').distinct().count() + def compute_new_assets_amount_this_week(self): + monday_time = local_monday() + assets = self.get_assets().filter(date_created__gte=monday_time) + return assets.count() - def compute_total_count_online_sessions(self): + @staticmethod + def compute_total_count_online_users(): + return Session.objects.filter( + is_finished=False + ).values_list('user_id').distinct().count() + + @staticmethod + def compute_total_count_online_sessions(): return Session.objects.filter(is_finished=False).count() + + @staticmethod + def compute_total_count_today_active_assets(): + t = local_zero_hour() + return Session.objects.filter( + date_start__gte=t, is_success=False + ).values('asset_id').distinct().count() + + @staticmethod + def compute_total_count_today_failed_sessions(): + t = local_zero_hour() + return Session.objects.filter(date_start__gte=t, is_success=False).count() diff --git a/apps/orgs/filters.py b/apps/orgs/filters.py index ee68a0ed8..139597f9c 100644 --- a/apps/orgs/filters.py +++ b/apps/orgs/filters.py @@ -1,6 +1,2 @@ -from django_filters.rest_framework import filters -class UUIDInFilter(filters.BaseInFilter, filters.UUIDFilter): - pass - diff --git a/apps/orgs/migrations/0010_auto_20210219_1241.py b/apps/orgs/migrations/0010_auto_20210219_1241.py index facc6a654..6fc3b0577 100644 --- a/apps/orgs/migrations/0010_auto_20210219_1241.py +++ b/apps/orgs/migrations/0010_auto_20210219_1241.py @@ -34,13 +34,13 @@ def migrate_default_org_id(apps, schema_editor): for app, models_name in org_app_models: for model_name in models_name: t_start = time.time() - print("Migrate model org id: {}".format(model_name), end='') + print("\tMigrate model org id: {}".format(model_name), end='') sys.stdout.flush() model_cls = apps.get_model(app, model_name) model_cls.objects.filter(org_id='').update(org_id=default_id) interval = round((time.time() - t_start) * 1000, 2) - print(" done, use {} ms".format(interval)) + print("\tdone, use {} ms".format(interval)) def add_all_user_to_default_org(apps, schema_editor): @@ -53,16 +53,16 @@ def add_all_user_to_default_org(apps, schema_editor): t_start = time.time() count = users_qs.count() - print(f'Will add users to default org: {count}') + print(f'\tWill add users to default org: {count}') batch_size = 1000 for i in range(0, count, batch_size): users = list(users_qs[i:i + batch_size]) members = [org_members_model(user=user, org=default_org) for user in users] org_members_model.objects.bulk_create(members, ignore_conflicts=True) - print(f'Add users to default org: {i+1}-{i+len(users)}') + print(f'\t Add users to default org: {i+1}-{i+len(users)}') interval = round((time.time() - t_start) * 1000, 2) - print(f'done, use {interval} ms') + print(f'\tdone, use {interval} ms') class Migration(migrations.Migration): diff --git a/apps/orgs/migrations/0013_alter_organization_options.py b/apps/orgs/migrations/0013_alter_organization_options.py index e868a87a3..6dfd004da 100644 --- a/apps/orgs/migrations/0013_alter_organization_options.py +++ b/apps/orgs/migrations/0013_alter_organization_options.py @@ -14,4 +14,7 @@ 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 new file mode 100644 index 000000000..6541fe1a7 --- /dev/null +++ b/apps/orgs/migrations/0014_organization_builtin.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.14 on 2022-10-26 09:07 + +from django.db import migrations, models + + +def update_builtin_org(apps, schema_editor): + org_model = apps.get_model('orgs', 'Organization') + org_model.objects.create( + id='00000000-0000-0000-0000-000000000004', + name='SYSTEM', builtin=True + ) + + # 更新 Default + org_model.objects.filter(name='DEFAULT').update(builtin=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('orgs', '0013_alter_organization_options'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='builtin', + field=models.BooleanField(default=False, verbose_name='Builtin'), + ), + migrations.RunPython(update_builtin_org), + ] diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 83e4aeac2..0795edc2e 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -6,6 +6,7 @@ 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 ..utils import ( set_current_org, get_current_org, current_org, filter_org_queryset ) @@ -14,21 +15,20 @@ from ..models import Organization logger = get_logger(__file__) __all__ = [ - 'OrgManager', 'OrgModelMixin', 'Organization' + 'OrgManager', 'OrgModelMixin', 'JMSOrgBaseModel' ] class OrgManager(models.Manager): - def all_group_by_org(self): from ..models import Organization orgs = list(Organization.objects.all()) - querysets = {} + org_queryset = {} for org in orgs: org_id = org.id queryset = super(OrgManager, self).get_queryset().filter(org_id=org_id) - querysets[org] = queryset - return querysets + org_queryset[org] = queryset + return org_queryset def get_queryset(self): queryset = super(OrgManager, self).get_queryset() @@ -45,7 +45,7 @@ class OrgManager(models.Manager): for obj in objs: if org.is_root(): if not obj.org_id: - raise ValidationError('Please save in a organization') + raise ValidationError('Please save in a org') else: obj.org_id = org.id return super().bulk_create(objs, batch_size, ignore_conflicts) @@ -53,20 +53,24 @@ class OrgManager(models.Manager): class OrgModelMixin(models.Model): org_id = models.CharField( - max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True + max_length=36, blank=True, default='', + verbose_name=_("Organization"), db_index=True ) objects = OrgManager() - sep = '@' def save(self, *args, **kwargs): - org = get_current_org() + locking_org = getattr(self, 'LOCKING_ORG', None) + if locking_org: + org = Organization.get_instance(locking_org) + else: + org = get_current_org() # 这里不可以优化成, 因为 root 组织下可以设置组织 id 来保存 # if org.is_root() and not self.org_id: # raise ... if org.is_root(): if not self.org_id: - raise ValidationError('Please save in a organization') + raise ValidationError('Please save in a org') else: self.org_id = org.id return super().save(*args, **kwargs) @@ -86,8 +90,6 @@ class OrgModelMixin(models.Model): name = getattr(self, attr) elif hasattr(self, 'name'): name = self.name - elif hasattr(self, 'hostname'): - name = self.hostname return name + self.sep + self.org_name def validate_unique(self, exclude=None): @@ -112,3 +114,8 @@ class OrgModelMixin(models.Model): class Meta: abstract = True + + +class JMSOrgBaseModel(JMSBaseModel, OrgModelMixin): + class Meta: + abstract = True diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py index fed9d1713..8f70814bc 100644 --- a/apps/orgs/mixins/serializers.py +++ b/apps/orgs/mixins/serializers.py @@ -5,7 +5,7 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from common.validators import ProjectUniqueValidator -from common.mixins import BulkSerializerMixin, CommonSerializerMixin +from common.drf.serializers import BulkSerializerMixin, CommonSerializerMixin from ..utils import get_current_org_id_for_serializer @@ -24,21 +24,22 @@ class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer): """ org_id = serializers.ReadOnlyField(default=get_current_org_id_for_serializer, label=_("Organization")) org_name = serializers.ReadOnlyField(label=_("Org name")) + add_org_fields = True def get_validators(self): _validators = super().get_validators() validators = [] for v in _validators: - if isinstance(v, UniqueTogetherValidator) \ - and "org_id" in v.fields: + if isinstance(v, UniqueTogetherValidator) and "org_id" in v.fields: v = ProjectUniqueValidator(v.queryset, v.fields) validators.append(v) return validators def get_field_names(self, declared_fields, info): fields = super().get_field_names(declared_fields, info) - fields.extend(["org_id", "org_name"]) + if self.add_org_fields: + fields.extend(["org_id", "org_name"]) return fields diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 4bcb14e33..73dc3c6ad 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -69,6 +69,7 @@ class Organization(OrgRoleMixin, models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) 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( @@ -78,7 +79,9 @@ class Organization(OrgRoleMixin, models.Model): ROOT_ID = '00000000-0000-0000-0000-000000000000' ROOT_NAME = _('GLOBAL') DEFAULT_ID = '00000000-0000-0000-0000-000000000002' - DEFAULT_NAME = 'Default' + DEFAULT_NAME = _('DEFAULT') + SYSTEM_ID = '00000000-0000-0000-0000-000000000004' + SYSTEM_NAME = _('SYSTEM') orgs_mapping = None class Meta: @@ -138,12 +141,15 @@ class Organization(OrgRoleMixin, models.Model): def default(cls): defaults = dict(id=cls.DEFAULT_ID, name=cls.DEFAULT_NAME) obj, created = cls.objects.get_or_create(defaults=defaults, id=cls.DEFAULT_ID) + if not obj.builtin: + obj.builtin = True + obj.save() return obj @classmethod def root(cls): name = settings.GLOBAL_ORG_DISPLAY_NAME or cls.ROOT_NAME - return cls(id=cls.ROOT_ID, name=name) + return cls(id=cls.ROOT_ID, name=name, builtin=True) def is_root(self): return self.id == self.ROOT_ID @@ -205,30 +211,3 @@ class Organization(OrgRoleMixin, models.Model): def delete(self, *args, **kwargs): self.delete_related_models() return super().delete(*args, **kwargs) - - -class OrganizationMember(models.Model): - """ - 注意:直接调用该 `Model.delete` `Model.objects.delete` 不会触发清理该用户的信号 - """ - - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - org = models.ForeignKey( - Organization, related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('Organization') - ) - user = models.ForeignKey( - 'users.User', related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('User') - ) - role = models.CharField(max_length=16, default='User', verbose_name=_("Role")) - 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')) - - # objects = OrgMemberManager() - - class Meta: - unique_together = [('org', 'user', 'role')] - db_table = 'orgs_organization_members' - - def __str__(self): - return '{} | {}'.format(self.user, self.org) diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 0ae933044..c080332dd 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -11,14 +11,10 @@ class ResourceStatisticsSerializer(serializers.Serializer): assets_amount = serializers.IntegerField(required=False) nodes_amount = serializers.IntegerField(required=False) - admin_users_amount = serializers.IntegerField(required=False) - system_users_amount = serializers.IntegerField(required=False) domains_amount = serializers.IntegerField(required=False) gateways_amount = serializers.IntegerField(required=False) - applications_amount = serializers.IntegerField(required=False) asset_perms_amount = serializers.IntegerField(required=False) - app_perms_amount = serializers.IntegerField(required=False) class OrgSerializer(ModelSerializer): diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 8bd2ca609..505422377 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -1,16 +1,15 @@ -from functools import wraps from django.db.models.signals import post_save, pre_delete, pre_save, post_delete from django.dispatch import receiver from orgs.models import Organization -from assets.models import Node -from perms.models import AssetPermission, ApplicationPermission +from assets.models import Node, Account +from perms.models import AssetPermission +from audits.models import UserLoginLog from users.models import UserGroup, User from users.signals import pre_user_leave_org -from applications.models import Application from terminal.models import Session from rbac.models import OrgRoleBinding, SystemRoleBinding, RoleBinding -from assets.models import Asset, SystemUser, Domain, Gateway +from assets.models import Asset, Domain from orgs.caches import OrgResourceStatisticsCache from orgs.utils import current_org from common.utils import get_logger @@ -76,16 +75,14 @@ def on_user_delete_refresh_cache(sender, instance, **kwargs): class OrgResourceStatisticsRefreshUtil: model_cache_field_mapper = { - ApplicationPermission: ['app_perms_amount'], - AssetPermission: ['asset_perms_amount'], - Application: ['applications_amount'], - Gateway: ['gateways_amount'], - Domain: ['domains_amount'], - SystemUser: ['system_users_amount', 'admin_users_amount'], Node: ['nodes_amount'], - Asset: ['assets_amount'], + Domain: ['domains_amount'], UserGroup: ['groups_amount'], - RoleBinding: ['users_amount'] + Account: ['accounts_amount'], + RoleBinding: ['users_amount', 'new_users_amount_this_week'], + Asset: ['assets_amount', 'new_assets_amount_this_week'], + AssetPermission: ['asset_perms_amount'], + } @classmethod @@ -94,7 +91,7 @@ class OrgResourceStatisticsRefreshUtil: if not cache_field_name: return OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name) - if instance.org: + if getattr(instance, 'org', None): OrgResourceStatisticsCache(instance.org).expire(*cache_field_name) diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py index 05e628883..b32cba2cd 100644 --- a/apps/orgs/signal_handlers/common.py +++ b/apps/orgs/signal_handlers/common.py @@ -3,23 +3,23 @@ from collections import defaultdict from functools import partial -from django.db.models.signals import m2m_changed -from django.db.models.signals import post_save, pre_delete +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 assets.models import CommandFilterRule -from assets.models import SystemUser +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 -from perms.models import (AssetPermission, ApplicationPermission) -from users.models import UserGroup, User from users.signals import post_user_leave_org logger = get_logger(__file__) @@ -45,6 +45,12 @@ def expire_orgs_mapping_for_memory(org_id): def subscribe_orgs_mapping_expire(sender, **kwargs): logger.debug("Start subscribe for expire orgs mapping from memory") + if settings.DEBUG: + try: + set_to_default_org() + except (ProgrammingError, OperationalError): + pass + orgs_mapping_for_memory_pub_sub.subscribe( lambda org_id: Organization.expire_orgs_mapping() ) @@ -129,12 +135,12 @@ def _clear_users_from_org(org, users): if not users: return - models = (AssetPermission, ApplicationPermission, UserGroup, SystemUser) + models = (AssetPermission, UserGroup) for m in models: _remove_users(m, users, org) - _remove_users(CommandFilterRule, users, org, user_field_name='reviewers') + # _remove_users(CommandFilterRule, users, org, user_field_name='reviewers') @receiver(post_save, sender=User) diff --git a/apps/orgs/tasks.py b/apps/orgs/tasks.py index a33456913..04992f52a 100644 --- a/apps/orgs/tasks.py +++ b/apps/orgs/tasks.py @@ -1,11 +1,12 @@ from celery import shared_task +from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger logger = get_logger(__file__) -@shared_task -def refresh_org_cache_task(cache, *fields): - logger.info(f'CACHE: refresh {cache.key}.{fields}') - cache.refresh(*fields) +@shared_task(verbose_name=_("Refresh organization cache")) +def refresh_org_cache_task(*fields): + from .caches import OrgResourceStatisticsCache + OrgResourceStatisticsCache.refresh(*fields) diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 29b1f03e9..38cd0e764 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -88,22 +88,32 @@ def tmp_to_org(org): set_current_org(ori_org) -def get_org_filters(): - kwargs = {} - - _current_org = get_current_org() - if _current_org is None: - return kwargs - if _current_org.is_root(): - return kwargs - kwargs['org_id'] = _current_org.id - return kwargs +@contextmanager +def tmp_to_builtin_org(system=0, default=0): + if system: + org_id = Organization.SYSTEM_ID + elif default: + org_id = Organization.DEFAULT_ID + else: + raise ValueError("Must set system or default") + ori_org = get_current_org() + set_current_org(org_id) + yield + if ori_org is not None: + set_current_org(ori_org) def filter_org_queryset(queryset): - kwargs = get_org_filters() + locking_org = getattr(queryset.model, 'LOCKING_ORG', None) + org = get_current_org() + + if locking_org: + kwargs = {'org_id': locking_org} + elif org is None or org.is_root(): + kwargs = {} + else: + kwargs = {'org_id': org.id} - # # lines = traceback.format_stack() # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") # for line in lines[-10:-1]: diff --git a/apps/perms/api/__init__.py b/apps/perms/api/__init__.py index d9a7afe0f..67986edc6 100644 --- a/apps/perms/api/__init__.py +++ b/apps/perms/api/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # -from .asset import * -from .application import * -from .system_user_permission import * +from .user_permission import * +from .asset_permission import * +from .asset_permission_relation import * +from .user_group_permission import * diff --git a/apps/perms/api/application/__init__.py b/apps/perms/api/application/__init__.py deleted file mode 100644 index 05e4ff6d2..000000000 --- a/apps/perms/api/application/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .user_permission import * -from .application_permission import * -from .application_permission_relation import * -from .user_group_permission import * diff --git a/apps/perms/api/application/application_permission.py b/apps/perms/api/application/application_permission.py deleted file mode 100644 index bd8fb3452..000000000 --- a/apps/perms/api/application/application_permission.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework.response import Response -from rest_framework.generics import RetrieveAPIView - -from perms import serializers -from perms.models import ApplicationPermission -from applications.models import Application -from common.permissions import IsValidUser -from ..base import BasePermissionViewSet - - -class ApplicationPermissionViewSet(BasePermissionViewSet): - """ - 应用授权列表的增删改查API - """ - model = ApplicationPermission - serializer_class = serializers.ApplicationPermissionSerializer - filterset_fields = { - 'name': ['exact'], - 'category': ['exact'], - 'type': ['exact', 'in'], - 'from_ticket': ['exact'] - } - search_fields = ['name', 'category', 'type'] - custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [ - 'application_id', 'application', 'app', 'app_name' - ] - ordering_fields = ('name',) - ordering = ('name',) - - def get_queryset(self): - queryset = super().get_queryset().prefetch_related( - "applications", "users", "user_groups", "system_users" - ) - return queryset - - def filter_application(self, queryset): - app_id = self.request.query_params.get('application_id') or \ - self.request.query_params.get('app') - app_name = self.request.query_params.get('application') or \ - self.request.query_params.get('app_name') - - if app_id: - applications = Application.objects.filter(pk=app_id) - elif app_name: - applications = Application.objects.filter(name=app_name) - else: - return queryset - if not applications: - return queryset.none() - queryset = queryset.filter(applications__in=applications) - return queryset - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.filter_application(queryset) - return queryset - - -class ApplicationPermissionActionsApi(RetrieveAPIView): - permission_classes = (IsValidUser,) - - def retrieve(self, request, *args, **kwargs): - category = request.GET.get('category') - actions = ApplicationPermission.get_include_actions_choices(category=category) - return Response(data=actions) diff --git a/apps/perms/api/application/application_permission_relation.py b/apps/perms/api/application/application_permission_relation.py deleted file mode 100644 index 611d0930e..000000000 --- a/apps/perms/api/application/application_permission_relation.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework import generics -from django.db.models import F, Value -from django.db.models.functions import Concat -from django.shortcuts import get_object_or_404 - -from applications.models import Application -from orgs.mixins.api import OrgRelationMixin -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.utils import current_org -from perms import serializers -from perms import models - -__all__ = [ - 'ApplicationPermissionUserRelationViewSet', - 'ApplicationPermissionUserGroupRelationViewSet', - 'ApplicationPermissionApplicationRelationViewSet', - 'ApplicationPermissionSystemUserRelationViewSet', - 'ApplicationPermissionAllApplicationListApi', - 'ApplicationPermissionAllUserListApi', -] - - -class RelationMixin(OrgRelationMixin, OrgBulkModelViewSet): - perm_model = models.ApplicationPermission - - def get_queryset(self): - queryset = super().get_queryset() - org_id = current_org.org_id() - if org_id is not None: - queryset = queryset.filter(applicationpermission__org_id=org_id) - queryset = queryset.annotate(applicationpermission_display=F('applicationpermission__name')) - return queryset - - -class ApplicationPermissionUserRelationViewSet(RelationMixin): - serializer_class = serializers.ApplicationPermissionUserRelationSerializer - m2m_field = models.ApplicationPermission.users.field - filterset_fields = [ - 'id', "user", "applicationpermission", - ] - search_fields = ("user__name", "user__username", "applicationpermission__name") - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(user_display=F('user__name')) - return queryset - - -class ApplicationPermissionUserGroupRelationViewSet(RelationMixin): - serializer_class = serializers.ApplicationPermissionUserGroupRelationSerializer - m2m_field = models.ApplicationPermission.user_groups.field - filterset_fields = [ - 'id', "usergroup", "applicationpermission" - ] - search_fields = ["usergroup__name", "applicationpermission__name"] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(usergroup_display=F('usergroup__name')) - return queryset - - -class ApplicationPermissionApplicationRelationViewSet(RelationMixin): - serializer_class = serializers.ApplicationPermissionApplicationRelationSerializer - m2m_field = models.ApplicationPermission.applications.field - filterset_fields = [ - 'id', 'application', 'applicationpermission', - ] - search_fields = ["id", "application__name", "applicationpermission__name"] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(application_display=F('application__name')) - return queryset - - -class ApplicationPermissionSystemUserRelationViewSet(RelationMixin): - serializer_class = serializers.ApplicationPermissionSystemUserRelationSerializer - m2m_field = models.ApplicationPermission.system_users.field - filterset_fields = [ - 'id', 'systemuser', 'applicationpermission', - ] - search_fields = [ - "applicactionpermission__name", "systemuser__name", "systemuser__username" - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - systemuser_display=Concat( - F('systemuser__name'), Value('('), F('systemuser__username'), - Value(')') - )) - return queryset - - -class ApplicationPermissionAllApplicationListApi(generics.ListAPIView): - serializer_class = serializers.ApplicationPermissionAllApplicationSerializer - only_fields = serializers.ApplicationPermissionAllApplicationSerializer.Meta.only_fields - filterset_fields = ('name',) - search_fields = filterset_fields - - def get_queryset(self): - pk = self.kwargs.get('pk') - perm = get_object_or_404(models.ApplicationPermission, pk=pk) - applications = Application.objects.filter(granted_by_permissions=perm) \ - .only(*self.only_fields).distinct() - return applications - - -class ApplicationPermissionAllUserListApi(generics.ListAPIView): - serializer_class = serializers.ApplicationPermissionAllUserSerializer - only_fields = serializers.ApplicationPermissionAllUserSerializer.Meta.only_fields - filterset_fields = ('username', 'name') - search_fields = filterset_fields - - def get_queryset(self): - pk = self.kwargs.get('pk') - perm = get_object_or_404(models.ApplicationPermission, pk=pk) - users = perm.get_all_users().only(*self.only_fields).distinct() - return users diff --git a/apps/perms/api/application/user_group_permission.py b/apps/perms/api/application/user_group_permission.py deleted file mode 100644 index e8061e9fc..000000000 --- a/apps/perms/api/application/user_group_permission.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.db.models import Q -from rest_framework.generics import ListAPIView - -from common.mixins.api import CommonApiMixin -from applications.models import Application -from perms import serializers - -__all__ = [ - 'UserGroupGrantedApplicationsApi' -] - - -class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView): - """ - 获取用户组直接授权的应用 - """ - serializer_class = serializers.AppGrantedSerializer - only_fields = serializers.AppGrantedSerializer.Meta.only_fields - filterset_fields = ['id', 'name', 'category', 'type', 'comment'] - search_fields = ['name', 'comment'] - rbac_perms = { - 'list': 'perms.view_applicationpermission' - } - - def get_queryset(self): - user_group_id = self.kwargs.get('pk') - if not user_group_id: - return Application.objects.none() - - queryset = Application.objects\ - .filter(Q(granted_by_permissions__user_groups__id=user_group_id))\ - .distinct().only(*self.only_fields) - return queryset diff --git a/apps/perms/api/application/user_permission/__init__.py b/apps/perms/api/application/user_permission/__init__.py deleted file mode 100644 index 50aed175b..000000000 --- a/apps/perms/api/application/user_permission/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .user_permission_applications import * -from .common import * diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py deleted file mode 100644 index 34fcc4dd1..000000000 --- a/apps/perms/api/application/user_permission/common.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# -import time - -from django.shortcuts import get_object_or_404 -from django.utils.decorators import method_decorator -from rest_framework.views import APIView, Response -from rest_framework import status -from rest_framework.generics import ( - ListAPIView, get_object_or_404 -) - -from orgs.utils import tmp_to_root_org, get_current_org -from applications.models import Application -from perms.utils.application.permission import ( - get_application_system_user_ids, - validate_permission, -) -from .mixin import AppRoleAdminMixin, AppRoleUserMixin -from perms.hands import User, SystemUser -from perms import serializers - - -__all__ = [ - 'UserGrantedApplicationSystemUsersApi', - 'MyGrantedApplicationSystemUsersApi', - 'ValidateUserApplicationPermissionApi' -] - - -class BaseGrantedApplicationSystemUsersApi(ListAPIView): - serializer_class = serializers.ApplicationSystemUserSerializer - only_fields = serializers.ApplicationSystemUserSerializer.Meta.only_fields - user: None - - def get_application_system_user_ids(self, application): - return get_application_system_user_ids(self.user, application) - - def get_queryset(self): - application_id = self.kwargs.get('application_id') - application = get_object_or_404(Application, id=application_id) - system_user_ids = self.get_application_system_user_ids(application) - system_users = SystemUser.objects.filter(id__in=system_user_ids)\ - .only(*self.only_fields).order_by('priority') - return system_users - - -class UserGrantedApplicationSystemUsersApi(AppRoleAdminMixin, BaseGrantedApplicationSystemUsersApi): - pass - - -class MyGrantedApplicationSystemUsersApi(AppRoleUserMixin, BaseGrantedApplicationSystemUsersApi): - pass - - -@method_decorator(tmp_to_root_org(), name='get') -class ValidateUserApplicationPermissionApi(APIView): - rbac_perms = { - 'GET': 'perms.view_applicationpermission' - } - - def get(self, request, *args, **kwargs): - user_id = request.query_params.get('user_id', '') - application_id = request.query_params.get('application_id', '') - system_user_id = request.query_params.get('system_user_id', '') - - data = { - 'has_permission': False, - 'expire_at': int(time.time()), - 'actions': [] - } - if not all((user_id, application_id, system_user_id)): - return Response(data) - - user = User.objects.get(id=user_id) - application = Application.objects.get(id=application_id) - system_user = SystemUser.objects.get(id=system_user_id) - has_perm, actions, expire_at = validate_permission(user, application, system_user) - status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN - data = { - 'has_permission': has_perm, - 'expire_at': int(expire_at), - 'actions': actions - } - return Response(data, status=status_code) diff --git a/apps/perms/api/application/user_permission/mixin.py b/apps/perms/api/application/user_permission/mixin.py deleted file mode 100644 index 6e8f91090..000000000 --- a/apps/perms/api/application/user_permission/mixin.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from common.mixins.api import RoleAdminMixin as _RoleAdminMixin -from common.mixins.api import RoleUserMixin as _RoleUserMixin -from orgs.utils import tmp_to_root_org - - -class AppRoleAdminMixin(_RoleAdminMixin): - rbac_perms = ( - ('list', 'perms.view_userapp'), - ('retrieve', 'perms.view_userapps'), - ('get_tree', 'perms.view_userapps'), - ('GET', 'perms.view_userapps'), - ) - - -class AppRoleUserMixin(_RoleUserMixin): - rbac_perms = ( - ('list', 'perms.view_myapps'), - ('retrieve', 'perms.view_myapps'), - ('get_tree', 'perms.view_myapps'), - ('GET', 'perms.view_myapps'), - ) diff --git a/apps/perms/api/application/user_permission/user_permission_applications.py b/apps/perms/api/application/user_permission/user_permission_applications.py deleted file mode 100644 index 82b0ab025..000000000 --- a/apps/perms/api/application/user_permission/user_permission_applications.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# -from typing import Callable - -from rest_framework.generics import ListAPIView -from rest_framework.response import Response - -from common.mixins.api import CommonApiMixin -from common.tree import TreeNodeSerializer -from perms import serializers -from perms.tree.app import GrantedAppTreeUtil -from perms.utils.application.user_permission import ( - get_user_granted_all_applications -) -from .mixin import AppRoleAdminMixin, AppRoleUserMixin - - -__all__ = [ - 'UserAllGrantedApplicationsApi', - 'MyAllGrantedApplicationsApi', - 'UserAllGrantedApplicationsAsTreeApi', - 'MyAllGrantedApplicationsAsTreeApi', -] - - -class AllGrantedApplicationsApi(CommonApiMixin, ListAPIView): - only_fields = serializers.AppGrantedSerializer.Meta.only_fields - serializer_class = serializers.AppGrantedSerializer - filterset_fields = { - 'id': ['exact'], - 'name': ['exact'], - 'category': ['exact'], - 'type': ['exact', 'in'], - 'comment': ['exact'], - } - search_fields = ['name', 'comment'] - user: None - - def get_queryset(self): - queryset = get_user_granted_all_applications(self.user) - return queryset.only(*self.only_fields) - - -class UserAllGrantedApplicationsApi(AppRoleAdminMixin, AllGrantedApplicationsApi): - pass - - -class MyAllGrantedApplicationsApi(AppRoleUserMixin, AllGrantedApplicationsApi): - pass - - -class ApplicationsAsTreeMixin: - """ - 将应用序列化成树的结构返回 - """ - serializer_class = TreeNodeSerializer - user: None - filter_queryset: Callable - get_queryset: Callable - get_serializer: Callable - - def list(self, request, *args, **kwargs): - tree_id = request.query_params.get('tree_id', None) - parent_info = request.query_params.get('parentInfo', None) - queryset = self.filter_queryset(self.get_queryset()) - util = GrantedAppTreeUtil() - - if not tree_id: - tree_nodes = util.create_tree_nodes(queryset) - else: - tree_nodes = util.get_children_nodes(tree_id, parent_info, self.user) - serializer = self.get_serializer(tree_nodes, many=True) - return Response(data=serializer.data) - - -class UserAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, UserAllGrantedApplicationsApi): - pass - - -class MyAllGrantedApplicationsAsTreeApi(ApplicationsAsTreeMixin, MyAllGrantedApplicationsApi): - pass diff --git a/apps/perms/api/asset/__init__.py b/apps/perms/api/asset/__init__.py deleted file mode 100644 index 49c998e95..000000000 --- a/apps/perms/api/asset/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .user_permission import * -from .asset_permission import * -from .asset_permission_relation import * -from .user_group_permission import * diff --git a/apps/perms/api/asset/user_permission/__init__.py b/apps/perms/api/asset/user_permission/__init__.py deleted file mode 100644 index 590235cc6..000000000 --- a/apps/perms/api/asset/user_permission/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -# -from .common import * -from .user_permission_nodes import * -from .user_permission_assets import * -from .user_permission_nodes_with_assets import * diff --git a/apps/perms/api/asset/user_permission/common.py b/apps/perms/api/asset/user_permission/common.py deleted file mode 100644 index 86bff2123..000000000 --- a/apps/perms/api/asset/user_permission/common.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -# -import uuid -import time - -from django.shortcuts import get_object_or_404 -from django.utils.decorators import method_decorator -from rest_framework.views import APIView, Response -from rest_framework import status -from rest_framework.generics import ( - ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView -) - -from orgs.utils import tmp_to_root_org -from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission -from common.permissions import IsValidUser -from common.utils import get_logger, lazyproperty - -from perms.hands import User, Asset, SystemUser -from perms import serializers - -logger = get_logger(__name__) - -__all__ = [ - 'UserGrantedAssetSystemUsersForAdminApi', - 'ValidateUserAssetPermissionApi', - 'GetUserAssetPermissionActionsApi', - 'UserAssetPermissionsCacheApi', - 'MyGrantedAssetSystemUsersApi', -] - - -@method_decorator(tmp_to_root_org(), name='get') -class GetUserAssetPermissionActionsApi(RetrieveAPIView): - serializer_class = serializers.ActionsSerializer - rbac_perms = { - 'retrieve': 'perms.view_userassets', - 'GET': 'perms.view_userassets', - } - - def get_user(self): - user_id = self.request.query_params.get('user_id', '') - user = get_object_or_404(User, id=user_id) - return user - - def get_object(self): - asset_id = self.request.query_params.get('asset_id', '') - system_id = self.request.query_params.get('system_user_id', '') - - try: - asset_id = uuid.UUID(asset_id) - system_id = uuid.UUID(system_id) - except ValueError: - return Response({'msg': False}, status=403) - - asset = get_object_or_404(Asset, id=asset_id) - system_user = get_object_or_404(SystemUser, id=system_id) - - system_users_actions = get_asset_system_user_ids_with_actions_by_user(self.get_user(), asset) - actions = system_users_actions.get(system_user.id) - return {"actions": actions} - - -@method_decorator(tmp_to_root_org(), name='get') -class ValidateUserAssetPermissionApi(APIView): - rbac_perms = { - 'GET': 'perms.view_userassets' - } - - def get(self, request, *args, **kwargs): - user_id = self.request.query_params.get('user_id', '') - asset_id = request.query_params.get('asset_id', '') - system_id = request.query_params.get('system_user_id', '') - action_name = request.query_params.get('action_name', '') - - data = { - 'has_permission': False, - 'expire_at': int(time.time()), - 'actions': [] - } - - if not all((user_id, asset_id, system_id, action_name)): - return Response(data) - - user = User.objects.get(id=user_id) - asset = Asset.objects.valid().get(id=asset_id) - system_user = SystemUser.objects.get(id=system_id) - - has_perm, actions, expire_at = validate_permission(user, asset, system_user, action_name) - status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN - data = { - 'has_permission': has_perm, - 'actions': actions, - 'expire_at': int(expire_at) - } - return Response(data, status=status_code) - - -class UserGrantedAssetSystemUsersForAdminApi(ListAPIView): - serializer_class = serializers.AssetSystemUserSerializer - only_fields = serializers.AssetSystemUserSerializer.Meta.only_fields - rbac_perms = { - 'list': 'perms.view_userassets' - } - - @lazyproperty - def user(self): - user_id = self.kwargs.get('pk') - return User.objects.get(id=user_id) - - @lazyproperty - def system_users_with_actions(self): - asset_id = self.kwargs.get('asset_id') - asset = get_object_or_404(Asset, id=asset_id, is_active=True) - return self.get_asset_system_user_ids_with_actions(asset) - - def get_asset_system_user_ids_with_actions(self, asset): - return get_asset_system_user_ids_with_actions_by_user(self.user, asset) - - def get_queryset(self): - system_user_ids = self.system_users_with_actions.keys() - system_users = SystemUser.objects.filter(id__in=system_user_ids) \ - .only(*self.serializer_class.Meta.only_fields) \ - .order_by('name') - return system_users - - def paginate_queryset(self, queryset): - page = super().paginate_queryset(queryset) - - if page: - page = self.set_systemusers_action(page) - else: - self.set_systemusers_action(queryset) - return page - - def set_systemusers_action(self, queryset): - queryset_list = list(queryset) - for system_user in queryset_list: - actions = self.system_users_with_actions.get(system_user.id, 0) - system_user.actions = actions - return queryset_list - - -class MyGrantedAssetSystemUsersApi(UserGrantedAssetSystemUsersForAdminApi): - permission_classes = (IsValidUser,) - - @lazyproperty - def user(self): - return self.request.user - - -# TODO 删除 -class UserAssetPermissionsCacheApi(DestroyAPIView): - def destroy(self, request, *args, **kwargs): - return Response(status=204) diff --git a/apps/perms/api/asset/user_permission/mixin.py b/apps/perms/api/asset/user_permission/mixin.py deleted file mode 100644 index c28da6c2d..000000000 --- a/apps/perms/api/asset/user_permission/mixin.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework.request import Request - -from common.http import is_true -from common.mixins.api import RoleAdminMixin as _RoleAdminMixin -from common.mixins.api import RoleUserMixin as _RoleUserMixin -from orgs.utils import tmp_to_root_org -from users.models import User -from perms.utils.asset.user_permission import UserGrantedTreeRefreshController - - -class PermBaseMixin: - 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) - - -class AssetRoleAdminMixin(PermBaseMixin, _RoleAdminMixin): - rbac_perms = ( - ('list', 'perms.view_userassets'), - ('retrieve', 'perms.view_userassets'), - ('get_tree', 'perms.view_userassets'), - ('GET', 'perms.view_userassets'), - ) - - -class AssetRoleUserMixin(PermBaseMixin, _RoleUserMixin): - rbac_perms = ( - ('list', 'perms.view_myassets'), - ('retrieve', 'perms.view_myassets'), - ('get_tree', 'perms.view_myassets'), - ('GET', 'perms.view_myassets'), - ) diff --git a/apps/perms/api/asset/user_permission/user_permission_assets/__init__.py b/apps/perms/api/asset/user_permission/user_permission_assets/__init__.py deleted file mode 100644 index 6b274abdd..000000000 --- a/apps/perms/api/asset/user_permission/user_permission_assets/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .views import * diff --git a/apps/perms/api/asset/user_permission/user_permission_assets/mixin.py b/apps/perms/api/asset/user_permission/user_permission_assets/mixin.py deleted file mode 100644 index ef0c8964a..000000000 --- a/apps/perms/api/asset/user_permission/user_permission_assets/mixin.py +++ /dev/null @@ -1,106 +0,0 @@ -from rest_framework.response import Response -from rest_framework.request import Request - -from users.models import User -from assets.api.mixin import SerializeToTreeNodeMixin -from common.utils import get_logger -from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination -from assets.models import Asset, Node -from perms import serializers -from perms.utils.asset.user_permission import UserGrantedAssetsQueryUtils - -logger = get_logger(__name__) - - -# 获取数据的 ------------------------------------------------------------ - -class UserDirectGrantedAssetsQuerysetMixin: - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - user: User - - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() - user = self.user - assets = UserGrantedAssetsQueryUtils(user) \ - .get_direct_granted_assets() \ - .prefetch_related('platform') \ - .only(*self.only_fields) - return assets - - -class UserAllGrantedAssetsQuerysetMixin: - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - pagination_class = AllGrantedAssetPagination - user: User - ordering_fields = ("hostname", "ip", "port", "cpu_cores") - ordering = ('hostname', ) - - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() - queryset = UserGrantedAssetsQueryUtils(self.user) \ - .get_all_granted_assets() - queryset = queryset.prefetch_related('platform').only(*self.only_fields) - return queryset - - -class UserFavoriteGrantedAssetsMixin: - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - user: User - - 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 = assets.prefetch_related('platform').only(*self.only_fields) - return assets - - -class UserGrantedNodeAssetsMixin: - only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - pagination_class = NodeGrantedAssetPagination - pagination_node: Node - user: User - - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Asset.objects.none() - 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) - self.pagination_node = node - return assets - - -# 控制格式的 ---------------------------------------------------- - - -class AssetsSerializerFormatMixin: - serializer_class = serializers.AssetGrantedSerializer - filterset_fields = ['hostname', 'ip', 'id', 'comment'] - search_fields = ['hostname', 'ip', 'comment'] - - -class AssetsTreeFormatMixin(SerializeToTreeNodeMixin): - """ - 将 资产 序列化成树的结构返回 - """ - filterset_fields = ['hostname', 'ip', 'id', 'comment'] - search_fields = ['hostname', 'ip', '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.hostname) - data = self.serialize_assets(queryset, None) - return Response(data=data) diff --git a/apps/perms/api/asset/user_permission/user_permission_assets/views.py b/apps/perms/api/asset/user_permission/user_permission_assets/views.py deleted file mode 100644 index da304ee6c..000000000 --- a/apps/perms/api/asset/user_permission/user_permission_assets/views.py +++ /dev/null @@ -1,99 +0,0 @@ -from rest_framework.generics import ListAPIView -from django.conf import settings - -from common.utils import get_logger -from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin -from .mixin import ( - UserAllGrantedAssetsQuerysetMixin, UserDirectGrantedAssetsQuerysetMixin, UserFavoriteGrantedAssetsMixin, - UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, AssetsTreeFormatMixin, -) - -__all__ = [ - 'UserDirectGrantedAssetsForAdminApi', 'MyDirectGrantedAssetsApi', 'UserFavoriteGrantedAssetsForAdminApi', - 'MyFavoriteGrantedAssetsApi', 'UserDirectGrantedAssetsAsTreeForAdminApi', 'MyUngroupAssetsAsTreeApi', - 'UserAllGrantedAssetsApi', 'MyAllGrantedAssetsApi', 'MyAllAssetsAsTreeApi', 'UserGrantedNodeAssetsForAdminApi', - 'MyGrantedNodeAssetsApi', -] - -logger = get_logger(__name__) - - -class UserDirectGrantedAssetsForAdminApi(UserDirectGrantedAssetsQuerysetMixin, - AssetRoleAdminMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class MyDirectGrantedAssetsApi(UserDirectGrantedAssetsQuerysetMixin, - AssetRoleUserMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class UserFavoriteGrantedAssetsForAdminApi(UserFavoriteGrantedAssetsMixin, - AssetRoleAdminMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class MyFavoriteGrantedAssetsApi(UserFavoriteGrantedAssetsMixin, - AssetRoleUserMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class UserDirectGrantedAssetsAsTreeForAdminApi(UserDirectGrantedAssetsQuerysetMixin, - AssetRoleAdminMixin, - AssetsTreeFormatMixin, - ListAPIView): - pass - - -class MyUngroupAssetsAsTreeApi(UserDirectGrantedAssetsQuerysetMixin, - AssetRoleUserMixin, - AssetsTreeFormatMixin, - ListAPIView): - def get_queryset(self): - queryset = super().get_queryset() - if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: - queryset = queryset.none() - return queryset - - -class UserAllGrantedAssetsApi(UserAllGrantedAssetsQuerysetMixin, - AssetRoleAdminMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class MyAllGrantedAssetsApi(UserAllGrantedAssetsQuerysetMixin, - AssetRoleUserMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class MyAllAssetsAsTreeApi(UserAllGrantedAssetsQuerysetMixin, - AssetRoleUserMixin, - AssetsTreeFormatMixin, - ListAPIView): - pass - - -class UserGrantedNodeAssetsForAdminApi(AssetRoleAdminMixin, - UserGrantedNodeAssetsMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass - - -class MyGrantedNodeAssetsApi(AssetRoleUserMixin, - UserGrantedNodeAssetsMixin, - AssetsSerializerFormatMixin, - ListAPIView): - pass diff --git a/apps/perms/api/asset/asset_permission.py b/apps/perms/api/asset_permission.py similarity index 95% rename from apps/perms/api/asset/asset_permission.py rename to apps/perms/api/asset_permission.py index afadc456c..de15a6c6f 100644 --- a/apps/perms/api/asset/asset_permission.py +++ b/apps/perms/api/asset_permission.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- # -from perms.filters import AssetPermissionFilter -from perms.models import AssetPermission from orgs.mixins.api import OrgBulkModelViewSet from perms import serializers - +from perms.filters import AssetPermissionFilter +from perms.models import AssetPermission __all__ = ['AssetPermissionViewSet'] @@ -18,4 +17,4 @@ class AssetPermissionViewSet(OrgBulkModelViewSet): filterset_class = AssetPermissionFilter search_fields = ('name',) ordering_fields = ('name',) - ordering = ('name', ) + ordering = ('name',) diff --git a/apps/perms/api/asset/asset_permission_relation.py b/apps/perms/api/asset_permission_relation.py similarity index 71% rename from apps/perms/api/asset/asset_permission_relation.py rename to apps/perms/api/asset_permission_relation.py index b0a67f858..c2c116248 100644 --- a/apps/perms/api/asset/asset_permission_relation.py +++ b/apps/perms/api/asset_permission_relation.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # from rest_framework import generics -from django.db.models import F, Value -from django.db.models.functions import Concat +from django.db.models import F from django.shortcuts import get_object_or_404 from orgs.mixins.api import OrgRelationMixin @@ -10,13 +9,14 @@ from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org from perms import serializers from perms import models -from perms.utils.asset.user_permission import UserGrantedAssetsQueryUtils +from perms.utils.user_permission import UserGrantedAssetsQueryUtils +from assets.serializers import AccountSerializer __all__ = [ 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', 'AssetPermissionAssetRelationViewSet', 'AssetPermissionNodeRelationViewSet', - 'AssetPermissionSystemUserRelationViewSet', 'AssetPermissionAllAssetListApi', - 'AssetPermissionAllUserListApi', + 'AssetPermissionAllAssetListApi', 'AssetPermissionAllUserListApi', + 'AssetPermissionAccountListApi', ] @@ -42,8 +42,7 @@ class AssetPermissionUserRelationViewSet(RelationMixin): def get_queryset(self): queryset = super().get_queryset() - queryset = queryset \ - .annotate(user_display=F('user__name')) + queryset = queryset.annotate(user_display=F('user__name')) return queryset @@ -71,8 +70,7 @@ class AssetPermissionUserGroupRelationViewSet(RelationMixin): def get_queryset(self): queryset = super().get_queryset() - queryset = queryset \ - .annotate(usergroup_display=F('usergroup__name')) + queryset = queryset.annotate(usergroup_display=F('usergroup__name')) return queryset @@ -82,18 +80,17 @@ class AssetPermissionAssetRelationViewSet(RelationMixin): filterset_fields = [ 'id', 'asset', 'assetpermission', ] - search_fields = ["id", "asset__hostname", "asset__ip", "assetpermission__name"] + search_fields = ["id", "asset__name", "asset__address", "assetpermission__name"] def get_queryset(self): queryset = super().get_queryset() - queryset = queryset \ - .annotate(asset_display=F('asset__hostname')) + queryset = queryset.annotate(asset_display=F('asset__name')) return queryset class AssetPermissionAllAssetListApi(generics.ListAPIView): serializer_class = serializers.AssetPermissionAllAssetSerializer - filterset_fields = ("hostname", "ip") + filterset_fields = ("name", "address") search_fields = filterset_fields def get_queryset(self): @@ -113,25 +110,19 @@ class AssetPermissionNodeRelationViewSet(RelationMixin): def get_queryset(self): queryset = super().get_queryset() - queryset = queryset \ - .annotate(node_key=F('node__key')) + queryset = queryset.annotate(node_key=F('node__key')) return queryset -class AssetPermissionSystemUserRelationViewSet(RelationMixin): - serializer_class = serializers.AssetPermissionSystemUserRelationSerializer - m2m_field = models.AssetPermission.system_users.field - filterset_fields = [ - 'id', 'systemuser', 'assetpermission', - ] - search_fields = [ - "assetpermission__name", "systemuser__name", "systemuser__username" - ] +class AssetPermissionAccountListApi(generics.ListAPIView): + serializer_class = AccountSerializer + filterset_fields = ("name", "username", "privileged", "version") + search_fields = filterset_fields def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - systemuser_display=Concat( - F('systemuser__name'), Value('('), F('systemuser__username'), Value(')') - )) - return queryset + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.AssetPermission, pk=pk) + accounts = perm.get_all_accounts() + return accounts + + diff --git a/apps/perms/api/base.py b/apps/perms/api/base.py deleted file mode 100644 index 0e285796e..000000000 --- a/apps/perms/api/base.py +++ /dev/null @@ -1,95 +0,0 @@ -from django.db.models import Q -from common.utils import get_object_or_none -from orgs.mixins.api import OrgBulkModelViewSet -from assets.models import SystemUser -from users.models import User, UserGroup - - -__all__ = ['BasePermissionViewSet'] - - -class BasePermissionViewSet(OrgBulkModelViewSet): - custom_filter_fields = [ - 'user_id', 'username', 'system_user_id', 'system_user', - 'user_group_id', 'user_group' - ] - - def filter_valid(self, queryset): - valid_query = self.request.query_params.get('is_valid', None) - if valid_query is None: - return queryset - invalid = valid_query in ['0', 'N', 'false', 'False'] - if invalid: - queryset = queryset.invalid() - else: - queryset = queryset.valid() - return queryset - - def is_query_all(self): - query_all = self.request.query_params.get('all', '1') == '1' - return query_all - - def filter_user(self, queryset): - user_id = self.request.query_params.get('user_id') - username = self.request.query_params.get('username') - if user_id: - user = get_object_or_none(User, pk=user_id) - elif username: - user = get_object_or_none(User, username=username) - else: - return queryset - if not user: - return queryset.none() - if not self.is_query_all(): - queryset = queryset.filter(users=user) - return queryset - groups = list(user.groups.all().values_list('id', flat=True)) - queryset = queryset.filter( - Q(users=user) | Q(user_groups__in=groups) - ).distinct() - return queryset - - def filter_keyword(self, queryset): - keyword = self.request.query_params.get('search') - if not keyword: - return queryset - queryset = queryset.filter(name__icontains=keyword) - return queryset - - def filter_system_user(self, queryset): - system_user_id = self.request.query_params.get('system_user_id') - system_user_name = self.request.query_params.get('system_user') - if system_user_id: - system_user = get_object_or_none(SystemUser, pk=system_user_id) - elif system_user_name: - system_user = get_object_or_none(SystemUser, name=system_user_name) - else: - return queryset - if not system_user: - return queryset.none() - queryset = queryset.filter(system_users=system_user) - return queryset - - def filter_user_group(self, queryset): - user_group_id = self.request.query_params.get('user_group_id') - user_group_name = self.request.query_params.get('user_group') - if user_group_id: - group = get_object_or_none(UserGroup, pk=user_group_id) - elif user_group_name: - group = get_object_or_none(UserGroup, name=user_group_name) - else: - return queryset - if not group: - return queryset.none() - queryset = queryset.filter(user_groups=group) - return queryset - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = self.filter_valid(queryset) - queryset = self.filter_user(queryset) - queryset = self.filter_system_user(queryset) - queryset = self.filter_user_group(queryset) - queryset = self.filter_keyword(queryset) - queryset = queryset.distinct() - return queryset diff --git a/apps/perms/api/perm_token.py b/apps/perms/api/perm_token.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/perms/api/system_user_permission.py b/apps/perms/api/system_user_permission.py deleted file mode 100644 index 6d7569192..000000000 --- a/apps/perms/api/system_user_permission.py +++ /dev/null @@ -1,21 +0,0 @@ -from rest_framework import generics - -from assets.models import SystemUser -from common.permissions import IsValidUser -from perms.utils.asset.user_permission import get_user_all_asset_perm_ids -from .. import serializers - - -class SystemUserPermission(generics.ListAPIView): - permission_classes = (IsValidUser,) - serializer_class = serializers.SystemUserSerializer - - def get_queryset(self): - user = self.request.user - - asset_perm_ids = get_user_all_asset_perm_ids(user) - queryset = SystemUser.objects.filter( - granted_by_permissions__id__in=asset_perm_ids - ).distinct() - - return queryset diff --git a/apps/perms/api/asset/user_group_permission.py b/apps/perms/api/user_group_permission.py similarity index 88% rename from apps/perms/api/asset/user_group_permission.py rename to apps/perms/api/user_group_permission.py index 9b6499bb3..48f94f6f7 100644 --- a/apps/perms/api/asset/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -9,9 +9,9 @@ from rest_framework.response import Response from common.utils import lazyproperty from perms.models import AssetPermission from assets.models import Asset, Node -from perms.api.asset import user_permission as uapi +from . import user_permission as uapi from perms import serializers -from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_group +from perms.utils import PermAccountUtil from assets.api.mixin import SerializeToTreeNodeMixin from users.models import UserGroup @@ -19,22 +19,14 @@ __all__ = [ 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedNodeAssetsApi', 'UserGroupGrantedNodeChildrenAsTreeApi', - 'UserGroupGrantedAssetSystemUsersApi', ] -class UserGroupMixin: - @lazyproperty - def group(self): - group_id = self.kwargs.get('pk') - return UserGroup.objects.get(id=group_id) - - class UserGroupGrantedAssetsApi(ListAPIView): serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filterset_fields = ['hostname', 'ip', 'id', 'comment'] - search_fields = ['hostname', 'ip', 'comment'] + filterset_fields = ['name', 'address', 'id', 'comment'] + search_fields = ['name', 'address', 'comment'] rbac_perms = { 'list': 'perms.view_usergroupassets', } @@ -70,8 +62,8 @@ class UserGroupGrantedAssetsApi(ListAPIView): class UserGroupGrantedNodeAssetsApi(ListAPIView): serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filterset_fields = ['hostname', 'ip', 'id', 'comment'] - search_fields = ['hostname', 'ip', 'comment'] + filterset_fields = ['name', 'address', 'id', 'comment'] + search_fields = ['name', 'address', 'comment'] rbac_perms = { 'list': 'perms.view_usergroupassets', } @@ -198,8 +190,3 @@ class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIVie nodes = self.get_nodes() nodes = self.serialize_nodes(nodes) return Response(data=nodes) - - -class UserGroupGrantedAssetSystemUsersApi(UserGroupMixin, uapi.UserGrantedAssetSystemUsersForAdminApi): - def get_asset_system_user_ids_with_actions(self, asset): - return get_asset_system_user_ids_with_actions_by_group(self.group, asset) diff --git a/apps/perms/api/user_permission/__init__.py b/apps/perms/api/user_permission/__init__.py new file mode 100644 index 000000000..55bc108b4 --- /dev/null +++ b/apps/perms/api/user_permission/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# +from .nodes import * +from .assets import * +from .nodes_with_assets import * +from .accounts import * diff --git a/apps/perms/api/user_permission/accounts.py b/apps/perms/api/user_permission/accounts.py new file mode 100644 index 000000000..96c09667f --- /dev/null +++ b/apps/perms/api/user_permission/accounts.py @@ -0,0 +1,30 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.generics import ListAPIView, get_object_or_404 + +from common.utils import get_logger, lazyproperty +from perms import serializers +from perms.hands import Asset +from perms.utils import PermAccountUtil +from .mixin import SelfOrPKUserMixin + +logger = get_logger(__name__) + +__all__ = [ + 'UserPermedAssetAccountsApi', +] + + +class UserPermedAssetAccountsApi(SelfOrPKUserMixin, ListAPIView): + serializer_class = serializers.AccountsPermedSerializer + + @lazyproperty + def asset(self): + asset_id = self.kwargs.get('asset_id') + kwargs = {'id': asset_id, 'is_active': True} + asset = get_object_or_404(Asset, **kwargs) + return asset + + def get_queryset(self): + util = PermAccountUtil() + accounts = util.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 new file mode 100644 index 000000000..d3ee274aa --- /dev/null +++ b/apps/perms/api/user_permission/assets.py @@ -0,0 +1,109 @@ +from django.conf import settings +from rest_framework.generics import ListAPIView + +from assets.models import Asset, Node +from common.utils import get_logger +from perms import serializers +from perms.pagination import AllGrantedAssetPagination +from perms.pagination import NodeGrantedAssetPagination +from perms.utils.user_permission import UserGrantedAssetsQueryUtils +from .mixin import ( + SelfOrPKUserMixin, RebuildTreeMixin, + PermedAssetSerializerMixin, AssetsTreeFormatMixin +) + +__all__ = [ + 'UserDirectPermedAssetsApi', + 'UserFavoriteAssetsApi', + 'UserDirectPermedAssetsAsTreeApi', + 'UserUngroupAssetsAsTreeApi', + 'UserAllPermedAssetsApi', + 'UserPermedNodeAssetsApi', +] + +logger = get_logger(__name__) + + +class UserDirectPermedAssetsApi(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() + + 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 = assets.prefetch_related('platform').only(*self.only_fields) + return assets + + +class UserDirectPermedAssetsAsTreeApi(RebuildTreeMixin, AssetsTreeFormatMixin, UserDirectPermedAssetsApi): + """ 用户直接授权的资产作为树 """ + only_fields = serializers.AssetGrantedSerializer.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 UserUngroupAssetsAsTreeApi(UserDirectPermedAssetsAsTreeApi): + """ 用户未分组节点下的资产作为树 """ + + def get_queryset(self): + queryset = super().get_queryset() + if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + queryset = queryset.none() + return queryset + + +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 UserPermedNodeAssetsApi(SelfOrPKUserMixin, PermedAssetSerializerMixin, ListAPIView): + only_fields = serializers.AssetGrantedSerializer.Meta.only_fields + pagination_class = NodeGrantedAssetPagination + kwargs: dict + pagination_node: Node + + def get_queryset(self): + if getattr(self, 'swagger_fake_view', False): + return Asset.objects.none() + 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) + self.pagination_node = node + return assets diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py new file mode 100644 index 000000000..7be96caed --- /dev/null +++ b/apps/perms/api/user_permission/mixin.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# +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 + + +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) + + +class SelfOrPKUserMixin: + kwargs: dict + request: Request + permission_classes = (RBACPermission,) + + def get_rbac_perms(self): + if self.request_user_is_self(): + return self.self_rbac_perms + else: + return self.admin_rbac_perms + + @property + def self_rbac_perms(self): + return ( + ('list', 'perms.view_myassets'), + ('retrieve', 'perms.view_myassets'), + ('get_tree', 'perms.view_myassets'), + ('GET', 'perms.view_myassets'), + ('OPTIONS', 'perms.view_myassets'), + ) + + @property + def admin_rbac_perms(self): + return ( + ('list', 'perms.view_userassets'), + ('retrieve', 'perms.view_userassets'), + ('get_tree', 'perms.view_userassets'), + ('GET', 'perms.view_userassets'), + ('OPTIONS', 'perms.view_userassets'), + ) + + @property + def user(self): + if self.request_user_is_self(): + user = self.request.user + elif is_uuid(self.kwargs.get('user')): + user = get_object_or_404(User, pk=self.kwargs.get('user')) + elif hasattr(self, 'swagger_fake_view'): + user = self.request.user + else: + raise JMSObjectDoesNotExist(object_name=_('User')) + return user + + 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/asset/user_permission/user_permission_nodes.py b/apps/perms/api/user_permission/nodes.py similarity index 61% rename from apps/perms/api/asset/user_permission/user_permission_nodes.py rename to apps/perms/api/user_permission/nodes.py index e2b8aec71..eb0d321d5 100644 --- a/apps/perms/api/asset/user_permission/user_permission_nodes.py +++ b/apps/perms/api/user_permission/nodes.py @@ -1,31 +1,25 @@ # -*- coding: utf-8 -*- # import abc -from rest_framework.generics import ( - ListAPIView -) -from rest_framework.response import Response from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.generics import ListAPIView -from assets.api.mixin import SerializeToTreeNodeMixin from common.utils import get_logger -from .mixin import AssetRoleAdminMixin, AssetRoleUserMixin -from perms.hands import User +from assets.api.mixin import SerializeToTreeNodeMixin from perms import serializers +from perms.hands import User +from perms.utils.user_permission import UserGrantedNodesQueryUtils -from perms.utils.asset.user_permission import UserGrantedNodesQueryUtils - +from .mixin import SelfOrPKUserMixin, RebuildTreeMixin logger = get_logger(__name__) __all__ = [ - 'UserGrantedNodesForAdminApi', - 'MyGrantedNodesApi', - 'MyGrantedNodesAsTreeApi', - 'UserGrantedNodeChildrenForAdminApi', - 'MyGrantedNodeChildrenApi', - 'UserGrantedNodeChildrenAsTreeForAdminApi', - 'MyGrantedNodeChildrenAsTreeApi', + 'UserGrantedNodesApi', + 'UserGrantedNodesAsTreeApi', + 'UserGrantedNodeChildrenApi', + 'UserGrantedNodeChildrenAsTreeApi', 'BaseGrantedNodeAsTreeApi', 'UserGrantedNodesMixin', ] @@ -98,35 +92,42 @@ class UserGrantedNodesMixin: return nodes -# ------------------------------------------ -# 最终的 api -class UserGrantedNodeChildrenForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi): +# API + + +class UserGrantedNodeChildrenApi( + SelfOrPKUserMixin, + UserGrantedNodeChildrenMixin, + BaseNodeChildrenApi +): + """ 用户授权的节点下的子节点""" pass -class MyGrantedNodeChildrenApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi): +class UserGrantedNodeChildrenAsTreeApi( + SelfOrPKUserMixin, + RebuildTreeMixin, + UserGrantedNodeChildrenMixin, + BaseNodeChildrenAsTreeApi +): + """ 用户授权的节点下的子节点树""" pass -class UserGrantedNodeChildrenAsTreeForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi): +class UserGrantedNodesApi( + SelfOrPKUserMixin, + UserGrantedNodesMixin, + BaseGrantedNodeApi +): + """ 用户授权的节点 """ pass -class MyGrantedNodeChildrenAsTreeApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi): - def get_permissions(self): - permissions = super().get_permissions() - return permissions - - -class UserGrantedNodesForAdminApi(AssetRoleAdminMixin, UserGrantedNodesMixin, BaseGrantedNodeApi): +class UserGrantedNodesAsTreeApi( + SelfOrPKUserMixin, + RebuildTreeMixin, + UserGrantedNodesMixin, + BaseGrantedNodeAsTreeApi +): + """ 用户授权的节点树 """ pass - - -class MyGrantedNodesApi(AssetRoleUserMixin, UserGrantedNodesMixin, BaseGrantedNodeApi): - pass - - -class MyGrantedNodesAsTreeApi(AssetRoleUserMixin, UserGrantedNodesMixin, BaseGrantedNodeAsTreeApi): - pass - -# ------------------------------------------ diff --git a/apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py b/apps/perms/api/user_permission/nodes_with_assets.py similarity index 93% rename from apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py rename to apps/perms/api/user_permission/nodes_with_assets.py index 4d4408355..20cda7e00 100644 --- a/apps/perms/api/asset/user_permission/user_permission_nodes_with_assets.py +++ b/apps/perms/api/user_permission/nodes_with_assets.py @@ -1,24 +1,25 @@ # -*- 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 django.db.models import F, Value, CharField -from django.conf import settings -from common.utils.common import timeit -from orgs.utils import tmp_to_root_org -from common.permissions import IsValidUser from common.utils import get_logger, get_object_or_none -from .mixin import AssetRoleUserMixin, AssetRoleAdminMixin -from perms.utils.asset.user_permission import ( - UserGrantedTreeBuildUtils, get_user_all_asset_perm_ids, - UserGrantedNodesQueryUtils, UserGrantedAssetsQueryUtils, -) -from perms.models import AssetPermission, PermNode +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__) @@ -148,9 +149,10 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin, return Response(data=all_tree_nodes) -class UserGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin): - pass - - -class MyGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleUserMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin): +class UserGrantedNodeChildrenWithAssetsAsTreeApi( + SelfOrPKUserMixin, + RebuildTreeMixin, + GrantedNodeChildrenWithAssetsAsTreeApiMixin +): + """ 用户授权的节点的子节点与资产树 """ pass diff --git a/apps/perms/apps.py b/apps/perms/apps.py index 432c194cd..f9d1e6f42 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/const.py b/apps/perms/const.py index ec51c5a2b..690fb2742 100644 --- a/apps/perms/const.py +++ b/apps/perms/const.py @@ -1,2 +1,45 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import ugettext_lazy as _ + +from common.db.fields import BitChoices +from common.utils.integer import bit + +__all__ = ["ActionChoices"] + + +class ActionChoices(BitChoices): + connect = bit(1), _("Connect") + upload = bit(2), _("Upload") + download = bit(3), _("Download") + copy = bit(4), _("Copy") + paste = bit(5), _("Paste") + + @classmethod + def is_tree(cls): + return True + + @classmethod + def branches(cls): + return ( + cls.connect, + (_("Transfer"), [cls.upload, cls.download]), + (_("Clipboard"), [cls.copy, cls.paste]), + ) + + @classmethod + def transfer(cls): + return cls.upload | cls.download + + @classmethod + def clipboard(cls): + return cls.copy | cls.paste + + @classmethod + def contains(cls, total, action): + action_value = getattr(cls, action) + return action_value & total == action_value + + @classmethod + def display(cls, value): + return ', '.join([str(c.label) for c in cls if c.value & value == c.value]) diff --git a/apps/perms/exceptions.py b/apps/perms/exceptions.py deleted file mode 100644 index 684a5da88..000000000 --- a/apps/perms/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -from rest_framework import status -from django.utils.translation import gettext_lazy as _ - -from common.exceptions import JMSException - - -class AdminIsModifyingPerm(JMSException): - status_code = status.HTTP_409_CONFLICT - default_detail = _('The administrator is modifying permissions. Please wait') - - -class CanNotRemoveAssetPermNow(JMSException): - status_code = status.HTTP_409_CONFLICT - default_detail = _('The authorization cannot be revoked for the time being') diff --git a/apps/perms/filters.py b/apps/perms/filters.py index da82b5090..e64e919ea 100644 --- a/apps/perms/filters.py +++ b/apps/perms/filters.py @@ -5,7 +5,7 @@ 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 -from assets.models import Node, Asset, SystemUser +from assets.models import Node, Asset from perms.models import AssetPermission @@ -30,7 +30,6 @@ class PermissionBaseFilter(BaseFilterSet): qs = super().qs qs = self.filter_valid(qs) qs = self.filter_user(qs) - qs = self.filter_system_user(qs) qs = self.filter_user_group(qs) return qs @@ -74,21 +73,6 @@ class PermissionBaseFilter(BaseFilterSet): ).distinct() return queryset - def filter_system_user(self, queryset): - system_user_id = self.get_query_param('system_user_id') - system_user_name = self.get_query_param('system_user') - - if system_user_id: - system_user = get_object_or_none(SystemUser, pk=system_user_id) - elif system_user_name: - system_user = get_object_or_none(SystemUser, name=system_user_name) - else: - return queryset - if not system_user: - return queryset.none() - queryset = queryset.filter(system_users=system_user) - return queryset - def filter_user_group(self, queryset): user_group_id = self.get_query_param('user_group_id') user_group_name = self.get_query_param('user_group') @@ -110,14 +94,14 @@ class AssetPermissionFilter(PermissionBaseFilter): node_id = filters.UUIDFilter(method='do_nothing') node = filters.CharFilter(method='do_nothing') asset_id = filters.UUIDFilter(method='do_nothing') - hostname = filters.CharFilter(method='do_nothing') + asset_name = filters.CharFilter(method='do_nothing') ip = filters.CharFilter(method='do_nothing') class Meta: model = AssetPermission fields = ( - 'user_id', 'username', 'system_user_id', 'system_user', 'user_group_id', - 'user_group', 'node_id', 'node', 'asset_id', 'hostname', 'ip', 'name', + 'user_id', 'username', 'user_group_id', + 'user_group', 'node_id', 'node', 'asset_id', 'name', 'ip', 'name', 'all', 'asset_id', 'is_valid', 'is_effective', 'from_ticket' ) @@ -157,36 +141,36 @@ class AssetPermissionFilter(PermissionBaseFilter): def filter_asset(self, queryset): is_query_all = self.get_query_param('all', True) asset_id = self.get_query_param('asset_id') - hostname = self.get_query_param('hostname') - ip = self.get_query_param('ip') + asset_name = self.get_query_param('asset_name') + ip = self.get_query_param('address') if asset_id: assets = Asset.objects.filter(pk=asset_id) - elif hostname: - assets = Asset.objects.filter(hostname=hostname) + elif asset_name: + assets = Asset.objects.filter(name=asset_name) elif ip: assets = Asset.objects.filter(ip=ip) else: return queryset if not assets: return queryset.none() - assetids = list(assets.values_list('id', flat=True)) + asset_ids = list(assets.values_list('id', flat=True)) if not is_query_all: - queryset = queryset.filter(assets__in=assetids) + queryset = queryset.filter(assets__in=asset_ids) return queryset - inherit_all_nodekeys = set() - inherit_nodekeys = set(assets.values_list('nodes__key', flat=True)) + inherit_all_node_keys = set() + inherit_node_keys = set(assets.values_list('nodes__key', flat=True)) - for key in inherit_nodekeys: + for key in inherit_node_keys: ancestor_keys = Node.get_node_ancestor_keys(key, with_self=True) - inherit_all_nodekeys.update(ancestor_keys) + inherit_all_node_keys.update(ancestor_keys) - inherit_all_nodeids = Node.objects.filter(key__in=inherit_all_nodekeys).values_list('id', flat=True) - inherit_all_nodeids = list(inherit_all_nodeids) + 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=assetids).distinct() - qs2 = queryset.filter(nodes__in=inherit_all_nodeids).distinct() + qs1 = queryset.filter(assets__in=asset_ids).distinct() + qs2 = queryset.filter(nodes__in=inherit_all_node_ids).distinct() qs = UnionQuerySet(qs1, qs2) return qs @@ -199,22 +183,15 @@ class AssetPermissionFilter(PermissionBaseFilter): if is_effective: have_user_q = Q(users__isnull=False) | Q(user_groups__isnull=False) have_asset_q = Q(assets__isnull=False) | Q(nodes__isnull=False) - have_system_user_q = Q(system_users__isnull=False) have_action_q = Q(actions__gt=0) - queryset = queryset.filter( - have_user_q & have_asset_q & have_system_user_q & have_action_q - ) + queryset = queryset.filter(have_user_q & have_asset_q & have_action_q) queryset &= AssetPermission.objects.valid() else: not_have_user_q = Q(users__isnull=True) & Q(user_groups__isnull=True) not_have_asset_q = Q(assets__isnull=True) & Q(nodes__isnull=True) - not_have_system_user_q = Q(system_users__isnull=True) not_have_action_q = Q(actions=0) - queryset = queryset.filter( - not_have_user_q | not_have_asset_q | not_have_system_user_q | - not_have_action_q - ) + queryset = queryset.filter(not_have_user_q | not_have_asset_q | not_have_action_q) queryset |= AssetPermission.objects.invalid() return queryset diff --git a/apps/perms/hands.py b/apps/perms/hands.py index 25902fddf..dabb9f7c0 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -2,12 +2,12 @@ # from users.models import User, UserGroup -from assets.models import Asset, SystemUser, Node, Label, FavoriteAsset +from assets.models import Asset, Node, Label, FavoriteAsset, Account from assets.serializers import NodeSerializer __all__ = [ 'User', 'UserGroup', - 'Asset', 'SystemUser', 'Node', 'Label', 'FavoriteAsset', - 'NodeSerializer', + 'Asset', 'Node', 'Label', 'FavoriteAsset', + 'NodeSerializer', 'Account' ] diff --git a/apps/perms/locks.py b/apps/perms/locks.py index a6ffa6b98..96c766fb8 100644 --- a/apps/perms/locks.py +++ b/apps/perms/locks.py @@ -5,7 +5,5 @@ class UserGrantedTreeRebuildLock(DistributedLock): name_template = 'perms.user.asset.node.tree.rebuid.' def __init__(self, user_id): - name = self.name_template.format( - user_id=user_id - ) + name = self.name_template.format(user_id=user_id) super().__init__(name=name, release_on_transaction_commit=True) diff --git a/apps/perms/migrations/0011_auto_20200721_1739.py b/apps/perms/migrations/0011_auto_20200721_1739.py index 352bd6b79..1dcb33633 100644 --- a/apps/perms/migrations/0011_auto_20200721_1739.py +++ b/apps/perms/migrations/0011_auto_20200721_1739.py @@ -3,13 +3,12 @@ from django.db import migrations, models from django.db.models import F -from ..models.base import Action def migrate_asset_permission(apps, schema_editor): # 已有的资产权限默认拥有剪切板复制粘贴动作 - AssetPermission = apps.get_model('perms', 'AssetPermission') - AssetPermission.objects.all().update(actions=F('actions').bitor(Action.CLIPBOARD_COPY_PASTE)) + asset_permission_model = apps.get_model('perms', 'AssetPermission') + asset_permission_model.objects.all().update(actions=F('actions').bitor(24)) class Migration(migrations.Migration): diff --git a/apps/perms/migrations/0029_auto_20220728_1728.py b/apps/perms/migrations/0029_auto_20220728_1728.py new file mode 100644 index 000000000..4e312a57a --- /dev/null +++ b/apps/perms/migrations/0029_auto_20220728_1728.py @@ -0,0 +1,71 @@ +from django.db import migrations + + +def migrate_app_perms_to_assets(apps, schema_editor): + asset_permission_model = apps.get_model("perms", "AssetPermission") + app_permission_model = apps.get_model("perms", "ApplicationPermission") + + count = 0 + bulk_size = 1000 + while True: + app_perms = app_permission_model.objects.all()[count:bulk_size] + if not app_perms: + break + count += len(app_perms) + attrs = [ + 'id', 'name', 'actions', 'is_active', 'date_start', + 'date_expired', 'created_by', 'from_ticket', 'comment', + ] + asset_permissions = [] + + for app_perm in app_perms: + asset_permission = asset_permission_model() + for attr in attrs: + setattr(asset_permission, attr, getattr(app_perm, attr)) + asset_permissions.append(asset_permission) + asset_permission_model.objects.bulk_create(asset_permissions, ignore_conflicts=True) + + +def migrate_relations(apps, schema_editor): + asset_permission_model = apps.get_model("perms", "AssetPermission") + app_permission_model = apps.get_model("perms", "ApplicationPermission") + m2m_names = [ + ('applications', 'assets', 'application_id', 'asset_id'), + ('users', 'users', 'user_id', 'user_id'), + ('user_groups', 'user_groups', 'usergroup_id', 'usergroup_id'), + ('system_users', 'system_users', 'systemuser_id', 'systemuser_id'), + ] + + for app_name, asset_name, app_attr, asset_attr in m2m_names: + app_through = getattr(app_permission_model, app_name).through + asset_through = getattr(asset_permission_model, asset_name).through + + count = 0 + bulk_size = 1000 + + while True: + app_permission_relations = app_through.objects.all()[count:bulk_size] + if not app_permission_relations: + break + count += len(app_permission_relations) + asset_through_relations = [] + + for app_relation in app_permission_relations: + asset_relation = asset_through() + asset_relation.assetpermission_id = app_relation.applicationpermission_id + setattr(asset_relation, asset_attr, getattr(app_relation, app_attr)) + asset_through_relations.append(asset_relation) + + asset_through.objects.bulk_create(asset_through_relations, ignore_conflicts=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0028_auto_20220316_2028'), + ] + + operations = [ + migrations.RunPython(migrate_app_perms_to_assets), + migrations.RunPython(migrate_relations), + ] diff --git a/apps/perms/migrations/0030_auto_20220816_1132.py b/apps/perms/migrations/0030_auto_20220816_1132.py new file mode 100644 index 000000000..348000823 --- /dev/null +++ b/apps/perms/migrations/0030_auto_20220816_1132.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.14 on 2022-08-16 03:32 +from django.db import migrations, models + + +def migrate_system_user_to_accounts(apps, schema_editor): + asset_permission_model = apps.get_model("perms", "AssetPermission") + + count = 0 + bulk_size = 10000 + while True: + asset_permissions = asset_permission_model.objects \ + .prefetch_related('system_users')[count:bulk_size] + if not asset_permissions: + break + count += len(asset_permissions) + + updated = [] + for asset_permission in asset_permissions: + asset_permission.accounts = [s.username for s in asset_permission.system_users.all()] + updated.append(asset_permission) + asset_permission_model.objects.bulk_update(updated, ['accounts']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0029_auto_20220728_1728'), + ] + + operations = [ + migrations.AddField( + model_name='assetpermission', + name='accounts', + field=models.JSONField(default=list, verbose_name='Accounts'), + ), + migrations.RunPython(migrate_system_user_to_accounts), + migrations.RemoveField( + model_name='assetpermission', + name='system_users', + ), + ] diff --git a/apps/perms/migrations/0031_auto_20220816_1600.py b/apps/perms/migrations/0031_auto_20220816_1600.py new file mode 100644 index 000000000..b5ccbc24c --- /dev/null +++ b/apps/perms/migrations/0031_auto_20220816_1600.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.14 on 2022-08-16 08:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0030_auto_20220816_1132'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='applicationpermission', + unique_together=None, + ), + migrations.RemoveField( + model_name='applicationpermission', + name='applications', + ), + migrations.RemoveField( + model_name='applicationpermission', + name='system_users', + ), + migrations.RemoveField( + model_name='applicationpermission', + name='user_groups', + ), + migrations.RemoveField( + model_name='applicationpermission', + name='users', + ), + migrations.DeleteModel( + name='PermedApplication', + ), + migrations.DeleteModel( + name='ApplicationPermission', + ), + ] diff --git a/apps/perms/migrations/0032_auto_20221111_1919.py b/apps/perms/migrations/0032_auto_20221111_1919.py new file mode 100644 index 000000000..3f3c56533 --- /dev/null +++ b/apps/perms/migrations/0032_auto_20221111_1919.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.14 on 2022-11-11 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0111_alter_automationexecution_status'), + ('perms', '0031_auto_20220816_1600'), + ] + + operations = [ + migrations.CreateModel( + name='PermedAccount', + fields=[ + ], + options={ + 'verbose_name': 'Permed account', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.account',), + ), + migrations.AlterField( + model_name='assetpermission', + name='actions', + field=models.IntegerField(default=0, verbose_name='Actions'), + ), + ] diff --git a/apps/perms/migrations/0033_alter_assetpermission_actions.py b/apps/perms/migrations/0033_alter_assetpermission_actions.py new file mode 100644 index 000000000..cfa39f6e3 --- /dev/null +++ b/apps/perms/migrations/0033_alter_assetpermission_actions.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-18 02:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0032_auto_20221111_1919'), + ] + + operations = [ + migrations.AlterField( + model_name='assetpermission', + name='actions', + field=models.IntegerField(default=1, verbose_name='Actions'), + ), + ] diff --git a/apps/perms/models/__init__.py b/apps/perms/models/__init__.py index e2ac0c1d6..9041990f2 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -2,5 +2,5 @@ # from .asset_permission import * -from .application_permission import * -from .base import * +from .perm_node import * +from .perm_token import * diff --git a/apps/perms/models/application_permission.py b/apps/perms/models/application_permission.py deleted file mode 100644 index 6bea6e47f..000000000 --- a/apps/perms/models/application_permission.py +++ /dev/null @@ -1,117 +0,0 @@ -# coding: utf-8 -# - -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 .base import BasePermission, Action -from applications.models import Application -from users.models import User -from applications.const import AppCategory, AppType - -__all__ = [ - 'ApplicationPermission', -] - - -class ApplicationPermission(BasePermission): - category = models.CharField( - max_length=16, choices=AppCategory.choices, verbose_name=_('Category') - ) - type = models.CharField( - max_length=16, choices=AppType.choices, verbose_name=_('Type') - ) - applications = models.ManyToManyField( - 'applications.Application', related_name='granted_by_permissions', blank=True, - verbose_name=_("Application") - ) - system_users = models.ManyToManyField( - 'assets.SystemUser', - related_name='granted_by_application_permissions', blank=True, - verbose_name=_("System user") - ) - - class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _('Application permission') - permissions = [ - ] - ordering = ('name',) - - @property - def category_remote_app(self): - return self.category == AppCategory.remote_app.value - - @property - def category_db(self): - return self.category == AppCategory.db.value - - @property - def category_cloud(self): - return self.category == AppCategory.cloud.value - - @lazyproperty - def users_amount(self): - return self.users.count() - - @lazyproperty - def user_groups_amount(self): - return self.user_groups.count() - - @lazyproperty - def applications_amount(self): - return self.applications.count() - - @lazyproperty - def system_users_amount(self): - return self.system_users.count() - - def get_all_users(self): - user_ids = self.users.all().values_list('id', flat=True) - user_group_ids = self.user_groups.all().values_list('id', flat=True) - users = User.objects.filter( - Q(id__in=user_ids) | Q(groups__id__in=user_group_ids) - ) - return users - - @classmethod - def get_include_actions_choices(cls, category=None): - actions = {Action.ALL, Action.CONNECT} - if category == AppCategory.db: - _actions = [Action.UPLOAD, Action.DOWNLOAD] - elif category == AppCategory.remote_app: - _actions = [ - Action.UPLOAD, Action.DOWNLOAD, - Action.CLIPBOARD_COPY, Action.CLIPBOARD_PASTE - ] - else: - _actions = [] - actions.update(_actions) - - if (Action.UPLOAD in actions) or (Action.DOWNLOAD in actions): - actions.update([Action.UPDOWNLOAD]) - if (Action.CLIPBOARD_COPY in actions) or (Action.CLIPBOARD_PASTE in actions): - actions.update([Action.CLIPBOARD_COPY_PASTE]) - - choices = [Action.NAME_MAP[action] for action in actions] - return choices - - @classmethod - def get_exclude_actions_choices(cls, category=None): - include_choices = cls.get_include_actions_choices(category) - exclude_choices = set(Action.NAME_MAP.values()) - set(include_choices) - return exclude_choices - - -class PermedApplication(Application): - class Meta: - proxy = True - verbose_name = _('Permed application') - default_permissions = [] - permissions = [ - ('view_myapps', _('Can view my apps')), - ('view_userapps', _('Can view user apps')), - ('view_usergroupapps', _('Can view usergroup apps')), - ] diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index ea795d889..aa61ffe14 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,189 +1,133 @@ 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 django.db.models import F, TextChoices +from assets.models import Asset, Account +from common.db.models import UnionQuerySet +from common.utils import date_expired_default +from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgModelMixin -from common.db import models -from common.utils import lazyproperty -from assets.models import Asset, SystemUser, Node, FamilyMixin +from perms.const import ActionChoices -from .base import BasePermission - - -__all__ = [ - 'AssetPermission', 'PermNode', 'UserAssetGrantedTreeNodeRelation', -] +__all__ = ['AssetPermission', 'ActionChoices'] # 使用场景 logger = logging.getLogger(__name__) -class AssetPermission(BasePermission): - assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) - nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) - system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', blank=True, verbose_name=_("System user")) +class AssetPermissionQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + def valid(self): + return self.active().filter(date_start__lt=timezone.now()) \ + .filter(date_expired__gt=timezone.now()) + + def inactive(self): + return self.filter(is_active=False) + + def invalid(self): + now = timezone.now() + q = (Q(is_active=False) | Q(date_start__gt=now) | Q(date_expired__lt=now)) + return self.filter(q) + + def filter_by_accounts(self, accounts): + q = Q(accounts__contains=list(accounts)) | \ + Q(accounts__contains=Account.AliasAccount.ALL.value) + return self.filter(q) + + +class AssetPermissionManager(OrgManager): + def valid(self): + return self.get_queryset().valid() + + +class AssetPermission(OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + users = models.ManyToManyField( + 'users.User', related_name='%(class)ss', blank=True, verbose_name=_("User") + ) + user_groups = models.ManyToManyField( + 'users.UserGroup', related_name='%(class)ss', blank=True, verbose_name=_("User group") + ) + assets = models.ManyToManyField( + 'assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset") + ) + nodes = models.ManyToManyField( + 'assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes") + ) + # 特殊的账号: @ALL, @INPUT @USER 默认包含,将来在全局设置中进行控制. + accounts = models.JSONField(default=list, verbose_name=_("Accounts")) + actions = models.IntegerField(default=ActionChoices.connect, verbose_name=_("Actions")) + date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) + 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)() class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset permission") ordering = ('name',) - permissions = [ - ] + permissions = [] - @lazyproperty - def users_amount(self): - return self.users.count() + def __str__(self): + return self.name - @lazyproperty - def user_groups_amount(self): - return self.user_groups.count() + @property + def is_expired(self): + if self.date_expired > timezone.now() > self.date_start: + return False + return True - @lazyproperty - def assets_amount(self): - return self.assets.count() + @property + def is_valid(self): + if not self.is_expired and self.is_active: + return True + return False - @lazyproperty - def nodes_amount(self): - return self.nodes.count() + def get_all_users(self): + from users.models import User + user_ids = self.users.all().values_list('id', flat=True) + 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) + return qs - @lazyproperty - def system_users_amount(self): - return self.system_users.count() - - @classmethod - def get_queryset_with_prefetch(cls): - return cls.objects.all().valid().prefetch_related( - models.Prefetch('nodes', queryset=Node.objects.all().only('key')), - models.Prefetch('assets', queryset=Asset.objects.all().only('id')), - models.Prefetch('system_users', queryset=SystemUser.objects.all().only('id')) - ).order_by() - - def get_all_assets(self): + def get_all_assets(self, flat=False): from assets.models import Node nodes_keys = self.nodes.all().values_list('key', flat=True) asset_ids = set(self.assets.all().values_list('id', flat=True)) nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys) asset_ids.update(nodes_asset_ids) + if flat: + return asset_ids assets = Asset.objects.filter(id__in=asset_ids) return assets - def users_display(self): - names = [user.username for user in self.users.all()] - return names - - def user_groups_display(self): - names = [group.name for group in self.user_groups.all()] - return names - - def assets_display(self): - names = [asset.hostname for asset in self.assets.all()] - return names - - def system_users_display(self): - names = [system_user.name for system_user in self.system_users.all()] - return names - - def nodes_display(self): - names = [node.full_value for node in self.nodes.all()] - return names - - -class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel): - class NodeFrom(TextChoices): - granted = 'granted', 'Direct node granted' - child = 'child', 'Have children node' - asset = 'asset', 'Direct asset granted' - - 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_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_from = models.CharField(choices=NodeFrom.choices, max_length=16, db_index=True) - node_assets_amount = models.IntegerField(default=0) - - @property - def key(self): - return self.node_key - - @property - def parent_key(self): - return self.node_parent_key - - @classmethod - def get_node_granted_status(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 - return '', None - - -class PermNode(Node): - class Meta: - proxy = True - ordering = [] - - # 特殊节点 - UNGROUPED_NODE_KEY = 'ungrouped' - UNGROUPED_NODE_VALUE = _('Ungrouped') - FAVORITE_NODE_KEY = 'favorite' - FAVORITE_NODE_VALUE = _('Favorite') - - node_from = '' - granted_assets_amount = 0 - - annotate_granted_node_rel_fields = { - 'granted_assets_amount': F('granted_node_rels__node_assets_amount'), - 'node_from': F('granted_node_rels__node_from') - } - - def use_granted_assets_amount(self): - self.assets_amount = self.granted_assets_amount - - @classmethod - def get_ungrouped_node(cls, assets_amount): - return cls( - id=cls.UNGROUPED_NODE_KEY, - key=cls.UNGROUPED_NODE_KEY, - value=cls.UNGROUPED_NODE_VALUE, - assets_amount=assets_amount - ) - - @classmethod - def get_favorite_node(cls, assets_amount): - node = cls( - id=cls.FAVORITE_NODE_KEY, - key=cls.FAVORITE_NODE_KEY, - value=cls.FAVORITE_NODE_VALUE, - ) - 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 save(self): - # 这是个只读 Model - raise NotImplementedError - - -class PermedAsset(Asset): - class Meta: - proxy = True - verbose_name = _('Permed asset') - permissions = [ - ('view_myassets', _('Can view my assets')), - ('view_userassets', _('Can view user assets')), - ('view_usergroupassets', _('Can view usergroup assets')), - ] - + def get_all_accounts(self, flat=False): + """ + :return: 返回授权的所有账号对象 Account + """ + asset_ids = self.get_all_assets(flat=True) + q = Q(asset_id__in=asset_ids) + if Account.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) diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py deleted file mode 100644 index 7132a3475..000000000 --- a/apps/perms/models/base.py +++ /dev/null @@ -1,157 +0,0 @@ -# coding: utf-8 -# - -import uuid -from django.utils.translation import ugettext_lazy as _ -from django.db import models -from django.db.models import Q -from django.utils import timezone - -from orgs.mixins.models import OrgModelMixin, OrgManager -from common.db.models import UnionQuerySet, BitOperationChoice -from common.utils import date_expired_default, lazyproperty - - -__all__ = [ - 'BasePermission', 'BasePermissionQuerySet', 'Action' -] - - -class BasePermissionQuerySet(models.QuerySet): - def active(self): - return self.filter(is_active=True) - - def valid(self): - return self.active().filter(date_start__lt=timezone.now()) \ - .filter(date_expired__gt=timezone.now()) - - def inactive(self): - return self.filter(is_active=False) - - def invalid(self): - now = timezone.now() - q = (Q(is_active=False) | Q(date_start__gt=now) | Q(date_expired__lt=now)) - return self.filter(q) - - -class BasePermissionManager(OrgManager): - def valid(self): - return self.get_queryset().valid() - - -class Action(BitOperationChoice): - ALL = 0xff - - CONNECT = 0b1 - UPLOAD = 0b1 << 1 - DOWNLOAD = 0b1 << 2 - CLIPBOARD_COPY = 0b1 << 3 - CLIPBOARD_PASTE = 0b1 << 4 - UPDOWNLOAD = UPLOAD | DOWNLOAD - CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE - - DB_CHOICES = ( - (ALL, _('All')), - (CONNECT, _('Connect')), - (UPLOAD, _('Upload file')), - (DOWNLOAD, _('Download file')), - (UPDOWNLOAD, _("Upload download")), - (CLIPBOARD_COPY, _('Clipboard copy')), - (CLIPBOARD_PASTE, _('Clipboard paste')), - (CLIPBOARD_COPY_PASTE, _('Clipboard copy paste')) - ) - - NAME_MAP = { - ALL: "all", - CONNECT: "connect", - UPLOAD: "upload_file", - DOWNLOAD: "download_file", - UPDOWNLOAD: "updownload", - CLIPBOARD_COPY: 'clipboard_copy', - CLIPBOARD_PASTE: 'clipboard_paste', - CLIPBOARD_COPY_PASTE: 'clipboard_copy_paste' - } - - NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} - CHOICES = [] - for i, j in DB_CHOICES: - CHOICES.append((NAME_MAP[i], j)) - - -class BasePermission(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss') - user_groups = models.ManyToManyField( - 'users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss') - actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) - is_active = models.BooleanField(default=True, verbose_name=_('Active')) - date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) - date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired')) - created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) - comment = models.TextField(verbose_name=_('Comment'), blank=True) - from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket')) - - objects = BasePermissionManager.from_queryset(BasePermissionQuerySet)() - - class Meta: - abstract = True - - def __str__(self): - return self.name - - @property - def id_str(self): - return str(self.id) - - @property - def is_expired(self): - if self.date_expired > timezone.now() > self.date_start: - return False - return True - - @property - def is_valid(self): - if not self.is_expired and self.is_active: - return True - return False - - @property - def all_users(self): - from users.models import User - - users_query = self._meta.get_field('users').related_query_name() - user_groups_query = self._meta.get_field('user_groups').related_query_name() - - users_q = Q(**{ - f'{users_query}': self - }) - - user_groups_q = Q(**{ - f'groups__{user_groups_query}': self - }) - - return User.objects.filter(users_q | user_groups_q).distinct() - - def get_all_users(self): - from users.models import User - user_ids = self.users.all().values_list('id', flat=True) - 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) - return qs - - @lazyproperty - def users_amount(self): - return self.users.count() - - @lazyproperty - def user_groups_amount(self): - return self.user_groups.count() diff --git a/apps/perms/models/perm_node.py b/apps/perms/models/perm_node.py new file mode 100644 index 000000000..ce061297e --- /dev/null +++ b/apps/perms/models/perm_node.py @@ -0,0 +1,119 @@ + +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from django.db.models import F, TextChoices + +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 + + +class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, BaseCreateUpdateModel): + class NodeFrom(TextChoices): + granted = 'granted', 'Direct node granted' + child = 'child', 'Have children node' + asset = 'asset', 'Direct asset granted' + + 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_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_from = models.CharField(choices=NodeFrom.choices, max_length=16, db_index=True) + node_assets_amount = models.IntegerField(default=0) + + @property + def key(self): + return self.node_key + + @property + def parent_key(self): + return self.node_parent_key + + @classmethod + def get_node_granted_status(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 + return '', None + + +class PermNode(Node): + class Meta: + proxy = True + ordering = [] + + # 特殊节点 + UNGROUPED_NODE_KEY = 'ungrouped' + UNGROUPED_NODE_VALUE = _('Ungrouped') + FAVORITE_NODE_KEY = 'favorite' + FAVORITE_NODE_VALUE = _('Favorite') + + node_from = '' + granted_assets_amount = 0 + + annotate_granted_node_rel_fields = { + 'granted_assets_amount': F('granted_node_rels__node_assets_amount'), + 'node_from': F('granted_node_rels__node_from') + } + + def use_granted_assets_amount(self): + self.assets_amount = self.granted_assets_amount + + @classmethod + def get_ungrouped_node(cls, assets_amount): + return cls( + id=cls.UNGROUPED_NODE_KEY, + key=cls.UNGROUPED_NODE_KEY, + value=cls.UNGROUPED_NODE_VALUE, + assets_amount=assets_amount + ) + + @classmethod + def get_favorite_node(cls, assets_amount): + node = cls( + id=cls.FAVORITE_NODE_KEY, + key=cls.FAVORITE_NODE_KEY, + value=cls.FAVORITE_NODE_VALUE, + ) + 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 save(self): + # 这是个只读 Model + raise NotImplementedError + + +class PermedAsset(Asset): + class Meta: + proxy = True + verbose_name = _('Permed asset') + permissions = [ + ('view_myassets', _('Can view my assets')), + ('view_userassets', _('Can view user assets')), + ('view_usergroupassets', _('Can view usergroup assets')), + ] + + +class PermedAccount(Account): + @lazyproperty + def actions(self): + return 0 + + class Meta: + proxy = True + verbose_name = _('Permed account') diff --git a/apps/perms/models/perm_token.py b/apps/perms/models/perm_token.py new file mode 100644 index 000000000..368750c63 --- /dev/null +++ b/apps/perms/models/perm_token.py @@ -0,0 +1,21 @@ +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/notifications.py b/apps/perms/notifications.py index b40ccab7c..9e074cd03 100644 --- a/apps/perms/notifications.py +++ b/apps/perms/notifications.py @@ -79,78 +79,3 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage): perms = AssetPermission.objects.all()[:10] org = Organization.objects.first() return cls(user, perms, org) - - -class PermedAppsWillExpireUserMsg(UserMessage): - def __init__(self, user, apps, day_count=0): - super().__init__(user) - self.apps = apps - self.day_count = _('today') if day_count == 0 else day_count - - def get_html_msg(self) -> dict: - subject = _("Your permed applications is about to expire") - context = { - 'name': self.user.name, - 'count': str(self.day_count), - 'item_type': _('permed applications'), - 'items': [str(app) for app in self.apps] - } - message = render_to_string('perms/_msg_permed_items_expire.html', context) - return { - 'subject': subject, - 'message': message - } - - @classmethod - def gen_test_msg(cls): - from users.models import User - from applications.models import Application - - user = User.objects.first() - apps = Application.objects.all()[:10] - return cls(user, apps) - - -class AppPermsWillExpireForOrgAdminMsg(UserMessage): - def __init__(self, user, perms, org, day_count=0): - super().__init__(user) - self.perms = perms - self.org = org - self.day_count = _('today') if day_count == 0 else day_count - - def get_items_with_url(self): - items_with_url = [] - for perm in self.perms: - url = js_reverse( - 'perms:application-permission-detail', - kwargs={'pk': perm.id}, external=True, - api_to_ui=True, is_console=True - ) + f'?oid={perm.org_id}' - items_with_url.append([perm.name, url]) - return items_with_url - - def get_html_msg(self) -> dict: - items = self.get_items_with_url() - subject = _('Application permissions is about to expire') - context = { - 'name': self.user.name, - 'count': str(self.day_count), - 'item_type': _('application permissions of organization {}').format(self.org), - 'items_with_url': items - } - message = render_to_string('perms/_msg_item_permissions_expire.html', context) - return { - 'subject': subject, - 'message': message - } - - @classmethod - def gen_test_msg(cls): - from users.models import User - from perms.models import ApplicationPermission - from orgs.models import Organization - - user = User.objects.first() - perms = ApplicationPermission.objects.all()[:10] - org = Organization.objects.first() - return cls(user, perms, org) diff --git a/apps/perms/pagination.py b/apps/perms/pagination.py index 248958a3e..622306924 100644 --- a/apps/perms/pagination.py +++ b/apps/perms/pagination.py @@ -9,6 +9,8 @@ logger = get_logger(__name__) class GrantedAssetPaginationBase(AssetPaginationBase): + _user: object + def init_attrs(self, queryset, request: Request, view=None): super().init_attrs(queryset, request, view) self._user = view.user @@ -18,10 +20,12 @@ class NodeGrantedAssetPagination(GrantedAssetPaginationBase): def get_count_from_nodes(self, queryset): node = getattr(self._view, 'pagination_node', None) if node: - logger.debug(f'Hit node.assets_amount[{node.assets_amount}] -> {self._request.get_full_path()}') + logger.debug(f'Hit node.assets_amount[{node.assets_amount}] -> ' + f'{self._request.get_full_path()}') return node.assets_amount else: - logger.warn(f'Not hit node.assets_amount[{node}] because {self._view} not has `pagination_node` -> {self._request.get_full_path()}') + logger.warn(f'Not hit node.assets_amount[{node}] because {self._view} ' + f'not has `pagination_node` -> {self._request.get_full_path()}') return None diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index a4a701773..a28be49ec 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -1,6 +1,5 @@ # coding: utf-8 # -from .base import * -from .asset import * -from .application import * -from .system_user_permission import * +from .permission import * +from .permission_relation import * +from .user_permission import * diff --git a/apps/perms/serializers/application/__init__.py b/apps/perms/serializers/application/__init__.py deleted file mode 100644 index 5fb99849f..000000000 --- a/apps/perms/serializers/application/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .permission import * -from .permission_relation import * -from .user_permission import * diff --git a/apps/perms/serializers/application/permission.py b/apps/perms/serializers/application/permission.py deleted file mode 100644 index ae65ea869..000000000 --- a/apps/perms/serializers/application/permission.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from perms.models import ApplicationPermission, Action -from ..base import ActionsField, BasePermissionSerializer - -__all__ = [ - 'ApplicationPermissionSerializer' -] - - -class ApplicationPermissionSerializer(BasePermissionSerializer): - actions = ActionsField(required=False, allow_null=True, label=_("Actions")) - category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - is_valid = serializers.BooleanField(read_only=True, label=_('Is valid')) - is_expired = serializers.BooleanField(read_only=True, label=_("Is expired")) - - class Meta: - model = ApplicationPermission - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'category', 'category_display', 'type', 'type_display', - 'actions', - 'is_active', 'is_expired', 'is_valid', - 'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket' - ] - fields_m2m = [ - 'users', 'user_groups', 'applications', 'system_users', - 'users_amount', 'user_groups_amount', 'applications_amount', - 'system_users_amount', - ] - fields = fields_small + fields_m2m - read_only_fields = ['created_by', 'date_created', 'from_ticket'] - extra_kwargs = { - 'is_expired': {'label': _('Is expired')}, - 'is_valid': {'label': _('Is valid')}, - 'actions': {'label': _('Actions')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'system_users_amount': {'label': _('System users amount')}, - 'applications_amount': {'label': _('Apps amount')}, - } - - def _filter_actions_choices(self, choices): - if request := self.context.get('request'): - category = request.query_params.get('category') - else: - category = None - exclude_choices = ApplicationPermission.get_exclude_actions_choices(category=category) - for choice in exclude_choices: - choices.pop(choice, None) - return choices - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - 'users', 'user_groups', 'applications', 'system_users' - ) - return queryset - - def validate_applications(self, applications): - if self.instance: - permission_type = self.instance.type - else: - permission_type = self.initial_data['type'] - - other_type_applications = [ - application for application in applications - if application.type != permission_type - ] - if len(other_type_applications) > 0: - error = _( - 'The application list contains applications ' - 'that are different from the permission type. ({})' - ).format(', '.join([application.name for application in other_type_applications])) - raise serializers.ValidationError(error) - return applications diff --git a/apps/perms/serializers/application/permission_relation.py b/apps/perms/serializers/application/permission_relation.py deleted file mode 100644 index 941b03127..000000000 --- a/apps/perms/serializers/application/permission_relation.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -# -from rest_framework import serializers - -from common.mixins import BulkSerializerMixin -from perms.models import ApplicationPermission - -__all__ = [ - 'ApplicationPermissionUserRelationSerializer', - 'ApplicationPermissionUserGroupRelationSerializer', - 'ApplicationPermissionApplicationRelationSerializer', - 'ApplicationPermissionSystemUserRelationSerializer', - 'ApplicationPermissionAllApplicationSerializer', - 'ApplicationPermissionAllUserSerializer' -] - - -class RelationMixin(BulkSerializerMixin, serializers.Serializer): - applicationpermission_display = serializers.ReadOnlyField() - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(['applicationpermission', "applicationpermission_display"]) - return fields - - -class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - user_display = serializers.ReadOnlyField() - - class Meta: - model = ApplicationPermission.users.through - fields = [ - 'id', 'user', 'user_display', - ] - - -class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): - usergroup_display = serializers.ReadOnlyField() - - class Meta: - model = ApplicationPermission.user_groups.through - fields = [ - 'id', 'usergroup', "usergroup_display", - ] - - -class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer): - application_display = serializers.ReadOnlyField() - - class Meta: - model = ApplicationPermission.applications.through - fields = [ - 'id', "application", "application_display", - ] - - -class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - systemuser_display = serializers.ReadOnlyField() - - class Meta: - model = ApplicationPermission.system_users.through - fields = [ - 'id', 'systemuser', 'systemuser_display' - ] - - -class ApplicationPermissionAllApplicationSerializer(serializers.Serializer): - application = serializers.UUIDField(read_only=True, source='id') - application_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'name'] - - @staticmethod - def get_application_display(obj): - return str(obj) - - -class ApplicationPermissionAllUserSerializer(serializers.Serializer): - user = serializers.UUIDField(read_only=True, source='id') - user_display = serializers.SerializerMethodField() - - class Meta: - only_fields = ['id', 'username', 'name'] - - @staticmethod - def get_user_display(obj): - return str(obj) diff --git a/apps/perms/serializers/application/user_permission.py b/apps/perms/serializers/application/user_permission.py deleted file mode 100644 index 63b681d5a..000000000 --- a/apps/perms/serializers/application/user_permission.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from assets.models import SystemUser -from applications.models import Application -from applications.serializers import AppSerializerMixin - -__all__ = [ - 'AppGrantedSerializer', 'ApplicationSystemUserSerializer' -] - - -class ApplicationSystemUserSerializer(serializers.ModelSerializer): - """ - 查看授权的应用系统用户的数据结构,这个和SystemUserSerializer不同,字段少 - """ - class Meta: - model = SystemUser - only_fields = ( - 'id', 'name', 'username', 'priority', 'protocol', 'login_mode' - ) - fields = list(only_fields) - read_only_fields = fields - - -class AppGrantedSerializer(AppSerializerMixin, serializers.ModelSerializer): - """ - 被授权应用的数据结构 - """ - category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category')) - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) - - class Meta: - model = Application - only_fields = [ - 'id', 'name', 'domain', 'category', 'type', 'attrs', 'comment', 'org_id' - ] - fields = only_fields + ['category_display', 'type_display', 'org_name'] - read_only_fields = fields diff --git a/apps/perms/serializers/asset/__init__.py b/apps/perms/serializers/asset/__init__.py deleted file mode 100644 index 5fb99849f..000000000 --- a/apps/perms/serializers/asset/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .permission import * -from .permission_relation import * -from .user_permission import * diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py deleted file mode 100644 index cd6c24723..000000000 --- a/apps/perms/serializers/asset/permission.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ -from django.db.models import Q - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from perms.models import AssetPermission, Action -from assets.models import Asset, Node, SystemUser -from users.models import User, UserGroup -from ..base import ActionsField, BasePermissionSerializer - -__all__ = ['AssetPermissionSerializer'] - - -class AssetPermissionSerializer(BasePermissionSerializer): - actions = ActionsField(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')) - users_display = serializers.ListField(child=serializers.CharField(), label=_('Users display'), required=False) - user_groups_display = serializers.ListField(child=serializers.CharField(), label=_('User groups display'), required=False) - assets_display = serializers.ListField(child=serializers.CharField(), label=_('Assets display'), required=False) - nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes display'), required=False) - system_users_display = serializers.ListField(child=serializers.CharField(), label=_('System users display'), required=False) - - class Meta: - model = AssetPermission - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'is_active', 'is_expired', 'is_valid', 'actions', - 'created_by', 'date_created', 'date_expired', - 'date_start', 'comment', 'from_ticket' - ] - fields_m2m = [ - 'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', - 'assets_display', 'nodes', 'nodes_display', 'system_users', 'system_users_display', - 'users_amount', 'user_groups_amount', 'assets_amount', - 'nodes_amount', 'system_users_amount', - ] - fields = fields_small + fields_m2m - read_only_fields = ['created_by', 'date_created', 'from_ticket'] - extra_kwargs = { - 'is_expired': {'label': _('Is expired')}, - 'is_valid': {'label': _('Is valid')}, - 'actions': {'label': _('Actions')}, - 'users_amount': {'label': _('Users amount')}, - 'user_groups_amount': {'label': _('User groups amount')}, - 'assets_amount': {'label': _('Assets amount')}, - 'nodes_amount': {'label': _('Nodes amount')}, - 'system_users_amount': {'label': _('System users amount')}, - } - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - 'users', 'user_groups', 'assets', 'nodes', 'system_users' - ) - return queryset - - def to_internal_value(self, data): - if 'system_users_display' in data: - # system_users_display 转化为 system_users - system_users = data.get('system_users', []) - system_users_display = data.pop('system_users_display') - - for name in system_users_display: - system_user = SystemUser.objects.filter(name=name).first() - if system_user and system_user.id not in system_users: - system_users.append(system_user.id) - data['system_users'] = system_users - return super().to_internal_value(data) - - @staticmethod - def perform_display_create(instance, **kwargs): - # 用户 - users_to_set = User.objects.filter( - Q(name__in=kwargs.get('users_display')) | - Q(username__in=kwargs.get('users_display')) - ).distinct() - instance.users.add(*users_to_set) - # 用户组 - user_groups_to_set = UserGroup.objects.filter( - name__in=kwargs.get('user_groups_display') - ).distinct() - instance.user_groups.add(*user_groups_to_set) - # 资产 - assets_to_set = Asset.objects.filter( - Q(ip__in=kwargs.get('assets_display')) | - Q(hostname__in=kwargs.get('assets_display')) - ).distinct() - instance.assets.add(*assets_to_set) - # 节点 - nodes_to_set = Node.objects.filter( - full_value__in=kwargs.get('nodes_display') - ).distinct() - instance.nodes.add(*nodes_to_set) - - def create(self, validated_data): - display = { - 'users_display': validated_data.pop('users_display', ''), - 'user_groups_display': validated_data.pop('user_groups_display', ''), - 'assets_display': validated_data.pop('assets_display', ''), - 'nodes_display': validated_data.pop('nodes_display', '') - } - instance = super().create(validated_data) - self.perform_display_create(instance, **display) - return instance - diff --git a/apps/perms/serializers/asset/user_permission.py b/apps/perms/serializers/asset/user_permission.py deleted file mode 100644 index d2ad97894..000000000 --- a/apps/perms/serializers/asset/user_permission.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from assets.models import Node, SystemUser, Asset, Platform -from assets.serializers import ProtocolsField -from perms.serializers.base import ActionsField - -__all__ = [ - 'NodeGrantedSerializer', - 'AssetGrantedSerializer', - 'ActionsSerializer', 'AssetSystemUserSerializer', - 'RemoteAppSystemUserSerializer', - 'DatabaseAppSystemUserSerializer', - 'K8sAppSystemUserSerializer', -] - - -class AssetSystemUserSerializer(serializers.ModelSerializer): - """ - 查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少 - """ - actions = ActionsField(read_only=True) - - class Meta: - model = SystemUser - only_fields = ( - 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', - 'sftp_root', 'username_same_with_user', 'su_enabled', 'su_from', - ) - fields = list(only_fields) + ["actions"] - read_only_fields = fields - - -class AssetGrantedSerializer(serializers.ModelSerializer): - """ - 被授权资产的数据结构 - """ - protocols = ProtocolsField(label=_('Protocols'), required=False, read_only=True) - platform = serializers.SlugRelatedField( - slug_field='name', queryset=Platform.objects.all(), label=_("Platform") - ) - - class Meta: - model = Asset - only_fields = [ - "id", "hostname", "ip", "protocols", "os", 'domain', - "platform", "comment", "org_id", "is_active" - ] - fields = only_fields + ['org_name'] - read_only_fields = fields - - -class NodeGrantedSerializer(serializers.ModelSerializer): - class Meta: - model = Node - fields = [ - 'id', 'name', 'key', 'value', 'org_id', "assets_amount" - ] - read_only_fields = fields - - -class ActionsSerializer(serializers.Serializer): - actions = ActionsField(read_only=True) - - -# TODO: 删除 -class RemoteAppSystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - only_fields = ( - 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', - ) - fields = list(only_fields) - read_only_fields = fields - - -class DatabaseAppSystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - only_fields = ( - 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', - ) - fields = list(only_fields) - read_only_fields = fields - - -class K8sAppSystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - only_fields = ( - 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', - ) - fields = list(only_fields) - read_only_fields = fields - diff --git a/apps/perms/serializers/base.py b/apps/perms/serializers/base.py deleted file mode 100644 index cbed4a2f8..000000000 --- a/apps/perms/serializers/base.py +++ /dev/null @@ -1,56 +0,0 @@ -from rest_framework import serializers -from perms.models import Action -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from rest_framework.fields import empty - -__all__ = ['ActionsDisplayField', 'ActionsField', 'BasePermissionSerializer'] - - -class ActionsField(serializers.MultipleChoiceField): - def __init__(self, *args, **kwargs): - kwargs['choices'] = Action.CHOICES - super().__init__(*args, **kwargs) - - def run_validation(self, data=empty): - data = super(ActionsField, self).run_validation(data) - if isinstance(data, list): - data = Action.choices_to_value(value=data) - return data - - def to_representation(self, value): - return Action.value_to_choices(value) - - def to_internal_value(self, data): - if not self.allow_empty and not data: - self.fail('empty') - - if not data: - return data - - return Action.choices_to_value(data) - - -class ActionsDisplayField(ActionsField): - def to_representation(self, value): - values = super().to_representation(value) - choices = dict(Action.CHOICES) - return [choices.get(i) for i in values] - - -class BasePermissionSerializer(BulkOrgResourceModelSerializer): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_actions_field() - - def set_actions_field(self): - actions = self.fields.get('actions') - if not actions: - return - choices = actions._choices - choices = self._filter_actions_choices(choices) - actions._choices = choices - actions.default = list(choices.keys()) - - def _filter_actions_choices(self, choices): - return choices diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py new file mode 100644 index 000000000..fc2bf1cf3 --- /dev/null +++ b/apps/perms/serializers/permission.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# + +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from assets.models import Asset, Node +from common.drf.fields import BitChoicesField, ObjectRelatedField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from perms.models import ActionChoices, AssetPermission +from users.models import User, UserGroup + +__all__ = ["AssetPermissionSerializer", "ActionChoicesField"] + + +class ActionChoicesField(BitChoicesField): + def __init__(self, **kwargs): + super().__init__(choice_cls=ActionChoices, **kwargs) + + +class AssetPermissionSerializer(BulkOrgResourceModelSerializer): + users = ObjectRelatedField(queryset=User.objects, many=True, required=False) + user_groups = ObjectRelatedField( + queryset=UserGroup.objects, many=True, required=False + ) + assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False) + nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False) + 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")) + accounts = serializers.ListField(label=_("Accounts"), required=False) + + class Meta: + model = AssetPermission + fields_mini = ["id", "name"] + fields_small = fields_mini + [ + "accounts", + "is_active", + "is_expired", + "is_valid", + "actions", + "created_by", + "date_created", + "date_expired", + "date_start", + "comment", + "from_ticket", + ] + fields_m2m = [ + "users", + "user_groups", + "assets", + "nodes", + ] + fields = fields_small + fields_m2m + read_only_fields = ["created_by", "date_created", "from_ticket"] + extra_kwargs = { + "actions": {"label": _("Actions")}, + "is_expired": {"label": _("Is expired")}, + "is_valid": {"label": _("Is valid")}, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_actions_field() + + def set_actions_field(self): + actions = self.fields.get("actions") + if not actions: + return + choices = actions._choices + actions._choices = choices + actions.default = list(choices.keys()) + + @classmethod + def setup_eager_loading(cls, queryset): + """Perform necessary eager loading of data.""" + queryset = queryset.prefetch_related( + "users", + "user_groups", + "assets", + "nodes", + ) + return queryset + + @staticmethod + def perform_display_create(instance, **kwargs): + # 用户 + users_to_set = User.objects.filter( + Q(name__in=kwargs.get("users_display")) + | Q(username__in=kwargs.get("users_display")) + ).distinct() + instance.users.add(*users_to_set) + # 用户组 + user_groups_to_set = UserGroup.objects.filter( + name__in=kwargs.get("user_groups_display") + ).distinct() + instance.user_groups.add(*user_groups_to_set) + # 资产 + assets_to_set = Asset.objects.filter( + Q(address__in=kwargs.get("assets_display")) + | Q(name__in=kwargs.get("assets_display")) + ).distinct() + instance.assets.add(*assets_to_set) + # 节点 + nodes_to_set = Node.objects.filter( + full_value__in=kwargs.get("nodes_display") + ).distinct() + instance.nodes.add(*nodes_to_set) + + def create(self, validated_data): + display = { + "users_display": validated_data.pop("users_display", ""), + "user_groups_display": validated_data.pop("user_groups_display", ""), + "assets_display": validated_data.pop("assets_display", ""), + "nodes_display": validated_data.pop("nodes_display", ""), + } + instance = super().create(validated_data) + self.perform_display_create(instance, **display) + return instance diff --git a/apps/perms/serializers/asset/permission_relation.py b/apps/perms/serializers/permission_relation.py similarity index 84% rename from apps/perms/serializers/asset/permission_relation.py rename to apps/perms/serializers/permission_relation.py index ee1e05112..3e469106a 100644 --- a/apps/perms/serializers/asset/permission_relation.py +++ b/apps/perms/serializers/permission_relation.py @@ -2,17 +2,14 @@ # from rest_framework import serializers -from common.mixins import BulkSerializerMixin -from assets.models import Asset, Node +from common.drf.serializers import BulkSerializerMixin from perms.models import AssetPermission -from users.models import User __all__ = [ 'AssetPermissionUserRelationSerializer', 'AssetPermissionUserGroupRelationSerializer', "AssetPermissionAssetRelationSerializer", 'AssetPermissionNodeRelationSerializer', - 'AssetPermissionSystemUserRelationSerializer', 'AssetPermissionAllAssetSerializer', 'AssetPermissionAllUserSerializer', ] @@ -84,7 +81,7 @@ class AssetPermissionAllAssetSerializer(serializers.Serializer): asset_display = serializers.SerializerMethodField() class Meta: - only_fields = ['id', 'hostname', 'ip'] + only_fields = ['id', 'name', 'address'] @staticmethod def get_asset_display(obj): @@ -99,13 +96,3 @@ class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSeri fields = [ 'id', 'node', "node_display", ] - - -class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - systemuser_display = serializers.ReadOnlyField() - - class Meta: - model = AssetPermission.system_users.through - fields = [ - 'id', 'systemuser', 'systemuser_display' - ] diff --git a/apps/perms/serializers/system_user_permission.py b/apps/perms/serializers/system_user_permission.py deleted file mode 100644 index 3452d7af3..000000000 --- a/apps/perms/serializers/system_user_permission.py +++ /dev/null @@ -1,19 +0,0 @@ -from rest_framework import serializers -from ..hands import SystemUser - -__all__ = [ - 'SystemUserSerializer', -] - - -class SystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - fields = [ - 'id', 'name', 'username', 'protocol', - 'login_mode', 'login_mode_display', - 'priority', 'username_same_with_user', - 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', - 'sftp_root', 'date_created', 'created_by' - ] - ref_name = 'PermedSystemUserSerializer' diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py new file mode 100644 index 000000000..a85770a50 --- /dev/null +++ b/apps/perms/serializers/user_permission.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# + +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from assets.const import Category, AllTypes +from assets.models import Node, Asset, Platform, Account +from assets.serializers.asset.common import AssetProtocolsSerializer +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from perms.serializers.permission import ActionChoicesField + +__all__ = [ + 'NodeGrantedSerializer', 'AssetGrantedSerializer', + 'AccountsPermedSerializer' +] + + +class AssetGrantedSerializer(serializers.ModelSerializer): + """ 被授权资产的数据结构 """ + platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) + protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) + category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) + type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) + + class Meta: + model = Asset + only_fields = [ + "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 Meta: + model = Node + fields = [ + 'id', 'name', 'key', 'value', 'org_id', "assets_amount" + ] + read_only_fields = fields + + +class AccountsPermedSerializer(serializers.ModelSerializer): + actions = ActionChoicesField(read_only=True) + + class Meta: + model = Account + fields = ['id', 'name', 'has_username', '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 68a531887..6a8ef9467 100644 --- a/apps/perms/signal_handlers/__init__.py +++ b/apps/perms/signal_handlers/__init__.py @@ -1,3 +1,2 @@ from . import asset_permission -from . import app_permission from . import refresh_perms diff --git a/apps/perms/signal_handlers/app_permission.py b/apps/perms/signal_handlers/app_permission.py deleted file mode 100644 index 104f56e9a..000000000 --- a/apps/perms/signal_handlers/app_permission.py +++ /dev/null @@ -1,106 +0,0 @@ -import itertools - -from django.db.models.signals import m2m_changed -from django.dispatch import receiver - -from users.models import User, UserGroup -from assets.models import Asset, SystemUser -from applications.models import Application -from common.utils import get_logger -from common.exceptions import M2MReverseNotAllowed -from common.decorator import on_transaction_commit -from common.const.signals import POST_ADD -from perms.models import ApplicationPermission -from applications.models import Account as AppAccount - - -logger = get_logger(__file__) - - -@receiver(m2m_changed, sender=ApplicationPermission.applications.through) -@on_transaction_commit -def on_app_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Application permission applications change signal received") - system_users = instance.system_users.all() - set_remote_app_asset_system_users_if_need(instance, system_users=system_users) - - apps = Application.objects.filter(pk__in=pk_set) - set_app_accounts(apps, system_users) - - -def set_app_accounts(apps, system_users): - for app, system_user in itertools.product(apps, system_users): - AppAccount.objects.get_or_create( - defaults={'app': app, 'systemuser': system_user}, - app=app, systemuser=system_user - ) - - -def set_remote_app_asset_system_users_if_need(instance: ApplicationPermission, system_users=None, - users=None, groups=None): - if not instance.category_remote_app: - return - - attrs = instance.applications.all().values_list('attrs', flat=True) - asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')] - # 远程应用中资产可能在资产表里不存在 - asset_ids = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True) - if not asset_ids: - return - - system_users = system_users or instance.system_users.all() - for system_user in system_users: - system_user.add_related_assets(asset_ids) - - if system_user.username_same_with_user: - users = users or instance.users.all() - groups = groups or instance.user_groups.all() - system_user.groups.add(*groups) - system_user.users.add(*users) - - -@receiver(m2m_changed, sender=ApplicationPermission.system_users.through) -@on_transaction_commit -def on_app_permission_system_users_changed(sender, instance, action, reverse, pk_set, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Application permission system_users change signal received") - system_users = SystemUser.objects.filter(pk__in=pk_set) - - set_remote_app_asset_system_users_if_need(instance, system_users=system_users) - apps = instance.applications.all() - set_app_accounts(apps, system_users) - - -@receiver(m2m_changed, sender=ApplicationPermission.users.through) -@on_transaction_commit -def on_app_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Application permission users change signal received") - users = User.objects.filter(pk__in=pk_set) - set_remote_app_asset_system_users_if_need(instance, users=users) - - -@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through) -@on_transaction_commit -def on_app_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Application permission user groups change signal received") - groups = UserGroup.objects.filter(pk__in=pk_set) - set_remote_app_asset_system_users_if_need(instance, groups=groups) diff --git a/apps/perms/signal_handlers/asset_permission.py b/apps/perms/signal_handlers/asset_permission.py index e889c318a..68503b6df 100644 --- a/apps/perms/signal_handlers/asset_permission.py +++ b/apps/perms/signal_handlers/asset_permission.py @@ -1,138 +1,10 @@ # -*- coding: utf-8 -*- # -from django.db.models.signals import m2m_changed -from django.dispatch import receiver - -from users.models import User -from assets.models import SystemUser -from common.utils import get_logger -from common.decorator import on_transaction_commit -from common.exceptions import M2MReverseNotAllowed -from common.const.signals import POST_ADD -from perms.models import AssetPermission - - -logger = get_logger(__file__) - - -@receiver(m2m_changed, sender=User.groups.through) -@on_transaction_commit -def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs): - """ - UserGroup 增加 User 时,增加的 User 需要与 UserGroup 关联的动态系统用户相关联 - """ - user: User - - if action != POST_ADD: - return - - if not reverse: - # 一个用户添加了多个用户组 - user_ids = [instance.id] - system_users = SystemUser.objects.filter(groups__id__in=pk_set).distinct() - else: - # 一个用户组添加了多个用户 - user_ids = pk_set - system_users = SystemUser.objects.filter(groups__id=instance.pk).distinct() - - for system_user in system_users: - system_user.users.add(*user_ids) - - -@receiver(m2m_changed, sender=AssetPermission.nodes.through) -@on_transaction_commit -def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission nodes change signal received") - nodes = model.objects.filter(pk__in=pk_set) - system_users = instance.system_users.all() - - # TODO 待优化 - for system_user in system_users: - system_user.nodes.add(*nodes) - - -@receiver(m2m_changed, sender=AssetPermission.assets.through) -@on_transaction_commit -def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission assets change signal received") - assets = model.objects.filter(pk__in=pk_set) - - # TODO 待优化 - system_users = instance.system_users.all() - for system_user in system_users: - system_user: SystemUser - system_user.add_related_assets(assets) - - -@receiver(m2m_changed, sender=AssetPermission.system_users.through) -@on_transaction_commit -def on_asset_permission_system_users_changed(instance, action, reverse, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission system_users change signal received") - system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - assets = instance.assets.all().values_list('id', flat=True) - nodes = instance.nodes.all().values_list('id', flat=True) - - for system_user in system_users: - system_user.nodes.add(*tuple(nodes)) - system_user.add_related_assets(assets) - - # 动态系统用户,需要关联用户和用户组了 - if system_user.username_same_with_user: - users = instance.users.all().values_list('id', flat=True) - groups = instance.user_groups.all().values_list('id', flat=True) - system_user.groups.add(*tuple(groups)) - system_user.users.add(*tuple(users)) - - -@receiver(m2m_changed, sender=AssetPermission.users.through) -@on_transaction_commit -def on_asset_permission_users_changed(instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission users change signal received") - users = model.objects.filter(pk__in=pk_set) - system_users = instance.system_users.all() - - # TODO 待优化 - for system_user in system_users: - if system_user.username_same_with_user: - system_user.users.add(*tuple(users)) - - -@receiver(m2m_changed, sender=AssetPermission.user_groups.through) -@on_transaction_commit -def on_asset_permission_user_groups_changed(instance, action, pk_set, model, reverse, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission user groups change signal received") - groups = model.objects.filter(pk__in=pk_set) - system_users = instance.system_users.all() - - # TODO 待优化 - for system_user in system_users: - if system_user.username_same_with_user: - system_user.groups.add(*tuple(groups)) + + + + + diff --git a/apps/perms/signal_handlers/refresh_perms.py b/apps/perms/signal_handlers/refresh_perms.py index fc2d0da1c..2e66c4475 100644 --- a/apps/perms/signal_handlers/refresh_perms.py +++ b/apps/perms/signal_handlers/refresh_perms.py @@ -10,7 +10,7 @@ from common.utils import get_logger from common.exceptions import M2MReverseNotAllowed from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR from perms.models import AssetPermission -from perms.utils.asset.user_permission import UserGrantedTreeRefreshController +from perms.utils.user_permission import UserGrantedTreeRefreshController logger = get_logger(__file__) diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py index 786b7b285..564e5657e 100644 --- a/apps/perms/tasks.py +++ b/apps/perms/tasks.py @@ -10,13 +10,13 @@ from celery import shared_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.const.crontab import CRONTAB_AT_AM_TEN from ops.celery.decorator import register_as_period_task from perms.notifications import ( PermedAssetsWillExpireUserMsg, AssetPermsWillExpireForOrgAdminMsg, - PermedAppsWillExpireUserMsg, AppPermsWillExpireForOrgAdminMsg ) -from perms.models import AssetPermission, ApplicationPermission -from perms.utils.asset.user_permission import UserGrantedTreeRefreshController +from perms.models import AssetPermission +from perms.utils.user_permission import UserGrantedTreeRefreshController logger = get_logger(__file__) @@ -55,7 +55,7 @@ def check_asset_permission_expired(): UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids_cross_orgs(asset_perm_ids) -@register_as_period_task(crontab='0 10 * * *') +@register_as_period_task(crontab=CRONTAB_AT_AM_TEN) @shared_task() @atomic() @tmp_to_root_org() @@ -101,48 +101,3 @@ def check_asset_permission_will_expired(): org_admins = org.admins.all() for org_admin in org_admins: AssetPermsWillExpireForOrgAdminMsg(org_admin, perms, org, day_count).publish_async() - - -@register_as_period_task(crontab='0 10 * * *') -@shared_task() -@atomic() -@tmp_to_root_org() -def check_app_permission_will_expired(): - start = local_now() - end = start + timedelta(days=3) - - app_perms = ApplicationPermission.objects.filter( - date_expired__gte=start, - date_expired__lte=end - ).distinct() - - user_app_remain_day_mapper = defaultdict(dict) - org_perm_remain_day_mapper = defaultdict(dict) - - for app_perm in app_perms: - date_expired = dt_parser(app_perm.date_expired) - remain_days = (date_expired - start).days - - org = app_perm.org - if org in org_perm_remain_day_mapper[remain_days]: - org_perm_remain_day_mapper[remain_days][org].add(app_perm) - else: - org_perm_remain_day_mapper[remain_days][org] = {app_perm, } - - users = app_perm.get_all_users() - apps = app_perm.applications.all() - for u in users: - if u in user_app_remain_day_mapper[remain_days]: - user_app_remain_day_mapper[remain_days][u].update(apps) - else: - user_app_remain_day_mapper[remain_days][u] = set(apps) - - for day_count, user_app_mapper in user_app_remain_day_mapper.items(): - for user, apps in user_app_mapper.items(): - PermedAppsWillExpireUserMsg(user, apps, day_count).publish_async() - - for day_count, org_perm_mapper in org_perm_remain_day_mapper.items(): - for org, perms in org_perm_mapper.items(): - org_admins = org.admins.all() - for org_admin in org_admins: - AppPermsWillExpireForOrgAdminMsg(org_admin, perms, org, day_count).publish_async() diff --git a/apps/perms/tests.py b/apps/perms/tests.py index 344266b19..e69de29bb 100644 --- a/apps/perms/tests.py +++ b/apps/perms/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -from django.contrib.sessions.backends import file, db, cache diff --git a/apps/perms/tree/app.py b/apps/perms/tree/app.py deleted file mode 100644 index 6a6d6c74f..000000000 --- a/apps/perms/tree/app.py +++ /dev/null @@ -1,103 +0,0 @@ -from urllib.parse import urlencode, parse_qsl - -from django.utils.translation import ugettext as _ -from rest_framework.generics import get_object_or_404 - -from common.tree import TreeNode -from orgs.models import Organization -from assets.models import SystemUser -from applications.utils import KubernetesTree -from applications.models import Application -from perms.utils.application.permission import get_application_system_user_ids - - -class GrantedAppTreeUtil: - @staticmethod - def filter_organizations(applications): - organization_ids = set(applications.values_list('org_id', flat=True)) - organizations = [Organization.get_instance(org_id) for org_id in organization_ids] - organizations.sort(key=lambda x: x.name) - return organizations - - @staticmethod - def create_root_node(): - name = _('My applications') - node = TreeNode(**{ - 'id': 'applications', - 'name': name, - 'title': name, - 'pId': '', - 'open': True, - 'iconSkin': 'applications', - 'isParent': True, - 'meta': { - 'type': 'root' - } - }) - return node - - @staticmethod - def create_empty_node(): - name = _("Empty") - node = TreeNode(**{ - 'id': 'empty', - 'name': name, - 'title': name, - 'pId': '', - 'isParent': True, - 'children': [], - 'meta': { - 'type': 'application' - } - }) - return node - - @staticmethod - def get_children_nodes(tree_id, parent_info, user): - tree_nodes = [] - parent_info = dict(parse_qsl(parent_info)) - pod_name = parent_info.get('pod') - app_id = parent_info.get('app_id') - namespace = parent_info.get('namespace') - system_user_id = parent_info.get('system_user_id') - - if app_id and not any([pod_name, namespace, system_user_id]): - app = get_object_or_404(Application, id=app_id) - system_user_ids = get_application_system_user_ids(user, app) - system_users = SystemUser.objects.filter(id__in=system_user_ids).order_by('priority') - for system_user in system_users: - system_user_node = KubernetesTree(tree_id).as_system_user_tree_node( - system_user, parent_info - ) - tree_nodes.append(system_user_node) - return tree_nodes - tree_nodes = KubernetesTree(tree_id).async_tree_node(parent_info) - return tree_nodes - - def create_tree_nodes(self, applications): - tree_nodes = [] - if not applications: - return [self.create_empty_node()] - - root_node = self.create_root_node() - organizations = self.filter_organizations(applications) - - for i, org in enumerate(organizations): - tree_id = urlencode({'org_id': str(org.id)}) - apps = applications.filter(org_id=org.id) - - # 组织节点 - org_node = org.as_tree_node(oid=tree_id, pid=root_node.id) - org_node.name += '({})'.format(apps.count()) - tree_nodes.append(org_node) - - # 类别节点 - category_type_nodes = Application.create_category_type_tree_nodes( - apps, tree_id, show_empty=False - ) - tree_nodes += category_type_nodes - - for app in apps: - app_node = app.as_tree_node(tree_id, k8s_as_tree=True) - tree_nodes.append(app_node) - return tree_nodes diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py index c2dfe2380..5fce2cb39 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -1,19 +1,8 @@ # coding:utf-8 -from django.urls import re_path -from common import api as capi from .asset_permission import asset_permission_urlpatterns -from .application_permission import application_permission_urlpatterns -from .system_user_permission import system_users_permission_urlpatterns +from .user_permission import user_permission_urlpatterns app_name = 'perms' -old_version_urlpatterns = [ - re_path('(?Puser|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api) -] - -urlpatterns = [] -urlpatterns += asset_permission_urlpatterns -urlpatterns += application_permission_urlpatterns -urlpatterns += system_users_permission_urlpatterns -urlpatterns += old_version_urlpatterns +urlpatterns = asset_permission_urlpatterns + user_permission_urlpatterns diff --git a/apps/perms/urls/application_permission.py b/apps/perms/urls/application_permission.py deleted file mode 100644 index 50772a8d5..000000000 --- a/apps/perms/urls/application_permission.py +++ /dev/null @@ -1,50 +0,0 @@ -# coding: utf-8 -# - -from django.urls import path, include -from rest_framework_bulk.routes import BulkRouter -from .. import api - - -router = BulkRouter() -router.register('application-permissions', api.ApplicationPermissionViewSet, 'application-permission') -router.register('application-permissions-users-relations', api.ApplicationPermissionUserRelationViewSet, 'application-permissions-users-relation') -router.register('application-permissions-user-groups-relations', api.ApplicationPermissionUserGroupRelationViewSet, 'application-permissions-user-groups-relation') -router.register('application-permissions-applications-relations', api.ApplicationPermissionApplicationRelationViewSet, 'application-permissions-application-relation') -router.register('application-permissions-system-users-relations', api.ApplicationPermissionSystemUserRelationViewSet, 'application-permissions-system-users-relation') - -user_permission_urlpatterns = [ - path('/applications/', api.UserAllGrantedApplicationsApi.as_view(), name='user-applications'), - path('applications/', api.MyAllGrantedApplicationsApi.as_view(), name='my-applications'), - - # Application As Tree - path('/applications/tree/', api.UserAllGrantedApplicationsAsTreeApi.as_view(), name='user-applications-as-tree'), - path('applications/tree/', api.MyAllGrantedApplicationsAsTreeApi.as_view(), name='my-applications-as-tree'), - - # Application System Users - path('/applications//system-users/', api.UserGrantedApplicationSystemUsersApi.as_view(), name='user-application-system-users'), - path('applications//system-users/', api.MyGrantedApplicationSystemUsersApi.as_view(), name='my-application-system-users'), -] - -user_group_permission_urlpatterns = [ - path('/applications/', api.UserGroupGrantedApplicationsApi.as_view(), name='user-group-applications'), -] - -permission_urlpatterns = [ - # 授权规则中授权的用户和应用 - path('/applications/all/', api.ApplicationPermissionAllApplicationListApi.as_view(), name='application-permission-all-applications'), - path('/users/all/', api.ApplicationPermissionAllUserListApi.as_view(), name='application-permission-all-users'), - - # 验证用户是否有某个应用的权限 - path('user/validate/', api.ValidateUserApplicationPermissionApi.as_view(), name='validate-user-application-permission'), - - path('applications/actions/', api.ApplicationPermissionActionsApi.as_view(), name='application-actions'), -] - -application_permission_urlpatterns = [ - path('users/', include(user_permission_urlpatterns)), - path('user-groups/', include(user_group_permission_urlpatterns)), - path('application-permissions/', include(permission_urlpatterns)) -] - -application_permission_urlpatterns += router.urls diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index f24b1b8ba..41ffe444a 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -7,105 +7,24 @@ from .. import api router = BulkRouter() router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') -router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, 'asset-permissions-users-relation') -router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, 'asset-permissions-user-groups-relation') -router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, 'asset-permissions-assets-relation') -router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation') -router.register('asset-permissions-system-users-relations', api.AssetPermissionSystemUserRelationViewSet, 'asset-permissions-system-users-relation') - -user_permission_urlpatterns = [ - # 统一说明: - # ``: `User.pk` - # 直接授权:在 `AssetPermission` 中关联的对象 - - # --------------------------------------------------------- - # 以 serializer 格式返回 - path('/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'), - path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'), - - # Tree Node 的数据格式返回 - path('/assets/tree/', api.UserDirectGrantedAssetsAsTreeForAdminApi.as_view(), name='user-assets-as-tree'), - path('assets/tree/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), - path('ungroup/assets/tree/', api.MyUngroupAssetsAsTreeApi.as_view(), name='my-ungroup-assets-as-tree'), - # ^--------------------------------------------------------^ - - # 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点 - # 以 serializer 格式返回 - path('/nodes/', api.UserGrantedNodesForAdminApi.as_view(), name='user-nodes'), - path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'), - - # 以 Tree Node 的数据格式返回 - path('/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), - path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), - # ^--------------------------------------------------------^ - - # 一层一层的获取用户授权的节点, - # 以 Serializer 的数据格式返回 - path('/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'), - path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'), - - # 以 Tree Node 的数据格式返回 - path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(), name='user-nodes-children-as-tree'), - # 部分调用位置 - # - 普通用户 -> 我的资产 -> 展开节点 时调用 - path('nodes/children/tree/', api.MyGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), - # ^--------------------------------------------------------^ - - # 此接口会返回整棵树 - # 普通用户 -> 命令执行 -> 左侧树 - path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), - - # 主要用于 luna 页面,带资产的节点树 - path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'), - path('nodes/children-with-assets/tree/', api.MyGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'), - - # 查询授权树上某个节点的所有资产 - path('/nodes//assets/', api.UserGrantedNodeAssetsForAdminApi.as_view(), name='user-node-assets'), - path('nodes//assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'), - - # 未分组的资产 - path('/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsForAdminApi.as_view(), name='user-ungrouped-assets'), - path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'), - - # 收藏的资产 - path('/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsForAdminApi.as_view(), name='user-ungrouped-assets'), - path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), name='my-ungrouped-assets'), - - # Asset System users - path('/assets//system-users/', api.UserGrantedAssetSystemUsersForAdminApi.as_view(), name='user-asset-system-users'), - path('assets//system-users/', api.MyGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), - - # TODO 要废弃 Expire user permission cache - path('/asset-permissions/cache/', api.UserAssetPermissionsCacheApi.as_view(), - name='user-asset-permission-cache'), - path('asset-permissions/cache/', api.UserAssetPermissionsCacheApi.as_view(), name='my-asset-permission-cache'), -] - -user_group_permission_urlpatterns = [ - # 查询某个用户组授权的资产和资产组 - path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), - path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), - path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'), - path('/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'), - path('/nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), - path('/assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'), -] +router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, + 'asset-permissions-users-relation') +router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, + 'asset-permissions-user-groups-relation') +router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, + 'asset-permissions-assets-relation') +router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, + 'asset-permissions-nodes-relation') permission_urlpatterns = [ # 授权规则中授权的资产 path('/assets/all/', api.AssetPermissionAllAssetListApi.as_view(), name='asset-permission-all-assets'), path('/users/all/', api.AssetPermissionAllUserListApi.as_view(), name='asset-permission-all-users'), - - # 验证用户是否有某个资产和系统用户的权限 - path('user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), - path('user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), - + path('/accounts/', api.AssetPermissionAccountListApi.as_view(), name='asset-permission-accounts'), ] asset_permission_urlpatterns = [ # Assets - path('users/', include(user_permission_urlpatterns)), - path('user-groups/', include(user_group_permission_urlpatterns)), path('asset-permissions/', include(permission_urlpatterns)), ] diff --git a/apps/perms/urls/system_user_permission.py b/apps/perms/urls/system_user_permission.py deleted file mode 100644 index e5a5ba1e4..000000000 --- a/apps/perms/urls/system_user_permission.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import path -from .. import api - -system_users_permission_urlpatterns = [ - path('system-users-permission/', api.SystemUserPermission.as_view(), name='system-users-permission'), -] diff --git a/apps/perms/urls/user_permission.py b/apps/perms/urls/user_permission.py new file mode 100644 index 000000000..c3a555375 --- /dev/null +++ b/apps/perms/urls/user_permission.py @@ -0,0 +1,63 @@ +from django.urls import path, include + +from .. import api + +user_permission_urlpatterns = [ + # such as: my | self | user.id + + # assets + path('/assets/', api.UserAllPermedAssetsApi.as_view(), + name='user-assets'), + path('/assets/tree/', api.UserDirectPermedAssetsAsTreeApi.as_view(), + name='user-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'), + + 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'), + + # accounts + path('/assets//accounts/', api.UserPermedAssetAccountsApi.as_view(), + name='user-permed-asset-accounts'), +] + +user_group_permission_urlpatterns = [ + # 查询某个用户组授权的资产和资产组 + path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), + name='user-group-assets'), + path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), + name='user-group-nodes'), + path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), + name='user-group-nodes-children'), + path('/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), + name='user-group-nodes-children-as-tree'), + path('/nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), + name='user-group-node-assets'), +] + +user_permission_urlpatterns = [ + path('users/', include(user_permission_urlpatterns)), + path('user-groups/', include(user_group_permission_urlpatterns)), +] diff --git a/apps/perms/utils/__init__.py b/apps/perms/utils/__init__.py index e204cd61b..fc2a94e88 100644 --- a/apps/perms/utils/__init__.py +++ b/apps/perms/utils/__init__.py @@ -1,5 +1,3 @@ -# coding: utf-8 -# - -from .asset import * -from .application import * +from .permission import * +from .user_permission import * +from .account import * diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py new file mode 100644 index 000000000..7c0caf988 --- /dev/null +++ b/apps/perms/utils/account.py @@ -0,0 +1,77 @@ +from collections import defaultdict + +from assets.models import Account +from .permission import AssetPermissionUtil + +__all__ = ['PermAccountUtil'] + + +class PermAccountUtil(AssetPermissionUtil): + """ 资产授权账号相关的工具 """ + + def validate_permission(self, user, asset, account_name): + """ 校验用户有某个资产下某个账号名的权限 + :param user: User + :param asset: Asset + :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} + account = accounts_mapper.get(account_name) + return account + + def get_permed_accounts_for_user(self, user, asset): + """ 获取授权给用户某个资产的账号 """ + perms = self.get_permissions_for_user_asset(user, asset) + permed_accounts = self.get_permed_accounts_from_perms(perms, user, asset) + return permed_accounts + + @staticmethod + def get_permed_accounts_from_perms(perms, user, asset): + # alias: is a collection of account usernames and special accounts [@ALL, @INPUT, @USER] + alias_action_bit_mapper = defaultdict(int) + alias_expired_mapper = defaultdict(list) + + for perm in perms: + for alias in perm.accounts: + alias_action_bit_mapper[alias] |= perm.actions + alias_expired_mapper[alias].append(perm.date_expired) + + asset_accounts = asset.accounts.all() + username_account_mapper = {account.username: account for account in asset_accounts} + + cleaned_accounts_action_bit = defaultdict(int) + cleaned_accounts_expired = defaultdict(list) + + # @ALL 账号先处理,后面的每个最多映射一个账号 + all_action_bit = alias_action_bit_mapper.pop(Account.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] + ) + + for alias, action_bit in alias_action_bit_mapper.items(): + if alias == Account.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: + account = Account.get_manual_account() + elif alias in username_account_mapper: + account = username_account_mapper[alias] + else: + account = None + + if account: + cleaned_accounts_action_bit[account] |= action_bit + cleaned_accounts_expired[account].extend(alias_expired_mapper[alias]) + + accounts = [] + for account, action_bit in cleaned_accounts_action_bit.items(): + account.actions = action_bit + account.date_expired = max(cleaned_accounts_expired[account]) + accounts.append(account) + return accounts diff --git a/apps/perms/utils/application/__init__.py b/apps/perms/utils/application/__init__.py deleted file mode 100644 index ea3cb14de..000000000 --- a/apps/perms/utils/application/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .permission import * -from .user_permission import * diff --git a/apps/perms/utils/application/permission.py b/apps/perms/utils/application/permission.py deleted file mode 100644 index 955743e25..000000000 --- a/apps/perms/utils/application/permission.py +++ /dev/null @@ -1,82 +0,0 @@ -import time -from functools import reduce - -from django.db.models import Q - -from common.utils import get_logger -from perms.models import ApplicationPermission, Action - -logger = get_logger(__file__) - - -def get_user_all_app_perm_ids(user) -> set: - app_perm_ids = set() - user_perm_id = ApplicationPermission.users.through.objects \ - .filter(user_id=user.id) \ - .values_list('applicationpermission_id', flat=True) \ - .distinct() - app_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 = ApplicationPermission.user_groups.through.objects \ - .filter(usergroup_id__in=group_ids) \ - .values_list('applicationpermission_id', flat=True) \ - .distinct() - app_perm_ids.update(groups_perm_id) - - app_perm_ids = ApplicationPermission.objects.filter( - id__in=app_perm_ids).valid().values_list('id', flat=True) - app_perm_ids = set(app_perm_ids) - return app_perm_ids - - -def validate_permission(user, application, system_user, action='connect'): - app_perm_ids = get_user_all_app_perm_ids(user) - app_perm_ids = ApplicationPermission.applications.through.objects.filter( - applicationpermission_id__in=app_perm_ids, - application_id=application.id - ).values_list('applicationpermission_id', flat=True) - app_perm_ids = set(app_perm_ids) - app_perm_ids = ApplicationPermission.system_users.through.objects.filter( - applicationpermission_id__in=app_perm_ids, - systemuser_id=system_user.id - ).values_list('applicationpermission_id', flat=True) - app_perm_ids = set(app_perm_ids) - app_perms = ApplicationPermission.objects.filter( - id__in=app_perm_ids - ).order_by('-date_expired') - - if app_perms: - actions = set() - actions_values = app_perms.values_list('actions', flat=True) - for value in actions_values: - _actions = Action.value_to_choices(value) - actions.update(_actions) - actions = list(actions) - app_perm: ApplicationPermission = app_perms.first() - expire_at = app_perm.date_expired.timestamp() - else: - actions = [] - expire_at = time.time() - - # TODO: 组件改造API完成后统一通过actions判断has_perm - has_perm = action in actions - return has_perm, actions, expire_at - - -def get_application_system_user_ids(user, application): - queryset = ApplicationPermission.objects.valid()\ - .filter( - Q(users=user) | Q(user_groups__users=user), - Q(applications=application) - ).values_list('system_users', flat=True) - return queryset - - -def has_application_system_permission(user, application, system_user): - system_user_ids = get_application_system_user_ids(user, application) - return system_user.id in system_user_ids diff --git a/apps/perms/utils/application/user_permission.py b/apps/perms/utils/application/user_permission.py deleted file mode 100644 index 524d5cd42..000000000 --- a/apps/perms/utils/application/user_permission.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db.models import Q -from perms.models import ApplicationPermission -from applications.models import Application - - -def get_user_all_applicationpermission_ids(user): - application_perm_ids = ApplicationPermission.objects.valid().filter( - Q(users=user) | Q(user_groups__users=user) - ).distinct().values_list('id', flat=True) - return application_perm_ids - - -def get_user_granted_all_applications(user): - application_perm_ids = get_user_all_applicationpermission_ids(user) - applications = Application.objects.filter( - granted_by_permissions__id__in=application_perm_ids - ).distinct() - return applications diff --git a/apps/perms/utils/asset/__init__.py b/apps/perms/utils/asset/__init__.py deleted file mode 100644 index ea3cb14de..000000000 --- a/apps/perms/utils/asset/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .permission import * -from .user_permission import * diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py deleted file mode 100644 index e749e630b..000000000 --- a/apps/perms/utils/asset/permission.py +++ /dev/null @@ -1,111 +0,0 @@ -import time -from collections import defaultdict - -from django.db.models import Q - -from common.utils import get_logger -from perms.models import AssetPermission, Action -from perms.hands import Asset, User, UserGroup, SystemUser, Node -from perms.utils.asset.user_permission import get_user_all_asset_perm_ids - -logger = get_logger(__file__) - - -def validate_permission(user, asset, system_user, action='connect'): - - if not system_user.protocol in asset.protocols_as_dict.keys(): - return False, time.time() - - asset_perm_ids = get_user_all_asset_perm_ids(user) - - asset_perm_ids_from_asset = AssetPermission.assets.through.objects.filter( - assetpermission_id__in=asset_perm_ids, - asset_id=asset.id - ).values_list('assetpermission_id', flat=True) - - nodes = asset.get_nodes() - node_keys = set() - for node in nodes: - ancestor_keys = node.get_ancestor_keys(with_self=True) - node_keys.update(ancestor_keys) - node_ids = Node.objects.filter(key__in=node_keys).values_list('id', flat=True) - - node_ids = set(node_ids) - - asset_perm_ids_from_node = AssetPermission.nodes.through.objects.filter( - assetpermission_id__in=asset_perm_ids, - node_id__in=node_ids - ).values_list('assetpermission_id', flat=True) - - asset_perm_ids = {*asset_perm_ids_from_asset, *asset_perm_ids_from_node} - - asset_perm_ids = AssetPermission.system_users.through.objects.filter( - assetpermission_id__in=asset_perm_ids, - systemuser_id=system_user.id - ).values_list('assetpermission_id', flat=True) - - asset_perm_ids = set(asset_perm_ids) - - asset_perms = AssetPermission.objects.filter( - id__in=asset_perm_ids - ).order_by('-date_expired') - - if asset_perms: - actions = set() - actions_values = asset_perms.values_list('actions', flat=True) - for value in actions_values: - _actions = Action.value_to_choices(value) - actions.update(_actions) - asset_perm: AssetPermission = asset_perms.first() - actions = list(actions) - expire_at = asset_perm.date_expired.timestamp() - else: - actions = [] - expire_at = time.time() - - # TODO: 组件改造API完成后统一通过actions判断has_perm - has_perm = action in actions - return has_perm, actions, expire_at - - -def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset): - nodes = asset.get_nodes() - node_keys = set() - for node in nodes: - ancestor_keys = node.get_ancestor_keys(with_self=True) - node_keys.update(ancestor_keys) - - queryset = AssetPermission.objects.filter(id__in=asset_perm_ids)\ - .filter(Q(assets=asset) | Q(nodes__key__in=node_keys)) - - asset_protocols = asset.protocols_as_dict.keys() - values = queryset.filter( - system_users__protocol__in=asset_protocols - ).distinct().values_list('system_users', 'actions') - system_users_actions = defaultdict(int) - - for system_user_id, actions in values: - if None in (system_user_id, actions): - continue - system_users_actions[system_user_id] |= actions - return system_users_actions - - -def get_asset_system_user_ids_with_actions_by_user(user: User, asset: Asset): - asset_perm_ids = get_user_all_asset_perm_ids(user) - return get_asset_system_user_ids_with_actions(asset_perm_ids, asset) - - -def has_asset_system_permission(user: User, asset: Asset, system_user: SystemUser): - systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset) - actions = systemuser_actions_mapper.get(system_user.id, 0) - if actions: - return True - return False - - -def get_asset_system_user_ids_with_actions_by_group(group: UserGroup, asset: Asset): - asset_perm_ids = AssetPermission.objects.filter( - user_groups=group - ).valid().values_list('id', flat=True).distinct() - return get_asset_system_user_ids_with_actions(asset_perm_ids, asset) diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py new file mode 100644 index 000000000..8e4cd9199 --- /dev/null +++ b/apps/perms/utils/permission.py @@ -0,0 +1,92 @@ + +from common.utils import get_logger +from perms.models import AssetPermission + +logger = get_logger(__file__) + + +class AssetPermissionUtil(object): + """ 资产授权相关的方法工具 """ + + def get_permissions_for_user(self, user, with_group=True, flat=False): + """ 获取用户的授权规则 """ + perm_ids = set() + # user + user_perm_ids = AssetPermission.users.through.objects.filter(user_id=user.id) \ + .values_list('assetpermission_id', flat=True).distinct() + perm_ids.update(user_perm_ids) + # group + if with_group: + groups = user.groups.all() + group_perm_ids = self.get_permissions_for_user_groups(groups, flat=True) + perm_ids.update(group_perm_ids) + if flat: + return perm_ids + perms = self.get_permissions(ids=perm_ids) + return perms + + def get_permissions_for_user_groups(self, user_groups, flat=False): + """ 获取用户组的授权规则 """ + if isinstance(user_groups, list): + 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 \ + .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 perms + + def get_permissions_for_asset(self, asset, 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() + perm_ids.update(asset_perm_ids) + if with_node: + nodes = asset.get_all_nodes() + node_perm_ids = self.get_permissions_for_nodes(nodes, flat=True) + perm_ids.update(node_perm_ids) + if flat: + return perm_ids + perms = self.get_permissions(ids=perm_ids) + return perms + + def get_permissions_for_nodes(self, nodes, with_ancestor=False, flat=False): + """ 获取节点的授权规则 """ + 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() + if flat: + return perm_ids + perms = self.get_permissions(ids=perm_ids) + return perms + + 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) + 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) + perm_ids = set(user_perm_ids) & set(asset_perm_ids) + perms = self.get_permissions(ids=perm_ids) + return perms + + @staticmethod + def get_permissions(ids): + perms = AssetPermission.objects.filter(id__in=ids).order_by('-date_expired') + return perms diff --git a/apps/perms/utils/asset/user_permission.py b/apps/perms/utils/user_permission.py similarity index 93% rename from apps/perms/utils/asset/user_permission.py rename to apps/perms/utils/user_permission.py index dd15c0b38..92a42a401 100644 --- a/apps/perms/utils/asset/user_permission.py +++ b/apps/perms/utils/user_permission.py @@ -1,27 +1,30 @@ +import time from collections import defaultdict from typing import List, Tuple -import time -from django.core.cache import cache 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 common.db.models import output_as_string, UnionQuerySet -from common.utils.common import lazyproperty, timeit -from assets.utils import NodeAssetsUtil -from common.utils import get_logger -from common.decorator import on_transaction_commit -from orgs.utils import tmp_to_org, current_org, ensure_in_real_or_default_org, tmp_to_root_org 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, + AssetPermission, PermNode, UserAssetGrantedTreeNodeRelation ) from users.models import User -from perms.locks import UserGrantedTreeRebuildLock NodeFrom = UserAssetGrantedTreeNodeRelation.NodeFrom NODE_ONLY_FIELDS = ('id', 'key', 'parent_key', 'org_id') @@ -117,8 +120,7 @@ class UserGrantedTreeRefreshController: 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} ' - f'orgs:{org_ids}') + 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): @@ -203,28 +205,30 @@ class UserGrantedTreeRefreshController: user = self.user with tmp_to_root_org(): - UserAssetGrantedTreeNodeRelation.objects.filter(user=user)\ - .exclude(org_id__in=self.org_ids)\ + UserAssetGrantedTreeNodeRelation.objects.filter(user=user) \ + .exclude(org_id__in=self.org_ids) \ .delete() - if force or self.have_need_refresh_orgs(): - 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() + if not force and not self.have_need_refresh_orgs(): + return - 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}' - ) + 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: @@ -425,8 +429,8 @@ class UserGrantedTreeBuildUtils(UserGrantedUtilsBase): for node_id, asset_id in node_asset_pairs: if node_id not in node_id_key_mapper: continue - nkey = node_id_key_mapper[node_id] - nodekey_assetsid_mapper[nkey].add(asset_id) + node_key = node_id_key_mapper[node_id] + nodekey_assetsid_mapper[node_key].add(asset_id) util = NodeAssetsUtil(nodes, nodekey_assetsid_mapper) util.generate() @@ -513,7 +517,7 @@ class UserGrantedAssetsQueryUtils(UserGrantedUtilsBase): assets = self._get_indirect_granted_node_assets(node.id) else: assets = Asset.objects.none() - assets = assets.order_by('hostname') + assets = assets.order_by('name') return assets def _get_indirect_granted_node_assets(self, id) -> AssetQuerySet: @@ -602,7 +606,10 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase): def get_top_level_nodes(self): nodes = self.get_special_nodes() real_nodes = self.get_indirect_granted_node_children('') - nodes.extend(self.sort(real_nodes)) + 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): @@ -644,14 +651,12 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase): def get_whole_tree_nodes(self, with_special=True): """ 这里的 granted nodes, 是整棵树需要的node,推算出来的也算 - :param user: + :param with_special: :return: """ - nodes = PermNode.objects.filter( - granted_node_rels__user=self.user - ).annotate( - **PermNode.annotate_granted_node_rel_fields - ).distinct() + 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() diff --git a/apps/rbac/api/role.py b/apps/rbac/api/role.py index 44cb899db..be8274e91 100644 --- a/apps/rbac/api/role.py +++ b/apps/rbac/api/role.py @@ -4,11 +4,11 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.decorators import action from common.drf.api import JMSModelViewSet +from common.mixins.api import PaginatedResponseMixin from ..filters import RoleFilter from ..serializers import RoleSerializer, RoleUserSerializer from ..models import Role, SystemRole, OrgRole from .permission import PermissionViewSet -from common.mixins.api import PaginatedResponseMixin __all__ = [ 'RoleViewSet', 'SystemRoleViewSet', 'OrgRoleViewSet', @@ -16,7 +16,7 @@ __all__ = [ ] -class RoleViewSet(PaginatedResponseMixin, JMSModelViewSet): +class RoleViewSet(JMSModelViewSet): queryset = Role.objects.all() serializer_classes = { 'default': RoleSerializer, diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py index c56326601..08e54374c 100644 --- a/apps/rbac/builtin.py +++ b/apps/rbac/builtin.py @@ -164,8 +164,9 @@ class BuiltinRole: @classmethod def sync_to_db(cls, show_msg=False): roles = cls.get_roles() + print(" - Update builtin roles") for pre_role in roles.values(): role, created = pre_role.update_or_create_role() if show_msg: - print("Update builtin Role: {} - {}".format(role.name, created)) + print(" - Update: {} - {}".format(role.name, created)) diff --git a/apps/rbac/const.py b/apps/rbac/const.py index f8789a110..541b0b3da 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -35,14 +35,13 @@ exclude_permissions = ( ('assets', 'assetgroup', '*', '*'), ('assets', 'cluster', '*', '*'), ('assets', 'favoriteasset', '*', '*'), - ('assets', 'historicalauthbook', '*', '*'), + ('assets', 'historicalaccount', '*', '*'), ('assets', 'assetuser', '*', '*'), ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), ('assets', 'accountbackupplanexecution', 'delete,change', 'accountbackupplanexecution'), - ('assets', 'authbook', 'change', 'authbook'), # TODO 暂时去掉历史账号的权限 - ('assets', 'authbook', '*', 'assethistoryaccount'), - ('assets', 'authbook', '*', 'assethistoryaccountsecret'), + ('assets', 'account', '*', 'assethistoryaccount'), + ('assets', 'account', '*', 'assethistoryaccountsecret'), ('perms', 'userassetgrantedtreenoderelation', '*', '*'), ('perms', 'usergrantedmappingnode', '*', '*'), @@ -58,7 +57,6 @@ exclude_permissions = ( ('rbac', 'role', '*', '*'), ('ops', 'adhoc', 'delete,change', '*'), ('ops', 'adhocexecution', 'add,delete,change', '*'), - ('ops', 'celerytask', '*', '*'), ('ops', 'task', 'add,change', 'task'), ('ops', 'commandexecution', 'delete,change', 'commandexecution'), ('orgs', 'organizationmember', '*', '*'), diff --git a/apps/rbac/migrations/0004_auto_20211201_1901.py b/apps/rbac/migrations/0004_auto_20211201_1901.py index 9d59d99fc..669af02cf 100644 --- a/apps/rbac/migrations/0004_auto_20211201_1901.py +++ b/apps/rbac/migrations/0004_auto_20211201_1901.py @@ -13,6 +13,7 @@ def migrate_system_role_binding(apps, schema_editor): count = 0 bulk_size = 1000 + print('') while True: users = user_model.objects.using(db_alias) \ .only('role', 'id') \ @@ -28,7 +29,7 @@ def migrate_system_role_binding(apps, schema_editor): role_bindings.append(role_binding) role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True) - print("Create role binding: {}-{} using: {:.2f}s".format( + print("\tCreate role binding: {}-{} using: {:.2f}s".format( count, count + len(users), time.time()-start )) count += len(users) @@ -61,7 +62,7 @@ def migrate_org_role_binding(apps, schema_editor): ) role_bindings.append(role_binding) role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True) - print("Create role binding: {}-{} using: {:.2f}s".format( + print("\tCreate role binding: {}-{} using: {:.2f}s".format( count, count + len(members), time.time()-start )) count += len(members) diff --git a/apps/rbac/models/role.py b/apps/rbac/models/role.py index 98a226e6f..05783f598 100644 --- a/apps/rbac/models/role.py +++ b/apps/rbac/models/role.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _, gettext from django.db import models -from common.db.models import JMSModel +from common.db.models import JMSBaseModel from common.utils import lazyproperty from .permission import Permission from ..builtin import BuiltinRole @@ -22,7 +22,7 @@ class OrgRoleManager(models.Manager): return queryset.filter(scope=const.Scope.org) -class Role(JMSModel): +class Role(JMSBaseModel): """ 定义 角色 | 角色-权限 关系 """ Scope = const.Scope diff --git a/apps/rbac/models/rolebinding.py b/apps/rbac/models/rolebinding.py index c1debdbfe..b7795a498 100644 --- a/apps/rbac/models/rolebinding.py +++ b/apps/rbac/models/rolebinding.py @@ -5,7 +5,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from rest_framework.serializers import ValidationError -from common.db.models import JMSModel, CASCADE_SIGNAL_SKIP +from common.db.models import JMSBaseModel, CASCADE_SIGNAL_SKIP from common.utils import lazyproperty from orgs.utils import current_org, tmp_to_root_org from .role import Role @@ -30,7 +30,7 @@ class RoleBindingManager(models.Manager): return self.get_queryset() -class RoleBinding(JMSModel): +class RoleBinding(JMSBaseModel): Scope = Scope """ 定义 用户-角色 关系 """ scope = models.CharField( diff --git a/apps/rbac/permissions.py b/apps/rbac/permissions.py index 1992db90d..3b59a4812 100644 --- a/apps/rbac/permissions.py +++ b/apps/rbac/permissions.py @@ -93,7 +93,8 @@ class RBACPermission(permissions.DjangoModelPermissions): try: queryset = self._queryset(view) model_cls = queryset.model - except: + except Exception as e: + logger.error(e) model_cls = None return model_cls diff --git a/apps/rbac/signal_handlers.py b/apps/rbac/signal_handlers.py index 86eefb180..37eed870a 100644 --- a/apps/rbac/signal_handlers.py +++ b/apps/rbac/signal_handlers.py @@ -11,7 +11,7 @@ def after_migrate_update_builtin_role_permissions(sender, app_config, **kwargs): # 最后一个 app migrations 后执行, 更新内置角色的权限 last_app = list(apps.get_app_configs())[-1] if app_config.name == last_app.name: - print("After migration, update builtin role permissions") + print("\nAfter migration, update builtin role permissions") BuiltinRole.sync_to_db() diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index 0b08c565d..abce5759a 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -61,7 +61,7 @@ extra_nodes_data = [ # 将 model 放到其它节点下,而不是本来的 app 中 special_pid_mapper = { 'common.permission': 'view_other', - "assets.authbook": "accounts", + "assets.account": "accounts", "applications.account": "accounts", 'xpack.account': 'cloud_import', 'xpack.syncinstancedetail': 'cloud_import', diff --git a/apps/settings/serializers/terminal.py b/apps/settings/serializers/terminal.py index 757e21210..87ce12fb7 100644 --- a/apps/settings/serializers/terminal.py +++ b/apps/settings/serializers/terminal.py @@ -6,7 +6,7 @@ class TerminalSettingSerializer(serializers.Serializer): PREFIX_TITLE = _('Terminal') SORT_BY_CHOICES = ( - ('hostname', _('Hostname')), + ('name', _('Hostname')), ('ip', _('IP')) ) diff --git a/apps/static/img/logo_text_white.png b/apps/static/img/logo_text_white.png index 39dea6778..f791baa71 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/__init__.py b/apps/terminal/api/__init__.py index 4cdabc9dd..c4a60efb6 100644 --- a/apps/terminal/api/__init__.py +++ b/apps/terminal/api/__init__.py @@ -1,11 +1,6 @@ # -*- coding: utf-8 -*- # -from .terminal import * from .session import * -from .command import * -from .task import * -from .storage import * -from .status import * -from .sharing import * -from .endpoint import * +from .component import * +from .applet import * from .db_listen_port import * diff --git a/apps/terminal/api/applet/__init__.py b/apps/terminal/api/applet/__init__.py new file mode 100644 index 000000000..a950652a4 --- /dev/null +++ b/apps/terminal/api/applet/__init__.py @@ -0,0 +1,3 @@ +from .applet import * +from .host import * +from .relation import * diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py new file mode 100644 index 000000000..a00814dd3 --- /dev/null +++ b/apps/terminal/api/applet/applet.py @@ -0,0 +1,120 @@ +import shutil +import zipfile +import yaml +import os.path +from typing import Callable + +from django.http import HttpResponse +from django.core.files.storage import default_storage +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 terminal import serializers +from terminal.models import AppletPublication, Applet + + +__all__ = ['AppletViewSet', 'AppletPublicationViewSet'] + + +class DownloadUploadMixin: + get_serializer: Callable + request: Request + get_object: Callable + + def extract_and_check_file(self, request): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + + file = serializer.validated_data['file'] + save_to = 'applets/{}'.format(file.name + '.tmp.zip') + if default_storage.exists(save_to): + default_storage.delete(save_to) + rel_path = default_storage.save(save_to, file) + path = default_storage.path(rel_path) + extract_to = default_storage.path('applets/{}.tmp'.format(file.name)) + if os.path.exists(extract_to): + shutil.rmtree(extract_to) + + with zipfile.ZipFile(path) as zp: + if zp.testzip() is not None: + return Response({'msg': 'Invalid Zip file'}, status=400) + 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'}) + return manifest, tmp_dir + + @action(detail=False, methods=['post'], serializer_class=FileSerializer) + def upload(self, request, *args, **kwargs): + manifest, tmp_dir = self.extract_and_check_file(request) + name = manifest['name'] + update = request.query_params.get('update') + + instance = Applet.objects.filter(name=name).first() + if instance and not update: + return Response({'error': 'Applet already exists: {}'.format(name)}, status=400) + + serializer = serializers.AppletSerializer(data=manifest, instance=instance) + serializer.is_valid(raise_exception=True) + save_to = default_storage.path('applets/{}'.format(name)) + if os.path.exists(save_to): + shutil.rmtree(save_to) + shutil.move(tmp_dir, save_to) + serializer.save() + return Response(serializer.data, status=201) + + @action(detail=True, methods=['get']) + def download(self, request, *args, **kwargs): + instance = self.get_object() + 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) + return response + + +class AppletViewSet(DownloadUploadMixin, viewsets.ModelViewSet): + queryset = Applet.objects.all() + serializer_class = serializers.AppletSerializer + rbac_perms = { + 'upload': 'terminal.add_applet', + 'download': 'terminal.view_applet', + } + + def get_object(self): + pk = self.kwargs.get('pk') + if not is_uuid(pk): + return self.queryset.get(name=pk) + else: + return self.queryset.get(pk=pk) + + def perform_destroy(self, instance): + if not instance.name: + raise ValidationError('Applet is not null') + path = default_storage.path('applets/{}'.format(instance.name)) + if os.path.exists(path): + shutil.rmtree(path) + instance.delete() + + +class AppletPublicationViewSet(viewsets.ModelViewSet): + queryset = AppletPublication.objects.all() + serializer_class = serializers.AppletPublicationSerializer + filterset_fields = ['host', 'applet', 'status'] + search_fields = ['applet__name', 'applet__display_name', 'host__name'] diff --git a/apps/terminal/api/applet/host.py b/apps/terminal/api/applet/host.py new file mode 100644 index 000000000..4542f3b8d --- /dev/null +++ b/apps/terminal/api/applet/host.py @@ -0,0 +1,63 @@ +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from common.drf.api import JMSModelViewSet +from common.permissions import IsServiceAccount +from orgs.utils import tmp_to_builtin_org +from terminal.models import AppletHost, AppletHostDeployment +from terminal.serializers import ( + AppletHostSerializer, AppletHostDeploymentSerializer, + AppletHostStartupSerializer, AppletHostDeployAppletSerializer +) +from terminal.tasks import run_applet_host_deployment, run_applet_host_deployment_install_applet + +__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet'] + + +class AppletHostViewSet(JMSModelViewSet): + serializer_class = AppletHostSerializer + queryset = AppletHost.objects.all() + + def dispatch(self, request, *args, **kwargs): + with tmp_to_builtin_org(system=1): + return super().dispatch(request, *args, **kwargs) + + def get_permissions(self): + if self.action == 'startup': + return [IsServiceAccount()] + return super().get_permissions() + + @action(methods=['post'], detail=True, serializer_class=AppletHostStartupSerializer) + def startup(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance.check_terminal_binding(request) + return Response({'msg': 'ok'}) + + +class AppletHostDeploymentViewSet(viewsets.ModelViewSet): + serializer_class = AppletHostDeploymentSerializer + queryset = AppletHostDeployment.objects.all() + rbac_perms = ( + ('applets', 'terminal.view_AppletHostDeployment'), + ) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + task = run_applet_host_deployment.delay(instance.id) + instance.save_task(task.id) + return Response({'task': str(task.id)}, status=201) + + @action(methods=['post'], detail=False, serializer_class=AppletHostDeployAppletSerializer) + def applets(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + applet_id = serializer.validated_data.get('applet_id') + instance = serializer.save() + task = run_applet_host_deployment_install_applet.delay(instance.id, applet_id) + instance.save_task(task.id) + return Response({'task': str(task.id)}, status=201) diff --git a/apps/terminal/api/applet/relation.py b/apps/terminal/api/applet/relation.py new file mode 100644 index 000000000..e31c6ba5c --- /dev/null +++ b/apps/terminal/api/applet/relation.py @@ -0,0 +1,86 @@ +from typing import Callable + +from django.shortcuts import get_object_or_404 +from django.conf import settings +from rest_framework.request import Request +from rest_framework.decorators import action +from rest_framework.response import Response + +from common.drf.api import JMSModelViewSet +from common.permissions import IsServiceAccount +from common.utils import is_uuid +from orgs.utils import tmp_to_builtin_org +from rbac.permissions import RBACPermission +from terminal.models import AppletHost +from terminal.serializers import ( + AppletHostAccountSerializer, + AppletPublicationSerializer, + AppletHostAppletReportSerializer, +) + + +class HostMixin: + request: Request + permission_denied: Callable + kwargs: dict + rbac_perms = ( + ('list', 'terminal.view_applethost'), + ('retrieve', 'terminal.view_applethost'), + ) + + def get_permissions(self): + if self.kwargs.get('host') and settings.DEBUG: + return [RBACPermission()] + else: + return [IsServiceAccount()] + + def self_host(self): + try: + return self.request.user.terminal.applet_host + except AttributeError: + raise self.permission_denied(self.request, 'User has no applet host') + + def pk_host(self): + return get_object_or_404(AppletHost, id=self.kwargs.get('host')) + + @property + def host(self): + if self.kwargs.get('host'): + return self.pk_host() + else: + return self.self_host() + + +class AppletHostAccountsViewSet(HostMixin, JMSModelViewSet): + serializer_class = AppletHostAccountSerializer + + def get_queryset(self): + with tmp_to_builtin_org(system=1): + queryset = self.host.accounts.all() + return queryset + + +class AppletHostAppletViewSet(HostMixin, JMSModelViewSet): + host: AppletHost + serializer_class = AppletPublicationSerializer + + def get_object(self): + pk = self.kwargs.get('pk') + if not is_uuid(pk): + return self.host.publications.get(applet__name=pk) + else: + return self.host.publications.get(pk=pk) + + def get_queryset(self): + queryset = self.host.publications.all() + return queryset + + @action(methods=['post'], detail=False) + def reports(self, request, *args, **kwargs): + serializer = AppletHostAppletReportSerializer(data=request.data, many=True) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + self.host.check_applets_state(data) + publications = self.host.publications.all() + serializer = AppletPublicationSerializer(publications, many=True) + return Response(serializer.data) diff --git a/apps/terminal/api/component/__init__.py b/apps/terminal/api/component/__init__.py new file mode 100644 index 000000000..afefe0c18 --- /dev/null +++ b/apps/terminal/api/component/__init__.py @@ -0,0 +1,4 @@ +from .terminal import * +from .storage import * +from .status import * +from .endpoint import * diff --git a/apps/terminal/api/endpoint.py b/apps/terminal/api/component/endpoint.py similarity index 85% rename from apps/terminal/api/endpoint.py rename to apps/terminal/api/component/endpoint.py index 37de98576..43ef3bba6 100644 --- a/apps/terminal/api/endpoint.py +++ b/apps/terminal/api/component/endpoint.py @@ -1,18 +1,16 @@ -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework import status -from rest_framework.request import Request -from common.drf.api import JMSBulkModelViewSet -from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 -from assets.models import Asset -from orgs.utils import tmp_to_root_org -from applications.models import Application -from terminal.models import Session -from ..models import Endpoint, EndpointRule -from .. import serializers -from common.permissions import IsValidUserOrConnectionToken +from django.utils.translation import ugettext_lazy as _ +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response +from assets.models import Asset +from common.drf.api import JMSBulkModelViewSet +from common.permissions import IsValidUserOrConnectionToken +from orgs.utils import tmp_to_root_org +from terminal import serializers +from terminal.models import Session, Endpoint, EndpointRule __all__ = ['EndpointViewSet', 'EndpointRuleViewSet'] @@ -25,8 +23,7 @@ class SmartEndpointViewMixin: target_instance: None target_protocol: None - @action(methods=['get'], detail=False, permission_classes=[IsValidUserOrConnectionToken], - url_path='smart') + @action(methods=['get'], detail=False, permission_classes=[IsValidUserOrConnectionToken]) def smart(self, request, *args, **kwargs): self.target_instance = self.get_target_instance() self.target_protocol = self.get_target_protocol() @@ -58,21 +55,16 @@ class SmartEndpointViewMixin: def get_target_instance(self): request = self.request asset_id = request.GET.get('asset_id') - app_id = request.GET.get('app_id') session_id = request.GET.get('session_id') token_id = request.GET.get('token') + if token_id: from authentication.models import ConnectionToken token = ConnectionToken.objects.filter(id=token_id).first() - if token: - if token.asset: - asset_id = token.asset.id - elif token.application: - app_id = token.application.id + if token and token.asset: + asset_id = token.asset.id if asset_id: pk, model = asset_id, Asset - elif app_id: - pk, model = app_id, Application elif session_id: pk, model = session_id, Session else: @@ -84,7 +76,10 @@ class SmartEndpointViewMixin: return instance def get_target_protocol(self): - return self.request.GET.get('protocol') + protocol = None + if not protocol: + protocol = self.request.GET.get('protocol') + return protocol class EndpointViewSet(SmartEndpointViewMixin, JMSBulkModelViewSet): diff --git a/apps/terminal/api/status.py b/apps/terminal/api/component/status.py similarity index 91% rename from apps/terminal/api/status.py rename to apps/terminal/api/component/status.py index 3ec00436b..4c4e31f27 100644 --- a/apps/terminal/api/status.py +++ b/apps/terminal/api/component/status.py @@ -9,9 +9,9 @@ from rest_framework import viewsets, generics from rest_framework.views import Response from rest_framework import status -from ..models import Terminal, Status, Session -from .. import serializers -from ..utils import TypedComponentsStatusMetricsUtil +from terminal.models import Terminal, Status, Session +from terminal import serializers +from terminal.utils import TypedComponentsStatusMetricsUtil logger = logging.getLogger(__file__) @@ -21,7 +21,7 @@ __all__ = ['StatusViewSet', 'ComponentsMetricsAPIView'] class StatusViewSet(viewsets.ModelViewSet): queryset = Status.objects.all() - serializer_class = serializers.StatusSerializer + serializer_class = serializers.StatSerializer session_serializer_class = serializers.SessionSerializer task_serializer_class = serializers.TaskSerializer diff --git a/apps/terminal/api/storage.py b/apps/terminal/api/component/storage.py similarity index 96% rename from apps/terminal/api/storage.py rename to apps/terminal/api/component/storage.py index d9694c337..c912e3131 100644 --- a/apps/terminal/api/storage.py +++ b/apps/terminal/api/component/storage.py @@ -11,8 +11,8 @@ from django_filters import utils from terminal import const from common.const.http import GET from terminal.filters import CommandStorageFilter, CommandFilter, CommandFilterForStorageTree -from ..models import CommandStorage, ReplayStorage -from ..serializers import CommandStorageSerializer, ReplayStorageSerializer +from terminal.models import CommandStorage, ReplayStorage +from terminal.serializers import CommandStorageSerializer, ReplayStorageSerializer __all__ = [ 'CommandStorageViewSet', 'CommandStorageTestConnectiveApi', @@ -61,7 +61,7 @@ class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): if not filterset.is_valid(): raise utils.translate_validation(filterset.errors) command_qs = filterset.qs - if storage.type == const.CommandStorageTypeChoices.es: + if storage.type == const.CommandStorageType.es: command_count = command_qs.count(limit_to_max_result_window=False) else: command_count = command_qs.count() diff --git a/apps/terminal/api/terminal.py b/apps/terminal/api/component/terminal.py similarity index 59% rename from apps/terminal/api/terminal.py rename to apps/terminal/api/component/terminal.py index 209492baa..df14296f5 100644 --- a/apps/terminal/api/terminal.py +++ b/apps/terminal/api/component/terminal.py @@ -1,26 +1,25 @@ # -*- coding: utf-8 -*- # import logging -import uuid -from django.core.cache import cache -from rest_framework import generics -from rest_framework.views import APIView, Response -from rest_framework import status from django.conf import settings from django.utils.translation import gettext_lazy as _ +from rest_framework import generics +from rest_framework import status +from rest_framework.views import APIView, Response -from common.exceptions import JMSException from common.drf.api import JMSBulkModelViewSet -from common.utils import get_object_or_none, get_request_ip +from common.exceptions import JMSException +from common.permissions import IsValidUser from common.permissions import WithBootstrapToken -from ..models import Terminal -from .. import serializers -from .. import exceptions +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', + 'TerminalViewSet', 'TerminalConfig', + 'TerminalRegistrationApi', 'ConnectMethodListApi' ] logger = logging.getLogger(__file__) @@ -42,43 +41,12 @@ class TerminalViewSet(JMSBulkModelViewSet): self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) - def create(self, request, *args, **kwargs): - if isinstance(request.data, list): - raise exceptions.BulkCreateNotSupport() - - name = request.data.get('name') - remote_ip = request.META.get('REMOTE_ADDR') - x_real_ip = request.META.get('X-Real-IP') - remote_addr = x_real_ip or remote_ip - - terminal = get_object_or_none(Terminal, name=name, is_deleted=False) - if terminal: - msg = 'Terminal name %s already used' % name - return Response({'msg': msg}, status=409) - - serializer = self.serializer_class(data={ - 'name': name, 'remote_addr': remote_addr - }) - - if serializer.is_valid(): - terminal = serializer.save() - - # App should use id, token get access key, if accepted - token = uuid.uuid4().hex - cache.set(token, str(terminal.id), 3600) - data = {"id": str(terminal.id), "token": token, "msg": "Need accept"} - return Response(data, status=201) - else: - data = serializer.errors - logger.error("Register terminal error: {}".format(data)) - return Response(data, status=400) - def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) s = self.request.query_params.get('status') if not s: return queryset - filtered_queryset_id = [str(q.id) for q in queryset if q.latest_status == s] + filtered_queryset_id = [str(q.id) for q in queryset if q.load == s] queryset = queryset.filter(id__in=filtered_queryset_id) return queryset @@ -103,3 +71,16 @@ class TerminalRegistrationApi(generics.CreateAPIView): data = {"error": "service account registration disabled"} 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/session/__init__.py b/apps/terminal/api/session/__init__.py new file mode 100644 index 000000000..a046d4b3d --- /dev/null +++ b/apps/terminal/api/session/__init__.py @@ -0,0 +1,4 @@ +from .session import * +from .sharing import * +from .command import * +from .task import * diff --git a/apps/terminal/api/command.py b/apps/terminal/api/session/command.py similarity index 96% rename from apps/terminal/api/command.py rename to apps/terminal/api/session/command.py index 5b60a114a..53fc0ca8f 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/session/command.py @@ -13,11 +13,11 @@ from common.drf.api import JMSBulkModelViewSet from common.utils import get_logger from terminal.backends.command.serializers import InsecureCommandAlertSerializer from terminal.exceptions import StorageInvalid -from ..backends import ( +from terminal.backends import ( get_command_storage, get_multi_command_storage, SessionCommandSerializer, ) -from ..notifications import CommandAlertMessage +from terminal.notifications import CommandAlertMessage logger = get_logger(__name__) __all__ = ['CommandViewSet', 'InsecureCommandAlertAPI'] @@ -26,7 +26,7 @@ __all__ = ['CommandViewSet', 'InsecureCommandAlertAPI'] class CommandQueryMixin: command_store = get_command_storage() filterset_fields = [ - "asset", "system_user", "user", "session", + "asset", "account", "user", "session", "risk_level", "input" ] default_days_ago = 5 @@ -56,7 +56,7 @@ class CommandQueryMixin: multi_command_storage = get_multi_command_storage() queryset = multi_command_storage.filter( date_from=date_from, date_to=date_to, - user=q.get("user"), asset=q.get("asset"), system_user=q.get("system_user"), + user=q.get("user"), asset=q.get("asset"), account=q.get("account"), input=q.get("input"), session=q.get("session_id", q.get('session')), risk_level=self.get_query_risk_level(), org_id=self.get_org_id(), ) @@ -91,7 +91,7 @@ class CommandViewSet(JMSBulkModelViewSet): { "user": "admin", "asset": "localhost", - "system_user": "web", + "account": "web", "session": "xxxxxx", "input": "whoami", "output": "d2hvbWFp", # base64.b64encode(s) diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session/session.py similarity index 94% rename from apps/terminal/api/session.py rename to apps/terminal/api/session/session.py index e99999ab4..25475cdc4 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session/session.py @@ -3,43 +3,44 @@ import os import tarfile -from django.db.models import F -from django.shortcuts import get_object_or_404, reverse -from django.utils.translation import ugettext as _ -from django.utils.encoding import escape_uri_path -from django.http import FileResponse from django.core.files.storage import default_storage +from django.db.models import F +from django.http import FileResponse +from django.shortcuts import get_object_or_404, reverse +from django.utils.encoding import escape_uri_path +from django.utils.translation import ugettext as _ +from rest_framework import generics from rest_framework import viewsets, views -from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated -from rest_framework import generics +from rest_framework.response import Response -from common.utils import data_to_json from common.const.http import GET -from common.utils import get_logger, get_object_or_none -from common.mixins.api import AsyncApiMixin from common.drf.filters import DatetimeRangeFilter from common.drf.renders import PassthroughRenderer +from common.mixins.api import AsyncApiMixin +from common.utils import data_to_json +from common.utils import get_logger, get_object_or_none from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_root_org, tmp_to_org +from terminal import serializers +from terminal.models import Session +from terminal.utils import ( + find_session_replay_local, download_session_replay, + is_session_approver, get_session_replay_url +) from users.models import User -from .. import utils -from ..utils import find_session_replay_local, download_session_replay -from ..models import Session -from .. import serializers -from terminal.utils import is_session_approver __all__ = [ - 'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI', - 'MySessionAPIView', + 'SessionViewSet', 'SessionReplayViewSet', + 'SessionJoinValidateAPI', 'MySessionAPIView', ] logger = get_logger(__name__) class MySessionAPIView(generics.ListAPIView): - permission_classes = (IsAuthenticated, ) + permission_classes = (IsAuthenticated,) serializer_class = serializers.SessionSerializer def get_queryset(self): @@ -55,7 +56,7 @@ class SessionViewSet(OrgBulkModelViewSet): 'display': serializers.SessionDisplaySerializer, } search_fields = [ - "user", "asset", "system_user", "remote_addr", + "user", "asset", "account", "remote_addr", "protocol", "is_finished", 'login_from', ] filterset_fields = search_fields + ['terminal'] @@ -93,7 +94,7 @@ class SessionViewSet(OrgBulkModelViewSet): url_name='replay-download') def download(self, request, *args, **kwargs): session = self.get_object() - local_path, url = utils.get_session_replay_url(session) + local_path, url = get_session_replay_url(session) if local_path is None: return Response({"error": url}, status=404) file = self.prepare_offline_file(session, local_path) @@ -108,7 +109,7 @@ class SessionViewSet(OrgBulkModelViewSet): return response def get_queryset(self): - queryset = super().get_queryset().prefetch_related('terminal')\ + queryset = super().get_queryset().prefetch_related('terminal') \ .annotate(terminal_display=F('terminal__name')) return queryset @@ -168,7 +169,7 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): data = { 'type': tp, 'src': url, 'user': session.user, 'asset': session.asset, - 'system_user': session.system_user, + 'system_user': session.account, 'date_start': session.date_start, 'date_end': session.date_end, 'download_url': download_url, diff --git a/apps/terminal/api/sharing.py b/apps/terminal/api/session/sharing.py similarity index 97% rename from apps/terminal/api/sharing.py rename to apps/terminal/api/session/sharing.py index 1a9545e54..a3d324205 100644 --- a/apps/terminal/api/sharing.py +++ b/apps/terminal/api/session/sharing.py @@ -5,9 +5,8 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ from common.const.http import PATCH -from common.permissions import IsValidUser from orgs.mixins.api import OrgModelViewSet -from .. import serializers, models +from terminal import serializers, models __all__ = ['SessionSharingViewSet', 'SessionJoinRecordsViewSet'] diff --git a/apps/terminal/api/task.py b/apps/terminal/api/session/task.py similarity index 96% rename from apps/terminal/api/task.py rename to apps/terminal/api/session/task.py index c7e1a2681..80fee6097 100644 --- a/apps/terminal/api/task.py +++ b/apps/terminal/api/session/task.py @@ -7,10 +7,10 @@ from rest_framework import status from rest_framework.permissions import IsAuthenticated from common.utils import get_object_or_none -from ..models import Session, Task -from .. import serializers -from terminal.utils import is_session_approver from orgs.utils import tmp_to_root_org +from terminal.models import Session, Task +from terminal import serializers +from terminal.utils import is_session_approver __all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI'] logger = logging.getLogger(__file__) diff --git a/apps/terminal/automations/__init__.py b/apps/terminal/automations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/terminal/automations/deploy_applet_host/__init__.py b/apps/terminal/automations/deploy_applet_host/__init__.py new file mode 100644 index 000000000..c5e903f35 --- /dev/null +++ b/apps/terminal/automations/deploy_applet_host/__init__.py @@ -0,0 +1,127 @@ +import datetime +import os + +import yaml +from django.conf import settings +from django.utils import timezone + +from common.db.utils import safe_db_connection +from common.utils import get_logger +from ops.ansible import PlaybookRunner, JMSInventory +from terminal.models import Applet, AppletHostDeployment + +logger = get_logger(__name__) +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class DeployAppletHostManager: + def __init__(self, deployment: AppletHostDeployment, applet: Applet = None): + self.deployment = deployment + self.applet = applet + self.run_dir = self.get_run_dir() + + @staticmethod + def get_run_dir(): + base = os.path.join(settings.ANSIBLE_DIR, "applet_host_deploy") + now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + return os.path.join(base, now) + + def run(self, **kwargs): + self._run(self._run_initial_deploy, **kwargs) + + def install_applet(self, **kwargs): + self._run(self._run_install_applet, **kwargs) + + def _run_initial_deploy(self, **kwargs): + playbook = self.generate_initial_playbook + return self._run_playbook(playbook, **kwargs) + + def _run_install_applet(self, **kwargs): + if self.applet: + generate_playbook = self.generate_install_applet_playbook + else: + generate_playbook = self.generate_install_all_playbook + return self._run_playbook(generate_playbook, **kwargs) + + def generate_initial_playbook(self): + site_url = settings.SITE_URL + download_host = settings.APPLET_DOWNLOAD_HOST + bootstrap_token = settings.BOOTSTRAP_TOKEN + host_id = str(self.deployment.host.id) + if not site_url: + site_url = "http://localhost:8080" + if not download_host: + download_host = site_url + options = self.deployment.host.deploy_options + site_url = site_url.rstrip("/") + download_host = download_host.rstrip("/") + + def handler(plays): + for play in plays: + play["vars"].update(options) + play["vars"]["APPLET_DOWNLOAD_HOST"] = download_host + play["vars"]["CORE_HOST"] = site_url + play["vars"]["BOOTSTRAP_TOKEN"] = bootstrap_token + play["vars"]["HOST_ID"] = host_id + play["vars"]["HOST_NAME"] = self.deployment.host.name + return plays + + return self._generate_playbook("playbook.yml", handler) + + def generate_install_all_playbook(self): + return self._generate_playbook("install_all.yml") + + def generate_install_applet_playbook(self): + applet_name = self.applet.name + options = self.deployment.host.deploy_options + + def handler(plays): + for play in plays: + play["vars"].update(options) + play["vars"]["applet_name"] = applet_name + return plays + + return self._generate_playbook("install_applet.yml", handler) + + def generate_inventory(self): + inventory = JMSInventory( + [self.deployment.host], account_policy="privileged_only" + ) + inventory_dir = os.path.join(self.run_dir, "inventory") + inventory_path = os.path.join(inventory_dir, "hosts.yml") + inventory.write_to_file(inventory_path) + return inventory_path + + def _generate_playbook(self, playbook_template_name, plays_handler: callable = None): + playbook_src = os.path.join(CURRENT_DIR, playbook_template_name) + with open(playbook_src) as f: + plays = yaml.safe_load(f) + if plays_handler: + plays = plays_handler(plays) + playbook_dir = os.path.join(self.run_dir, "playbook") + playbook_dst = os.path.join(playbook_dir, "main.yml") + os.makedirs(playbook_dir, exist_ok=True) + with open(playbook_dst, "w") as f: + yaml.safe_dump(plays, f) + return playbook_dst + + def _run_playbook(self, generate_playbook: callable, **kwargs): + inventory = self.generate_inventory() + playbook = generate_playbook() + runner = PlaybookRunner( + inventory=inventory, playbook=playbook, project_dir=self.run_dir + ) + return runner.run(**kwargs) + + def _run(self, cb_func: callable, **kwargs): + try: + self.deployment.date_start = timezone.now() + cb = cb_func(**kwargs) + self.deployment.status = cb.status + except Exception as e: + logger.error("Error: {}".format(e)) + self.deployment.status = "error" + finally: + self.deployment.date_finished = timezone.now() + with safe_db_connection(): + self.deployment.save() diff --git a/apps/terminal/automations/deploy_applet_host/install_all.yml b/apps/terminal/automations/deploy_applet_host/install_all.yml new file mode 100644 index 000000000..bf3da06b4 --- /dev/null +++ b/apps/terminal/automations/deploy_applet_host/install_all.yml @@ -0,0 +1,8 @@ +--- + +- hosts: all + + tasks: + - name: Install all applets + ansible.windows.win_shell: + "tinkerd install all" diff --git a/apps/terminal/automations/deploy_applet_host/install_applet.yml b/apps/terminal/automations/deploy_applet_host/install_applet.yml new file mode 100644 index 000000000..5c216773f --- /dev/null +++ b/apps/terminal/automations/deploy_applet_host/install_applet.yml @@ -0,0 +1,11 @@ +--- + +- hosts: all + vars: + applet_name: chrome + + tasks: + - name: Install applet + ansible.windows.win_shell: + "tinkerd install --name {{ applet_name }}" + when: applet_name != 'all' diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml new file mode 100644 index 000000000..3d86ea52a --- /dev/null +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -0,0 +1,181 @@ +--- + +- hosts: all + vars: + APPLET_DOWNLOAD_HOST: https://demo.jumpserver.org + HOST_NAME: test + HOST_ID: 00000000-0000-0000-0000-000000000000 + CORE_HOST: https://demo.jumpserver.org + BOOTSTRAP_TOKEN: PleaseChangeMe + RDS_Licensing: true + RDS_LicenseServer: 127.0.0.1 + RDS_LicensingMode: 4 + RDS_fSingleSessionPerUser: 1 + RDS_MaxDisconnectionTime: 60000 + RDS_RemoteAppLogoffTimeLimit: 0 + TinkerInstaller: Tinker_Installer_v0.0.1.exe + + tasks: + - name: Install RDS-Licensing (RDS) + ansible.windows.win_feature: + name: RDS-Licensing + state: present + include_management_tools: yes + when: RDS_Licensing + + - name: Install RDS-RD-Server (RDS) + ansible.windows.win_feature: + name: RDS-RD-Server + state: present + include_management_tools: yes + register: rds_install + + - name: Download JumpServer Tinker installer (jumpserver) + ansible.windows.win_get_url: + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/{{ TinkerInstaller }}" + dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" + + - name: Install JumpServer Tinker (jumpserver) + ansible.windows.win_package: + path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" + arguments: + - /VERYSILENT + - /SUPPRESSMSGBOXES + - /NORESTART + state: present + + - name: Set remote-server on the global system path (remote-server) + ansible.windows.win_path: + elements: + - '%USERPROFILE%\AppData\Local\Programs\Tinker\' + scope: user + + - name: Download python-3.10.8 + ansible.windows.win_get_url: + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/python-3.10.8-amd64.exe" + dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + + - name: Install the python-3.10.8 + ansible.windows.win_package: + path: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + product_id: '{371d0d73-d418-4ffe-b280-58c3e7987525}' + arguments: + - /quiet + - InstallAllUsers=1 + - PrependPath=1 + - Include_test=0 + - Include_launcher=0 + state: present + register: win_install_python + + - name: Reboot if installing requires it + ansible.windows.win_reboot: + post_reboot_delay: 10 + test_command: whoami + when: rds_install.reboot_required or win_install_python.reboot_required + + - name: Set RDS LicenseServer (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: LicenseServers + data: "{{ RDS_LicenseServer }}" + type: string + + - name: Set RDS LicensingMode (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: LicensingMode + data: "{{ RDS_LicensingMode }}" + type: dword + + - name: Set RDS fSingleSessionPerUser (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: fSingleSessionPerUser + data: "{{ RDS_fSingleSessionPerUser }}" + type: dword + + - name: Set RDS MaxDisconnectionTime (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: MaxDisconnectionTime + data: "{{ RDS_MaxDisconnectionTime }}" + type: dword + when: RDS_MaxDisconnectionTime >= 60000 + + - name: Set RDS RemoteAppLogoffTimeLimit (regedit) + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services + name: RemoteAppLogoffTimeLimit + data: "{{ RDS_RemoteAppLogoffTimeLimit }}" + type: dword + + - name: Download pip packages + ansible.windows.win_get_url: + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/pip_packages.zip" + dest: "{{ ansible_env.TEMP }}\\pip_packages.zip" + + - name: Unzip pip_packages + community.windows.win_unzip: + src: "{{ ansible_env.TEMP }}\\pip_packages.zip" + dest: "{{ ansible_env.TEMP }}\\pip_packages" + + - name: Install python requirements offline + ansible.windows.win_shell: > + pip install -r '{{ ansible_env.TEMP }}\pip_packages\requirements.txt' + --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages' + + - name: Download chromedriver (chrome) + ansible.windows.win_get_url: + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/chromedriver_win32.zip" + dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip" + + - name: Unzip chromedriver (chrome) + community.windows.win_unzip: + src: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip" + dest: C:\Program Files\JumpServer\drivers + + - name: Set chromedriver on the global system path (chrome) + ansible.windows.win_path: + elements: + - 'C:\Program Files\JumpServer\drivers' + + - name: Download chrome msi package (chrome) + ansible.windows.win_get_url: + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/googlechromestandaloneenterprise64.msi" + dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" + + - name: Install chrome (chrome) + ansible.windows.win_package: + path: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" + state: present + arguments: + - /quiet + + - name: Generate tinkerd component config + ansible.windows.win_shell: + "tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} + --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}" + + - name: Install tinkerd service + ansible.windows.win_shell: + "tinkerd service install" + + - name: Start tinkerd service + ansible.windows.win_shell: + "tinkerd service start" + + - name: Wait Tinker api health + ansible.windows.win_uri: + url: http://localhost:6068/api/health/ + status_code: 200 + method: GET + register: _result + until: _result.status_code == 200 + retries: 30 + delay: 5 + + - name: Sync all remote applets + ansible.windows.win_shell: + "tinkerd install all" + diff --git a/apps/terminal/backends/command/base.py b/apps/terminal/backends/command/base.py index 4bb85e127..a07c7090a 100644 --- a/apps/terminal/backends/command/base.py +++ b/apps/terminal/backends/command/base.py @@ -15,13 +15,13 @@ class CommandBase(object): @abc.abstractmethod def filter(self, date_from=None, date_to=None, - user=None, asset=None, system_user=None, + user=None, asset=None, account=None, input=None, session=None, risk_level=None, org_id=None): pass @abc.abstractmethod def count(self, date_from=None, date_to=None, - user=None, asset=None, system_user=None, + user=None, asset=None, account=None, input=None, session=None): pass diff --git a/apps/terminal/backends/command/db.py b/apps/terminal/backends/command/db.py index 8b11569e9..1cdc56bda 100644 --- a/apps/terminal/backends/command/db.py +++ b/apps/terminal/backends/command/db.py @@ -21,7 +21,7 @@ class CommandStore(CommandBase): """ self.model.objects.create( user=command["user"], asset=command["asset"], - system_user=command["system_user"], input=command["input"], + account=command["account"], input=command["input"], output=command["output"], session=command["session"], risk_level=command.get("risk_level", 0), org_id=command["org_id"], timestamp=command["timestamp"] @@ -36,7 +36,7 @@ class CommandStore(CommandBase): cmd_input = pretty_string(c['input']) cmd_output = pretty_string(c['output'], max_length=1024) _commands.append(self.model( - user=c["user"], asset=c["asset"], system_user=c["system_user"], + user=c["user"], asset=c["asset"], account=c["account"], input=cmd_input, output=cmd_output, session=c["session"], risk_level=c.get("risk_level", 0), org_id=c["org_id"], timestamp=c["timestamp"] @@ -64,7 +64,7 @@ class CommandStore(CommandBase): @staticmethod def make_filter_kwargs( date_from=None, date_to=None, - user=None, asset=None, system_user=None, + user=None, asset=None, account=None, input=None, session=None, risk_level=None, org_id=None): filter_kwargs = {} date_from_default = timezone.now() - datetime.timedelta(days=7) @@ -87,8 +87,8 @@ class CommandStore(CommandBase): filter_kwargs["user__startswith"] = user if asset: filter_kwargs['asset'] = asset - if system_user: - filter_kwargs['system_user'] = system_user + if account: + filter_kwargs['account'] = account if input: filter_kwargs['input__icontains'] = input if session: @@ -100,22 +100,22 @@ class CommandStore(CommandBase): return filter_kwargs def filter(self, date_from=None, date_to=None, - user=None, asset=None, system_user=None, + user=None, asset=None, account=None, input=None, session=None, risk_level=None, org_id=None): filter_kwargs = self.make_filter_kwargs( date_from=date_from, date_to=date_to, user=user, - asset=asset, system_user=system_user, input=input, + asset=asset, account=account, input=input, session=session, risk_level=risk_level, org_id=org_id, ) queryset = self.model.objects.filter(**filter_kwargs) return queryset def count(self, date_from=None, date_to=None, - user=None, asset=None, system_user=None, + user=None, asset=None, account=None, input=None, session=None): filter_kwargs = self.make_filter_kwargs( date_from=date_from, date_to=date_to, user=user, - asset=asset, system_user=system_user, input=input, + asset=asset, account=account, input=input, session=session, ) count = self.model.objects.filter(**filter_kwargs).count() diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index 3601e3894..f87d169ff 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -37,7 +37,7 @@ class CommandStore(ES): def make_data(command): data = dict( user=command["user"], asset=command["asset"], - system_user=command["system_user"], input=command["input"], + account=command["account"], input=command["input"], output=command["output"], risk_level=command["risk_level"], session=command["session"], timestamp=command["timestamp"], org_id=command["org_id"] diff --git a/apps/terminal/backends/command/models.py b/apps/terminal/backends/command/models.py index d6eb7a458..bf9c56f81 100644 --- a/apps/terminal/backends/command/models.py +++ b/apps/terminal/backends/command/models.py @@ -19,7 +19,7 @@ class AbstractSessionCommand(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=64, db_index=True, verbose_name=_("User")) asset = models.CharField(max_length=128, db_index=True, verbose_name=_("Asset")) - system_user = models.CharField(max_length=64, db_index=True, verbose_name=_("System user")) + account = models.CharField(max_length=64, db_index=True, verbose_name=_("Account")) input = models.CharField(max_length=128, db_index=True, verbose_name=_("Input")) output = models.CharField(max_length=1024, blank=True, verbose_name=_("Output")) session = models.CharField(max_length=36, db_index=True, verbose_name=_("Session")) diff --git a/apps/terminal/backends/command/serializers.py b/apps/terminal/backends/command/serializers.py index 83b9d55d2..ccf6984b8 100644 --- a/apps/terminal/backends/command/serializers.py +++ b/apps/terminal/backends/command/serializers.py @@ -33,7 +33,8 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer): """使用这个类作为基础Command Log Serializer类, 用来序列化""" id = serializers.UUIDField(read_only=True) - system_user = serializers.CharField(label=_("System user")) # 限制 64 字符,不能直接迁移成 128 字符,命令表数据量会比较大 + # 限制 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')) @@ -45,7 +46,7 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer): risk_mapper = dict(AbstractSessionCommand.RISK_LEVEL_CHOICES) return risk_mapper.get(obj.risk_level) - def validate_system_user(self, value): + def validate_account(self, value): if len(value) > 64: value = pretty_string(value, 64) return value diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 7289f3180..c5ceb3c94 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -1,14 +1,18 @@ # -*- 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 ReplayStorageTypeChoices(TextChoices): +class ReplayStorageType(TextChoices): null = 'null', 'Null', server = 'server', 'Server' s3 = 's3', 'S3' @@ -20,7 +24,7 @@ class ReplayStorageTypeChoices(TextChoices): cos = 'cos', 'COS' -class CommandStorageTypeChoices(TextChoices): +class CommandStorageType(TextChoices): null = 'null', 'Null', server = 'server', 'Server' es = 'es', 'Elasticsearch' @@ -29,7 +33,7 @@ class CommandStorageTypeChoices(TextChoices): # Component Status Choices # ------------------------ -class ComponentStatusChoices(TextChoices): +class ComponentLoad(TextChoices): critical = 'critical', _('Critical') high = 'high', _('High') normal = 'normal', _('Normal') @@ -40,7 +44,129 @@ class ComponentStatusChoices(TextChoices): return set(dict(cls.choices).keys()) -class TerminalTypeChoices(TextChoices): +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_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' omnidb = 'omnidb', 'OmniDB' @@ -48,10 +174,114 @@ class TerminalTypeChoices(TextChoices): lion = 'lion', 'Lion' core = 'core', 'Core' celery = 'celery', 'Celery' - magnus = 'magnus', 'Magnus' - razor = 'razor', 'Razor' + magnus = 'magnus', 'Magnus' + razor = 'razor', 'Razor' + tinker = 'tinker', 'Tinker' @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.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/filters.py b/apps/terminal/filters.py index 79f4048fe..a0287b5bd 100644 --- a/apps/terminal/filters.py +++ b/apps/terminal/filters.py @@ -16,7 +16,7 @@ class CommandFilter(filters.FilterSet): class Meta: model = Command fields = [ - 'asset', 'system_user', 'user', 'session', 'risk_level', 'input', + 'asset', 'account', 'user', 'session', 'risk_level', 'input', 'date_from', 'date_to', 'session_id', 'risk_level', 'command_storage_id', ] @@ -49,14 +49,14 @@ class CommandFilter(filters.FilterSet): class CommandFilterForStorageTree(CommandFilter): asset = filters.CharFilter(method='do_nothing') - system_user = filters.CharFilter(method='do_nothing') + account = filters.CharFilter(method='do_nothing') session = filters.CharFilter(method='do_nothing') risk_level = filters.NumberFilter(method='do_nothing') class Meta: model = CommandStorage fields = [ - 'asset', 'system_user', 'user', 'session', 'risk_level', 'input', + 'asset', 'account', 'user', 'session', 'risk_level', 'input', 'date_from', 'date_to', 'session_id', 'risk_level', 'command_storage_id', ] diff --git a/apps/terminal/migrations/0050_auto_20220606_1745.py b/apps/terminal/migrations/0050_auto_20220606_1745.py index 88e7cc138..e88d37971 100644 --- a/apps/terminal/migrations/0050_auto_20220606_1745.py +++ b/apps/terminal/migrations/0050_auto_20220606_1745.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='terminal', name='type', - field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), ('razor', 'Razor')], default='koko', max_length=64, verbose_name='type'), + field=models.CharField(choices=[ + ('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), + ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), + ('magnus', 'Magnus'), ('razor', 'Razor'), ('tinker', 'Tinker'), + ], default='koko', max_length=64, verbose_name='type'), ), ] diff --git a/apps/terminal/migrations/0052_auto_20220713_1417.py b/apps/terminal/migrations/0052_auto_20220713_1417.py index 87ad6ba6a..c30032c23 100644 --- a/apps/terminal/migrations/0052_auto_20220713_1417.py +++ b/apps/terminal/migrations/0052_auto_20220713_1417.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.12 on 2022-07-13 06:17 +# Generated by Django 3.1.14 on 2022-04-07 09:26 import common.db.fields import django.core.validators @@ -6,7 +6,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('terminal', '0051_sessionsharing_users'), ] @@ -15,11 +14,15 @@ class Migration(migrations.Migration): migrations.AddField( model_name='endpoint', name='oracle_11g_port', - field=common.db.fields.PortField(default=15211, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g Port'), + field=common.db.fields.PortField(default=15211, validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g Port'), ), migrations.AddField( model_name='endpoint', name='oracle_12c_port', - field=common.db.fields.PortField(default=15212, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c Port'), + field=common.db.fields.PortField(default=15212, validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c Port'), ), ] diff --git a/apps/terminal/migrations/0053_auto_20220830_1244.py b/apps/terminal/migrations/0053_auto_20220830_1244.py new file mode 100644 index 000000000..90c188df9 --- /dev/null +++ b/apps/terminal/migrations/0053_auto_20220830_1244.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.13 on 2022-08-30 04:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0052_auto_20220713_1417'), + ] + + operations = [ + migrations.AlterField( + model_name='session', + name='protocol', + field=models.CharField(db_index=True, default='ssh', max_length=16), + ), + migrations.RenameField( + model_name='session', + old_name='system_user', + new_name='account', + ), + migrations.RemoveField( + model_name='session', + name='system_user_id', + ), + migrations.AlterField( + model_name='session', + name='account', + field=models.CharField(db_index=True, max_length=128, verbose_name='Account'), + ), + ] diff --git a/apps/terminal/migrations/0054_auto_20221027_1125.py b/apps/terminal/migrations/0054_auto_20221027_1125.py new file mode 100644 index 000000000..9fde61be2 --- /dev/null +++ b/apps/terminal/migrations/0054_auto_20221027_1125.py @@ -0,0 +1,88 @@ +# Generated by Django 3.2.14 on 2022-10-27 03:25 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0107_auto_20221019_1115'), + ('terminal', '0053_auto_20220830_1244'), + ] + + operations = [ + migrations.CreateModel( + name='Applet', + 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(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)), + ('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), + ('display_name', models.CharField(max_length=128, verbose_name='Display name')), + ('version', models.CharField(max_length=16, verbose_name='Version')), + ('author', models.CharField(max_length=128, verbose_name='Author')), + ('type', models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, verbose_name='Type')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('protocols', models.JSONField(default=list, verbose_name='Protocol')), + ('tags', models.JSONField(default=list, verbose_name='Tags')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AppletHost', + fields=[ + ('host_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.host')), + ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), + ('status', models.CharField(max_length=16, verbose_name='Status')), + ], + options={ + 'abstract': False, + }, + bases=('assets.host',), + ), + migrations.CreateModel( + name='AppletPublication', + 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(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)), + ('status', models.CharField(default='', max_length=16, verbose_name='Status')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applet', verbose_name='Applet')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applethost', verbose_name='Host')), + ], + options={ + 'unique_together': {('applet', 'host')}, + }, + ), + migrations.CreateModel( + name='AppletHostDeployment', + 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(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)), + ('status', models.CharField(max_length=16, default='', verbose_name='Status')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', verbose_name='Hosting')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='applethost', + name='applets', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', verbose_name='Applet'), + ), + ] diff --git a/apps/terminal/migrations/0055_auto_20221031_1848.py b/apps/terminal/migrations/0055_auto_20221031_1848.py new file mode 100644 index 000000000..e36ed5a9b --- /dev/null +++ b/apps/terminal/migrations/0055_auto_20221031_1848.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.14 on 2022-10-31 10:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0054_auto_20221027_1125'), + ] + + operations = [ + migrations.AddField( + model_name='applet', + name='hosts', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.AppletHost', verbose_name='Hosts'), + ), + migrations.AddField( + model_name='applethost', + name='date_inited', + field=models.DateTimeField(blank=True, null=True, verbose_name='Date inited'), + ), + migrations.AddField( + model_name='applethost', + name='inited', + field=models.BooleanField(default=False, verbose_name='Inited'), + ), + migrations.AddField( + model_name='applethostdeployment', + name='date_finished', + field=models.DateTimeField(null=True, verbose_name='Date finished'), + ), + migrations.AddField( + model_name='applethostdeployment', + name='date_start', + field=models.DateTimeField(db_index=True, null=True, verbose_name='Date start'), + ), + migrations.AlterField( + model_name='appletpublication', + name='applet', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applet', verbose_name='Applet'), + ), + migrations.AlterField( + model_name='appletpublication', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'), + ), + ] diff --git a/apps/terminal/migrations/0056_auto_20221101_1353.py b/apps/terminal/migrations/0056_auto_20221101_1353.py new file mode 100644 index 000000000..798420e2c --- /dev/null +++ b/apps/terminal/migrations/0056_auto_20221101_1353.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-11-01 05:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0055_auto_20221031_1848'), + ] + + operations = [ + migrations.AddField( + model_name='applethost', + name='deploy_options', + field=models.JSONField(default=dict, verbose_name='Deploy options'), + ), + migrations.AddField( + model_name='applethostdeployment', + name='initial', + field=models.BooleanField(default=False, verbose_name='Initial'), + ), + ] diff --git a/apps/terminal/migrations/0057_auto_20221102_1941.py b/apps/terminal/migrations/0057_auto_20221102_1941.py new file mode 100644 index 000000000..56f1e699a --- /dev/null +++ b/apps/terminal/migrations/0057_auto_20221102_1941.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.14 on 2022-11-02 11:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0056_auto_20221101_1353'), + ] + + operations = [ + migrations.AddField( + model_name='applethost', + name='terminal', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='applet_host', to='terminal.terminal', verbose_name='Terminal'), + ), + migrations.AlterField( + model_name='appletpublication', + name='status', + field=models.CharField(default='ready', max_length=16, verbose_name='Status'), + ), + ] diff --git a/apps/terminal/migrations/0058_auto_20221103_1624.py b/apps/terminal/migrations/0058_auto_20221103_1624.py new file mode 100644 index 000000000..0c8091e5c --- /dev/null +++ b/apps/terminal/migrations/0058_auto_20221103_1624.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.14 on 2022-11-03 08:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0057_auto_20221102_1941'), + ] + + operations = [ + migrations.AlterModelOptions( + name='terminal', + options={'permissions': (('view_terminalconfig', 'Can view terminal config'),), 'verbose_name': 'Terminal'}, + ), + migrations.RemoveField( + model_name='terminal', + name='http_port', + ), + migrations.RemoveField( + model_name='terminal', + name='is_accepted', + ), + migrations.RemoveField( + model_name='terminal', + name='ssh_port', + ), + migrations.RemoveField( + model_name='applethost', + name='status', + ), + ] diff --git a/apps/terminal/migrations/0059_applethostdeployment_task.py b/apps/terminal/migrations/0059_applethostdeployment_task.py new file mode 100644 index 000000000..5f455c9c6 --- /dev/null +++ b/apps/terminal/migrations/0059_applethostdeployment_task.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-15 05:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0058_auto_20221103_1624'), + ] + + operations = [ + migrations.AddField( + model_name='applethostdeployment', + name='task', + field=models.UUIDField(null=True, verbose_name='Task'), + ), + ] diff --git a/apps/terminal/migrations/0060_alter_applethostdeployment_options.py b/apps/terminal/migrations/0060_alter_applethostdeployment_options.py new file mode 100644 index 000000000..c38e2ba29 --- /dev/null +++ b/apps/terminal/migrations/0060_alter_applethostdeployment_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.14 on 2022-11-18 02:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0059_applethostdeployment_task'), + ] + + operations = [ + migrations.AlterModelOptions( + name='applethostdeployment', + options={'ordering': ('-date_start',)}, + ), + ] diff --git a/apps/terminal/migrations/0061_rename_system_user_command_account.py b/apps/terminal/migrations/0061_rename_system_user_command_account.py new file mode 100644 index 000000000..ab7ee1f32 --- /dev/null +++ b/apps/terminal/migrations/0061_rename_system_user_command_account.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-12-05 05:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0060_alter_applethostdeployment_options'), + ] + + operations = [ + migrations.RenameField( + model_name='command', + old_name='system_user', + new_name='account', + ), + migrations.AlterField( + model_name='command', + name='account', + field=models.CharField(db_index=True, max_length=64, verbose_name='Account'), + ), + ] diff --git a/apps/terminal/models/__init__.py b/apps/terminal/models/__init__.py index be079721d..268727394 100644 --- a/apps/terminal/models/__init__.py +++ b/apps/terminal/models/__init__.py @@ -1,9 +1,3 @@ -from .command import * from .session import * -from .status import * -from .storage import * -from .task import * -from .terminal import * -from .sharing import * -from .replay import * -from .endpoint import * +from .component import * +from .applet import * diff --git a/apps/terminal/models/applet/__init__.py b/apps/terminal/models/applet/__init__.py new file mode 100644 index 000000000..b2a4cac34 --- /dev/null +++ b/apps/terminal/models/applet/__init__.py @@ -0,0 +1,2 @@ +from .applet import * +from .host import * diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py new file mode 100644 index 000000000..cf854b036 --- /dev/null +++ b/apps/terminal/models/applet/applet.py @@ -0,0 +1,64 @@ +import yaml +import os.path + +from django.conf import settings +from django.core.files.storage import default_storage +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.db.models import JMSBaseModel + + +__all__ = ['Applet', 'AppletPublication'] + + +class Applet(JMSBaseModel): + class Type(models.TextChoices): + general = 'general', _('General') + web = 'web', _('Web') + + name = models.SlugField(max_length=128, verbose_name=_('Name'), unique=True) + display_name = models.CharField(max_length=128, verbose_name=_('Display name')) + version = models.CharField(max_length=16, verbose_name=_('Version')) + 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')) + 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')) + hosts = models.ManyToManyField( + through_fields=('applet', 'host'), through='AppletPublication', + to='AppletHost', verbose_name=_('Hosts') + ) + + def __str__(self): + return self.name + + @property + def path(self): + return default_storage.path('applets/{}'.format(self.name)) + + @property + def manifest(self): + path = os.path.join(self.path, 'manifest.yml') + if not os.path.exists(path): + return None + with open(path, 'r') as f: + return yaml.safe_load(f) + + @property + def icon(self): + path = os.path.join(self.path, 'icon.png') + if not os.path.exists(path): + return None + return os.path.join(settings.MEDIA_URL, 'applets', self.name, 'icon.png') + + +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')) + status = models.CharField(max_length=16, default='ready', verbose_name=_('Status')) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + + class Meta: + unique_together = ('applet', 'host') diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py new file mode 100644 index 000000000..020c4d98a --- /dev/null +++ b/apps/terminal/models/applet/host.py @@ -0,0 +1,130 @@ +import os +from collections import defaultdict + +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import ValidationError +from simple_history.utils import bulk_create_with_history + +from assets.models import Host +from common.db.models import JMSBaseModel +from common.utils import random_string + +__all__ = ['AppletHost', 'AppletHostDeployment'] + + +class AppletHost(Host): + deploy_options = models.JSONField(default=dict, verbose_name=_('Deploy options')) + inited = models.BooleanField(default=False, verbose_name=_('Inited')) + date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date inited')) + date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced')) + terminal = models.OneToOneField( + 'terminal.Terminal', on_delete=models.PROTECT, null=True, blank=True, + related_name='applet_host', verbose_name=_('Terminal') + ) + applets = models.ManyToManyField( + 'Applet', verbose_name=_('Applet'), + through='AppletPublication', through_fields=('host', 'applet'), + ) + LOCKING_ORG = '00000000-0000-0000-0000-000000000004' + + def __str__(self): + return self.name + + @property + def load(self): + if not self.terminal: + return 'offline' + return self.terminal.load + + def check_terminal_binding(self, request): + request_terminal = getattr(request.user, 'terminal', None) + if not request_terminal: + raise ValidationError('Request user has no terminal') + + self.date_synced = timezone.now() + if self.terminal == request_terminal: + self.save(update_fields=['date_synced']) + else: + self.terminal = request_terminal + self.save(update_fields=['terminal', 'date_synced']) + + def check_applets_state(self, applets_value_list): + applets = self.applets.all() + name_version_mapper = { + value['name']: value['version'] + for value in applets_value_list + } + + status_applets = defaultdict(list) + for applet in applets: + if applet.name not in name_version_mapper: + status_applets['unpublished'].append(applet) + elif applet.version != name_version_mapper[applet.name]: + status_applets['not_match'].append(applet) + else: + status_applets['published'].append(applet) + + for status, applets in status_applets.items(): + self.publications.filter(applet__in=applets) \ + .exclude(status=status) \ + .update(status=status) + + @staticmethod + def random_username(): + return 'jms_' + random_string(8) + + @staticmethod + def random_password(): + return random_string(16, special_char=True) + + def generate_accounts(self): + amount = int(os.getenv('TERMINAL_ACCOUNTS_AMOUNT', 100)) + now_count = self.accounts.filter(privileged=False).count() + need = amount - now_count + + accounts = [] + account_model = self.accounts.model + for i in range(need): + username = self.random_username() + password = self.random_password() + account = account_model( + username=username, secret=password, name=username, + asset_id=self.id, secret_type='password', version=1, + org_id=self.LOCKING_ORG, is_active=False, + ) + accounts.append(account) + bulk_create_with_history(accounts, account_model, batch_size=20) + + +class AppletHostDeployment(JMSBaseModel): + host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting')) + initial = models.BooleanField(default=False, verbose_name=_('Initial')) + status = models.CharField(max_length=16, default='', verbose_name=_('Status')) + date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) + date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + task = models.UUIDField(null=True, verbose_name=_('Task')) + + class Meta: + ordering = ('-date_start',) + + def start(self, **kwargs): + from ...automations.deploy_applet_host import DeployAppletHostManager + manager = DeployAppletHostManager(self) + manager.run(**kwargs) + + def install_applet(self, applet_id, **kwargs): + from ...automations.deploy_applet_host import DeployAppletHostManager + from .applet import Applet + if applet_id: + applet = Applet.objects.get(id=applet_id) + else: + applet = None + manager = DeployAppletHostManager(self, applet=applet) + manager.install_applet(**kwargs) + + def save_task(self, task): + self.task = task + self.save(update_fields=['task']) diff --git a/apps/terminal/models/component/__init__.py b/apps/terminal/models/component/__init__.py new file mode 100644 index 000000000..b136a5da3 --- /dev/null +++ b/apps/terminal/models/component/__init__.py @@ -0,0 +1,5 @@ +from .terminal import * +from .task import * +from .endpoint import * +from .status import * +from .storage import * diff --git a/apps/terminal/models/endpoint.py b/apps/terminal/models/component/endpoint.py similarity index 97% rename from apps/terminal/models/endpoint.py rename to apps/terminal/models/component/endpoint.py index fec3a1282..cb0f6bd6f 100644 --- a/apps/terminal/models/endpoint.py +++ b/apps/terminal/models/component/endpoint.py @@ -1,16 +1,16 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator -from applications.models import Application -from ..utils import db_port_manager, DBPortManager -from common.db.models import JMSModel + +from common.db.models import JMSBaseModel from common.db.fields import PortField from common.utils.ip import contains_ip +from ..utils import db_port_manager, DBPortManager db_port_manager: DBPortManager -class Endpoint(JMSModel): +class Endpoint(JMSBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) host = models.CharField(max_length=256, blank=True, verbose_name=_('Host')) # value=0 表示 disabled @@ -83,7 +83,7 @@ class Endpoint(JMSModel): return endpoint -class EndpointRule(JMSModel): +class EndpointRule(JMSBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) ip_group = models.JSONField(default=list, verbose_name=_('IP group')) priority = models.IntegerField( diff --git a/apps/terminal/models/component/status.py b/apps/terminal/models/component/status.py new file mode 100644 index 000000000..3da13d9f0 --- /dev/null +++ b/apps/terminal/models/component/status.py @@ -0,0 +1,29 @@ +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger + + +logger = get_logger(__name__) + + +class Status(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + session_online = models.IntegerField(verbose_name=_("Session Online"), default=0) + cpu_load = models.FloatField(verbose_name=_("CPU Load"), default=0) + memory_used = models.FloatField(verbose_name=_("Memory Used")) + disk_used = models.FloatField(verbose_name=_("Disk Used"), default=0) + connections = models.IntegerField(verbose_name=_("Connections"), default=0) + threads = models.IntegerField(verbose_name=_("Threads"), default=0) + boot_time = models.FloatField(verbose_name=_("Boot Time"), default=0) + terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.CASCADE) + date_created = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = 'terminal_status' + get_latest_by = 'date_created' + verbose_name = _("Status") + + diff --git a/apps/terminal/models/storage.py b/apps/terminal/models/component/storage.py similarity index 85% rename from apps/terminal/models/storage.py rename to apps/terminal/models/component/storage.py index 0793a7325..b6878fb64 100644 --- a/apps/terminal/models/storage.py +++ b/apps/terminal/models/component/storage.py @@ -1,14 +1,13 @@ from __future__ import unicode_literals - import copy import os - from importlib import import_module import jms_storage 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.plugins.es import QuerySet as ESQuerySet from common.utils import get_logger @@ -16,8 +15,8 @@ from common.db.fields import EncryptJsonDictTextField from common.utils.timezone import local_now_date_display from terminal.backends import TYPE_ENGINE_MAPPING from .terminal import Terminal -from .command import Command -from .. import const +from ..session.command import Command +from terminal import const logger = get_logger(__file__) @@ -54,21 +53,21 @@ class CommonStorageModelMixin(models.Model): class CommandStorage(CommonStorageModelMixin, CommonModelMixin): type = models.CharField( - max_length=16, choices=const.CommandStorageTypeChoices.choices, - default=const.CommandStorageTypeChoices.server.value, verbose_name=_('Type'), + max_length=16, choices=const.CommandStorageType.choices, + default=const.CommandStorageType.server.value, verbose_name=_('Type'), ) @property def type_null(self): - return self.type == const.CommandStorageTypeChoices.null.value + return self.type == const.CommandStorageType.null.value @property def type_server(self): - return self.type == const.CommandStorageTypeChoices.server.value + return self.type == const.CommandStorageType.server.value @property def type_es(self): - return self.type == const.CommandStorageTypeChoices.es.value + return self.type == const.CommandStorageType.es.value @property def type_null_or_server(self): @@ -143,17 +142,17 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin): class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): type = models.CharField( - max_length=16, choices=const.ReplayStorageTypeChoices.choices, - default=const.ReplayStorageTypeChoices.server.value, verbose_name=_('Type') + max_length=16, choices=const.ReplayStorageType.choices, + default=const.ReplayStorageType.server.value, verbose_name=_('Type') ) @property def type_null(self): - return self.type == const.ReplayStorageTypeChoices.null.value + return self.type == const.ReplayStorageType.null.value @property def type_server(self): - return self.type == const.ReplayStorageTypeChoices.server.value + return self.type == const.ReplayStorageType.server.value @property def type_null_or_server(self): @@ -161,11 +160,11 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): @property def type_swift(self): - return self.type == const.ReplayStorageTypeChoices.swift.value + return self.type == const.ReplayStorageType.swift.value @property def type_ceph(self): - return self.type == const.ReplayStorageTypeChoices.ceph.value + return self.type == const.ReplayStorageType.ceph.value @property def config(self): @@ -173,7 +172,7 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): # add type config if self.type_ceph: - _type = const.ReplayStorageTypeChoices.s3.value + _type = const.ReplayStorageType.s3.value else: _type = self.type _config.update({'TYPE': _type}) diff --git a/apps/terminal/models/task.py b/apps/terminal/models/component/task.py similarity index 100% rename from apps/terminal/models/task.py rename to apps/terminal/models/component/task.py diff --git a/apps/terminal/models/terminal.py b/apps/terminal/models/component/terminal.py similarity index 75% rename from apps/terminal/models/terminal.py rename to apps/terminal/models/component/terminal.py index 6d7b67815..b5638874a 100644 --- a/apps/terminal/models/terminal.py +++ b/apps/terminal/models/component/terminal.py @@ -1,60 +1,39 @@ +import time import uuid -from django.db import models -from django.core.cache import cache -from django.utils.translation import ugettext_lazy as _ from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger +from common.utils import get_logger, lazyproperty from common.const.signals import SKIP_SIGNAL -from users.models import User from orgs.utils import tmp_to_root_org -from .status import Status -from .. import const -from ..const import ComponentStatusChoices as StatusChoice -from .session import Session - +from terminal.const import TerminalType as TypeChoices +from users.models import User +from ..session import Session logger = get_logger(__file__) class TerminalStatusMixin: - ALIVE_KEY = 'TERMINAL_ALIVE_{}' id: str + ALIVE_KEY = 'TERMINAL_ALIVE_{}' + status_set: models.Manager - @property - def latest_status(self): - return Status.get_terminal_latest_status(self) + @lazyproperty + def last_stat(self): + return self.status_set.order_by('date_created').last() - @property - def latest_status_display(self): - return self.latest_status.label - - @property - def latest_stat(self): - return Status.get_terminal_latest_stat(self) - - @property - def is_normal(self): - return self.latest_status == StatusChoice.normal - - @property - def is_high(self): - return self.latest_status == StatusChoice.high - - @property - def is_critical(self): - return self.latest_status == StatusChoice.critical + @lazyproperty + def load(self): + from ...utils import ComputeLoadUtil + return ComputeLoadUtil.compute_load(self.last_stat) @property def is_alive(self): - key = self.ALIVE_KEY.format(self.id) - # return self.latest_status != StatusChoice.offline - return cache.get(key, False) - - def set_alive(self, ttl=120): - key = self.ALIVE_KEY.format(self.id) - cache.set(key, True, ttl) + if not self.last_stat: + return False + return time.time() - self.last_stat.date_created.timestamp() < 150 class StorageMixin: @@ -100,17 +79,14 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) type = models.CharField( - choices=const.TerminalTypeChoices.choices, default=const.TerminalTypeChoices.koko.value, + choices=TypeChoices.choices, default=TypeChoices.koko, max_length=64, verbose_name=_('type') ) remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) - ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) - http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') user = models.OneToOneField(User, related_name='terminal', verbose_name=_('Application User'), null=True, on_delete=models.CASCADE) is_accepted = models.BooleanField(default=False, verbose_name=_('Is Accepted')) - is_deleted = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) comment = models.TextField(blank=True, verbose_name=_('Comment')) @@ -169,9 +145,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): def __str__(self): status = "Active" - if not self.is_accepted: - status = "NotAccept" - elif self.is_deleted: + if self.is_deleted: status = "Deleted" elif not self.is_active: status = "Disable" @@ -180,10 +154,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): return '%s: %s' % (self.name, status) class Meta: - ordering = ('is_accepted',) db_table = "terminal" verbose_name = _("Terminal") permissions = ( ('view_terminalconfig', _('Can view terminal config')), ) - diff --git a/apps/terminal/models/session/__init__.py b/apps/terminal/models/session/__init__.py new file mode 100644 index 000000000..073c9d078 --- /dev/null +++ b/apps/terminal/models/session/__init__.py @@ -0,0 +1,4 @@ +from .command import * +from .session import * +from .replay import * +from .sharing import * diff --git a/apps/terminal/models/command.py b/apps/terminal/models/session/command.py similarity index 94% rename from apps/terminal/models/command.py rename to apps/terminal/models/session/command.py index 44edf013c..3d377523b 100644 --- a/apps/terminal/models/command.py +++ b/apps/terminal/models/session/command.py @@ -4,7 +4,7 @@ from django.db import models from django.db.models.signals import post_save from django.utils.translation import ugettext_lazy as _ -from ..backends.command.models import AbstractSessionCommand +from terminal.backends.command.models import AbstractSessionCommand class CommandManager(models.Manager): @@ -33,7 +33,7 @@ class Command(AbstractSessionCommand): cls(**{ 'user': random_string(6), 'asset': random_string(10), - 'system_user': random_string(6), + 'account': random_string(6), 'session': str(uuid.uuid4()), 'input': random_string(16), 'output': random_string(64), diff --git a/apps/terminal/models/replay.py b/apps/terminal/models/session/replay.py similarity index 100% rename from apps/terminal/models/replay.py rename to apps/terminal/models/session/replay.py diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session/session.py similarity index 86% rename from apps/terminal/models/session.py rename to apps/terminal/models/session/session.py index 3f6a8c338..0a095a401 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session/session.py @@ -11,12 +11,12 @@ from django.core.files.storage import default_storage from django.core.cache import cache from assets.models import Asset -from applications.models import Application +from assets.const import Protocol from users.models import User from orgs.mixins.models import OrgModelMixin from django.db.models import TextChoices from common.utils import get_object_or_none, lazyproperty -from ..backends import get_multi_command_storage +from terminal.backends import get_multi_command_storage class Session(OrgModelMixin): @@ -26,28 +26,13 @@ class Session(OrgModelMixin): WT = 'WT', 'Web Terminal' DT = 'DT', 'DB Terminal' - class PROTOCOL(TextChoices): - SSH = 'ssh', 'ssh' - RDP = 'rdp', 'rdp' - VNC = 'vnc', 'vnc' - TELNET = 'telnet', 'telnet' - MYSQL = 'mysql', 'mysql' - ORACLE = 'oracle', 'oracle' - MARIADB = 'mariadb', 'mariadb' - SQLSERVER = 'sqlserver', 'sqlserver' - POSTGRESQL = 'postgresql', 'postgresql' - REDIS = 'redis', 'redis' - MONGODB = 'mongodb', 'MongoDB' - CLICKHOUSE = 'clickhouse', 'ClickHouse' - K8S = 'k8s', 'kubernetes' - id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True) user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) asset = models.CharField(max_length=128, verbose_name=_("Asset"), db_index=True) asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True) - system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True) - system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) + 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")) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) is_success = models.BooleanField(default=True, db_index=True) @@ -55,7 +40,6 @@ class Session(OrgModelMixin): has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) has_command = models.BooleanField(default=False, verbose_name=_("Command")) terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.DO_NOTHING, db_constraint=False) - protocol = models.CharField(choices=PROTOCOL.choices, default='ssh', max_length=16, db_index=True) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) @@ -134,29 +118,20 @@ class Session(OrgModelMixin): @property def can_join(self): - _PROTOCOL = self.PROTOCOL if self.is_finished: return False if self.login_from == self.LOGIN_FROM.RT: return False if self.protocol in [ - _PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, - _PROTOCOL.TELNET, _PROTOCOL.K8S + Protocol.ssh, Protocol.vnc, Protocol.rdp, + Protocol.telnet, Protocol.k8s ]: return True else: return False - @property - def db_protocols(self): - _PROTOCOL = self.PROTOCOL - return [_PROTOCOL.MYSQL, _PROTOCOL.MARIADB, _PROTOCOL.ORACLE, - _PROTOCOL.POSTGRESQL, _PROTOCOL.SQLSERVER, _PROTOCOL.CLICKHOUSE, - _PROTOCOL.REDIS, _PROTOCOL.MONGODB] - @property def can_terminate(self): - _PROTOCOL = self.PROTOCOL if self.is_finished: return False else: diff --git a/apps/terminal/models/sharing.py b/apps/terminal/models/session/sharing.py similarity index 100% rename from apps/terminal/models/sharing.py rename to apps/terminal/models/session/sharing.py diff --git a/apps/terminal/models/status.py b/apps/terminal/models/status.py deleted file mode 100644 index 048bca103..000000000 --- a/apps/terminal/models/status.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import unicode_literals - -import uuid - -from django.db import models -from django.forms.models import model_to_dict -from django.core.cache import cache -from django.utils.translation import ugettext_lazy as _ - -from common.utils import get_logger - - -logger = get_logger(__name__) - - -class Status(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - session_online = models.IntegerField(verbose_name=_("Session Online"), default=0) - cpu_load = models.FloatField(verbose_name=_("CPU Load"), default=0) - memory_used = models.FloatField(verbose_name=_("Memory Used")) - disk_used = models.FloatField(verbose_name=_("Disk Used"), default=0) - connections = models.IntegerField(verbose_name=_("Connections"), default=0) - threads = models.IntegerField(verbose_name=_("Threads"), default=0) - boot_time = models.FloatField(verbose_name=_("Boot Time"), default=0) - terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.CASCADE) - date_created = models.DateTimeField(auto_now_add=True) - - CACHE_KEY = 'TERMINAL_STATUS_{}' - - class Meta: - db_table = 'terminal_status' - get_latest_by = 'date_created' - verbose_name = _("Status") - - def save_to_cache(self): - if not self.terminal: - return - key = self.CACHE_KEY.format(self.terminal.id) - data = model_to_dict(self) - cache.set(key, data, 60*3) - return data - - @classmethod - def get_terminal_latest_status(cls, terminal): - from ..utils import ComputeStatUtil - stat = cls.get_terminal_latest_stat(terminal) - return ComputeStatUtil.compute_component_status(stat) - - @classmethod - def get_terminal_latest_stat(cls, terminal): - key = cls.CACHE_KEY.format(terminal.id) - data = cache.get(key) - if not data: - return None - data.pop('terminal', None) - stat = cls(**data) - stat.terminal = terminal - stat.is_alive = terminal.is_alive - stat.keep_one_decimal_place() - return stat - - def keep_one_decimal_place(self): - keys = ['cpu_load', 'memory_used', 'disk_used'] - for key in keys: - value = getattr(self, key, 0) - if not isinstance(value, (int, float)): - continue - value = '%.1f' % value - setattr(self, key, float(value)) - - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - self.terminal.set_alive(ttl=120) - return self.save_to_cache() - # return super().save() - - diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py index e1312ebae..019c3623c 100644 --- a/apps/terminal/serializers/__init__.py +++ b/apps/terminal/serializers/__init__.py @@ -5,3 +5,5 @@ from .session import * from .storage import * from .sharing import * from .endpoint import * +from .applet import * +from .applet_host import * diff --git a/apps/terminal/serializers/applet.py b/apps/terminal/serializers/applet.py new file mode 100644 index 000000000..35af7e07b --- /dev/null +++ b/apps/terminal/serializers/applet.py @@ -0,0 +1,44 @@ +from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ +from django.db import models + +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from ..models import Applet, AppletPublication, AppletHost + + +__all__ = [ + 'AppletSerializer', 'AppletPublicationSerializer', +] + + +class AppletPublicationSerializer(serializers.ModelSerializer): + class Status(models.TextChoices): + PUBLISHED = 'published', _('Published') + UNPUBLISHED = 'unpublished', _('Unpublished') + NOT_MATCH = 'not_match', _('Not match') + + applet = ObjectRelatedField(attrs=('id', 'name', 'display_name', 'icon', 'version'), queryset=Applet.objects.all()) + host = ObjectRelatedField(queryset=AppletHost.objects.all()) + status = LabeledChoiceField(choices=Status.choices, label=_("Status")) + + class Meta: + model = AppletPublication + fields_mini = ['id', 'applet', 'host'] + read_only_fields = ['date_created', 'date_updated'] + fields = fields_mini + ['status', 'comment'] + read_only_fields + + +class AppletSerializer(serializers.ModelSerializer): + icon = serializers.ReadOnlyField(label=_("Icon")) + type = LabeledChoiceField(choices=Applet.Type.choices, label=_("Type")) + + class Meta: + model = Applet + fields_mini = ['id', 'name', 'display_name'] + read_only_fields = [ + 'icon', 'date_created', 'date_updated', + ] + fields = fields_mini + [ + 'version', 'author', 'type', 'protocols', + 'tags', 'comment' + ] + read_only_fields diff --git a/apps/terminal/serializers/applet_host.py b/apps/terminal/serializers/applet_host.py new file mode 100644 index 000000000..c81258892 --- /dev/null +++ b/apps/terminal/serializers/applet_host.py @@ -0,0 +1,122 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from assets.models import Platform, Account +from assets.serializers import HostSerializer +from common.drf.fields import LabeledChoiceField +from common.validators import ProjectUniqueValidator +from .applet import AppletSerializer +from .. import const +from ..models import AppletHost, AppletHostDeployment + +__all__ = [ + 'AppletHostSerializer', 'AppletHostDeploymentSerializer', + 'AppletHostAccountSerializer', 'AppletHostAppletReportSerializer', + 'AppletHostStartupSerializer', 'AppletHostDeployAppletSerializer' +] + + +class DeployOptionsSerializer(serializers.Serializer): + LICENSE_MODE_CHOICES = ( + (4, _('Per Session')), + (2, _('Per Device')), + ) + SESSION_PER_USER = ( + (1, _("Disabled")), + (0, _("Enabled")), + ) + RDS_Licensing = serializers.BooleanField(default=False, label=_("RDS Licensing")) + RDS_LicenseServer = serializers.CharField(default='127.0.0.1', label=_('RDS License Server'), max_length=1024) + RDS_LicensingMode = serializers.ChoiceField(choices=LICENSE_MODE_CHOICES, default=4, label=_('RDS Licensing Mode')) + RDS_fSingleSessionPerUser = serializers.ChoiceField(choices=SESSION_PER_USER, default=1, + label=_("RDS fSingleSessionPerUser")) + RDS_MaxDisconnectionTime = serializers.IntegerField(default=60000, label=_("RDS Max Disconnection Time")) + RDS_RemoteAppLogoffTimeLimit = serializers.IntegerField(default=0, label=_("RDS Remote App Logoff Time Limit")) + + +class AppletHostSerializer(HostSerializer): + deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options")) + load = LabeledChoiceField( + read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices, + ) + + class Meta(HostSerializer.Meta): + model = AppletHost + fields = HostSerializer.Meta.fields + [ + 'load', 'date_synced', 'deploy_options' + ] + extra_kwargs = { + 'date_synced': {'read_only': True} + } + + def __init__(self, *args, data=None, **kwargs): + if data: + self.set_initial_data(data) + kwargs['data'] = data + super().__init__(*args, **kwargs) + + @staticmethod + def set_initial_data(data): + platform = Platform.objects.get(name='RemoteAppHost') + data.update({ + 'platform': platform.id, + 'nodes_display': [ + 'RemoteAppHosts' + ] + }) + + def get_validators(self): + validators = super().get_validators() + # 不知道为啥没有继承过来 + uniq_validator = ProjectUniqueValidator( + queryset=AppletHost.objects.all(), + fields=('org_id', 'name') + ) + validators.append(uniq_validator) + return validators + + +class HostAppletSerializer(AppletSerializer): + publication = serializers.SerializerMethodField() + + class Meta(AppletSerializer.Meta): + fields = AppletSerializer.Meta.fields + ['publication'] + + +class AppletHostDeploymentSerializer(serializers.ModelSerializer): + class Meta: + model = AppletHostDeployment + fields_mini = ['id', 'host', 'status', 'task'] + read_only_fields = [ + 'status', 'date_created', 'date_updated', + 'date_start', 'date_finished' + ] + fields = fields_mini + ['comment'] + read_only_fields + + +class AppletHostDeployAppletSerializer(AppletHostDeploymentSerializer): + applet_id = serializers.UUIDField(write_only=True, allow_null=True, required=False) + + class Meta(AppletHostDeploymentSerializer.Meta): + fields = AppletHostDeploymentSerializer.Meta.fields + ['applet_id'] + + def create(self, validated_data): + applet_id = validated_data.pop('applet_id', None) + deployment = super().create(validated_data) + return deployment + + +class AppletHostAccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['id', 'username', 'secret', 'is_active', 'date_updated'] + + +class AppletHostAppletReportSerializer(serializers.Serializer): + id = serializers.UUIDField(read_only=True) + name = serializers.CharField() + version = serializers.CharField() + + +class AppletHostStartupSerializer(serializers.Serializer): + pass diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index e769d9c5c..148217306 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -2,6 +2,8 @@ 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 ..models import Session __all__ = [ @@ -13,16 +15,15 @@ __all__ = [ 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")) class Meta: model = Session fields_mini = ["id"] fields_small = fields_mini + [ - "user", "asset", "system_user", - "user_id", "asset_id", "system_user_id", - "login_from", "login_from_display", "remote_addr", "protocol", - "is_success", "is_finished", "has_replay", - "date_start", "date_end", + "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", ] fields_fk = ["terminal", ] fields_custom = ["can_replay", "can_join", "can_terminate", 'terminal_display'] @@ -31,7 +32,6 @@ class SessionSerializer(BulkOrgResourceModelSerializer): "protocol": {'label': _('Protocol')}, 'user_id': {'label': _('User ID')}, 'asset_id': {'label': _('Asset ID')}, - 'system_user_id': {'label': _('System user ID')}, 'login_from_display': {'label': _('Login from display')}, 'is_success': {'label': _('Is success')}, 'can_replay': {'label': _('Can replay')}, diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index ff7d386de..cc4478c2d 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -118,13 +118,13 @@ class ReplayStorageTypeAzureSerializer(serializers.Serializer): # mapping replay_storage_type_serializer_classes_mapping = { - const.ReplayStorageTypeChoices.s3.value: ReplayStorageTypeS3Serializer, - const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer, - const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer, - const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer, - const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer, - const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer, - const.ReplayStorageTypeChoices.cos.value: ReplayStorageTypeCOSSerializer + const.ReplayStorageType.s3.value: ReplayStorageTypeS3Serializer, + const.ReplayStorageType.ceph.value: ReplayStorageTypeCephSerializer, + const.ReplayStorageType.swift.value: ReplayStorageTypeSwiftSerializer, + const.ReplayStorageType.oss.value: ReplayStorageTypeOSSSerializer, + const.ReplayStorageType.azure.value: ReplayStorageTypeAzureSerializer, + const.ReplayStorageType.obs.value: ReplayStorageTypeOBSSerializer, + const.ReplayStorageType.cos.value: ReplayStorageTypeCOSSerializer } @@ -172,7 +172,7 @@ class CommandStorageTypeESSerializer(serializers.Serializer): # mapping command_storage_type_serializer_classes_mapping = { - const.CommandStorageTypeChoices.es.value: CommandStorageTypeESSerializer + const.CommandStorageType.es.value: CommandStorageTypeESSerializer } diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index dd6565348..340a0b0f7 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -1,29 +1,27 @@ -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers -from common.drf.serializers import BulkModelSerializer, AdaptedBulkListSerializer -from common.utils import is_uuid +from common.drf.fields import LabeledChoiceField +from common.drf.serializers import BulkModelSerializer +from common.utils import get_request_ip, pretty_string, is_uuid from users.serializers import ServiceAccountSerializer -from common.utils import get_request_ip, pretty_string from .. import const - -from ..models import ( - Terminal, Status, Task, CommandStorage, ReplayStorage -) +from ..models import Terminal, Status, Task, CommandStorage, ReplayStorage -class StatusSerializer(serializers.ModelSerializer): +class StatSerializer(serializers.ModelSerializer): sessions = serializers.ListSerializer( - child=serializers.CharField(max_length=36), write_only=True + child=serializers.CharField(max_length=36), + write_only=True ) class Meta: + model = Status fields_mini = ['id'] fields_write_only = ['sessions', ] fields_small = fields_mini + fields_write_only + [ 'cpu_load', 'memory_used', 'disk_used', - 'session_online', - 'date_created' + 'session_online', 'date_created' ] fields_fk = ['terminal'] fields = fields_small + fields_fk @@ -32,30 +30,28 @@ class StatusSerializer(serializers.ModelSerializer): "memory_used": {'default': 0}, "disk_used": {'default': 0}, } - model = Status class TerminalSerializer(BulkModelSerializer): session_online = serializers.ReadOnlyField(source='get_online_session_count') is_alive = serializers.BooleanField(read_only=True) is_active = serializers.BooleanField(read_only=True, label='Is active') - status = serializers.ChoiceField( - read_only=True, choices=const.ComponentStatusChoices.choices, - source='latest_status', label=_('Load status') + load = LabeledChoiceField( + read_only=True, choices=const.ComponentLoad.choices, + label=_('Load status') ) - status_display = serializers.CharField(read_only=True, source='latest_status_display') - stat = StatusSerializer(read_only=True, source='latest_stat') + stat = StatSerializer(read_only=True, source='last_stat') class Meta: model = Terminal fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'type', 'remote_addr', 'http_port', 'ssh_port', - 'session_online', 'command_storage', 'replay_storage', - 'is_accepted', "is_active", 'is_alive', + 'type', 'remote_addr', 'session_online', + 'command_storage', 'replay_storage', + 'is_active', 'is_alive', 'date_created', 'comment', ] - fields_fk = ['status', 'status_display', 'stat'] + fields_fk = ['load', 'stat'] fields = fields_small + fields_fk read_only_fields = ['type', 'date_created'] extra_kwargs = { @@ -137,3 +133,11 @@ class TerminalRegistrationSerializer(serializers.ModelSerializer): instance.replay_storage = ReplayStorage.default().name instance.save() return instance + + +class ConnectMethodSerializer(serializers.Serializer): + value = serializers.CharField(max_length=128) + label = serializers.CharField(max_length=128) + type = serializers.CharField(max_length=128) + endpoint_protocol = serializers.CharField(max_length=128) + component = serializers.CharField(max_length=128) diff --git a/apps/terminal/signal_handlers.py b/apps/terminal/signal_handlers.py index 537d90bce..7868159a8 100644 --- a/apps/terminal/signal_handlers.py +++ b/apps/terminal/signal_handlers.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- # + from django.db.models.signals import post_save, post_delete from django.db.utils import ProgrammingError +from django.dispatch import receiver from common.signals import django_ready -from django.dispatch import receiver from common.utils import get_logger -from .models import Application +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 db_port_manager: DBPortManager @@ -15,6 +18,24 @@ db_port_manager: DBPortManager logger = get_logger(__file__) +@receiver(post_save, sender=AppletHost) +def on_applet_host_create(sender, instance, created=False, **kwargs): + if not created: + return + applets = Applet.objects.all() + instance.applets.set(applets) + with tmp_to_builtin_org(system=1): + instance.generate_accounts() + + +@receiver(post_save, sender=Applet) +def on_applet_create(sender, instance, created=False, **kwargs): + if not created: + return + hosts = AppletHost.objects.all() + instance.hosts.set(hosts) + + @receiver(django_ready) def init_db_port_mapper(sender, **kwargs): logger.info('Init db port mapper') @@ -24,17 +45,17 @@ def init_db_port_mapper(sender, **kwargs): pass -@receiver(post_save, sender=Application) -def on_db_app_created(sender, instance: Application, created, **kwargs): - if not instance.category_db: +@receiver(post_save, sender=Asset) +def on_db_app_created(sender, instance: Asset, created, **kwargs): + if not instance.category != 'database': return if not created: return db_port_manager.add(instance) -@receiver(post_delete, sender=Application) +@receiver(post_delete, sender=Asset) def on_db_app_delete(sender, instance, **kwargs): - if not instance.category_db: + if not instance.category != 'database': return db_port_manager.pop(instance) diff --git a/apps/terminal/startup.py b/apps/terminal/startup.py index a4c574ac2..36a740cb6 100644 --- a/apps/terminal/startup.py +++ b/apps/terminal/startup.py @@ -8,9 +8,9 @@ from common.db.utils import close_old_connections from common.decorator import Singleton from common.utils import get_disk_usage, get_cpu_load, get_memory_usage, get_logger -from .serializers.terminal import TerminalRegistrationSerializer, StatusSerializer -from .const import TerminalTypeChoices -from .models.terminal import Terminal +from .serializers.terminal import TerminalRegistrationSerializer, StatSerializer +from .const import TerminalType +from .models import Terminal __all__ = ['CoreTerminal', 'CeleryTerminal'] @@ -44,20 +44,23 @@ class BaseTerminal(object): def start_heartbeat(self): while True: + heartbeat_data = { + 'cpu_load': get_cpu_load(), + 'memory_used': get_memory_usage(), + 'disk_used': get_disk_usage(path=settings.BASE_DIR), + 'sessions': [], + } + status_serializer = StatSerializer(data=heartbeat_data) + status_serializer.is_valid() + status_serializer.validated_data.pop('sessions', None) + terminal = self.get_or_register_terminal() + status_serializer.validated_data['terminal'] = terminal + try: - heartbeat_data = { - 'cpu_load': get_cpu_load(), - 'memory_used': get_memory_usage(), - 'disk_used': get_disk_usage(path=settings.BASE_DIR), - 'sessions': [], - } - status_serializer = StatusSerializer(data=heartbeat_data) - status_serializer.is_valid() - status_serializer.validated_data.pop('sessions', None) - terminal = self.get_or_register_terminal() - status_serializer.validated_data['terminal'] = terminal status_serializer.save() + time.sleep(self.interval) except Exception: + print("Save status error, close old connections") close_old_connections() finally: time.sleep(self.interval) @@ -89,8 +92,8 @@ class CoreTerminal(BaseTerminal): def __init__(self): super().__init__( - suffix_name=TerminalTypeChoices.core.label, - _type=TerminalTypeChoices.core.value + suffix_name=TerminalType.core.label, + _type=TerminalType.core.value ) @@ -98,6 +101,6 @@ class CoreTerminal(BaseTerminal): class CeleryTerminal(BaseTerminal): def __init__(self): super().__init__( - suffix_name=TerminalTypeChoices.celery.label, - _type=TerminalTypeChoices.celery.value + suffix_name=TerminalType.celery.label, + _type=TerminalType.celery.value ) diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 59cf00fa4..4e67d1fc7 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -1,21 +1,25 @@ # -*- coding: utf-8 -*- # +import datetime import os import subprocess -import datetime from celery import shared_task from celery.utils.log import get_task_logger -from django.utils import timezone from django.core.files.storage import default_storage +from django.utils import timezone from common.utils import get_log_keep_day from ops.celery.decorator import ( - register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic + register_as_period_task, after_app_ready_start, + after_app_shutdown_clean_periodic ) -from .models import Status, Session, Command, Task +from orgs.utils import tmp_to_builtin_org from .backends import server_replay_storage +from .models import ( + Status, Session, Command, Task, AppletHostDeployment +) from .utils import find_session_replay_local CACHE_REFRESH_INTERVAL = 10 @@ -52,7 +56,7 @@ def clean_orphan_session(): @shared_task -@register_as_period_task(interval=3600*24) +@register_as_period_task(interval=3600 * 24) @after_app_ready_start @after_app_shutdown_clean_periodic def clean_expired_session_period(): @@ -84,18 +88,35 @@ def upload_session_replay_to_external_storage(session_id): if not session: logger.error(f'Session db item not found: {session_id}') return + local_path, foobar = find_session_replay_local(session) if not local_path: logger.error(f'Session replay not found, may be upload error: {local_path}') return + abs_path = default_storage.path(local_path) remote_path = session.get_relative_path_by_local_path(abs_path) ok, err = server_replay_storage.upload(abs_path, remote_path) if not ok: logger.error(f'Session replay upload to external error: {err}') return + try: default_storage.delete(local_path) except: pass return + + +@shared_task +def run_applet_host_deployment(did): + with tmp_to_builtin_org(system=1): + deployment = AppletHostDeployment.objects.get(id=did) + deployment.start() + + +@shared_task +def run_applet_host_deployment_install_applet(did, applet_id): + with tmp_to_builtin_org(system=1): + deployment = AppletHostDeployment.objects.get(id=did) + deployment.install_applet(applet_id) diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 8adcb8f52..5d3c373d2 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -12,8 +12,8 @@ app_name = 'terminal' router = BulkRouter() router.register(r'sessions', api.SessionViewSet, 'session') -router.register(r'terminals/(?P[a-zA-Z0-9\-]{36})?/?status', api.StatusViewSet, 'terminal-status') -router.register(r'terminals/(?P[a-zA-Z0-9\-]{36})?/?sessions', api.SessionViewSet, 'terminal-sessions') +router.register(r'terminals/((?P[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status') +router.register(r'terminals/((?P[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions') router.register(r'terminals', api.TerminalViewSet, 'terminal') router.register(r'tasks', api.TaskViewSet, 'tasks') router.register(r'commands', api.CommandViewSet, 'command') @@ -24,6 +24,12 @@ router.register(r'session-sharings', api.SessionSharingViewSet, 'session-sharing router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session-sharing-record') router.register(r'endpoints', api.EndpointViewSet, 'endpoint') router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule') +router.register(r'applets', api.AppletViewSet, 'applet') +router.register(r'applet-hosts/((?P[^/.]+)/)?accounts', api.AppletHostAccountsViewSet, 'applet-host-account') +router.register(r'applet-hosts/((?P[^/.]+)/)?applets', api.AppletHostAppletViewSet, 'applet-host-applet') +router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host') +router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication') +router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment') router.register(r'db-listen-ports', api.DBListenPortViewSet, 'db-listen-ports') urlpatterns = [ @@ -38,14 +44,13 @@ urlpatterns = [ path('tasks/kill-session-for-ticket/', api.KillSessionForTicketAPI.as_view(), name='kill-session-for-ticket'), path('terminals/config/', api.TerminalConfig.as_view(), name='terminal-config'), path('commands/insecure-command/', api.InsecureCommandAlertAPI.as_view(), name="command-alert"), - path('replay-storages//test-connective/', api.ReplayStorageTestConnectiveApi.as_view(), name='replay-storage-test-connective'), - path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective'), + path('replay-storages//test-connective/', api.ReplayStorageTestConnectiveApi.as_view(), + name='replay-storage-test-connective'), + path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), + name='command-storage-test-connective'), # components path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'), - # v2: get session's replay - # path('v2/sessions//replay/', - # api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}), - # name='session-replay-v2'), + path('components/connect-methods/', api.ConnectMethodListApi.as_view(), name='connect-methods'), ] old_version_urlpatterns = [ @@ -53,6 +58,3 @@ old_version_urlpatterns = [ ] urlpatterns += router.urls + old_version_urlpatterns - - - diff --git a/apps/terminal/utils/components.py b/apps/terminal/utils/components.py index 0610b0b79..2d563b026 100644 --- a/apps/terminal/utils/components.py +++ b/apps/terminal/utils/components.py @@ -1,13 +1,126 @@ # -*- coding: utf-8 -*- # +import os +import time +from itertools import groupby, chain +from collections import defaultdict + +from django.utils import timezone +from django.conf import settings +from django.core.files.storage import default_storage +import jms_storage from itertools import groupby from common.utils import get_logger +from tickets.models import TicketSession +from . import const +from ..models import ReplayStorage logger = get_logger(__name__) +def find_session_replay_local(session): + # 存在外部存储上,所有可能的路径名 + session_paths = session.get_all_possible_relative_path() + + # 存在本地存储上,所有可能的路径名 + local_paths = session.get_all_possible_local_path() + + for _local_path in chain(session_paths, local_paths): + if default_storage.exists(_local_path): + url = default_storage.url(_local_path) + return _local_path, url + return None, None + + +def download_session_replay(session): + replay_storages = ReplayStorage.objects.all() + configs = { + storage.name: storage.config + for storage in replay_storages + if not storage.type_null_or_server + } + if settings.SERVER_REPLAY_STORAGE: + configs['SERVER_REPLAY_STORAGE'] = settings.SERVER_REPLAY_STORAGE + if not configs: + msg = "Not found replay file, and not remote storage set" + return None, msg + storage = jms_storage.get_multi_object_storage(configs) + + # 获取外部存储路径名 + session_path = session.find_ok_relative_path_in_storage(storage) + if not session_path: + msg = "Not found session replay file" + return None, msg + + # 通过外部存储路径名后缀,构造真实的本地存储路径 + local_path = session.get_local_path_by_relative_path(session_path) + + # 保存到storage的路径 + target_path = os.path.join(default_storage.base_location, local_path) + target_dir = os.path.dirname(target_path) + if not os.path.isdir(target_dir): + os.makedirs(target_dir, exist_ok=True) + + ok, err = storage.download(session_path, target_path) + if not ok: + msg = "Failed download replay file: {}".format(err) + logger.error(msg) + return None, msg + url = default_storage.url(local_path) + return local_path, url + + +def get_session_replay_url(session): + local_path, url = find_session_replay_local(session) + if local_path is None: + local_path, url = download_session_replay(session) + return local_path, url + + +class ComputeLoadUtil: + # system status + @staticmethod + def _common_compute_system_status(value, thresholds): + if thresholds[0] <= value <= thresholds[1]: + return const.ComponentLoad.normal.value + elif thresholds[1] < value <= thresholds[2]: + return const.ComponentLoad.high.value + else: + return const.ComponentLoad.critical.value + + @classmethod + def _compute_system_stat_status(cls, stat): + system_stat_thresholds_mapper = { + 'cpu_load': [0, 5, 20], + 'memory_used': [0, 85, 95], + 'disk_used': [0, 80, 99] + } + system_status = {} + for stat_key, thresholds in system_stat_thresholds_mapper.items(): + stat_value = getattr(stat, stat_key) + if stat_value is None: + msg = 'stat: {}, stat_key: {}, stat_value: {}' + logger.debug(msg.format(stat, stat_key, stat_value)) + stat_value = 0 + status = cls._common_compute_system_status(stat_value, thresholds) + system_status[stat_key] = status + return system_status + + @classmethod + def compute_load(cls, stat): + if not stat or time.time() - stat.date_created.timestamp() > 150: + return const.ComponentLoad.offline + system_status_values = cls._compute_system_stat_status(stat).values() + if const.ComponentLoad.critical in system_status_values: + return const.ComponentLoad.critical + elif const.ComponentLoad.high in system_status_values: + return const.ComponentLoad.high + else: + return const.ComponentLoad.normal + + class TypedComponentsStatusMetricsUtil(object): def __init__(self): self.components = [] @@ -25,31 +138,15 @@ class TypedComponentsStatusMetricsUtil(object): def get_metrics(self): metrics = [] for _tp, components in self.grouped_components: - normal_count = high_count = critical_count = 0 - total_count = offline_count = session_online_total = 0 - + metric = { + 'normal': 0, 'high': 0, 'critical': 0, 'offline': 0, + 'total': 0, 'session_active': 0, 'type': _tp + } for component in components: - total_count += 1 - if not component.is_alive: - offline_count += 1 - continue - if component.is_normal: - normal_count += 1 - elif component.is_high: - high_count += 1 - else: - # critical - critical_count += 1 - session_online_total += component.get_online_session_count() - metrics.append({ - 'total': total_count, - 'normal': normal_count, - 'high': high_count, - 'critical': critical_count, - 'offline': offline_count, - 'session_active': session_online_total, - 'type': _tp, - }) + metric[component.load] += 1 + metric['total'] += 1 + metric['session_active'] += component.get_online_session_count() + metrics.append(metric) return metrics diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py index bec6f21d1..645133d8e 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -from .ticket import * from .flow import * +from .ticket import * from .comment import * -from .super_ticket import * from .relation import * +from .super_ticket import * diff --git a/apps/tickets/api/comment.py b/apps/tickets/api/comment.py index 1515f7c9b..e9eaf4ac3 100644 --- a/apps/tickets/api/comment.py +++ b/apps/tickets/api/comment.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # - from rest_framework import viewsets, mixins + from common.exceptions import JMSException from common.utils import lazyproperty from rbac.permissions import RBACPermission diff --git a/apps/tickets/api/flow.py b/apps/tickets/api/flow.py index b45479187..303af6d3f 100644 --- a/apps/tickets/api/flow.py +++ b/apps/tickets/api/flow.py @@ -9,7 +9,6 @@ __all__ = ['TicketFlowViewSet'] class TicketFlowViewSet(JMSBulkModelViewSet): serializer_class = serializers.TicketFlowSerializer - filterset_fields = ['id', 'type'] search_fields = ['id', 'type'] diff --git a/apps/tickets/api/relation.py b/apps/tickets/api/relation.py index 5061e6c00..c442c04ab 100644 --- a/apps/tickets/api/relation.py +++ b/apps/tickets/api/relation.py @@ -1,13 +1,13 @@ -from rest_framework.mixins import CreateModelMixin from rest_framework import views -from rest_framework.response import Response from rest_framework import status +from rest_framework.response import Response +from rest_framework.mixins import CreateModelMixin +from orgs.utils import tmp_to_root_org from common.drf.api import JMSGenericViewSet +from terminal.serializers import SessionSerializer from tickets.models import TicketSession from tickets.serializers import TicketSessionRelationSerializer -from terminal.serializers import SessionSerializer -from orgs.utils import tmp_to_root_org class TicketSessionRelationViewSet(CreateModelMixin, JMSGenericViewSet): diff --git a/apps/tickets/api/super_ticket.py b/apps/tickets/api/super_ticket.py index 32c4a56c0..ebe4c9142 100644 --- a/apps/tickets/api/super_ticket.py +++ b/apps/tickets/api/super_ticket.py @@ -1,9 +1,8 @@ from rest_framework.generics import RetrieveDestroyAPIView from orgs.utils import tmp_to_root_org -from ..serializers import SuperTicketSerializer from ..models import Ticket - +from ..serializers import SuperTicketSerializer __all__ = ['SuperTicketStatusAPI'] diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index e4dc94852..dacbb47f0 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -5,31 +5,28 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.exceptions import MethodNotAllowed -from common.const.http import POST, PUT, PATCH -from common.mixins.api import CommonApiMixin from orgs.utils import tmp_to_root_org - from rbac.permissions import RBACPermission - -from tickets import serializers +from common.mixins.api import CommonApiMixin +from common.const.http import POST, PUT, PATCH from tickets import filters -from tickets.permissions.ticket import IsAssignee, IsApplicant +from tickets import serializers from tickets.models import ( - Ticket, ApplyAssetTicket, ApplyApplicationTicket, - ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket + Ticket, ApplyAssetTicket, ApplyLoginTicket, + ApplyLoginAssetTicket, ApplyCommandTicket ) +from tickets.permissions.ticket import IsAssignee, IsApplicant __all__ = [ - 'TicketViewSet', 'ApplyAssetTicketViewSet', 'ApplyApplicationTicketViewSet', - 'ApplyLoginTicketViewSet', 'ApplyLoginAssetTicketViewSet', 'ApplyCommandTicketViewSet' + 'TicketViewSet', 'ApplyAssetTicketViewSet', + 'ApplyLoginTicketViewSet', 'ApplyLoginAssetTicketViewSet', + 'ApplyCommandTicketViewSet' ] class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): - serializer_class = serializers.TicketDisplaySerializer + serializer_class = serializers.TicketSerializer serializer_classes = { - 'list': serializers.TicketListSerializer, - 'open': serializers.TicketApplySerializer, 'approve': serializers.TicketApproveSerializer } model = Ticket @@ -39,8 +36,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): 'title', 'type', 'status' ] ordering_fields = ( - 'title', 'status', 'state', - 'action_display', 'date_created', 'serial_num', + 'title', 'status', 'state', 'action_display', + 'date_created', 'serial_num', ) ordering = ('-date_created',) rbac_perms = { @@ -95,38 +92,28 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): class ApplyAssetTicketViewSet(TicketViewSet): - serializer_class = serializers.ApplyAssetDisplaySerializer + model = ApplyAssetTicket + filterset_class = filters.ApplyAssetTicketFilter + serializer_class = serializers.ApplyAssetSerializer serializer_classes = { 'open': serializers.ApplyAssetSerializer, 'approve': serializers.ApproveAssetSerializer } - model = ApplyAssetTicket - filterset_class = filters.ApplyAssetTicketFilter - - -class ApplyApplicationTicketViewSet(TicketViewSet): - serializer_class = serializers.ApplyApplicationDisplaySerializer - serializer_classes = { - 'open': serializers.ApplyApplicationSerializer, - 'approve': serializers.ApproveApplicationSerializer - } - model = ApplyApplicationTicket - filterset_class = filters.ApplyApplicationTicketFilter class ApplyLoginTicketViewSet(TicketViewSet): - serializer_class = serializers.LoginConfirmSerializer model = ApplyLoginTicket filterset_class = filters.ApplyLoginTicketFilter + serializer_class = serializers.LoginConfirmSerializer class ApplyLoginAssetTicketViewSet(TicketViewSet): - serializer_class = serializers.LoginAssetConfirmSerializer model = ApplyLoginAssetTicket filterset_class = filters.ApplyLoginAssetTicketFilter + serializer_class = serializers.LoginAssetConfirmSerializer class ApplyCommandTicketViewSet(TicketViewSet): - serializer_class = serializers.ApplyCommandConfirmSerializer model = ApplyCommandTicket filterset_class = filters.ApplyCommandTicketFilter + serializer_class = serializers.ApplyCommandConfirmSerializer diff --git a/apps/tickets/const.py b/apps/tickets/const.py index d97e6716e..ccd044bbe 100644 --- a/apps/tickets/const.py +++ b/apps/tickets/const.py @@ -6,19 +6,18 @@ TICKET_DETAIL_URL = '/ui/#/tickets/tickets/{id}?type={type}' class TicketType(TextChoices): general = 'general', _("General") - login_confirm = 'login_confirm', _("Login confirm") apply_asset = 'apply_asset', _('Apply for asset') - apply_application = 'apply_application', _('Apply for application') - login_asset_confirm = 'login_asset_confirm', _('Login asset confirm') + login_confirm = 'login_confirm', _("Login confirm") command_confirm = 'command_confirm', _('Command confirm') + login_asset_confirm = 'login_asset_confirm', _('Login asset confirm') class TicketState(TextChoices): pending = 'pending', _('Open') - approved = 'approved', _('Approved') - rejected = 'rejected', _('Rejected') closed = 'closed', _("Cancel") reopen = 'reopen', _("Reopen") + approved = 'approved', _('Approved') + rejected = 'rejected', _('Rejected') class TicketStatus(TextChoices): @@ -28,23 +27,23 @@ class TicketStatus(TextChoices): class StepState(TextChoices): pending = 'pending', _('Pending') - approved = 'approved', _('Approved') - rejected = 'rejected', _('Rejected') closed = 'closed', _("Closed") reopen = 'reopen', _("Reopen") + approved = 'approved', _('Approved') + rejected = 'rejected', _('Rejected') class StepStatus(TextChoices): - pending = 'pending', _('Pending') active = 'active', _('Active') closed = 'closed', _("Closed") + pending = 'pending', _('Pending') class TicketAction(TextChoices): open = 'open', _("Open") close = 'close', _("Close") - approve = 'approve', _('Approve') reject = 'reject', _('Reject') + approve = 'approve', _('Approve') class TicketLevel(IntegerChoices): @@ -53,7 +52,7 @@ class TicketLevel(IntegerChoices): class TicketApprovalStrategy(TextChoices): - super_admin = 'super_admin', _("Super admin") org_admin = 'org_admin', _("Org admin") - super_org_admin = 'super_org_admin', _("Super admin and org admin") custom_user = 'custom_user', _("Custom user") + super_admin = 'super_admin', _("Super admin") + super_org_admin = 'super_org_admin', _("Super admin and org admin") diff --git a/apps/tickets/filters.py b/apps/tickets/filters.py index 659556c12..a90f88316 100644 --- a/apps/tickets/filters.py +++ b/apps/tickets/filters.py @@ -1,11 +1,11 @@ +from django.db.models import Subquery, OuterRef, Value, F, Q from django_filters import rest_framework as filters from django.db.models.functions import Concat -from django.db.models import Subquery, OuterRef, Value, F, Q from common.drf.filters import BaseFilterSet from tickets.models import ( - Ticket, TicketStep, ApplyAssetTicket, ApplyApplicationTicket, + Ticket, TicketStep, ApplyAssetTicket, ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket ) @@ -35,8 +35,8 @@ class TicketFilter(BaseFilterSet): def filter_relevant_asset(self, queryset, name, value): asset_ids = ApplyAssetTicket.objects.annotate( asset_str=Concat( - F('apply_assets__hostname'), Value('('), - F('apply_assets__ip'), Value(')') + F('apply_assets__name'), Value('('), + F('apply_assets__address'), Value(')') ) ).filter( asset_str__icontains=value @@ -44,8 +44,8 @@ class TicketFilter(BaseFilterSet): login_asset_ids = ApplyLoginAssetTicket.objects.annotate( asset_str=Concat( - F('apply_login_asset__hostname'), Value('('), - F('apply_login_asset__ip'), Value(')') + F('apply_login_asset__name'), Value('('), + F('apply_login_asset__address'), Value(')') ) ).filter( asset_str__icontains=value @@ -58,40 +58,6 @@ class TicketFilter(BaseFilterSet): ticket_ids = list(set(list(asset_ids) + list(login_asset_ids) + list(command_ids))) return queryset.filter(id__in=ticket_ids) - def filter_relevant_app(self, queryset, name, value): - app_ids = ApplyApplicationTicket.objects.filter( - apply_applications__name__icontains=value - ).values_list('id', flat=True) - - command_ids = ApplyCommandTicket.objects.filter( - apply_run_asset__icontains=value - ).values_list('id', flat=True) - - ticket_ids = list(set(list(app_ids) + list(command_ids))) - return queryset.filter(id__in=ticket_ids) - - def filter_relevant_system_user(self, queryset, name, value): - system_user_query = Q(apply_system_users__name__icontains=value) - asset_ids = ApplyAssetTicket.objects.filter( - system_user_query - ).values_list('id', flat=True) - - app_ids = ApplyApplicationTicket.objects.filter( - system_user_query - ).values_list('id', flat=True) - - login_asset_ids = ApplyLoginAssetTicket.objects.filter( - apply_login_system_user__name__icontains=value - ).values_list('id', flat=True) - - command_ids = ApplyCommandTicket.objects.filter( - apply_run_system_user__name__icontains=value - ).values_list('id', flat=True) - ticket_ids = list( - set(list(asset_ids) + list(app_ids) + list(login_asset_ids) + list(command_ids)) - ) - return queryset.filter(id__in=ticket_ids) - def filter_relevant_command(self, queryset, name, value): command_ids = ApplyCommandTicket.objects.filter( apply_run_command__icontains=value @@ -105,12 +71,6 @@ class ApplyAssetTicketFilter(BaseFilterSet): fields = ('id',) -class ApplyApplicationTicketFilter(BaseFilterSet): - class Meta: - model = ApplyApplicationTicket - fields = ('id',) - - class ApplyLoginTicketFilter(BaseFilterSet): class Meta: model = ApplyLoginTicket diff --git a/apps/tickets/handlers/apply_application.py b/apps/tickets/handlers/apply_application.py deleted file mode 100644 index 287c70c4a..000000000 --- a/apps/tickets/handlers/apply_application.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.utils.translation import ugettext as _ - -from orgs.utils import tmp_to_org -from perms.models import ApplicationPermission -from tickets.models import ApplyApplicationTicket -from .base import BaseHandler - - -class Handler(BaseHandler): - ticket: ApplyApplicationTicket - - def _on_step_approved(self, step): - is_finished = super()._on_step_approved(step) - if is_finished: - self._create_application_permission() - - # permission - def _create_application_permission(self): - org_id = self.ticket.org_id - with tmp_to_org(org_id): - application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first() - if application_permission: - return application_permission - - apply_applications = self.ticket.apply_applications.all() - apply_system_users = self.ticket.apply_system_users.all() - - apply_permission_name = self.ticket.apply_permission_name - apply_actions = self.ticket.apply_actions - apply_category = self.ticket.apply_category - apply_type = self.ticket.apply_type - apply_date_start = self.ticket.apply_date_start - apply_date_expired = self.ticket.apply_date_expired - permission_created_by = '{}:{}'.format( - str(self.ticket.__class__.__name__), str(self.ticket.id) - ) - permission_comment = _( - 'Created by the ticket, ' - 'ticket title: {}, ' - 'ticket applicant: {}, ' - 'ticket processor: {}, ' - 'ticket ID: {}' - ).format( - self.ticket.title, - self.ticket.applicant, - ','.join([i['processor_display'] for i in self.ticket.process_map]), - str(self.ticket.id) - ) - permissions_data = { - 'id': self.ticket.id, - 'name': apply_permission_name, - 'from_ticket': True, - 'category': apply_category, - 'actions': apply_actions, - 'type': apply_type, - 'comment': str(permission_comment), - 'created_by': permission_created_by, - 'date_start': apply_date_start, - 'date_expired': apply_date_expired, - } - with tmp_to_org(self.ticket.org_id): - application_permission = ApplicationPermission.objects.create(**permissions_data) - application_permission.users.add(self.ticket.applicant) - application_permission.applications.set(apply_applications) - application_permission.system_users.set(apply_system_users) - - return application_permission diff --git a/apps/tickets/handlers/apply_asset.py b/apps/tickets/handlers/apply_asset.py index e9d29697d..f2b2ee842 100644 --- a/apps/tickets/handlers/apply_asset.py +++ b/apps/tickets/handlers/apply_asset.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext as _ +from orgs.utils import tmp_to_org from perms.models import AssetPermission -from orgs.utils import tmp_to_org, tmp_to_root_org from tickets.models import ApplyAssetTicket from .base import BaseHandler @@ -14,7 +14,6 @@ class Handler(BaseHandler): if is_finished: self._create_asset_permission() - # permission def _create_asset_permission(self): org_id = self.ticket.org_id with tmp_to_org(org_id): @@ -24,10 +23,10 @@ class Handler(BaseHandler): apply_nodes = self.ticket.apply_nodes.all() apply_assets = self.ticket.apply_assets.all() - apply_system_users = self.ticket.apply_system_users.all() apply_permission_name = self.ticket.apply_permission_name apply_actions = self.ticket.apply_actions + apply_accounts = self.ticket.apply_accounts apply_date_start = self.ticket.apply_date_start apply_date_expired = self.ticket.apply_date_expired permission_created_by = '{}:{}'.format( @@ -47,20 +46,20 @@ class Handler(BaseHandler): ) permission_data = { - 'id': self.ticket.id, - 'name': apply_permission_name, 'from_ticket': True, - 'comment': str(permission_comment), - 'created_by': permission_created_by, + 'id': self.ticket.id, 'actions': apply_actions, + 'accounts': apply_accounts, + 'name': apply_permission_name, 'date_start': apply_date_start, 'date_expired': apply_date_expired, + 'comment': str(permission_comment), + 'created_by': permission_created_by, } with tmp_to_org(self.ticket.org_id): asset_permission = AssetPermission.objects.create(**permission_data) - asset_permission.users.add(self.ticket.applicant) asset_permission.nodes.set(apply_nodes) asset_permission.assets.set(apply_assets) - asset_permission.system_users.set(apply_system_users) + asset_permission.users.add(self.ticket.applicant) return asset_permission diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py index 81341ce8e..7b311799f 100644 --- a/apps/tickets/handlers/base.py +++ b/apps/tickets/handlers/base.py @@ -65,7 +65,7 @@ class BaseHandler: if state != TicketState.approved: return diff_context - if self.ticket.type not in [TicketType.apply_asset, TicketType.apply_application]: + if self.ticket.type == TicketType.apply_asset: return diff_context # 企业微信,钉钉审批不做diff diff --git a/apps/tickets/handlers/login_confirm.py b/apps/tickets/handlers/login_confirm.py index ad33ce476..e6498314e 100644 --- a/apps/tickets/handlers/login_confirm.py +++ b/apps/tickets/handlers/login_confirm.py @@ -1,22 +1,6 @@ -from django.utils.translation import ugettext as _ from tickets.models import ApplyLoginTicket from .base import BaseHandler class Handler(BaseHandler): ticket: ApplyLoginTicket - - def _construct_meta_body_of_open(self): - apply_login_ip = self.ticket.apply_login_ip - apply_login_city = self.ticket.apply_login_city - apply_login_datetime = self.ticket.apply_login_datetime - applied_body = ''' - {}: {} - {}: {} - {}: {} - '''.format( - _("Applied login IP"), apply_login_ip, - _("Applied login city"), apply_login_city, - _("Applied login datetime"), apply_login_datetime, - ) - return applied_body diff --git a/apps/tickets/migrations/0010_auto_20210812_1618.py b/apps/tickets/migrations/0010_auto_20210812_1618.py index 36e4db4e2..e858f9acf 100644 --- a/apps/tickets/migrations/0010_auto_20210812_1618.py +++ b/apps/tickets/migrations/0010_auto_20210812_1618.py @@ -82,7 +82,7 @@ def create_ticket_flow_and_approval_rule(apps, schema_editor): super_user = user_model.objects.filter(role='Admin') assignees_display = ['{0.name}({0.username})'.format(i) for i in super_user] with transaction.atomic(): - for ticket_type in [TicketType.apply_asset, TicketType.apply_application]: + for ticket_type in [TicketType.apply_asset, 'apply_application']: ticket_flow_instance = ticket_flow_model.objects.create(created_by='System', type=ticket_type, org_id=org_id) approval_rule_instance = approval_rule_model.objects.create(strategy=TicketApprovalStrategy.super_admin, assignees_display=assignees_display) approval_rule_instance.assignees.set(list(super_user)) diff --git a/apps/tickets/migrations/0013_ticket_serial_num.py b/apps/tickets/migrations/0013_ticket_serial_num.py index 4c457d21f..96d0cbc0d 100644 --- a/apps/tickets/migrations/0013_ticket_serial_num.py +++ b/apps/tickets/migrations/0013_ticket_serial_num.py @@ -12,7 +12,7 @@ def fill_ticket_serial_number(apps, schema_editor): curr_day = '00000000' curr_num = 1 - print(f'\nFill ticket serial number ... ', end='') + print(f'\n\tFill ticket serial number ... ') for ticket in tickets: # 跑这个脚本的时候,所有 ticket.serial_num == null date_created = as_current_tz(ticket.date_created) @@ -25,7 +25,6 @@ def fill_ticket_serial_number(apps, schema_editor): curr_num += 1 Ticket.objects.bulk_update(tickets, fields=('serial_num',)) - print(len(tickets), end='') class Migration(migrations.Migration): diff --git a/apps/tickets/migrations/0017_auto_20220623_1027.py b/apps/tickets/migrations/0017_auto_20220623_1027.py index 746d267c8..ba2351d65 100644 --- a/apps/tickets/migrations/0017_auto_20220623_1027.py +++ b/apps/tickets/migrations/0017_auto_20220623_1027.py @@ -7,7 +7,6 @@ from collections import defaultdict from django.utils import timezone as dj_timezone from django.db import migrations -from perms.models import Action from tickets.const import TicketType pt = re.compile(r'(\w+)\((\w+)\)') @@ -112,7 +111,7 @@ def apply_application_migrate(apps, *args): init_global_dict(apps) ticket_model = apps.get_model('tickets', 'Ticket') - tickets = ticket_model.objects.filter(type=TicketType.apply_application) + tickets = ticket_model.objects.filter(type='apply_application') ticket_apply_app_model = apps.get_model('tickets', 'ApplyApplicationTicket') for instance in tickets: diff --git a/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py b/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py index 74f5ed7a3..a2890c82d 100644 --- a/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py +++ b/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py @@ -1,10 +1,9 @@ -# Generated by Django 3.1.14 on 2022-07-22 08:03 +# Generated by Django 3.2.14 on 2022-07-28 03:25 from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('tickets', '0017_auto_20220623_1027'), ] @@ -13,6 +12,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='applyapplicationticket', name='apply_actions', - field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'), + field=models.IntegerField( + choices=[ + (255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), + (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste') + ], default=255, + verbose_name='Actions'), ), ] diff --git a/apps/tickets/migrations/0019_delete_applyapplicationticket.py b/apps/tickets/migrations/0019_delete_applyapplicationticket.py new file mode 100644 index 000000000..d198f2228 --- /dev/null +++ b/apps/tickets/migrations/0019_delete_applyapplicationticket.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.14 on 2022-08-16 08:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0018_applyapplicationticket_apply_actions'), + ] + + operations = [ + migrations.DeleteModel( + name='ApplyApplicationTicket', + ), + ] diff --git a/apps/tickets/migrations/0020_auto_20220817_1346.py b/apps/tickets/migrations/0020_auto_20220817_1346.py new file mode 100644 index 000000000..783021dc8 --- /dev/null +++ b/apps/tickets/migrations/0020_auto_20220817_1346.py @@ -0,0 +1,87 @@ +# Generated by Django 3.2.14 on 2022-08-17 05:46 + +import time +from django.db import migrations, models + + +def migrate_system_to_account(apps, schema_editor): + apply_asset_ticket_model = apps.get_model('tickets', 'ApplyAssetTicket') + apply_command_ticket_model = apps.get_model('tickets', 'ApplyCommandTicket') + apply_login_asset_ticket_model = apps.get_model('tickets', 'ApplyLoginAssetTicket') + + model_system_user_account = ( + (apply_asset_ticket_model, 'apply_system_users', 'apply_accounts', True), + (apply_command_ticket_model, 'apply_run_system_user', 'apply_run_account', False), + (apply_login_asset_ticket_model, 'apply_login_system_user', 'apply_login_account', False), + ) + + print("\n\tStart 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 + bulk_size = 1000 + + while True: + start = time.time() + objects = model.objects.all().prefetch_related(old_field)[count:bulk_size] + if not objects: + break + count += len(objects) + + updated = [] + for obj in objects: + if m2m: + old_value = getattr(obj, old_field).all() + new_value = [s.username for s in old_value] + else: + old_value = getattr(obj, old_field) + new_value = old_value.username if old_value else '' + setattr(obj, new_field, new_value) + updated.append(obj) + model.objects.bulk_update(updated, [new_field]) + print(" Migrate account: {}-{} using: {:.2f}s".format( + count - len(objects), count, time.time()-start + )) + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0019_delete_applyapplicationticket'), + ] + + operations = [ + migrations.AlterField( + model_name='applyassetticket', + name='apply_permission_name', + field=models.CharField(max_length=128, verbose_name='Permission name'), + ), + migrations.AddField( + model_name='applyassetticket', + name='apply_accounts', + field=models.JSONField(default=list, verbose_name='Apply accounts'), + ), + migrations.AddField( + model_name='applycommandticket', + name='apply_run_account', + field=models.CharField(default='', max_length=128, verbose_name='Run account'), + ), + migrations.AddField( + model_name='applyloginassetticket', + name='apply_login_account', + field=models.CharField(default='', max_length=128, verbose_name='Login account'), + ), + migrations.RunPython(migrate_system_to_account), + migrations.RemoveField( + model_name='applyassetticket', + name='apply_system_users', + ), + migrations.RemoveField( + model_name='applycommandticket', + name='apply_run_system_user', + ), + migrations.RemoveField( + model_name='applyloginassetticket', + name='apply_login_system_user', + ), + ] diff --git a/apps/tickets/migrations/0021_auto_20220921_1814.py b/apps/tickets/migrations/0021_auto_20220921_1814.py new file mode 100644 index 000000000..5e9db6e06 --- /dev/null +++ b/apps/tickets/migrations/0021_auto_20220921_1814.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.13 on 2022-09-21 10:14 + +from django.db import migrations, models + + +def migrate_remove_application_flow(apps, schema_editor): + flow_model = apps.get_model('tickets', 'TicketFlow') + flow_model.objects.filter(type='apply_application').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('tickets', '0020_auto_20220817_1346'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField( + choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), + ('login_asset_confirm', 'Login asset confirm'), ('command_confirm', 'Command confirm')], + default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketflow', + name='type', + field=models.CharField( + choices=[('general', 'General'), ('login_confirm', 'Login confirm'), ('apply_asset', 'Apply for asset'), + ('login_asset_confirm', 'Login asset confirm'), ('command_confirm', 'Command confirm')], + default='general', max_length=64, verbose_name='Type'), + ), + migrations.RunPython(migrate_remove_application_flow), + ] diff --git a/apps/tickets/migrations/0022_alter_applyassetticket_apply_actions.py b/apps/tickets/migrations/0022_alter_applyassetticket_apply_actions.py new file mode 100644 index 000000000..96f645e0d --- /dev/null +++ b/apps/tickets/migrations/0022_alter_applyassetticket_apply_actions.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-11 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0021_auto_20220921_1814'), + ] + + operations = [ + migrations.AlterField( + model_name='applyassetticket', + name='apply_actions', + field=models.IntegerField(default=1, verbose_name='Actions'), + ), + ] diff --git a/apps/tickets/migrations/0023_alter_applyassetticket_apply_actions.py b/apps/tickets/migrations/0023_alter_applyassetticket_apply_actions.py new file mode 100644 index 000000000..f401fe298 --- /dev/null +++ b/apps/tickets/migrations/0023_alter_applyassetticket_apply_actions.py @@ -0,0 +1,86 @@ +# Generated by Django 3.2.14 on 2022-11-18 03:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('tickets', '0022_alter_applyassetticket_apply_actions'), + ] + + operations = [ + migrations.AlterField( + model_name='applyassetticket', + name='apply_actions', + field=models.IntegerField(default=31, verbose_name='Actions'), + ), + migrations.AlterField( + model_name='approvalrule', + name='strategy', + field=models.CharField( + choices=[ + ('org_admin', 'Org admin'), ('custom_user', 'Custom user'), + ('super_admin', 'Super admin'), ('super_org_admin', 'Super admin and org admin') + ], default='super_admin', max_length=64, + verbose_name='Approve strategy'), + ), + migrations.AlterField( + model_name='ticket', + name='state', + field=models.CharField( + choices=[ + ('pending', 'Open'), ('closed', 'Cancel'), + ('reopen', 'Reopen'), ('approved', 'Approved'), + ('rejected', 'Rejected') + ], default='pending', max_length=16, verbose_name='State'), + ), + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField( + choices=[ + ('general', 'General'), ('apply_asset', 'Apply for asset'), + ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), + ('login_asset_confirm', 'Login asset confirm') + ], + default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketassignee', + name='state', + field=models.CharField( + choices=[ + ('pending', 'Open'), ('closed', 'Cancel'), + ('reopen', 'Reopen'), ('approved', 'Approved'), + ('rejected', 'Rejected') + ], default='pending', max_length=64), + ), + migrations.AlterField( + model_name='ticketflow', + name='type', + field=models.CharField( + choices=[ + ('general', 'General'), ('apply_asset', 'Apply for asset'), + ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), + ('login_asset_confirm', 'Login asset confirm') + ], + default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketstep', + name='state', + field=models.CharField( + choices=[ + ('pending', 'Pending'), ('closed', 'Closed'), + ('reopen', 'Reopen'), ('approved', 'Approved'), + ('rejected', 'Rejected') + ], default='pending', max_length=64, verbose_name='State'), + ), + migrations.AlterField( + model_name='ticketstep', + name='status', + field=models.CharField( + choices=[('active', 'Active'), ('closed', 'Closed'), ('pending', 'Pending')], + default='pending', max_length=16), + ), + ] diff --git a/apps/tickets/migrations/0024_auto_20221121_1800.py b/apps/tickets/migrations/0024_auto_20221121_1800.py new file mode 100644 index 000000000..acd203ef6 --- /dev/null +++ b/apps/tickets/migrations/0024_auto_20221121_1800.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.14 on 2022-11-21 10:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0023_alter_applyassetticket_apply_actions'), + ] + + operations = [ + migrations.AlterField( + model_name='approvalrule', + name='strategy', + field=models.CharField(choices=[('org_admin', 'Org admin'), ('custom_user', 'Custom user'), ('super_admin', 'Super admin'), ('super_org_admin', 'Super admin and org admin')], default='super_admin', max_length=64, verbose_name='Approve strategy'), + ), + migrations.AlterField( + model_name='ticket', + name='state', + field=models.CharField(choices=[('pending', 'Open'), ('closed', 'Cancel'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=16, verbose_name='State'), + ), + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('apply_asset', 'Apply for asset'), ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketassignee', + name='state', + field=models.CharField(choices=[('pending', 'Open'), ('closed', 'Cancel'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=64), + ), + migrations.AlterField( + model_name='ticketflow', + name='type', + field=models.CharField(choices=[('general', 'General'), ('apply_asset', 'Apply for asset'), ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketstep', + name='state', + field=models.CharField(choices=[('pending', 'Pending'), ('closed', 'Closed'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=64, verbose_name='State'), + ), + migrations.AlterField( + model_name='ticketstep', + name='status', + field=models.CharField(choices=[('active', 'Active'), ('closed', 'Closed'), ('pending', 'Pending')], default='pending', max_length=16), + ), + ] diff --git a/apps/tickets/models/ticket/__init__.py b/apps/tickets/models/ticket/__init__.py index c13cea9b1..89592be82 100644 --- a/apps/tickets/models/ticket/__init__.py +++ b/apps/tickets/models/ticket/__init__.py @@ -1,6 +1,6 @@ from .general import * from .apply_asset import * -from .apply_application import * +# from .apply_application import * from .command_confirm import * from .login_asset_confirm import * from .login_confirm import * diff --git a/apps/tickets/models/ticket/apply_application.py b/apps/tickets/models/ticket/apply_application.py index 378047db2..3c1f4b6f7 100644 --- a/apps/tickets/models/ticket/apply_application.py +++ b/apps/tickets/models/ticket/apply_application.py @@ -1,8 +1,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from perms.models import Action -from applications.const import AppCategory, AppType from .general import Ticket __all__ = ['ApplyApplicationTicket'] @@ -12,10 +10,10 @@ class ApplyApplicationTicket(Ticket): apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name')) # 申请信息 apply_category = models.CharField( - max_length=16, choices=AppCategory.choices, verbose_name=_('Category') + max_length=16, verbose_name=_('Category') ) apply_type = models.CharField( - max_length=16, choices=AppType.choices, verbose_name=_('Type') + max_length=16, verbose_name=_('Type') ) apply_applications = models.ManyToManyField( 'applications.Application', verbose_name=_('Apply applications'), @@ -24,22 +22,10 @@ class ApplyApplicationTicket(Ticket): 'assets.SystemUser', verbose_name=_('Apply system users'), ) apply_actions = models.IntegerField( - choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions') + choices=[ + (255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), + (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste') + ], default=255, verbose_name=_('Actions') ) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True) - - @property - def apply_category_display(self): - return AppCategory.get_label(self.apply_category) - - @property - def apply_type_display(self): - return AppType.get_label(self.apply_type) - - @property - def apply_actions_display(self): - return Action.value_to_choices_display(self.apply_actions) - - def get_apply_actions_display(self): - return ', '.join(self.apply_actions_display) diff --git a/apps/tickets/models/ticket/apply_asset.py b/apps/tickets/models/ticket/apply_asset.py index c3759dc9a..2fde56125 100644 --- a/apps/tickets/models/ticket/apply_asset.py +++ b/apps/tickets/models/ticket/apply_asset.py @@ -1,7 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from perms.models import Action +from perms.const import ActionChoices from .general import Ticket __all__ = ['ApplyAssetTicket'] @@ -14,18 +14,10 @@ class ApplyAssetTicket(Ticket): apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes')) # 申请信息 apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets')) - apply_system_users = models.ManyToManyField( - 'assets.SystemUser', verbose_name=_('Apply system users') - ) - apply_actions = models.IntegerField( - choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions') - ) + apply_accounts = models.JSONField(default=list, verbose_name=_('Apply accounts')) + apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all()) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True) - @property - def apply_actions_display(self): - return Action.value_to_choices_display(self.apply_actions) - def get_apply_actions_display(self): - return ', '.join(self.apply_actions_display) + return ActionChoices.display(self.apply_actions) diff --git a/apps/tickets/models/ticket/command_confirm.py b/apps/tickets/models/ticket/command_confirm.py index eb6b838ea..ac70eaadb 100644 --- a/apps/tickets/models/ticket/command_confirm.py +++ b/apps/tickets/models/ticket/command_confirm.py @@ -10,11 +10,8 @@ class ApplyCommandTicket(Ticket): null=True, verbose_name=_('Run user') ) apply_run_asset = models.CharField(max_length=128, verbose_name=_('Run asset')) - apply_run_system_user = models.ForeignKey( - 'assets.SystemUser', on_delete=models.SET_NULL, - null=True, verbose_name=_('Run system user') - ) 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_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 e58ffa04a..271b87166 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -5,26 +5,28 @@ from typing import Callable from django.db import models from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.forms import model_to_dict from django.db.utils import IntegrityError from django.db.models.fields import related -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.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 ) -from tickets.handlers import get_ticket_handler from tickets.errors import AlreadyClosed +from tickets.handlers import get_ticket_handler from ..flow import TicketFlow -__all__ = ['Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager'] +__all__ = [ + 'Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager' +] class TicketStep(CommonModelMixin): @@ -204,11 +206,11 @@ class StatusMixin: step_info = { 'state': state, - 'approval_level': step.level, 'assignees': assignee_ids, + 'processor': processor_id, + 'approval_level': step.level, 'assignees_display': assignees_display, 'approval_date': str(step.date_updated), - 'processor': processor_id, 'processor_display': processor_display } process_map.append(step_info) @@ -224,15 +226,15 @@ class StatusMixin: org_id = self.flow.org_id flow_rules = self.flow.rules.order_by('level') for rule in flow_rules: - step = TicketStep.objects.create(ticket=self, level=rule.level) assignees = rule.get_assignees(org_id=org_id) assignees = self.exclude_applicant(assignees, self.applicant) + step = TicketStep.objects.create(ticket=self, level=rule.level) step_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees] TicketAssignee.objects.bulk_create(step_assignees) def create_process_steps_by_assignees(self, assignees): - assignees = self.exclude_applicant(assignees, self.applicant) step = TicketStep.objects.create(ticket=self, level=1) + assignees = self.exclude_applicant(assignees, self.applicant) ticket_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees] TicketAssignee.objects.bulk_create(ticket_assignees) @@ -248,14 +250,13 @@ class StatusMixin: @property def processor(self): processor = self.current_step.ticket_assignees \ - .exclude(state=StepState.pending) \ - .first() + .exclude(state=StepState.pending).first() return processor.assignee if processor else None def has_current_assignee(self, assignee): return self.ticket_steps.filter( + level=self.approval_step, ticket_assignees__assignee=assignee, - level=self.approval_step ).exists() def has_all_assignee(self, assignee): @@ -282,19 +283,19 @@ class Ticket(StatusMixin, CommonModelMixin): ) # 申请人 applicant = models.ForeignKey( - 'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, - null=True, verbose_name=_("Applicant") + 'users.User', related_name='applied_tickets', null=True, + on_delete=models.SET_NULL, verbose_name=_("Applicant") ) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) flow = models.ForeignKey( - 'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, - null=True, verbose_name=_('TicketFlow') + 'TicketFlow', related_name='tickets', null=True, + on_delete=models.SET_NULL, verbose_name=_('TicketFlow') ) approval_step = models.SmallIntegerField( default=TicketLevel.one, choices=TicketLevel.choices, verbose_name=_('Approval step') ) - serial_num = models.CharField(_('Serial number'), max_length=128, unique=True, null=True) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) rel_snapshot = models.JSONField(verbose_name=_('Relation snapshot'), default=dict) + serial_num = models.CharField(_('Serial number'), max_length=128, unique=True, null=True) meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta")) org_id = models.CharField( max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True @@ -324,7 +325,7 @@ class Ticket(StatusMixin, CommonModelMixin): @classmethod def get_user_related_tickets(cls, user): queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user) - tickets = cls.objects.all().filter(queries).distinct() + tickets = cls.objects.filter(queries).distinct() return tickets def get_current_ticket_flow_approve(self): @@ -398,15 +399,17 @@ class Ticket(StatusMixin, CommonModelMixin): value = self.rel_snapshot[name] elif isinstance(field, related.ManyToManyField): value = ', '.join(self.rel_snapshot[name]) + elif isinstance(value, list): + value = ', '.join(value) return value def get_local_snapshot(self): + snapshot = {} + excludes = ['ticket_ptr'] fields = self._meta._forward_fields_map json_data = json.dumps(model_to_dict(self), cls=ModelJSONFieldEncoder) data = json.loads(json_data) - snapshot = {} local_fields = self._meta.local_fields + self._meta.local_many_to_many - excludes = ['ticket_ptr'] item_names = [field.name for field in local_fields if field.name not in excludes] for name in item_names: field = fields[name] diff --git a/apps/tickets/models/ticket/login_asset_confirm.py b/apps/tickets/models/ticket/login_asset_confirm.py index 5e5c53a47..8761bc7fe 100644 --- a/apps/tickets/models/ticket/login_asset_confirm.py +++ b/apps/tickets/models/ticket/login_asset_confirm.py @@ -8,14 +8,11 @@ __all__ = ['ApplyLoginAssetTicket'] class ApplyLoginAssetTicket(Ticket): apply_login_user = models.ForeignKey( - 'users.User', on_delete=models.SET_NULL, null=True, - verbose_name=_('Login user'), + 'users.User', on_delete=models.SET_NULL, null=True, verbose_name=_('Login user'), ) apply_login_asset = models.ForeignKey( - 'assets.Asset', on_delete=models.SET_NULL, null=True, - verbose_name=_('Login asset'), + 'assets.Asset', on_delete=models.SET_NULL, null=True, verbose_name=_('Login asset'), ) - apply_login_system_user = models.ForeignKey( - 'assets.SystemUser', on_delete=models.SET_NULL, null=True, - verbose_name=_('Login system user'), + apply_login_account = models.CharField( + max_length=128, default='', verbose_name=_('Login account') ) diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py index 26997b0dc..7e971f4cf 100644 --- a/apps/tickets/notifications.py +++ b/apps/tickets/notifications.py @@ -1,11 +1,11 @@ -from urllib.parse import urljoin import json +from urllib.parse import urljoin from django.conf import settings from django.core.cache import cache from django.shortcuts import reverse -from django.template.loader import render_to_string from django.forms import model_to_dict +from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ from notifications.notifications import UserMessage @@ -94,9 +94,9 @@ class BaseTicketMessage(UserMessage): class TicketAppliedToAssigneeMessage(BaseTicketMessage): def __init__(self, user, ticket): + self._token = None self.ticket = ticket super().__init__(user) - self._token = None @property def token(self): diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py index 26a4b9aa6..645133d8e 100644 --- a/apps/tickets/serializers/__init__.py +++ b/apps/tickets/serializers/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -from .ticket import * from .flow import * +from .ticket import * from .comment import * from .relation import * from .super_ticket import * diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py index 6cb8ab9d5..6fa9fa567 100644 --- a/apps/tickets/serializers/comment.py +++ b/apps/tickets/serializers/comment.py @@ -1,6 +1,7 @@ from rest_framework import serializers -from ..models import Comment + from common.drf.fields import ReadableHiddenField +from ..models import Comment __all__ = ['CommentSerializer'] @@ -23,8 +24,7 @@ class CommentSerializer(serializers.ModelSerializer): model = Comment fields_mini = ['id'] fields_small = fields_mini + [ - 'body', 'user_display', - 'date_created', 'date_updated' + 'body', 'user_display', 'date_created', 'date_updated' ] fields_fk = ['ticket', 'user', ] fields = fields_small + fields_fk diff --git a/apps/tickets/serializers/flow.py b/apps/tickets/serializers/flow.py index e8c066100..f48e16501 100644 --- a/apps/tickets/serializers/flow.py +++ b/apps/tickets/serializers/flow.py @@ -1,25 +1,29 @@ +from rest_framework import serializers from django.db.transaction import atomic from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers + from orgs.models import Organization from orgs.utils import get_current_org_id from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from common.drf.fields import LabeledChoiceField from tickets.models import TicketFlow, ApprovalRule -from tickets.const import TicketApprovalStrategy +from tickets.const import TicketApprovalStrategy, TicketType __all__ = ['TicketFlowSerializer'] class TicketFlowApproveSerializer(serializers.ModelSerializer): - strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy')) + strategy = LabeledChoiceField( + choices=TicketApprovalStrategy.choices, required=True, label=_('Approve strategy') + ) assignees_read_only = serializers.SerializerMethodField(label=_('Assignees')) assignees_display = serializers.SerializerMethodField(label=_('Assignees display')) class Meta: model = ApprovalRule fields_small = [ - 'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display' + 'level', 'strategy', 'assignees_read_only', 'assignees_display', ] fields_m2m = ['assignees', ] fields = fields_small + fields_m2m @@ -46,14 +50,16 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer): class TicketFlowSerializer(OrgResourceModelSerializerMixin): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) + type = LabeledChoiceField( + choices=TicketType.choices, required=True, label=_('Type') + ) rules = TicketFlowApproveSerializer(many=True, required=True) class Meta: model = TicketFlow fields_mini = ['id', ] fields_small = fields_mini + [ - 'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated', + 'type', 'approval_level', 'created_by', 'date_created', 'date_updated', 'org_id', 'org_name' ] fields = fields_small + ['rules', ] diff --git a/apps/tickets/serializers/ticket/__init__.py b/apps/tickets/serializers/ticket/__init__.py index 698906d3a..73fc3b122 100644 --- a/apps/tickets/serializers/ticket/__init__.py +++ b/apps/tickets/serializers/ticket/__init__.py @@ -1,7 +1,6 @@ +from .common import * from .ticket import * from .apply_asset import * -from .apply_application import * from .login_confirm import * -from .login_asset_confirm import * from .command_confirm import * -from .common import * +from .login_asset_confirm import * diff --git a/apps/tickets/serializers/ticket/apply_application.py b/apps/tickets/serializers/ticket/apply_application.py deleted file mode 100644 index 740eda645..000000000 --- a/apps/tickets/serializers/ticket/apply_application.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.utils.translation import ugettext as _ -from rest_framework import serializers - -from perms.models import ApplicationPermission -from perms.serializers.base import ActionsField -from orgs.utils import tmp_to_org -from applications.models import Application -from tickets.models import ApplyApplicationTicket -from .ticket import TicketApplySerializer -from .common import BaseApplyAssetApplicationSerializer - -__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'ApproveApplicationSerializer'] - - -class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): - apply_actions = ActionsField(required=True, allow_empty=False) - permission_model = ApplicationPermission - - class Meta: - model = ApplyApplicationTicket - writeable_fields = [ - 'id', 'title', 'type', 'apply_category', 'comment', - 'apply_type', 'apply_applications', 'apply_system_users', - 'apply_actions', 'apply_date_start', 'apply_date_expired', 'org_id' - ] - fields = TicketApplySerializer.Meta.fields + \ - writeable_fields + ['apply_permission_name', 'apply_actions_display'] - read_only_fields = list(set(fields) - set(writeable_fields)) - ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs - extra_kwargs = { - 'apply_applications': {'required': False, 'allow_empty': True}, - 'apply_system_users': {'required': False, 'allow_empty': True}, - } - extra_kwargs.update(ticket_extra_kwargs) - - def validate_apply_applications(self, applications): - if self.is_final_approval and not applications: - raise serializers.ValidationError(_('This field is required.')) - tp = self.initial_data.get('apply_type') - return self.filter_many_to_many_field(Application, applications, type=tp) - - -class ApproveApplicationSerializer(ApplyApplicationSerializer): - class Meta(ApplyApplicationSerializer.Meta): - read_only_fields = ApplyApplicationSerializer.Meta.read_only_fields + ['title', 'type'] - - -class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer): - apply_applications = serializers.SerializerMethodField() - apply_system_users = serializers.SerializerMethodField() - - class Meta: - model = ApplyApplicationSerializer.Meta.model - fields = ApplyApplicationSerializer.Meta.fields - read_only_fields = fields - - @staticmethod - def get_apply_applications(instance): - with tmp_to_org(instance.org_id): - return instance.apply_applications.values_list('id', flat=True) - - @staticmethod - def get_apply_system_users(instance): - with tmp_to_org(instance.org_id): - return instance.apply_system_users.values_list('id', flat=True) diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index b1de8cf53..a0938cd99 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -1,39 +1,37 @@ -from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ -from perms.serializers.base import ActionsField -from perms.models import AssetPermission -from orgs.utils import tmp_to_org from assets.models import Asset, Node - +from perms.models import AssetPermission +from perms.serializers.permission import ActionChoicesField +from common.drf.fields import ObjectRelatedField from tickets.models import ApplyAssetTicket +from .common import BaseApplyAssetSerializer from .ticket import TicketApplySerializer -from .common import BaseApplyAssetApplicationSerializer -__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer', 'ApproveAssetSerializer'] +__all__ = ['ApplyAssetSerializer', 'ApproveAssetSerializer'] asset_or_node_help_text = _("Select at least one asset or node") -class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): - apply_actions = ActionsField(required=True, allow_empty=False) +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_actions = ActionChoicesField(required=False, allow_null=True, label=_("Apply actions")) permission_model = AssetPermission - class Meta: + class Meta(TicketApplySerializer.Meta): model = ApplyAssetTicket writeable_fields = [ - 'id', 'title', 'type', 'apply_nodes', 'apply_assets', - 'apply_system_users', 'apply_actions', 'comment', - 'apply_date_start', 'apply_date_expired', 'org_id' + 'id', 'title', 'type', 'apply_nodes', 'apply_assets', 'apply_accounts', + 'apply_actions', 'apply_date_start', 'apply_date_expired', + 'comment', 'org_id' ] - fields = TicketApplySerializer.Meta.fields + \ - writeable_fields + ['apply_permission_name', 'apply_actions_display'] - read_only_fields = list(set(fields) - set(writeable_fields)) + read_only_fields = TicketApplySerializer.Meta.read_only_fields + ['apply_permission_name', ] + fields = TicketApplySerializer.Meta.fields_small + writeable_fields + read_only_fields ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { - 'apply_nodes': {'required': False, 'allow_empty': True}, - 'apply_assets': {'required': False, 'allow_empty': True}, - 'apply_system_users': {'required': False, 'allow_empty': True}, + 'apply_accounts': {'required': False}, } extra_kwargs.update(ticket_extra_kwargs) @@ -44,6 +42,7 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria return self.filter_many_to_many_field(Asset, assets) def validate(self, attrs): + attrs['type'] = 'apply_asset' attrs = super().validate(attrs) if self.is_final_approval and ( not attrs.get('apply_nodes') and not attrs.get('apply_assets') @@ -55,33 +54,13 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria return attrs + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.prefetch_related('apply_nodes', 'apply_assets') + return queryset + class ApproveAssetSerializer(ApplyAssetSerializer): class Meta(ApplyAssetSerializer.Meta): - read_only_fields = ApplyAssetSerializer.Meta.read_only_fields + ['title', 'type'] - - -class ApplyAssetDisplaySerializer(ApplyAssetSerializer): - apply_nodes = serializers.SerializerMethodField() - apply_assets = serializers.SerializerMethodField() - apply_system_users = serializers.SerializerMethodField() - - class Meta: - model = ApplyAssetSerializer.Meta.model - fields = ApplyAssetSerializer.Meta.fields - read_only_fields = fields - - @staticmethod - def get_apply_nodes(instance): - with tmp_to_org(instance.org_id): - return instance.apply_nodes.values_list('id', flat=True) - - @staticmethod - def get_apply_assets(instance): - with tmp_to_org(instance.org_id): - return instance.apply_assets.values_list('id', flat=True) - - @staticmethod - def get_apply_system_users(instance): - with tmp_to_org(instance.org_id): - return instance.apply_system_users.values_list('id', flat=True) + read_only_fields = TicketApplySerializer.Meta.fields_small + \ + ApplyAssetSerializer.Meta.read_only_fields diff --git a/apps/tickets/serializers/ticket/command_confirm.py b/apps/tickets/serializers/ticket/command_confirm.py index c52bfd153..5b3a39b0e 100644 --- a/apps/tickets/serializers/ticket/command_confirm.py +++ b/apps/tickets/serializers/ticket/command_confirm.py @@ -9,8 +9,8 @@ __all__ = [ class ApplyCommandConfirmSerializer(TicketApplySerializer): class Meta: model = ApplyCommandTicket - fields = TicketApplySerializer.Meta.fields + [ - 'apply_run_user', 'apply_run_asset', 'apply_run_system_user', - 'apply_run_command', 'apply_from_session', 'apply_from_cmd_filter', - 'apply_from_cmd_filter_rule' + writeable_fields = [ + 'apply_run_user', 'apply_run_asset', 'apply_run_account', 'apply_run_command', + 'apply_from_session', 'apply_from_cmd_filter', 'apply_from_cmd_filter_rule' ] + fields = TicketApplySerializer.Meta.fields + writeable_fields diff --git a/apps/tickets/serializers/ticket/common.py b/apps/tickets/serializers/ticket/common.py index 6957da3b0..1af361693 100644 --- a/apps/tickets/serializers/ticket/common.py +++ b/apps/tickets/serializers/ticket/common.py @@ -1,13 +1,12 @@ -from django.db.transaction import atomic from django.db.models import Model +from django.db.transaction import atomic from django.utils.translation import ugettext as _ from rest_framework import serializers -from assets.models import SystemUser from orgs.utils import tmp_to_org from tickets.models import Ticket -__all__ = ['DefaultPermissionName', 'get_default_permission_name', 'BaseApplyAssetApplicationSerializer'] +__all__ = ['DefaultPermissionName', 'get_default_permission_name', 'BaseApplyAssetSerializer'] def get_default_permission_name(ticket): @@ -35,7 +34,7 @@ class DefaultPermissionName(object): return self.default -class BaseApplyAssetApplicationSerializer(serializers.Serializer): +class BaseApplyAssetSerializer(serializers.Serializer): permission_model: Model @property @@ -54,17 +53,16 @@ class BaseApplyAssetApplicationSerializer(serializers.Serializer): qs = model.objects.filter(id__in=ids, **kwargs).values_list('id', flat=True) return list(qs) - def validate_apply_system_users(self, system_users): - if self.is_final_approval and not system_users: + def validate_apply_accounts(self, accounts): + if self.is_final_approval and not accounts: raise serializers.ValidationError(_('This field is required.')) - return self.filter_many_to_many_field(SystemUser, system_users) + return accounts def validate(self, attrs): attrs = super().validate(attrs) apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S') apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S') - if apply_date_expired <= apply_date_start: error = _('The expiration date should be greater than the start date') raise serializers.ValidationError({'apply_date_expired': error}) @@ -77,10 +75,11 @@ class BaseApplyAssetApplicationSerializer(serializers.Serializer): def create(self, validated_data): instance = super().create(validated_data) name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4]) - with tmp_to_org(instance.org_id): + org_id = instance.org_id + with tmp_to_org(org_id): if not self.permission_model.objects.filter(name=name).exists(): instance.apply_permission_name = name - instance.save() + instance.save(update_fields=['apply_permission_name']) return instance raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name))) diff --git a/apps/tickets/serializers/ticket/login_asset_confirm.py b/apps/tickets/serializers/ticket/login_asset_confirm.py index 9e3076f0f..4d3db5fc6 100644 --- a/apps/tickets/serializers/ticket/login_asset_confirm.py +++ b/apps/tickets/serializers/ticket/login_asset_confirm.py @@ -9,6 +9,5 @@ __all__ = [ class LoginAssetConfirmSerializer(TicketApplySerializer): class Meta: model = ApplyLoginAssetTicket - fields = TicketApplySerializer.Meta.fields + [ - 'apply_login_user', 'apply_login_asset', 'apply_login_system_user' - ] + writeable_fields = ['apply_login_user', 'apply_login_asset', 'apply_login_account'] + fields = TicketApplySerializer.Meta.fields + writeable_fields diff --git a/apps/tickets/serializers/ticket/login_confirm.py b/apps/tickets/serializers/ticket/login_confirm.py index e760c653f..128ac5971 100644 --- a/apps/tickets/serializers/ticket/login_confirm.py +++ b/apps/tickets/serializers/ticket/login_confirm.py @@ -7,8 +7,7 @@ __all__ = [ class LoginConfirmSerializer(TicketApplySerializer): - class Meta: + class Meta(TicketApplySerializer.Meta): model = ApplyLoginTicket - fields = TicketApplySerializer.Meta.fields + [ - 'apply_login_ip', 'apply_login_city', 'apply_login_datetime' - ] + writeable_fields = ['apply_login_ip', 'apply_login_city', 'apply_login_datetime'] + fields = TicketApplySerializer.Meta.fields + writeable_fields diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index 007be5aa8..16e827ea0 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -1,34 +1,37 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ +from common.drf.fields import LabeledChoiceField from users.models import User from orgs.models import Organization from orgs.mixins.serializers import OrgResourceModelSerializerMixin from tickets.models import Ticket, TicketFlow -from tickets.const import TicketType +from tickets.const import TicketType, TicketStatus, TicketState __all__ = [ - 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer', 'TicketApproveSerializer' + 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketSerializer', ] class TicketSerializer(OrgResourceModelSerializerMixin): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display')) + type = LabeledChoiceField(choices=TicketType.choices, read_only=True, label=_('Type')) + status = LabeledChoiceField(choices=TicketStatus.choices, read_only=True, label=_('Status')) + state = LabeledChoiceField(choices=TicketState.choices, read_only=True, label=_("State")) class Meta: model = Ticket fields_mini = ['id', 'title'] - fields_small = fields_mini + [ - 'type', 'type_display', 'status', 'status_display', - 'state', 'approval_step', 'rel_snapshot', 'comment', - 'date_created', 'date_updated', 'org_id', 'rel_snapshot', - 'process_map', 'org_name', 'serial_num' + fields_small = fields_mini + ['org_id', 'comment'] + read_only_fields = [ + 'serial_num', 'process_map', 'approval_step', 'type', 'state', 'applicant', + 'status', 'date_created', 'date_updated', 'org_name', 'rel_snapshot' ] - fields_fk = ['applicant', ] - fields = fields_small + fields_fk + fields = fields_small + read_only_fields + extra_kwargs = { + 'type': {'required': True} + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -42,27 +45,14 @@ class TicketSerializer(OrgResourceModelSerializerMixin): choices.pop(TicketType.general, None) tp._choices = choices - -class TicketListSerializer(TicketSerializer): - class Meta: - model = Ticket - fields = [ - 'id', 'title', 'serial_num', 'type', 'type_display', 'status', - 'state', 'rel_snapshot', 'date_created', 'rel_snapshot' - ] - read_only_fields = fields - - -class TicketDisplaySerializer(TicketSerializer): - class Meta: - model = Ticket - fields = TicketSerializer.Meta.fields - read_only_fields = fields + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.prefetch_related('ticket_steps') + return queryset class TicketApproveSerializer(TicketSerializer): - class Meta: - model = Ticket + class Meta(TicketSerializer.Meta): fields = TicketSerializer.Meta.fields read_only_fields = fields @@ -103,9 +93,12 @@ class TicketApplySerializer(TicketSerializer): ticket_type = attrs.get('type') org_id = attrs.get('org_id') - flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first() + flow = TicketFlow.get_org_related_flows(org_id=org_id) \ + .filter(type=ticket_type).first() + if flow: attrs['flow'] = flow + return attrs else: error = _('The ticket flow `{}` does not exist'.format(ticket_type)) raise serializers.ValidationError(error) diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index 22715c527..9cc72d815 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # from django.urls import path - from rest_framework_bulk.routes import BulkRouter from .. import api @@ -10,17 +9,16 @@ app_name = 'tickets' router = BulkRouter() router.register('tickets', api.TicketViewSet, 'ticket') -router.register('apply-asset-tickets', api.ApplyAssetTicketViewSet, 'apply-asset-ticket') -router.register('apply-app-tickets', api.ApplyApplicationTicketViewSet, 'apply-app-ticket') -router.register('apply-login-tickets', api.ApplyLoginTicketViewSet, 'apply-login-ticket') -router.register('apply-login-asset-tickets', api.ApplyLoginAssetTicketViewSet, 'apply-login-asset-ticket') -router.register('apply-command-tickets', api.ApplyCommandTicketViewSet, 'apply-command-ticket') router.register('flows', api.TicketFlowViewSet, 'flows') router.register('comments', api.CommentViewSet, 'comment') +router.register('apply-asset-tickets', api.ApplyAssetTicketViewSet, 'apply-asset-ticket') +router.register('apply-login-tickets', api.ApplyLoginTicketViewSet, 'apply-login-ticket') +router.register('apply-command-tickets', api.ApplyCommandTicketViewSet, 'apply-command-ticket') +router.register('apply-login-asset-tickets', api.ApplyLoginAssetTicketViewSet, 'apply-login-asset-ticket') router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation') urlpatterns = [ - path('tickets//session/', api.TicketSessionApi.as_view(), name='ticket-sesion'), + path('tickets//session/', api.TicketSessionApi.as_view(), name='ticket-session'), path('super-tickets//status/', api.SuperTicketStatusAPI.as_view(), name='super-ticket-status'), ] urlpatterns += router.urls diff --git a/apps/tickets/views/approve.py b/apps/tickets/views/approve.py index f5f20e3d9..a3a265005 100644 --- a/apps/tickets/views/approve.py +++ b/apps/tickets/views/approve.py @@ -2,18 +2,18 @@ # from __future__ import unicode_literals -from django.views.generic.base import TemplateView -from django.shortcuts import redirect, reverse from django.core.cache import cache +from django.shortcuts import redirect, reverse +from django.views.generic.base import TemplateView from django.utils.translation import ugettext as _ from orgs.utils import tmp_to_root_org -from tickets.models import ( - Ticket, ApplyAssetTicket, ApplyApplicationTicket, - ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket -) from tickets.const import TicketType from tickets.errors import AlreadyClosed +from tickets.models import ( + Ticket, ApplyAssetTicket, + ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket +) from common.utils import get_logger, FlashMessageUtil logger = get_logger(__name__) @@ -27,7 +27,6 @@ class TicketDirectApproveView(TemplateView): TICKET_SUB_MODEL_MAP = { TicketType.apply_asset: ApplyAssetTicket, - TicketType.apply_application: ApplyApplicationTicket, TicketType.login_confirm: ApplyLoginTicket, TicketType.login_asset_confirm: ApplyLoginAssetTicket, TicketType.command_confirm: ApplyCommandTicket, diff --git a/apps/users/migrations/0040_alter_user_source.py b/apps/users/migrations/0040_alter_user_source.py index 7d798e5c4..56ad9befc 100644 --- a/apps/users/migrations/0040_alter_user_source.py +++ b/apps/users/migrations/0040_alter_user_source.py @@ -1,5 +1,5 @@ -# Generated by Django 3.2.13 on 2022-08-24 02:57 +# Generated by Django 3.2.13 on 2022-08-24 02:57 from django.db import migrations, models diff --git a/apps/users/models/user.py b/apps/users/models/user.py index f08a487c2..b50b8e525 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -255,7 +255,7 @@ class RoleManager(models.Manager): self.user.expire_users_rbac_perms_cache() return result except Exception as e: - logger.error('Create role binding error: {}'.format(e)) + logger.error('\tCreate role binding error: {}'.format(e)) def set(self, roles, clear=False): if clear: diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py index 1638a5b91..6f75b402a 100644 --- a/apps/users/serializers/group.py +++ b/apps/users/serializers/group.py @@ -47,7 +47,5 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - Prefetch('users', queryset=User.objects.only('id')) - ).annotate(users_amount=Count('users')) + queryset = queryset.prefetch_related('users').annotate(users_amount=Count('users')) return queryset diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index eb03a0e6f..d84a7baa0 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -4,7 +4,7 @@ from functools import partial from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.mixins import CommonBulkSerializerMixin +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 @@ -15,8 +15,10 @@ from ..models import User from ..const import PasswordStrategy __all__ = [ - 'UserSerializer', 'MiniUserSerializer', - 'InviteSerializer', 'ServiceAccountSerializer', + "UserSerializer", + "MiniUserSerializer", + "InviteSerializer", + "ServiceAccountSerializer", ] logger = get_logger(__file__) @@ -25,15 +27,17 @@ logger = get_logger(__file__) class RolesSerializerMixin(serializers.Serializer): system_roles = serializers.ManyRelatedField( child_relation=serializers.PrimaryKeyRelatedField(queryset=Role.system_roles), - label=_('System roles'), + label=_("System roles"), ) org_roles = serializers.ManyRelatedField( required=False, child_relation=serializers.PrimaryKeyRelatedField(queryset=Role.org_roles), - label=_('Org roles'), + label=_("Org roles"), ) - system_roles_display = serializers.SerializerMethodField(label=_('System roles display')) - org_roles_display = serializers.SerializerMethodField(label=_('Org roles display')) + 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): @@ -44,20 +48,20 @@ class RolesSerializerMixin(serializers.Serializer): return user.org_roles.display def pop_roles_if_need(self, fields): - request = self.context.get('request') - view = self.context.get('view') + request = self.context.get("request") + view = self.context.get("view") - if not all([request, view, hasattr(view, 'action')]): + if not all([request, view, hasattr(view, "action")]): return fields if request.user.is_anonymous: return fields - action = view.action or 'list' - if action in ('partial_bulk_update', 'bulk_update', 'partial_update', 'update'): - action = 'create' + action = view.action or "list" + 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", "system_roles_display"], + OrgRoleBinding: ["org_roles", "system_roles_display"], } for model_cls, fields_names in model_cls_field_mapper.items(): @@ -75,97 +79,148 @@ class RolesSerializerMixin(serializers.Serializer): return fields -class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializers.ModelSerializer): +class UserSerializer( + RolesSerializerMixin, CommonBulkSerializerMixin, serializers.ModelSerializer +): password_strategy = serializers.ChoiceField( - choices=PasswordStrategy.choices, default=PasswordStrategy.email, required=False, - write_only=True, label=_('Password strategy') + choices=PasswordStrategy.choices, + default=PasswordStrategy.email, + required=False, + write_only=True, + label=_("Password strategy"), + ) + mfa_enabled = serializers.BooleanField(read_only=True, label=_("MFA enabled")) + mfa_force_enabled = serializers.BooleanField( + read_only=True, label=_("MFA force enabled") ) - mfa_enabled = serializers.BooleanField(read_only=True, label=_('MFA enabled')) - 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') + 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')) + 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') + source="can_use_ssh_key_login", label=_("Can public key authentication") ) password = EncryptedField( - label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024 + label=_("Password"), + required=False, + allow_blank=True, + 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] + "system_roles": [BuiltinRole.system_user], + "org_roles": [BuiltinRole.org_user], } class Meta: model = User # mini 是指能识别对象的最小单元 - fields_mini = ['id', 'name', 'username'] + 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", + "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_verbose = fields_small + [ - 'mfa_level_display', 'mfa_force_enabled', 'is_first_login', - 'date_password_last_updated', 'avatar_url', + "mfa_level_display", + "mfa_force_enabled", + "is_first_login", + "date_password_last_updated", + "avatar_url", ] # 外键的字段 fields_fk = [] # 多对多字段 fields_m2m = [ - 'groups', 'groups_display', 'system_roles', 'org_roles', - 'system_roles_display', 'org_roles_display' + "groups", + "groups_display", + "system_roles", + "org_roles", + "system_roles_display", + "org_roles_display", ] # 在serializer 上定义的字段 - fields_custom = ['login_blocked', 'password_strategy'] + 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', 'feishu_id' + "date_joined", + "last_login", + "created_by", + "is_first_login", + "wecom_id", + "dingtalk_id", + "feishu_id", ] - disallow_self_update_fields = ['is_active'] + disallow_self_update_fields = ["is_active"] extra_kwargs = { - 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, - 'public_key': {'write_only': True}, - 'is_first_login': {'label': _('Is first login'), 'read_only': True}, - 'is_active': {'label': _('Is active')}, - 'is_valid': {'label': _('Is valid')}, - 'is_service_account': {'label': _('Is service account')}, - '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')}, + "password": { + "write_only": True, + "required": False, + "allow_null": True, + "allow_blank": True, + }, + "public_key": {"write_only": True}, + "is_first_login": {"label": _("Is first login"), "read_only": True}, + "is_active": {"label": _("Is active")}, + "is_valid": {"label": _("Is valid")}, + "is_service_account": {"label": _("Is service account")}, + "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): - password_strategy = self.initial_data.get('password_strategy') + password_strategy = self.initial_data.get("password_strategy") if self.instance is None and password_strategy != PasswordStrategy.custom: # 创建用户,使用邮件设置密码 return @@ -176,32 +231,34 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer @staticmethod def change_password_to_raw(attrs): - password = attrs.pop('password', None) + password = attrs.pop("password", None) if password: - attrs['password_raw'] = password + attrs["password_raw"] = password return attrs @staticmethod def clean_auth_fields(attrs): - for field in ('password', 'public_key'): + for field in ("password", "public_key"): value = attrs.get(field) if not value: attrs.pop(field, None) return attrs def check_disallow_self_update_fields(self, attrs): - request = self.context.get('request') + request = self.context.get("request") if not request or not request.user.is_authenticated: return attrs if not self.instance: return attrs if request.user.id != self.instance.id: return attrs - disallow_fields = set(list(attrs.keys())) & set(self.Meta.disallow_self_update_fields) + disallow_fields = set(list(attrs.keys())) & set( + self.Meta.disallow_self_update_fields + ) if not disallow_fields: return attrs # 用户自己不能更新自己的一些字段 - logger.debug('Disallow update self fields: %s', disallow_fields) + logger.debug("Disallow update self fields: %s", disallow_fields) for field in disallow_fields: attrs.pop(field, None) return attrs @@ -210,7 +267,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer attrs = self.check_disallow_self_update_fields(attrs) attrs = self.change_password_to_raw(attrs) attrs = self.clean_auth_fields(attrs) - attrs.pop('password_strategy', None) + attrs.pop("password_strategy", None) return attrs def save_and_set_custom_m2m_fields(self, validated_data, save_handler, created): @@ -219,8 +276,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer roles = validated_data.pop(f, None) if created and not roles: roles = [ - Role.objects.filter(id=role.id).first() - for role in default_roles + Role.objects.filter(id=role.id).first() for role in default_roles ] m2m_values[f] = roles @@ -234,22 +290,26 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer def update(self, instance, validated_data): save_handler = partial(super().update, instance) - instance = self.save_and_set_custom_m2m_fields(validated_data, save_handler, created=False) + instance = self.save_and_set_custom_m2m_fields( + validated_data, save_handler, created=False + ) return instance def create(self, validated_data): save_handler = super().create - instance = self.save_and_set_custom_m2m_fields(validated_data, save_handler, created=True) + instance = self.save_and_set_custom_m2m_fields( + validated_data, save_handler, created=True + ) return instance class UserRetrieveSerializer(UserSerializer): login_confirm_settings = serializers.PrimaryKeyRelatedField( - read_only=True, source='login_confirm_setting.reviewers', many=True + read_only=True, source="login_confirm_setting.reviewers", many=True ) class Meta(UserSerializer.Meta): - fields = UserSerializer.Meta.fields + ['login_confirm_settings'] + fields = UserSerializer.Meta.fields + ["login_confirm_settings"] class MiniUserSerializer(serializers.ModelSerializer): @@ -260,8 +320,10 @@ class MiniUserSerializer(serializers.ModelSerializer): class InviteSerializer(RolesSerializerMixin, serializers.Serializer): users = serializers.PrimaryKeyRelatedField( - queryset=User.get_nature_users(), many=True, label=_('Select users'), - help_text=_('For security, only list several users') + queryset=User.get_nature_users(), + many=True, + label=_("Select users"), + help_text=_("For security, only list several users"), ) system_roles = None system_roles_display = None @@ -271,22 +333,23 @@ class InviteSerializer(RolesSerializerMixin, serializers.Serializer): class ServiceAccountSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'name', 'access_key', 'comment'] - read_only_fields = ['access_key'] + fields = ["id", "name", "access_key", "comment"] + read_only_fields = ["access_key"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from authentication.serializers import AccessKeySerializer - self.fields['access_key'] = AccessKeySerializer(read_only=True) + + self.fields["access_key"] = AccessKeySerializer(read_only=True) def get_username(self): - return self.initial_data.get('name') + return self.initial_data.get("name") def get_email(self): - name = self.initial_data.get('name') + name = self.initial_data.get("name") name_max_length = 128 - len(User.service_account_email_suffix) - name = pretty_string(name, max_length=name_max_length, ellipsis_str='-') - return '{}{}'.format(name, User.service_account_email_suffix) + name = pretty_string(name, max_length=name_max_length, ellipsis_str="-") + return "{}{}".format(name, User.service_account_email_suffix) def validate_name(self, name): email = self.get_email() @@ -296,12 +359,12 @@ class ServiceAccountSerializer(serializers.ModelSerializer): else: users = User.objects.all() if users.filter(email=email) or users.filter(username=username): - raise serializers.ValidationError(_('name not unique'), code='unique') + raise serializers.ValidationError(_("name not unique"), code="unique") return name def create(self, validated_data): - name = validated_data['name'] + name = validated_data["name"] email = self.get_email() - comment = validated_data.get('comment', '') + comment = validated_data.get("comment", "") user, ak = User.create_service_account(name, email, comment) return user diff --git a/apps/users/tasks.py b/apps/users/tasks.py index ea6426aa4..cddf0d2ec 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -15,6 +15,7 @@ from orgs.models import Organization from .models import User from users.notifications import UserExpirationReminderMsg from settings.utils import LDAPServerUtil, LDAPImportUtil +from common.const.crontab import CRONTAB_AT_AM_TEN, CRONTAB_AT_PM_TWO logger = get_logger(__file__) @@ -41,7 +42,7 @@ def check_password_expired_periodic(): 'check_password_expired_periodic': { 'task': check_password_expired.name, 'interval': None, - 'crontab': '0 10 * * *', + 'crontab': CRONTAB_AT_AM_TEN, 'enabled': True, } } @@ -72,7 +73,7 @@ def check_user_expired_periodic(): 'check_user_expired_periodic': { 'task': check_user_expired.name, 'interval': None, - 'crontab': '0 14 * * *', + 'crontab': CRONTAB_AT_PM_TWO, 'enabled': True, } } diff --git a/entrypoint.sh b/entrypoint.sh index a752ccc8c..58ed0b104 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -22,4 +22,3 @@ elif [[ "$action" == "sleep" ]];then else python jms "${action}" "${service}" fi - diff --git a/generateV3Data.py b/generateV3Data.py new file mode 100644 index 000000000..673037e99 --- /dev/null +++ b/generateV3Data.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# + +# >>> Django 环境配置 +import django +import os +import sys + +if os.path.exists('../apps'): + sys.path.insert(0, '../apps') +elif os.path.exists('./apps'): + sys.path.insert(0, './apps') + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +APPS_DIR = os.path.join(BASE_DIR, 'apps') +sys.path.insert(0, APPS_DIR) + +os.environ.setdefault('PYTHONOPTIMIZE', '1') +if os.getuid() == 0: + os.environ.setdefault('C_FORCE_ROOT', '1') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings") +django.setup() + +# <<< + + +class Generator(object): + + def generate(self): + pass + + def generate_assets(self): + pass + + +if __name__ == '__main__': + pass diff --git a/jms b/jms index 267d78447..2bb8f64c4 100755 --- a/jms +++ b/jms @@ -178,7 +178,7 @@ if __name__ == '__main__': help="The service to start", ) parser.add_argument('-d', '--daemon', nargs="?", const=True) - parser.add_argument('-w', '--worker', type=int, nargs="?", default=4) + parser.add_argument('-w', '--worker', type=int, nargs="?") parser.add_argument('-f', '--force', nargs="?", const=True) args = parser.parse_args() diff --git a/requirements/mac_pkg.sh b/requirements/mac_pkg.sh index 5108b36fa..45049eb0a 100644 --- a/requirements/mac_pkg.sh +++ b/requirements/mac_pkg.sh @@ -3,7 +3,9 @@ BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" PROJECT_DIR=$(dirname "$BASE_DIR") echo "1. 安装依赖" -brew install libtiff libjpeg webp little-cms2 openssl gettext git git-lfs mysql libxml2 libxmlsec1 pkg-config postgresql freetds openssl +brew install libtiff libjpeg webp little-cms2 openssl gettext git \ + git-lfs mysql libxml2 libxmlsec1 pkg-config postgresql freetds openssl \ + libffi echo "2. 下载 IP 数据库" ip_db_path="${PROJECT_DIR}/apps/common/utils/geoip/GeoLite2-City.mmdb" @@ -11,3 +13,10 @@ wget "https://download.jumpserver.org/files/GeoLite2-City.mmdb" -O "${ip_db_path echo "3. 安装依赖的插件" git lfs install + +if ! uname -a | grep 'ARM64' &> /dev/null;then + exit 0 +fi + +echo "4. For Apple processor" +LDFLAGS="-L$(brew --prefix freetds)/lib -L$(brew --prefix openssl@1.1)/lib" CFLAGS="-I$(brew --prefix freetds)/include" pip install $(grep 'pymssql' requirements.txt) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e62b034a2..5a93ed237 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,7 @@ +aiofiles==22.1.0 amqp==5.0.9 -ansible==2.10.7 +ansible==6.4.0 +ansible-runner==2.2.1 asn1crypto==0.24.0 bcrypt==3.1.4 billiard==3.6.4.0 @@ -23,7 +25,7 @@ paramiko==2.11.0 passlib==1.7.4 pyasn1==0.4.8 pycparser==2.21 -cryptography==36.0.1 +cryptography==37.0.4 pycryptodome==3.15.0 pycryptodomex==3.15.0 gmssl==3.2.1 @@ -75,6 +77,7 @@ djangorestframework==3.13.1 djangorestframework-bulk==0.2.1 django-simple-history==3.1.1 drf-nested-routers==0.93.4 +drf-writable-nested==0.6.4 rest_condition==1.0.3 drf-yasg==1.20.0 coreapi==2.3.3 @@ -87,6 +90,7 @@ django-proxy==1.2.1 channels-redis==4.0.0 channels==3.0.4 daphne==3.0.2 +channels-redis==3.4.0 python-daemon==2.3.0 eventlet==0.33.1 greenlet==1.1.2 @@ -95,6 +99,8 @@ celery==5.2.7 flower==1.2.0 django-celery-beat==2.3.0 kombu==5.2.4 +uvicorn==0.20.0 +websockets==10.4 # Auth python-ldap==3.4.0 ldap3==2.9.1 @@ -130,12 +136,13 @@ mysqlclient==2.1.0 PyMySQL==1.0.2 oracledb==1.0.1 psycopg2-binary==2.9.1 -pymssql==2.1.5 +pymssql==2.2.5 django-mysql==3.9.0 django-redis==5.2.0 python-redis-lock==3.7.0 pyOpenSSL==22.0.0 redis==4.3.3 +pyOpenSSL==22.0.0 pymongo==4.2.0 # Debug ipython==8.4.0 @@ -143,3 +150,4 @@ ForgeryPy3==0.3.1 django-debug-toolbar==3.5 Pympler==1.0.1 IPy==1.1 +psycopg2==2.9.4 diff --git a/utils/create_assets_user/bulk_create_user.py b/utils/create_assets_user/bulk_create_user.py index 14cbca820..2897d4f0c 100644 --- a/utils/create_assets_user/bulk_create_user.py +++ b/utils/create_assets_user/bulk_create_user.py @@ -113,7 +113,7 @@ class UserCreation: "username": username, "password": password, "protocol": protocol, - "auto_push": bool(int(auto_push)), + "auto_push_account": bool(int(auto_push)), "login_mode": "auto" } users.append(info) diff --git a/utils/generate_fake_data/generate.py b/utils/generate_fake_data/generate.py index 97340e8e6..a1e82cf7a 100644 --- a/utils/generate_fake_data/generate.py +++ b/utils/generate_fake_data/generate.py @@ -12,7 +12,7 @@ sys.path.insert(0, APPS_DIR) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings") django.setup() -from resources.assets import AssetsGenerator, NodesGenerator, SystemUsersGenerator, AdminUsersGenerator +from resources.assets import AssetsGenerator, NodesGenerator, PlatformGenerator from resources.users import UserGroupGenerator, UserGenerator from resources.perms import AssetPermissionGenerator from resources.terminal import CommandGenerator, SessionGenerator @@ -20,9 +20,8 @@ from resources.terminal import CommandGenerator, SessionGenerator resource_generator_mapper = { 'asset': AssetsGenerator, + 'platform': PlatformGenerator, 'node': NodesGenerator, - 'system_user': SystemUsersGenerator, - 'admin_user': AdminUsersGenerator, 'user': UserGenerator, 'user_group': UserGroupGenerator, 'asset_permission': AssetPermissionGenerator, diff --git a/utils/generate_fake_data/resources/assets.py b/utils/generate_fake_data/resources/assets.py index 3aa11cc99..df4703dc1 100644 --- a/utils/generate_fake_data/resources/assets.py +++ b/utils/generate_fake_data/resources/assets.py @@ -5,44 +5,7 @@ import forgery_py from .base import FakeDataGenerator from assets.models import * - - -class AdminUsersGenerator(FakeDataGenerator): - resource = 'admin_user' - - def do_generate(self, batch, batch_size): - admin_users = [] - for i in batch: - username = forgery_py.internet.user_name(True) - password = forgery_py.basic.password() - admin_users.append(AdminUser( - name=username.title(), - username=username, - password=password, - org_id=self.org.id, - created_by='Fake', - )) - AdminUser.objects.bulk_create(admin_users, ignore_conflicts=True) - - -class SystemUsersGenerator(FakeDataGenerator): - def do_generate(self, batch, batch_size): - system_users = [] - protocols = list(dict(SystemUser.Protocol.choices).keys()) - for i in batch: - username = forgery_py.internet.user_name(True) - protocol = random.choice(protocols) - name = username.title() - name = f'{name}-{protocol}' - system_users.append(SystemUser( - name=name, - username=username, - password=forgery_py.basic.password(), - protocol=protocol, - org_id=self.org.id, - created_by='Fake', - )) - SystemUser.objects.bulk_create(system_users, ignore_conflicts=True) +from assets.const import AllTypes class NodesGenerator(FakeDataGenerator): @@ -55,14 +18,37 @@ class NodesGenerator(FakeDataGenerator): parent.create_child() -class AssetsGenerator(FakeDataGenerator): - resource = 'asset' - admin_user_ids: list - node_ids: list +class PlatformGenerator(FakeDataGenerator): + resource = 'platform' + category_type: dict + categories: list + + def pre_generate(self): + self.category_type = dict(AllTypes.category_types()) + self.categories = list(self.category_type.keys()) + + def do_generate(self, batch, batch_size): + platforms = [] + for i in batch: + category = choice(self.categories) + tp = choice(self.category_type[category].choices) + data = { + 'name': forgery_py.name.company_name(), + 'category': category, + 'type': tp[0] + } + platforms.append(Platform(**data)) + Platform.objects.bulk_create(platforms, ignore_conflicts=True) + + +class AssetsGenerator(FakeDataGenerator): + resource = 'asset' + node_ids: list + platform_ids: list def pre_generate(self): - self.admin_user_ids = list(AdminUser.objects.all().values_list('id', flat=True)) self.node_ids = list(Node.objects.all().values_list('id', flat=True)) + self.platform_ids = list(Platform.objects.all().values_list('id', flat=True)) def set_assets_nodes(self, assets): for asset in assets: @@ -78,8 +64,8 @@ class AssetsGenerator(FakeDataGenerator): hostname = f'{hostname}-{ip}' data = dict( ip=ip, - hostname=hostname, - admin_user_id=choice(self.admin_user_ids), + name=hostname, + platform_id=choice(self.platform_ids), created_by='Fake', org_id=self.org.id ) diff --git a/utils/generate_fake_data/resources/perms.py b/utils/generate_fake_data/resources/perms.py index 953712a5d..d6a5248cb 100644 --- a/utils/generate_fake_data/resources/perms.py +++ b/utils/generate_fake_data/resources/perms.py @@ -19,7 +19,6 @@ class AssetPermissionGenerator(FakeDataGenerator): def pre_generate(self): self.node_ids = list(Node.objects.all().values_list('id', flat=True)) self.asset_ids = list(Asset.objects.all().values_list('id', flat=True)) - self.system_user_ids = list(SystemUser.objects.all().values_list('id', flat=True)) self.user_ids = list(User.objects.all().values_list('id', flat=True)) self.user_group_ids = list(UserGroup.objects.all().values_list('id', flat=True)) @@ -47,12 +46,6 @@ class AssetPermissionGenerator(FakeDataGenerator): relation_name = 'node_id' self.set_relations(perms, through, relation_name, choices) - def set_system_users(self, perms): - through = AssetPermission.system_users.through - choices = self.system_user_ids - relation_name = 'systemuser_id' - self.set_relations(perms, through, relation_name, choices) - def set_relations(self, perms, through, relation_name, choices, choice_count=None): relations = [] @@ -79,4 +72,3 @@ class AssetPermissionGenerator(FakeDataGenerator): self.set_user_groups(created) self.set_assets(created) self.set_nodes(created) - self.set_system_users(created) diff --git a/utils/generate_fake_data/resources/users.py b/utils/generate_fake_data/resources/users.py index c973fec5e..34a9d9703 100644 --- a/utils/generate_fake_data/resources/users.py +++ b/utils/generate_fake_data/resources/users.py @@ -4,7 +4,6 @@ import forgery_py from .base import FakeDataGenerator from users.models import * -from orgs.models import OrganizationMember class UserGroupGenerator(FakeDataGenerator): diff --git a/utils/playbooks/change_password/main.yml b/utils/playbooks/change_password/main.yml index 3a981fdb2..31748fe1c 100644 --- a/utils/playbooks/change_password/main.yml +++ b/utils/playbooks/change_password/main.yml @@ -7,7 +7,7 @@ tasks: - name: 监测特权用户密码 - ping: + ansible.builtin.ping: - name: 更改用户密码 user: @@ -19,5 +19,5 @@ vars: - ansible_user: '{{ user1 }}' ansible_ssh_password: '{{ user1password }}' - ping: + ansible.builtin.ping: diff --git a/utils/sync_node.py b/utils/sync_node.py index 4563a8ab0..983f6ae7c 100644 --- a/utils/sync_node.py +++ b/utils/sync_node.py @@ -28,7 +28,7 @@ def sync_node(src, target, cut=False): asset.save() new_asset = asset else: - new_asset = get_object_or_none(Asset, hostname=asset.hostname, org_id=target.org_id) + new_asset = get_object_or_none(Asset, hostname=asset.name, org_id=target.org_id) if new_asset is None: asset.id = None asset.org_id = target.org_id diff --git a/utils/test_run_migrations.py b/utils/test_run_migrations.py index 33c7c3c5c..70162ab68 100644 --- a/utils/test_run_migrations.py +++ b/utils/test_run_migrations.py @@ -42,7 +42,7 @@ def migrate_system_role_binding(apps, schema_editor): role_bindings.append(role_binding) role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True) - print("Create role binding: {}-{} using: {:.2f}s".format( + print("\tCreate role binding: {}-{} using: {:.2f}s".format( count, count + len(users), time.time()-start )) count += len(users)