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 9ff8dc4e2..c452e82b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,65 +1,78 @@ +FROM python:3.8-slim as stage-build +ARG TARGETARCH + +ARG VERSION +ENV VERSION=$VERSION + +WORKDIR /opt/jumpserver +ADD . . +RUN cd utils && bash -ixeu build.sh + FROM python:3.8-slim +ARG TARGETARCH MAINTAINER JumpServer Team ARG BUILD_DEPENDENCIES=" \ - g++ \ - make \ - pkg-config" + g++ \ + make \ + pkg-config" ARG DEPENDENCIES=" \ - default-libmysqlclient-dev \ - freetds-dev \ - libpq-dev \ - libffi-dev \ - libldap2-dev \ - libsasl2-dev \ - libxml2-dev \ - libxmlsec1-dev \ - libxmlsec1-openssl \ - libaio-dev \ - openssh-client \ - sshpass" + default-libmysqlclient-dev \ + freetds-dev \ + libpq-dev \ + libffi-dev \ + libjpeg-dev \ + libldap2-dev \ + libsasl2-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libxmlsec1-openssl \ + libaio-dev \ + openssh-client \ + sshpass" ARG TOOLS=" \ - curl \ - default-mysql-client \ - iproute2 \ - iputils-ping \ - locales \ - procps \ - redis-tools \ - telnet \ - vim \ - unzip \ - wget" + ca-certificates \ + curl \ + default-mysql-client \ + iputils-ping \ + locales \ + procps \ + redis-tools \ + telnet \ + vim \ + unzip \ + wget" -RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \ +ARG APT_MIRROR=http://mirrors.ustc.edu.cn + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ + sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ + && rm -f /etc/apt/apt.conf.d/docker-clean \ + && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && apt-get update \ && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${TOOLS} \ - && localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \ - && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && mkdir -p /root/.ssh/ \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ - && sed -i "s@# alias l@alias l@g" ~/.bashrc \ && echo "set mouse-=a" > ~/.vimrc \ && echo "no" | dpkg-reconfigure dash \ + && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ + && sed -i "s@# export @export @g" ~/.bashrc \ + && sed -i "s@# alias @alias @g" ~/.bashrc \ && rm -rf /var/lib/apt/lists/* -ARG TARGETARCH -ARG ORACLE_LIB_MAJOR=19 -ARG ORACLE_LIB_MINOR=10 -ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip" +ARG DOWNLOAD_URL=https://download.jumpserver.org RUN mkdir -p /opt/oracle/ \ && cd /opt/oracle/ \ - && wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \ - && unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \ - && mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \ - && echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \ + && wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \ + && unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \ + && sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \ && ldconfig \ - && rm -f ${ORACLE_FILE} + && rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip WORKDIR /tmp/build COPY ./requirements ./requirements @@ -68,27 +81,25 @@ ARG PIP_MIRROR=https://pypi.douban.com/simple ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR -# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有 -RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ - && pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ - && pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \ - && rm -rf ~/.cache/pip -ARG VERSION -ENV VERSION=$VERSION -ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules -ADD . . -RUN cd utils \ - && bash -ixeu build.sh \ - && mv ../release/jumpserver /opt/jumpserver \ - && rm -rf /tmp/build \ - && echo > /opt/jumpserver/config.yml +RUN --mount=type=cache,target=/root/.cache/pip \ + set -ex \ + && pip config set global.index-url ${PIP_MIRROR} \ + && pip install --upgrade pip \ + && pip install --upgrade setuptools wheel \ + && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ + && pip install -r requirements/requirements.txt + +COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver +RUN echo > /opt/jumpserver/config.yml \ + && rm -rf /tmp/build WORKDIR /opt/jumpserver 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 EXPOSE 8080 diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 new file mode 100644 index 000000000..580792776 --- /dev/null +++ b/Dockerfile.loong64 @@ -0,0 +1,96 @@ +FROM python:3.8-slim as stage-build +ARG TARGETARCH + +ARG VERSION +ENV VERSION=$VERSION + +WORKDIR /opt/jumpserver +ADD . . +RUN cd utils && bash -ixeu build.sh + +FROM python:3.8-slim +ARG TARGETARCH +MAINTAINER JumpServer Team + +ARG BUILD_DEPENDENCIES=" \ + g++ \ + make \ + pkg-config" + +ARG DEPENDENCIES=" \ + default-libmysqlclient-dev \ + freetds-dev \ + libpq-dev \ + libffi-dev \ + libjpeg-dev \ + libldap2-dev \ + libsasl2-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libxmlsec1-openssl \ + libaio-dev \ + openssh-client \ + sshpass" + +ARG TOOLS=" \ + ca-certificates \ + curl \ + default-mysql-client \ + iputils-ping \ + locales \ + netcat \ + redis-server \ + telnet \ + vim \ + unzip \ + wget" + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ + set -ex \ + && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && apt-get update \ + && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ + && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ + && apt-get -y install --no-install-recommends ${TOOLS} \ + && mkdir -p /root/.ssh/ \ + && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ + && echo "set mouse-=a" > ~/.vimrc \ + && echo "no" | dpkg-reconfigure dash \ + && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ + && sed -i "s@# export @export @g" ~/.bashrc \ + && sed -i "s@# alias @alias @g" ~/.bashrc \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /tmp/build +COPY ./requirements ./requirements + +ARG PIP_MIRROR=https://pypi.douban.com/simple +ENV PIP_MIRROR=$PIP_MIRROR +ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple +ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR + +RUN --mount=type=cache,target=/root/.cache/pip \ + set -ex \ + && pip config set global.index-url ${PIP_MIRROR} \ + && pip install --upgrade pip \ + && pip install --upgrade setuptools wheel \ + && pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \ + && pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \ + && pip install $(grep 'PyNaCl' requirements/requirements.txt) \ + && GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \ + && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ + && pip install -r requirements/requirements.txt + +COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver +RUN echo > /opt/jumpserver/config.yml \ + && rm -rf /tmp/build + +WORKDIR /opt/jumpserver +VOLUME /opt/jumpserver/data +VOLUME /opt/jumpserver/logs + +ENV LANG=zh_CN.UTF-8 + +EXPOSE 8070 +EXPOSE 8080 +ENTRYPOINT ["./entrypoint.sh"] diff --git a/apps/acls/api/connect_acl.py b/apps/acls/api/connect_acl.py new file mode 100644 index 000000000..aeff03ea2 --- /dev/null +++ b/apps/acls/api/connect_acl.py @@ -0,0 +1,52 @@ +from rest_framework.views import APIView +from rest_framework import status +from django.http.response import JsonResponse +from django.utils.translation import ugettext_lazy as _ + +from common.drf.api import JMSBulkModelViewSet +from common.const.choices import ConnectMethodChoices +from ..models import ConnectACL +from .. import serializers + +__all__ = ['ConnectACLViewSet', 'ConnectMethodsAPI', 'ConnectMethodPermissionsAPI'] + + +class ConnectACLViewSet(JMSBulkModelViewSet): + queryset = ConnectACL.objects.all() + filterset_fields = ('name', ) + search_fields = ('name',) + serializer_class = serializers.ConnectACLSerializer + + +class ConnectMethodsAPI(APIView): + rbac_perms = { + 'GET': 'acls.view_connnectacl', + } + + @staticmethod + def get(request, *args, **kwargs): + data = [] + for m in ConnectMethodChoices.choices: + data.append({'label': m[1], 'value': m[0]}) + return JsonResponse(data, safe=False) + + +class ConnectMethodPermissionsAPI(APIView): + rbac_perms = { + 'GET': 'acls.view_connnectacl', + } + + @staticmethod + def get(request, *args, **kwargs): + login_type = request.query_params.get('login_type') + if not login_type: + rules = ConnectACL().all_rules(request.user) + return JsonResponse({'rules': rules}) + + acl = ConnectACL.match(request.user, login_type) + if acl: + err = _('The current user is not allowed to login in this way') + return JsonResponse({'error': err}) + else: + return JsonResponse({'msg': 'ok'}) + diff --git a/apps/acls/migrations/0004_connectacl.py b/apps/acls/migrations/0004_connectacl.py new file mode 100644 index 000000000..3b640d1f3 --- /dev/null +++ b/apps/acls/migrations/0004_connectacl.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.16 on 2022-11-30 02:46 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0040_alter_user_source'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('acls', '0003_auto_20211130_1037'), + ] + + operations = [ + migrations.CreateModel( + name='ConnectACL', + 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')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('rules', models.JSONField(default=list, verbose_name='Rule')), + ('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')), + ('user_groups', models.ManyToManyField(blank=True, related_name='connect_acls', to='users.UserGroup', verbose_name='User group')), + ('users', models.ManyToManyField(blank=True, related_name='connect_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Connect acl', + 'ordering': ('priority', '-date_updated', 'name'), + }, + ), + ] diff --git a/apps/acls/models/connect_acl.py b/apps/acls/models/connect_acl.py new file mode 100644 index 000000000..a936f3cb4 --- /dev/null +++ b/apps/acls/models/connect_acl.py @@ -0,0 +1,120 @@ +from django.db import models +from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ + +from common.utils.connection import get_redis_client +from common.const.choices import ConnectMethodChoices +from orgs.mixins.models import OrgManager, OrgModelMixin +from .base import BaseACL, BaseACLQuerySet + + +class ACLManager(OrgManager): + + def valid(self): + return self.get_queryset().valid() + + +class ConnectACL(BaseACL, OrgModelMixin): + ConnectACLUserCacheKey = 'CONNECT_ACL_USER_{}' + ConnectACLUserCacheTTL = 600 + + class ActionChoices(models.TextChoices): + reject = 'reject', _('Reject') + + # 用户 + users = models.ManyToManyField( + 'users.User', related_name='connect_acls', blank=True, + verbose_name=_("User") + ) + user_groups = models.ManyToManyField( + 'users.UserGroup', related_name='connect_acls', blank=True, + verbose_name=_("User group"), + ) + rules = models.JSONField(default=list, verbose_name=_('Rule')) + # 动作 + action = models.CharField( + max_length=64, verbose_name=_('Action'), + choices=ActionChoices.choices, default=ActionChoices.reject + ) + + objects = ACLManager.from_queryset(BaseACLQuerySet)() + + class Meta: + ordering = ('priority', '-date_updated', 'name') + verbose_name = _('Connect acl') + + def __str__(self): + return self.name + + @property + def rules_display(self): + return ', '.join( + [ConnectMethodChoices.get_label(i) for i in self.rules] + ) + + def is_action(self, action): + return self.action == action + + @staticmethod + def match(user, connect_type): + if not user: + return + + user_acls = user.connect_acls.all().valid().distinct() + for acl in user_acls: + if connect_type in acl.rules: + return acl + + for user_group in user.groups.all(): + acls = user_group.connect_acls.all().valid().distinct() + for acl in acls: + if connect_type in acl.rules: + return acl + + def _get_all_rules_from_cache(self, user): + find = False + cache_key = self.ConnectACLUserCacheKey.format(user.id) + rules = cache.get(cache_key) + if rules is not None: + find = True + return rules, find + + @staticmethod + def _get_all_rules_from_db(user): + connect_rules = set() + user_acls = user.connect_acls.all().valid() + user_acl_rules = user_acls.values_list('id', 'rules') + for r_id, rule in user_acl_rules: + connect_rules.update(rule) + + for ug in user.groups.all(): + user_group_acls = ug.connect_acls.all().valid() + user_group_rules = user_group_acls.values_list('id', 'rules') + for r_id, rule in user_group_rules: + connect_rules.update(rule) + return list(connect_rules) + + def set_all_rules_to_cache(self, key, rules): + cache.set(key, rules, self.ConnectACLUserCacheTTL) + + def all_rules(self, user): + rules, find = self._get_all_rules_from_cache(user) + if not find: + rules = self._get_all_rules_from_db(user) + self.set_all_rules_to_cache( + self.ConnectACLUserCacheKey.format(user.id), rules + ) + return rules + + def clear_rules_cache(self): + cache.delete_pattern( + self.ConnectACLUserCacheKey.format('*') + ) + + def save(self, *args, **kwargs): + self.clear_rules_cache() + return super().save(*args, **kwargs) + + def delete(self, using=None, keep_parents=False): + self.clear_rules_cache() + return super().delete(using=using, keep_parents=keep_parents) diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 6fcff0231..71f202b15 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -53,12 +53,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.confirm) and \ + not acl.reviewers.exists(): continue ip_group = acl.rules.get('ip_group') time_periods = acl.rules.get('time_period') @@ -79,12 +80,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 37bea242a..2ad9363e5 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -86,12 +86,12 @@ class LoginAssetACL(BaseACL, OrgModelMixin): 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_account': str(account), - 'org_id': org_id, + 'type': TicketType.login_asset_confirm, } ticket = ApplyLoginAssetTicket.objects.create(**data) ticket.open_by_system(assignees) diff --git a/apps/acls/serializers/connnect_acl.py b/apps/acls/serializers/connnect_acl.py new file mode 100644 index 000000000..c4377491f --- /dev/null +++ b/apps/acls/serializers/connnect_acl.py @@ -0,0 +1,36 @@ +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from common.drf.serializers import BulkModelSerializer +from common.const.choices import ConnectMethodChoices +from ..models import ConnectACL + + +__all__ = ['ConnectACLSerializer', ] + + +class ConnectACLSerializer(BulkModelSerializer): + action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + + class Meta: + model = ConnectACL + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'priority', 'rules', 'rules_display', 'action', 'action_display', 'is_active', + 'date_created', 'date_updated', 'comment', 'created_by' + ] + fields_m2m = ['users', 'user_groups'] + fields = fields_small + fields_m2m + extra_kwargs = { + 'priority': {'default': 50}, + 'is_active': {'default': True} + } + + @staticmethod + def validate_rules(rules): + for r in rules: + label = ConnectMethodChoices.get_label(r) + if not label: + error = _('Invalid connection method: {}').format(r) + raise serializers.ValidationError(error) + return rules diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index a699ae1ea..f759da435 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -2,38 +2,57 @@ 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 from jumpserver.utils import has_valid_xpack_license +from users.models import User from ..models import LoginACL from .rules import RuleSerializer -__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_display = serializers.ReadOnlyField( + source="get_action_display", label=_("Action") + ) + 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", + "rules", + "action", + "action_display", + "is_active", + "user", + "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,7 +60,7 @@ 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 @@ -51,6 +70,3 @@ class LoginACLSerializer(BulkModelSerializer): 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 7282bf1a9..84bab6cc3 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -3,54 +3,66 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.models import Organization -from assets.const import Protocol +from common.drf.fields import LabeledChoiceField from acls import models -__all__ = ['LoginAssetACLSerializer'] +__all__ = ["LoginAssetACLSerializer"] -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 LoginAssetACLUsersSerializer(serializers.Serializer): username_group = serializers.ListField( - default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), - help_text=common_help_text + 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)' + "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 + 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 + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Hostname"), + help_text=common_help_text, ) class LoginAssetACLAccountsSerializer(serializers.Serializer): protocol_group_help_text = _( - 'Format for comma-delimited string, with * indicating a match all. ' - 'Protocol options: {}' + "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 + 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 + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, ) @@ -58,34 +70,48 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): users = LoginAssetACLUsersSerializer() assets = LoginAssetACLAssestsSerializer() accounts = LoginAssetACLAccountsSerializer() - reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') - action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + reviewers_amount = serializers.IntegerField( + read_only=True, source="reviewers.count" + ) + action = LabeledChoiceField( + choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action") + ) class Meta: model = models.LoginAssetACL - fields_mini = ['id', 'name'] + fields_mini = ["id", "name"] fields_small = fields_mini + [ - 'users', 'accounts', 'assets', - 'is_active', 'date_created', 'date_updated', - 'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id' + "users", + "accounts", + "assets", + "is_active", + "date_created", + "date_updated", + "priority", + "action", + "comment", + "created_by", + "org_id", ] - fields_m2m = ['reviewers', 'reviewers_amount'] + 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}, + "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_id = self.fields["org_id"].default() org = Organization.get_instance(org_id) if not org: - error = _('The organization `{}` does not exist'.format(org_id)) + 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)) + error = _( + "None of the reviewers belong to Organization `{}`".format(org.name) + ) raise serializers.ValidationError(error) return valid_reviewers diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 4e1176d17..f6cc509b3 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -1,89 +1,89 @@ # -*- coding: utf-8 -*- # + import django_filters from rest_framework.decorators import action from rest_framework.response import Response -from common.utils import get_logger +from assets import serializers +from assets.models import Asset +from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend +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 orgs.mixins.api import OrgBulkModelViewSet +from common.utils import get_logger from orgs.mixins import generics -from assets import serializers -from assets.models import Asset, Gateway -from assets.tasks import ( - push_accounts_to_assets, - verify_accounts_connectivity, - test_assets_connectivity_manual, - update_assets_hardware_info_manual, -) -from assets.filters import NodeFilterBackend, LabelFilterBackend, IpInFilterBackend +from orgs.mixins.api import OrgBulkModelViewSet from ..mixin import NodeFilterMixin logger = get_logger(__file__) __all__ = [ - 'AssetViewSet', 'AssetTaskCreateApi', 'AssetsTaskCreateApi', + "AssetViewSet", + "AssetTaskCreateApi", + "AssetsTaskCreateApi", ] 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') + 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 = ['name', 'address', 'is_active', 'type', 'category', 'hostname'] + fields = ["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',) + ordering = ("name",) serializer_classes = ( - ('default', serializers.AssetSerializer), - ('suggestion', serializers.MiniAssetSerializer), - ('platform', serializers.PlatformSerializer), - ('gateways', serializers.GatewayWithAuthSerializer) + ("default", serializers.AssetSerializer), + ("suggestion", serializers.MiniAssetSerializer), + ("platform", serializers.PlatformSerializer), + ("gateways", serializers.GatewayWithAuthSerializer), ) rbac_perms = ( - ('match', 'assets.match_asset'), - ('platform', 'assets.view_platform'), - ('gateways', 'assets.view_gateway') + ("match", "assets.match_asset"), + ("platform", "assets.view_platform"), + ("gateways", "assets.view_gateway"), ) - extra_filter_backends = [ - LabelFilterBackend, - IpInFilterBackend, - NodeFilterBackend - ] + extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] - @action(methods=['GET'], detail=True, url_path='platform') + @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') + @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() + gateways = Asset.objects.none() else: - gateways = asset.domain.gateways.filter(protocol='ssh') + gateways = asset.domain.gateways.filter(protocol="ssh") return self.get_paginated_response_from_queryset(gateways) class AssetsTaskMixin: def perform_assets_task(self, serializer): data = serializer.validated_data - assets = data.get('assets', []) + assets = data.get("assets", []) asset_ids = [asset.id for asset in assets] - if data['action'] == "refresh": + if data["action"] == "refresh": task = update_assets_hardware_info_manual.delay(asset_ids) else: task = test_assets_connectivity_manual.delay(asset_ids) @@ -94,9 +94,9 @@ class AssetsTaskMixin: self.set_task_to_serializer_data(serializer, task) def set_task_to_serializer_data(self, serializer, task): - data = getattr(serializer, '_data', {}) + data = getattr(serializer, "_data", {}) data["task"] = task.id - setattr(serializer, '_data', data) + setattr(serializer, "_data", data) class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): @@ -104,18 +104,18 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): serializer_class = serializers.AssetTaskSerializer def create(self, request, *args, **kwargs): - pk = self.kwargs.get('pk') - request.data['asset'] = pk - request.data['assets'] = [pk] + 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 = 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' + "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) @@ -126,19 +126,19 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): @staticmethod def perform_asset_task(serializer): data = serializer.validated_data - if data['action'] not in ['push_system_user', 'test_system_user']: + if data["action"] not in ["push_system_user", "test_system_user"]: return - asset = data['asset'] - accounts = data.get('accounts') + 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': + 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': + elif action == "test_account": task = verify_accounts_connectivity.delay(account_ids, asset_ids) else: task = None @@ -156,9 +156,9 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): serializer_class = serializers.AssetsTaskSerializer def check_permissions(self, request): - action = request.data.get('action') + action = request.data.get("action") action_perm_require = { - 'refresh': 'assets.refresh_assethardwareinfo', + "refresh": "assets.refresh_assethardwareinfo", } perm_required = action_perm_require.get(action) has = self.request.user.has_perm(perm_required) diff --git a/apps/assets/api/asset/host.py b/apps/assets/api/asset/host.py index 3094e15a8..fbc2e997c 100644 --- a/apps/assets/api/asset/host.py +++ b/apps/assets/api/asset/host.py @@ -1,4 +1,3 @@ - from assets.models import Host from assets.serializers import HostSerializer from .asset import AssetViewSet diff --git a/apps/assets/api/automations/base.py b/apps/assets/api/automations/base.py index 845b721c8..1b480cbdc 100644 --- a/apps/assets/api/automations/base.py +++ b/apps/assets/api/automations/base.py @@ -5,7 +5,6 @@ from rest_framework import status, mixins, viewsets from orgs.mixins import generics from assets import serializers -from assets.const import AutomationTypes from assets.tasks import execute_automation from assets.models import BaseAutomation, AutomationExecution from common.const.choices import Trigger @@ -111,8 +110,7 @@ class AutomationExecutionViewSet( serializer.is_valid(raise_exception=True) automation = serializer.validated_data.get('automation') tp = serializer.validated_data.get('type') - model = AutomationTypes.get_type_model(tp) task = execute_automation.delay( - pid=automation.pk, trigger=Trigger.manual, model=model + 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 index 112f0f93b..3cdea7e65 100644 --- a/apps/assets/api/automations/change_secret.py +++ b/apps/assets/api/automations/change_secret.py @@ -36,5 +36,5 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): execution = get_object_or_none(AutomationExecution, pk=eid) if execution: queryset = queryset.filter(execution=execution) - queryset = queryset.order_by('is_success', '-date_start') + queryset = queryset.order_by('-date_started') return queryset diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 39f2b44f9..bb705322c 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -7,21 +7,20 @@ from rest_framework.serializers import ValidationError from common.utils import get_logger from orgs.mixins.api import OrgBulkModelViewSet -from ..models import Domain, Gateway +from ..models import Domain, Host 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,21 +29,26 @@ class DomainViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet): - model = Gateway - filterset_fields = ("domain__name", "name", "username", "domain") - search_fields = ("domain__name", "name", "username", ) + filterset_fields = ("domain__name", "name", "domain") + search_fields = ("domain__name",) serializer_class = serializers.GatewaySerializer + def get_queryset(self): + queryset = Host.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 = Host.get_gateway_queryset() + return queryset + def post(self, request, *args, **kwargs): - self.object = self.get_object(Gateway.objects.all()) + self.object = self.get_object() local_port = self.request.data.get('port') or self.object.port try: local_port = int(local_port) diff --git a/apps/assets/const/__init__.py b/apps/assets/const/__init__.py index 81115b412..bc30f388d 100644 --- a/apps/assets/const/__init__.py +++ b/apps/assets/const/__init__.py @@ -1,3 +1,5 @@ +from .base import * +from .host import * from .types import * from .account import * from .protocol import * diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index 371ab3688..8be44db6f 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -1,5 +1,7 @@ from .base import BaseType +GATEWAY_NAME = 'Gateway' + class HostTypes(BaseType): LINUX = 'linux', 'Linux' @@ -67,7 +69,7 @@ class HostTypes(BaseType): return { cls.LINUX: [ {'name': 'Linux'}, - {'name': 'Gateway'} + {'name': GATEWAY_NAME} ], cls.UNIX: [ {'name': 'Unix'}, diff --git a/apps/assets/filters.py b/apps/assets/filters.py index de2550ceb..f1b869805 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # 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 django_filters import rest_framework as drf_filters +from assets.utils import get_node_from_request, is_query_node_all_assets from common.drf.filters import BaseFilterSet -from assets.utils import is_query_node_all_assets, get_node_from_request -from .models import Label, Node, Account + +from .models import Account, Label, Node class AssetByNodeFilterBackend(filters.BaseFilterBackend): 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..da43b3a84 --- /dev/null +++ b/apps/assets/migrations/0112_gateway_to_asset.py @@ -0,0 +1,73 @@ +# 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), + ] diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index 9aa007e53..cad5f9ded 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -3,7 +3,8 @@ from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords from common.utils import lazyproperty -from .base import BaseAccount, AbsConnectivity + +from .base import AbsConnectivity, BaseAccount __all__ = ['Account', 'AccountTemplate'] @@ -40,9 +41,10 @@ class AccountHistoricalRecords(HistoricalRecords): class Account(AbsConnectivity, BaseAccount): - class InnerAccount(models.TextChoices): - INPUT = '@INPUT', '@INPUT' - USER = '@USER', '@USER' + class AliasAccount(models.TextChoices): + ALL = '@ALL', _('All') + INPUT = '@INPUT', _('Manual input') + USER = '@USER', _('Dynamic user') asset = models.ForeignKey( 'assets.Asset', related_name='accounts', @@ -76,14 +78,14 @@ class Account(AbsConnectivity, BaseAccount): return '{}'.format(self.username) @classmethod - def get_input_account(cls): + def get_manual_account(cls): """ @INPUT 手动登录的账号(any) """ - return cls(name=cls.InnerAccount.INPUT.value, username='') + 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.InnerAccount.USER.value, username=username) + return cls(name=cls.AliasAccount.USER.label, username=cls.AliasAccount.USER.value) class AccountTemplate(BaseAccount): diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 8ea75bc2a..c9baf8818 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- # -import logging import uuid +import logging from collections import defaultdict from django.db import models diff --git a/apps/assets/models/asset/host.py b/apps/assets/models/asset/host.py index 4ce4be5c9..a1dbb6de3 100644 --- a/apps/assets/models/asset/host.py +++ b/apps/assets/models/asset/host.py @@ -1,6 +1,12 @@ -from assets.const import Category +from assets.const import GATEWAY_NAME from .common import Asset class Host(Asset): - pass + + @classmethod + def get_gateway_queryset(cls): + queryset = cls.objects.filter( + platform__name=GATEWAY_NAME + ) + return queryset diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index fd9bc422b..9977f6830 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -47,7 +47,7 @@ class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): 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._meta.model) + args = (str(self.id), Trigger.timing, self.type) kwargs = {} return name, task, args, kwargs diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py index c22b64f51..ecc0e98d4 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/assets/models/automations/change_secret.py @@ -65,3 +65,9 @@ class ChangeSecretRecord(JMSBaseModel): 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/base.py b/apps/assets/models/base.py index 7920d3798..90fb384e6 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -6,10 +6,10 @@ import sshpubkeys from hashlib import md5 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.utils import timezone from django.db.models import QuerySet +from django.utils.translation import ugettext_lazy as _ from common.utils import ( ssh_key_string_to_obj, ssh_key_gen, get_logger, diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 4abe8aa68..248e02f20 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -1,22 +1,25 @@ # -*- coding: utf-8 -*- # -import socket import uuid +import socket import random - -from django.core.cache import cache import paramiko + from django.db import models +from django.core.cache import cache +from django.db.models.query import QuerySet from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, lazyproperty from common.db import fields +from common.utils import get_logger, lazyproperty from orgs.mixins.models import OrgModelMixin +from assets.models import Host from .base import BaseAccount +from ..const import SecretType logger = get_logger(__file__) -__all__ = ['Domain', 'Gateway'] +__all__ = ['Domain', 'GatewayMixin'] class Domain(OrgModelMixin): @@ -33,12 +36,9 @@ class Domain(OrgModelMixin): def __str__(self): return self.name - def has_gateway(self): - return self.gateway_set.filter(is_active=True).exists() - @lazyproperty def gateways(self): - return self.gateway_set.filter(is_active=True) + return Host.get_gateway_queryset().filter(domain=self, is_active=True) def select_gateway(self): return self.random_gateway() @@ -53,18 +53,141 @@ class Domain(OrgModelMixin): return random.choice(self.gateways) -class Gateway(BaseAccount): - UNCONNECTIVE_KEY_TMPL = 'asset_unconnective_gateway_{}' - UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}' - UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5 +class GatewayMixin: + id: uuid.UUID + port: int + address: str + accounts: QuerySet + private_key_path: str + private_key_obj: paramiko.RSAKey + UNCONNECTED_KEY_TMPL = 'asset_unconnective_gateway_{}' + UNCONNECTED_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}' + UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5 + def set_unconnected(self): + unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id) + unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id) + unconnected_silence_period = cache.get( + unconnected_silence_period_key, self.UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE + ) + cache.set(unconnected_silence_period_key, unconnected_silence_period * 2) + cache.set(unconnected_key, unconnected_silence_period, unconnected_silence_period) + + def set_connective(self): + unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id) + unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id) + + cache.delete(unconnected_key) + cache.delete(unconnected_silence_period_key) + + def get_is_unconnected(self): + unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id) + return cache.get(unconnected_key, False) + + @property + def is_connective(self): + return not self.get_is_unconnected() + + @is_connective.setter + def is_connective(self, value): + if value: + self.set_connective() + else: + self.set_unconnected() + + def test_connective(self, local_port=None): + # TODO 走ansible runner + if local_port is None: + local_port = self.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.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 {address}') + err = err.format(port=self.port, ip=self.address) + elif err == 'Authentication failed.': + err = _('Authentication failed') + elif err == 'Connect failed': + err = _('Connect failed') + self.is_connective = False + 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_path, + sock=sock, + timeout=5) + except (paramiko.SSHException, + paramiko.ssh_exception.SSHException, + paramiko.ChannelException, + paramiko.AuthenticationException, + TimeoutError) as e: + + err = getattr(e, 'text', str(e)) + if err == 'Connect failed': + err = _('Connect failed') + self.is_connective = False + return False, err + finally: + client.close() + self.is_connective = True + return True, None + + @lazyproperty + def username(self): + account = self.accounts.all().first() + if account: + return account.username + logger.error(f'Gateway {self} has no account') + return '' + + def get_secret(self, secret_type): + account = self.accounts.filter(secret_type=secret_type).first() + if account: + return account.secret + logger.error(f'Gateway {self} has no {secret_type} account') + + @lazyproperty + def password(self): + secret_type = SecretType.PASSWORD + return self.get_secret(secret_type) + + @lazyproperty + def private_key(self): + secret_type = SecretType.SSH_KEY + return self.get_secret(secret_type) + + +class Gateway(BaseAccount): class Protocol(models.TextChoices): ssh = 'ssh', 'SSH' name = models.CharField(max_length=128, verbose_name='Name') 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")) + 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")) @@ -85,91 +208,3 @@ class Gateway(BaseAccount): permissions = [ ('test_gateway', _('Test gateway')) ] - - 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) - - 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 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) - - 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) - - @property - def is_connective(self): - return not self.get_is_unconnective() - - @is_connective.setter - def is_connective(self, value): - if value: - self.set_connective() - else: - self.set_unconnective() - - def test_connective(self, local_port=None): - if local_port is None: - local_port = self.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: - err = str(e) - if err.startswith('[Errno None] Unable to connect to port'): - err = _('Unable to connect to port {port} on {address}') - err = err.format(port=self.port, ip=self.ip) - elif err == 'Authentication failed.': - err = _('Authentication failed') - elif err == 'Connect failed': - err = _('Connect failed') - self.is_connective = False - 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, - paramiko.ssh_exception.SSHException, - paramiko.ChannelException, - paramiko.AuthenticationException, - TimeoutError) as e: - - err = getattr(e, 'text', str(e)) - if err == 'Connect failed': - err = _('Connect failed') - self.is_connective = False - return False, err - finally: - client.close() - self.is_connective = True - return True, None diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index a6c4b4fe2..179760922 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -15,7 +15,7 @@ from ...const import Category, AllTypes __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', - 'AssetTaskSerializer', 'AssetsTaskSerializer', + 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', ] diff --git a/apps/assets/serializers/automations/change_secret.py b/apps/assets/serializers/automations/change_secret.py index 3b9137bc4..b0149334d 100644 --- a/apps/assets/serializers/automations/change_secret.py +++ b/apps/assets/serializers/automations/change_secret.py @@ -42,6 +42,19 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ )}, }} + 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: @@ -93,8 +106,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer): class Meta: model = ChangeSecretRecord fields = [ - 'id', 'asset', 'account', 'date_started', - 'date_finished', 'is_success', 'error', 'execution', + 'id', 'asset', 'account', 'date_started', 'date_finished', + 'timedelta', 'is_success', 'error', 'execution', ] read_only_fields = fields diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 82fd9433f..64c8c032c 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -1,30 +1,33 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers +from rest_framework.generics import get_object_or_404 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 AuthValidateMixin +from common.drf.fields import ObjectRelatedField, EncryptedField +from assets.const import SecretType +from ..models import Domain, Asset, Account +from ..serializers import HostSerializer +from .utils import validate_password_for_ansible, validate_ssh_key class DomainSerializer(BulkOrgResourceModelSerializer): asset_count = serializers.SerializerMethodField(label=_('Assets 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', '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,32 +38,89 @@ class DomainSerializer(BulkOrgResourceModelSerializer): @staticmethod def get_gateway_count(obj): - return obj.gateway_set.all().count() + return obj.gateways.count() -class GatewaySerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): - is_connective = serializers.BooleanField(required=False, label=_('Connectivity')) +class GatewaySerializer(HostSerializer): + password = EncryptedField( + label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024, + validators=[validate_password_for_ansible], write_only=True + ) + private_key = EncryptedField( + label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, + max_length=16384, write_only=True + ) + passphrase = serializers.CharField( + label=_('Key password'), allow_blank=True, allow_null=True, required=False, write_only=True, + max_length=512, + ) + username = serializers.CharField( + label=_('Username'), allow_blank=True, max_length=128, required=True, + ) - class Meta: - model = Gateway - fields_mini = ['id', 'username'] - fields_write_only = [ - 'password', 'private_key', 'public_key', 'passphrase' + class Meta(HostSerializer.Meta): + fields = HostSerializer.Meta.fields + [ + 'username', 'password', 'private_key', 'passphrase' ] - fields_small = fields_mini + fields_write_only + [ - '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}, + + def validate_private_key(self, secret): + if not secret: + return + passphrase = self.initial_data.get('passphrase') + passphrase = passphrase if passphrase else None + validate_ssh_key(secret, passphrase) + return secret + + @staticmethod + def clean_auth_fields(validated_data): + username = validated_data.pop('username', None) + password = validated_data.pop('password', None) + private_key = validated_data.pop('private_key', None) + validated_data.pop('passphrase', None) + return username, password, private_key + + @staticmethod + def create_accounts(instance, username, password, private_key): + account_name = f'{instance.name}-{_("Gateway")}' + account_data = { + 'privileged': True, + 'name': account_name, + 'username': username, + 'asset_id': instance.id, + 'created_by': instance.created_by } + if password: + Account.objects.create( + **account_data, secret=password, secret_type=SecretType.PASSWORD + ) + if private_key: + Account.objects.create( + **account_data, secret=private_key, secret_type=SecretType.SSH_KEY + ) + + @staticmethod + def update_accounts(instance, username, password, private_key): + accounts = instance.accounts.filter(username=username) + if password: + account = get_object_or_404(accounts, SecretType.PASSWORD) + account.secret = password + account.save() + if private_key: + account = get_object_or_404(accounts, SecretType.SSH_KEY) + account.secret = private_key + account.save() + + def create(self, validated_data): + auth_fields = self.clean_auth_fields(validated_data) + instance = super().create(validated_data) + self.create_accounts(instance, *auth_fields) + return instance + + def update(self, instance, validated_data): + auth_fields = self.clean_auth_fields(validated_data) + instance = super().update(instance, validated_data) + self.update_accounts(instance, *auth_fields) + return instance class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer): diff --git a/apps/assets/serializers/mixin.py b/apps/assets/serializers/mixin.py index 45943dc2a..e69de29bb 100644 --- a/apps/assets/serializers/mixin.py +++ b/apps/assets/serializers/mixin.py @@ -1,11 +0,0 @@ -from rest_framework import serializers -from django.utils.translation import gettext_lazy as _ - - -class CategoryDisplayMixin(serializers.Serializer): - category_display = serializers.ReadOnlyField( - source='get_category_display', label=_("Category display") - ) - type_display = serializers.ReadOnlyField( - source='get_type_display', label=_("Type display") - ) diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 3bc02732f..8f8dcb5a3 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -1,61 +1,75 @@ -from rest_framework import serializers 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 ..models import Platform, PlatformProtocol, PlatformAutomation from ..const import Category, AllTypes +from ..models import Platform, PlatformProtocol, PlatformAutomation -__all__ = ['PlatformSerializer', 'PlatformOpsMethodSerializer'] +__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer"] class ProtocolSettingSerializer(serializers.Serializer): SECURITY_CHOICES = [ - ('any', 'Any'), - ('rdp', 'RDP'), - ('tls', 'TLS'), - ('nla', 'NLA'), + ("any", "Any"), + ("rdp", "RDP"), + ("tls", "TLS"), + ("nla", "NLA"), ] # RDP console = serializers.BooleanField(required=False) - security = serializers.ChoiceField(choices=SECURITY_CHOICES, default='any') + 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")) + 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")) + 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', + "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': '收集账号方式'}, + "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": "收集账号方式"}, } @@ -66,42 +80,62 @@ class PlatformProtocolsSerializer(serializers.ModelSerializer): class Meta: model = PlatformProtocol fields = [ - 'id', 'name', 'port', 'primary', 'default', - 'required', 'secret_types', 'setting', + "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) + 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' + choices=[("sudo", "sudo su -"), ("su", "su - ")], + label="切换方式", + required=False, + default="sudo", ) class Meta: model = Platform - fields_mini = ['id', 'name', 'internal'] + fields_mini = ["id", "name", "internal"] fields_small = fields_mini + [ - 'category', 'type', 'charset', + "category", + "type", + "charset", ] fields = fields_small + [ - 'protocols_enabled', 'protocols', 'domain_enabled', - 'su_enabled', 'su_method', 'automation', 'comment', + "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': "默认网域"}, + "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')) + 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/tasks/automation.py b/apps/assets/tasks/automation.py index 873e606b6..60f01836f 100644 --- a/apps/assets/tasks/automation.py +++ b/apps/assets/tasks/automation.py @@ -1,13 +1,16 @@ 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') -def execute_automation(pid, trigger, model): +@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: 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/gather_accounts.py b/apps/assets/tasks/gather_accounts.py index 4e372aca7..5e20bfe73 100644 --- a/apps/assets/tasks/gather_accounts.py +++ b/apps/assets/tasks/gather_accounts.py @@ -1,6 +1,7 @@ # ~*~ 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 @@ -24,7 +25,7 @@ def gather_asset_accounts_util(nodes, task_name): instance.execute() -@shared_task(queue="ansible") +@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") diff --git a/apps/assets/tasks/gather_facts.py b/apps/assets/tasks/gather_facts.py index 805f8b336..b3196abf5 100644 --- a/apps/assets/tasks/gather_facts.py +++ b/apps/assets/tasks/gather_facts.py @@ -2,6 +2,7 @@ # 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 @@ -40,7 +41,7 @@ def update_assets_hardware_info_util(assets=None, nodes=None, task_name=None): instance.execute() -@shared_task(queue="ansible") +@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(): @@ -49,7 +50,7 @@ def update_assets_hardware_info_manual(asset_ids): update_assets_hardware_info_util(assets=assets, task_name=task_name) -@shared_task(queue="ansible") +@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(): diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index c6ad2e8ba..f8d8d38a4 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -10,11 +10,10 @@ 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() @@ -32,6 +31,6 @@ def check_node_assets_amount_task(org_id=None): @register_as_period_task(crontab=CRONTAB_AT_AM_TWO) -@shared_task +@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 index f1bfc93d9..817f64b64 100644 --- a/apps/assets/tasks/ping.py +++ b/apps/assets/tasks/ping.py @@ -1,6 +1,7 @@ # ~*~ 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 @@ -29,7 +30,7 @@ def test_asset_connectivity_util(assets, task_name=None): instance.execute() -@shared_task(queue="ansible") +@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(): @@ -39,7 +40,7 @@ def test_assets_connectivity_manual(asset_ids): test_asset_connectivity_util(assets, task_name=task_name) -@shared_task(queue="ansible") +@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(): diff --git a/apps/assets/tasks/push_account.py b/apps/assets/tasks/push_account.py index cd5de975a..c2c7156e8 100644 --- a/apps/assets/tasks/push_account.py +++ b/apps/assets/tasks/push_account.py @@ -3,6 +3,7 @@ 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__ = [ @@ -27,7 +28,7 @@ def push_accounts_to_assets_util(accounts, assets): instance.execute() -@shared_task(queue="ansible") +@shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) def push_accounts_to_assets(account_ids, asset_ids): from assets.models import Asset, Account with tmp_to_root_org(): diff --git a/apps/assets/tasks/verify_account.py b/apps/assets/tasks/verify_account.py index 2874113d8..4538f2b2d 100644 --- a/apps/assets/tasks/verify_account.py +++ b/apps/assets/tasks/verify_account.py @@ -1,5 +1,6 @@ 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 @@ -26,7 +27,7 @@ def verify_accounts_connectivity_util(accounts, assets, task_name): instance.execute() -@shared_task(queue="ansible") +@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(): diff --git a/apps/audits/const.py b/apps/audits/const.py index 18033ee78..7a0a6bc6e 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -1,24 +1,74 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ +from django.db.models import TextChoices, IntegerChoices DEFAULT_CITY = _("Unknown") MODELS_NEED_RECORD = ( # users - 'User', 'UserGroup', + "User", + "UserGroup", # acls - 'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting', + "LoginACL", + "LoginAssetACL", + "LoginConfirmSetting", # assets - 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', - 'CommandFilter', 'Platform', 'Account', + "Asset", + "Node", + "AdminUser", + "SystemUser", + "Domain", + "Gateway", + "CommandFilterRule", + "CommandFilter", + "Platform", + "Account", # applications # orgs - 'Organization', + "Organization", # settings - 'Setting', + "Setting", # perms - 'AssetPermission', + "AssetPermission", # xpack - 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', + "License", + "Account", + "SyncInstanceTask", + "ChangeAuthPlan", + "GatherUserTask", ) + + +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/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/migrations/0016_alter_userloginlog_type.py b/apps/audits/migrations/0016_alter_userloginlog_type.py new file mode 100644 index 000000000..86145721d --- /dev/null +++ b/apps/audits/migrations/0016_alter_userloginlog_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-11-30 07:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0015_auto_20221011_1745'), + ] + + operations = [ + migrations.AlterField( + model_name='userloginlog', + name='type', + field=models.CharField(choices=[('web_cli', 'Web Client'), ('web_gui', 'Web GUI'), ('db_cli', 'DB Client'), ('db_gui', 'DB GUI'), ('rdp_cli', 'RDP Client'), ('rdp_file', 'RDP File'), ('ssh_cli', 'SSH Client'), ('web_sftp', 'Web SFTP')], max_length=128, verbose_name='Login type'), + ), + ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 5dd8eb0a2..6f0f8fcc0 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -8,63 +8,55 @@ from common.utils import lazyproperty 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) - datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True) + remote_addr = models.CharField( + max_length=128, verbose_name=_("Remote addr"), blank=True, null=True + ) + datetime = models.DateTimeField( + auto_now=True, verbose_name=_("Datetime"), db_index=True + ) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) @@ -84,50 +76,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): @@ -137,8 +127,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 ) @@ -146,18 +136,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 @@ -165,5 +156,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 0f595be25..46e383f6a 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -3,77 +3,99 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.serializers import BulkSerializerMixin +from common.drf.fields import LabeledChoiceField from terminal.models import Session 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")}, } 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): diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index f395965c3..7b07b3f1b 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -1,38 +1,34 @@ # -*- coding: utf-8 -*- # -import time - -from django.db.models.signals import ( - post_save, m2m_changed, pre_delete -) -from django.dispatch import receiver from django.conf import settings from django.db import transaction -from django.utils import timezone +from django.dispatch import receiver +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, m2m_changed, pre_delete from rest_framework.request import Request +from rest_framework.renderers import JSONRenderer -from assets.models import Asset -from authentication.signals import post_auth_failed, post_auth_success -from authentication.utils import check_different_city_login_if_need -from jumpserver.utils import current_request -from users.models import User -from users.signals import post_user_change_password -from terminal.models import Session, Command -from .utils import write_login_log, create_operate_log -from . import models, serializers -from .models import OperateLog from orgs.utils import current_org from perms.models import AssetPermission -from terminal.backends.command.serializers import SessionCommandSerializer +from users.models import User +from users.signals import post_user_change_password +from assets.models import Asset +from jumpserver.utils import current_request +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 terminal.serializers import SessionSerializer +from terminal.backends.command.serializers import SessionCommandSerializer from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR from common.utils import get_request_ip, get_logger, get_syslogger from common.utils.encode import data_to_json +from . import models, serializers +from .const import ActionChoices +from .utils import write_login_log, create_operate_log + logger = get_logger(__name__) sys_logger = get_syslogger(__name__) @@ -46,14 +42,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): @@ -65,41 +61,41 @@ AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() M2M_NEED_RECORD = { User.groups.through.__name__: ( - _('User and Group'), - _('{User} JOINED {UserGroup}'), - _('{User} LEFT {UserGroup}') + _("User and Group"), + _("{User} JOINED {UserGroup}"), + _("{User} LEFT {UserGroup}"), ), Asset.nodes.through.__name__: ( - _('Node and Asset'), - _('{Node} ADD {Asset}'), - _('{Node} REMOVE {Asset}') + _("Node and Asset"), + _("{Node} ADD {Asset}"), + _("{Node} REMOVE {Asset}"), ), AssetPermission.users.through.__name__: ( - _('User asset permissions'), - _('{AssetPermission} ADD {User}'), - _('{AssetPermission} REMOVE {User}'), + _("User asset permissions"), + _("{AssetPermission} ADD {User}"), + _("{AssetPermission} REMOVE {User}"), ), AssetPermission.user_groups.through.__name__: ( - _('User group asset permissions'), - _('{AssetPermission} ADD {UserGroup}'), - _('{AssetPermission} REMOVE {UserGroup}'), + _("User group asset permissions"), + _("{AssetPermission} ADD {UserGroup}"), + _("{AssetPermission} REMOVE {UserGroup}"), ), AssetPermission.assets.through.__name__: ( - _('Asset permission'), - _('{AssetPermission} ADD {Asset}'), - _('{AssetPermission} REMOVE {Asset}'), + _("Asset permission"), + _("{AssetPermission} ADD {Asset}"), + _("{AssetPermission} REMOVE {Asset}"), ), AssetPermission.nodes.through.__name__: ( - _('Node permission'), - _('{AssetPermission} ADD {Node}'), - _('{AssetPermission} REMOVE {Node}'), + _("Node permission"), + _("{AssetPermission} ADD {Node}"), + _("{AssetPermission} REMOVE {Node}"), ), } M2M_ACTION_MAPER = { - POST_ADD: OperateLog.ACTION_CREATE, - POST_REMOVE: OperateLog.ACTION_DELETE, - POST_CLEAR: OperateLog.ACTION_DELETE, + POST_ADD: ActionChoices.create, + POST_REMOVE: ActionChoices.delete, + POST_CLEAR: ActionChoices.delete, } @@ -117,12 +113,14 @@ def on_m2m_changed(sender, action, instance, model, pk_set, **kwargs): org_id = current_org.id remote_addr = get_request_ip(current_request) user = str(user) - resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name] + resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[ + sender_name + ] action = M2M_ACTION_MAPER[action] - if action == OperateLog.ACTION_CREATE: + if action == ActionChoices.create: resource_tmpl = resource_tmpl_add - elif action == OperateLog.ACTION_DELETE: + elif action == ActionChoices.delete: resource_tmpl = resource_tmpl_remove else: return @@ -139,41 +137,53 @@ def on_m2m_changed(sender, action, instance, model, pk_set, **kwargs): print("Instace name: ", instance_name, instance_value) for obj in objs: - resource = resource_tmpl.format(**{ - instance_name: instance_value, - model_name: str(obj) - })[:128] # `resource` 字段只有 128 个字符长 😔 + resource = resource_tmpl.format( + **{instance_name: instance_value, model_name: str(obj)} + )[ + :128 + ] # `resource` 字段只有 128 个字符长 😔 - to_create.append(OperateLog( - user=user, action=action, resource_type=resource_type, - resource=resource, remote_addr=remote_addr, org_id=org_id - )) - OperateLog.objects.bulk_create(to_create) + to_create.append( + models.OperateLog( + user=user, + action=action, + resource_type=resource_type, + resource=resource, + remote_addr=remote_addr, + org_id=org_id, + ) + ) + models.OperateLog.objects.bulk_create(to_create) @receiver(post_save) -def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): +def on_object_created_or_update( + sender, instance=None, created=False, update_fields=None, **kwargs +): # last_login 改变是最后登录日期, 每次登录都会改变 - if instance._meta.object_name == 'User' and \ - update_fields and 'last_login' in update_fields: + if ( + instance._meta.object_name == "User" + and update_fields + and "last_login" in update_fields + ): return if created: - action = models.OperateLog.ACTION_CREATE + action = ActionChoices.create else: - action = models.OperateLog.ACTION_UPDATE + action = ActionChoices.update create_operate_log(action, sender, instance) @receiver(pre_delete) def on_object_delete(sender, instance=None, **kwargs): - create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) + create_operate_log(ActionChoices.delete, sender, instance) @receiver(post_user_change_password, sender=User) def on_user_change_password(sender, user=None, **kwargs): if not current_request: - remote_addr = '127.0.0.1' - change_by = 'System' + remote_addr = "127.0.0.1" + change_by = "System" else: remote_addr = get_request_ip(current_request) if not current_request.user.is_authenticated: @@ -182,7 +192,8 @@ def on_user_change_password(sender, user=None, **kwargs): change_by = str(current_request.user) with transaction.atomic(): models.PasswordChangeLog.objects.create( - user=str(user), change_by=change_by, + user=str(user), + change_by=change_by, remote_addr=remote_addr, ) @@ -216,51 +227,52 @@ def on_audits_log_create(sender, instance=None, **kwargs): def get_login_backend(request): - backend = request.session.get('auth_backend', '') or \ - request.session.get(BACKEND_SESSION_KEY, '') + backend = request.session.get("auth_backend", "") or request.session.get( + BACKEND_SESSION_KEY, "" + ) backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None) if backend_label is None: - backend_label = '' + backend_label = "" return backend_label def generate_data(username, request, login_type=None): - user_agent = request.META.get('HTTP_USER_AGENT', '') - login_ip = get_request_ip(request) or '0.0.0.0' + user_agent = request.META.get("HTTP_USER_AGENT", "") + login_ip = get_request_ip(request) or "0.0.0.0" if login_type is None and isinstance(request, Request): - login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') + login_type = request.META.get("HTTP_X_JMS_LOGIN_TYPE", "U") if login_type is None: - login_type = 'W' + login_type = "W" - with translation.override('en'): + with translation.override("en"): backend = str(get_login_backend(request)) data = { - 'username': username, - 'ip': login_ip, - 'type': login_type, - 'user_agent': user_agent[0:254], - 'datetime': timezone.now(), - 'backend': backend, + "username": username, + "ip": login_ip, + "type": login_type, + "user_agent": user_agent[0:254], + "datetime": timezone.now(), + "backend": backend, } return data @receiver(post_auth_success) def on_user_auth_success(sender, user, request, login_type=None, **kwargs): - logger.debug('User login success: {}'.format(user.username)) + logger.debug("User login success: {}".format(user.username)) check_different_city_login_if_need(user, request) data = generate_data(user.username, request, login_type=login_type) - request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") - data.update({'mfa': int(user.mfa_enabled), 'status': True}) + request.session["login_time"] = data["datetime"].strftime("%Y-%m-%d %H:%M:%S") + data.update({"mfa": int(user.mfa_enabled), "status": True}) write_login_log(**data) @receiver(post_auth_failed) -def on_user_auth_failed(sender, username, request, reason='', **kwargs): - logger.debug('User login failed: {}'.format(username)) +def on_user_auth_failed(sender, username, request, reason="", **kwargs): + logger.debug("User login failed: {}".format(username)) data = generate_data(username, request) - data.update({'reason': reason[:128], 'status': False}) + data.update({"reason": reason[:128], "status": False}) write_login_log(**data) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 0c04531d5..1636c2cb1 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,33 +1,32 @@ -import os -import abc -import json -import time import base64 +import json +import os +import time import urllib.parse + from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from rest_framework.request import Request +from django.utils import timezone from rest_framework import status -from rest_framework.exceptions import PermissionDenied from rest_framework.decorators import action -from rest_framework.response import Response +from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request +from rest_framework.response import Response from common.drf.api import JMSModelViewSet from common.http import is_true from orgs.mixins.api import RootOrgViewMixin -from perms.models import Action +from orgs.utils import tmp_to_root_org +from perms.models import ActionChoices from terminal.models import EndpointRule +from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer, ) -from ..models import ConnectionToken __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] -# ExtraActionApiMixin - class RDPFileClientProtocolURLMixin: request: Request @@ -70,8 +69,7 @@ class RDPFileClientProtocolURLMixin: # 设置磁盘挂载 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'] = '*' # 设置全屏 @@ -179,22 +177,10 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): get_serializer: callable perform_create: callable - @action(methods=['POST'], detail=False, url_path='secret-info/detail') - 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('token') or '' - token = get_object_or_404(ConnectionToken, pk=token_id) - self.check_token_permission(token) - serializer = self.get_serializer(instance=token) - return Response(serializer.data, status=status.HTTP_200_OK) - @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_permission(token) + token.is_valid() filename, content = self.get_rdp_file_info(token) filename = '{}.rdp'.format(filename) response = HttpResponse(content, content_type='application/octet-stream') @@ -204,7 +190,7 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): @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_permission(token) + token.is_valid() try: protocol_data = self.get_client_protocol_data(token) except ValueError as e: @@ -222,12 +208,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): instance.expire() return Response(status=status.HTTP_204_NO_CONTENT) - @staticmethod - def check_token_permission(token: ConnectionToken): - is_valid, error = token.check_permission() - if not is_valid: - raise PermissionDenied(error) - 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) @@ -257,6 +237,22 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView '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('token') or '' + token = get_object_or_404(ConnectionToken, pk=token_id) + token.is_valid() + serializer = self.get_serializer(instance=token) + return Response(serializer.data, status=status.HTTP_200_OK) + + def dispatch(self, request, *args, **kwargs): + with tmp_to_root_org(): + return super().dispatch(request, *args, **kwargs) + def get_queryset(self): return ConnectionToken.objects.filter(user=self.request.user) @@ -264,25 +260,36 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return self.request.user def perform_create(self, serializer): - user = self.get_user(serializer) - asset = serializer.validated_data.get('asset') - account_username = serializer.validated_data.get('account_username') - self.validate_asset_permission(user, asset, account_username) - return super(ConnectionTokenViewSet, self).perform_create(serializer) + self.validate_serializer(serializer) + return super().perform_create(serializer) - @staticmethod - def validate_asset_permission(user, asset, account_username): + def validate_serializer(self, serializer): from perms.utils.account import PermAccountUtil - actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username) - if not actions: - error = 'No actions' - raise PermissionDenied(error) - if expire_at < time.time(): - error = 'Expired' - raise PermissionDenied(error) + data = serializer.validated_data + user = self.get_user(serializer) + asset = data.get('asset') + login = data.get('login') + data['org_id'] = asset.org_id + data['user'] = user -# SuperConnectionToken + util = PermAccountUtil() + permed_account = util.validate_permission(user, asset, login) + + if not permed_account or not permed_account.actions: + msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( + user, asset, login + ) + raise PermissionDenied(msg) + + if permed_account.date_expired < timezone.now(): + raise PermissionDenied('Expired') + + if permed_account.has_secret: + data['secret'] = '' + if permed_account.username != '@INPUT': + data['username'] = '' + return permed_account class SuperConnectionTokenViewSet(ConnectionTokenViewSet): diff --git a/apps/authentication/migrations/0012_auto_20220816_1629.py b/apps/authentication/migrations/0012_auto_20220816_1629.py index 8310c4fbb..07c9559ce 100644 --- a/apps/authentication/migrations/0012_auto_20220816_1629.py +++ b/apps/authentication/migrations/0012_auto_20220816_1629.py @@ -16,7 +16,7 @@ def migrate_system_user_to_account(apps, schema_editor): count += len(connection_tokens) updated = [] for connection_token in connection_tokens: - connection_token.account_username = connection_token.system_user.username + connection_token.account = connection_token.system_user.username updated.append(connection_token) connection_token_model.objects.bulk_update(updated, ['account_username']) 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..b198295a5 --- /dev/null +++ b/apps/authentication/migrations/0014_auto_20221122_2152.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.14 on 2022-11-22 13:52 + +import common.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0013_connectiontoken_protocol'), + ] + + operations = [ + migrations.RenameField( + model_name='connectiontoken', + old_name='account_username', + new_name='login' + ), + migrations.AlterField( + model_name='connectiontoken', + name='login', + field=models.CharField(max_length=128, verbose_name='Login account'), + ), + migrations.AddField( + model_name='connectiontoken', + name='username', + field=models.CharField(default='', max_length=128, verbose_name='Username'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='secret', + field=common.db.fields.EncryptCharField(default='', max_length=128, verbose_name='Secret'), + ), + ] diff --git a/apps/authentication/migrations/0015_alter_connectiontoken_login.py b/apps/authentication/migrations/0015_alter_connectiontoken_login.py new file mode 100644 index 000000000..f2c6abecb --- /dev/null +++ b/apps/authentication/migrations/0015_alter_connectiontoken_login.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-23 02:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0014_auto_20221122_2152'), + ] + + operations = [ + migrations.AlterField( + model_name='connectiontoken', + name='login', + field=models.CharField(max_length=128, verbose_name='Login account'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 48c61f954..058d07581 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -2,13 +2,15 @@ import time from datetime import timedelta from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from orgs.mixins.models import OrgModelMixin - from django.db import models -from common.utils import lazyproperty +from django.conf import settings +from rest_framework.exceptions import PermissionDenied + +from orgs.mixins.models import OrgModelMixin +from common.utils import lazyproperty, pretty_string from common.utils.timezone import as_current_tz from common.db.models import JMSBaseModel +from common.db.fields import EncryptCharField from assets.const import Protocol @@ -25,13 +27,14 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): 'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True, related_name='connection_tokens', verbose_name=_('Asset'), ) + login = models.CharField(max_length=128, verbose_name=_("Login account")) + username = models.CharField(max_length=128, default='', verbose_name=_("Username")) + secret = EncryptCharField(max_length=64, default='', verbose_name=_("Secret")) protocol = models.CharField( choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") ) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) - account_username = models.CharField(max_length=128, default='', verbose_name=_("Account")) - secret = models.CharField(max_length=64, default='', verbose_name=_("Secret")) date_expired = models.DateTimeField( default=date_expired_default, verbose_name=_("Date expired") ) @@ -43,10 +46,6 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): ('view_connectiontokensecret', _('Can view connection token secret')) ] - @property - def is_valid(self): - return not self.is_expired - @property def is_expired(self): return self.date_expired < timezone.now() @@ -59,9 +58,10 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): seconds = 0 return int(seconds) - @classmethod - def get_default_date_expired(cls): - return date_expired_default() + 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() @@ -69,48 +69,74 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): def renewal(self): """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ - self.date_expired = self.get_default_date_expired() + self.date_expired = date_expired_default() self.save() - # actions 和 expired_at 在 check_valid() 中赋值 - actions = expire_at = None + @lazyproperty + def permed_account(self): + from perms.utils import PermAccountUtil + permed_account = PermAccountUtil().validate_permission( + self.user, self.asset, self.login + ) + return permed_account - def check_permission(self): - from perms.utils.account import PermAccountUtil + @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: - is_valid = False error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) - return is_valid, error + raise PermissionDenied(error) if not self.user or not self.user.is_valid: - is_valid = False error = _('No user or invalid user') - return is_valid, error + 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_username: - is_valid = False + if not self.login: error = _('No account') - return is_valid, error - actions, expire_at = PermAccountUtil().validate_permission( - self.user, self.asset, self.account_username - ) - if not actions or expire_at < time.time(): - is_valid = False - error = _('User has no permission to access asset or permission expired') - return is_valid, error - self.actions = actions - self.expire_at = expire_at - is_valid, error = True, '' - return is_valid, error + 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.login + ) + 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(self): if not self.asset: return None - account = self.asset.accounts.filter(username=self.account_username).first() - return account + + account = self.asset.accounts.filter(name=self.login).first() + if self.login == '@INPUT' or not account: + return { + 'name': self.login, + 'username': self.username, + 'secret_type': 'password', + 'secret': self.secret + } + else: + return { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': account.secret_type or self.secret + } @lazyproperty def domain(self): diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 6e1f19be1..77981cd4a 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,14 +1,12 @@ +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 assets.serializers import PlatformSerializer +from assets.models import Asset, Domain, CommandFilterRule, Account, Platform from authentication.models import ConnectionToken -from common.utils import pretty_string -from common.utils.random import random_string -from assets.models import Asset, Gateway, Domain, CommandFilterRule, Account +from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from perms.serializers.permission import ActionChoicesField from users.models import User -from perms.serializers.permission import ActionsField - __all__ = [ 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', @@ -17,23 +15,25 @@ __all__ = [ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): - is_valid = serializers.BooleanField(read_only=True, label=_('Validity')) + username = serializers.CharField(max_length=128, label=_("Input username"), + allow_null=True, allow_blank=True) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) class Meta: model = ConnectionToken fields_mini = ['id'] fields_small = fields_mini + [ - 'secret', 'account_username', 'date_expired', - 'date_created', 'date_updated', - 'created_by', 'updated_by', 'org_id', 'org_name', + 'protocol', 'login', 'secret', 'username', + 'actions', 'date_expired', 'date_created', + 'date_updated', 'created_by', + 'updated_by', 'org_id', 'org_name', ] fields_fk = [ 'user', 'asset', ] read_only_fields = [ # 普通 Token 不支持指定 user - 'user', 'is_valid', 'expire_time', + 'user', 'expire_time', 'user_display', 'asset_display', ] fields = fields_small + fields_fk + read_only_fields @@ -46,32 +46,6 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): def get_user(self, attrs): return self.get_request_user() - def validate(self, attrs): - fields_attrs = self.construct_internal_fields_attrs(attrs) - attrs.update(fields_attrs) - return attrs - - def construct_internal_fields_attrs(self, attrs): - asset = attrs.get('asset') or '' - asset_display = pretty_string(str(asset), max_length=128) - user = self.get_user(attrs) - user_display = pretty_string(str(user), max_length=128) - secret = attrs.get('secret') or random_string(16) - date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired() - org_id = asset.org_id - if not isinstance(asset, Asset): - error = '' - raise serializers.ValidationError(error) - attrs = { - 'user': user, - 'secret': secret, - 'user_display': user_display, - 'asset_display': asset_display, - 'date_expired': date_expired, - 'org_id': org_id, - } - return attrs - class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer): class Meta(ConnectionTokenSerializer.Meta): @@ -86,7 +60,6 @@ class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer): class SuperConnectionTokenSerializer(ConnectionTokenSerializer): - class Meta(ConnectionTokenSerializer.Meta): read_only_fields = [ 'validity', 'user_display', 'system_user_display', @@ -104,6 +77,7 @@ class SuperConnectionTokenSerializer(ConnectionTokenSerializer): class ConnectionTokenUserSerializer(serializers.ModelSerializer): """ User """ + class Meta: model = User fields = ['id', 'name', 'username', 'email'] @@ -111,6 +85,7 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer): class ConnectionTokenAssetSerializer(serializers.ModelSerializer): """ Asset """ + class Meta: model = Asset fields = ['id', 'name', 'address', 'protocols', 'org_id'] @@ -118,18 +93,20 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer): class ConnectionTokenAccountSerializer(serializers.ModelSerializer): """ Account """ + class Meta: model = Account fields = [ - 'id', 'name', 'username', 'secret_type', 'secret', 'version' + 'name', 'username', 'secret_type', 'secret', ] class ConnectionTokenGatewaySerializer(serializers.ModelSerializer): """ Gateway """ + class Meta: - model = Gateway - fields = ['id', 'ip', 'port', 'username', 'password', 'private_key'] + model = Asset + fields = ['id', 'address', 'port', 'username', 'password', 'private_key'] class ConnectionTokenDomainSerializer(serializers.ModelSerializer): @@ -143,6 +120,7 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer): class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer): """ Command filter rule """ + class Meta: model = CommandFilterRule fields = [ @@ -151,21 +129,30 @@ class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer): ] +class ConnectionTokenPlatform(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) + platform = ConnectionTokenPlatform(read_only=True) account = ConnectionTokenAccountSerializer(read_only=True) gateway = ConnectionTokenGatewaySerializer(read_only=True) - domain = ConnectionTokenDomainSerializer(read_only=True) - cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) - actions = ActionsField() + # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) + actions = ActionChoicesField() expire_at = serializers.IntegerField() class Meta: model = ConnectionToken fields = [ - 'id', 'secret', - 'user', 'asset', 'account_username', 'account', 'protocol', - 'domain', 'gateway', 'cmd_filter_rules', - 'actions', 'expire_at', + 'id', 'secret', 'user', 'asset', 'account', + 'protocol', 'domain', 'gateway', + 'actions', 'expire_at', 'platform', ] 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/db/fields.py b/apps/common/db/fields.py index 72c4df898..00a77826a 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -1,19 +1,32 @@ # -*- 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 - __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", ] @@ -114,7 +127,7 @@ 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 not value: @@ -129,7 +142,7 @@ 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 @@ -139,7 +152,7 @@ class EncryptMixin: # 先 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) # 替换新的加密方式 @@ -153,12 +166,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) @@ -166,10 +179,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 @@ -183,10 +196,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/drf/fields.py b/apps/common/drf/fields.py index 97f9785f5..270c8d91a 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -1,17 +1,21 @@ # -*- coding: utf-8 -*- # import six - -from rest_framework.fields import ChoiceField -from rest_framework import serializers -from django.utils.translation import gettext_lazy as _ 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 __all__ = [ - 'ReadableHiddenField', 'EncryptedField', 'LabeledChoiceField', - 'ObjectRelatedField', + "ReadableHiddenField", + "EncryptedField", + "LabeledChoiceField", + "ObjectRelatedField", + "BitChoicesField", + "TreeChoicesMixin" ] @@ -20,14 +24,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 @@ -35,7 +40,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 super().__init__(**kwargs) def to_internal_value(self, value): @@ -54,26 +59,26 @@ class LabeledChoiceField(ChoiceField): if value is None: return value return { - 'value': value, - 'label': self.choice_mapper.get(six.text_type(value), value), + "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 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}.'), + "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) + self.attrs = kwargs.pop("attrs", None) or ("id", "name") + self.many = kwargs.get("many", False) super().__init__(**kwargs) def to_representation(self, value): @@ -86,13 +91,79 @@ class ObjectRelatedField(serializers.RelatedField): if not isinstance(data, dict): pk = data else: - pk = data.get('id') or data.get('pk') or data.get(self.attrs[0]) + 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) + self.fail("does_not_exist", pk_value=pk) except (TypeError, ValueError): - self.fail('incorrect_type', data_type=type(pk).__name__) + 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/metadata.py b/apps/common/drf/metadata.py index 939c1f314..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,70 +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) - - is_related_field = isinstance(field, (serializers.RelatedField, serializers.ManyRelatedField)) - if not is_related_field and hasattr(field, 'choices'): - field_info['choices'] = [ - { - 'value': choice_value, - 'label': force_text(choice_name, strings_only=True) - } - for choice_value, choice_name in dict(field.choices).items() - ] - - class_name = field.__class__.__name__ - if class_name == 'LabeledChoiceField': - field_info['type'] = 'labeled_choice' - elif class_name == 'ObjectRelatedField': - field_info['type'] = 'object_related_field' - elif class_name == 'ManyRelatedField': - child_relation_class_name = field.child_relation.__class__.__name__ - if child_relation_class_name == 'ObjectRelatedField': - field_info['type'] = 'm2m_related_field' - - # if field.label == '系统平台': - # print("Field: ", class_name, field, field_info) + 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) @@ -128,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/mixins/views.py b/apps/common/mixins/views.py index 9f824e5e2..3d1131c7d 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.utils import create_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/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 6f1e34b0a..03338e9aa 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -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/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/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/jumpserver/conf.py b/apps/jumpserver/conf.py index ca6ca4589..14c758301 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) @@ -499,6 +498,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/settings/base.py b/apps/jumpserver/settings/base.py index 24bf7b8b3..7aac0e505 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -1,4 +1,5 @@ import os + from django.urls import reverse_lazy from .. import const @@ -36,6 +37,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') @@ -313,7 +317,6 @@ PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', ] - GMSSL_ENABLED = CONFIG.GMSSL_ENABLED GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher' if GMSSL_ENABLED: @@ -329,4 +332,3 @@ if os.environ.get('DEBUG_TOOLBAR', False): DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.profiling.ProfilingPanel', ] - diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 093842b71..3e19617aa 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:07f1cfd07039142f4847b4139586bf815467f266119eae57476c073130f0ac92 -size 118098 +oid sha256:adfa9c01178d5f6490e616f62d41c71974d42f9e3bd078fcf1b3c7124384df0b +size 117024 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 59f5db0eb..dddb5b792 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-11-03 16:00+0800\n" +"POT-Creation-Date: 2022-11-16 20:11+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -22,20 +22,21 @@ msgstr "" msgid "Acls" msgstr "Acls" -#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:48 +#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:58 #: applications/models.py:10 assets/models/_user.py:33 #: assets/models/asset/common.py:81 assets/models/asset/common.py:91 -#: assets/models/base.py:57 assets/models/cmd_filter.py:25 +#: assets/models/base.py:50 assets/models/cmd_filter.py:25 #: assets/models/domain.py:24 assets/models/group.py:20 -#: assets/models/label.py:17 assets/models/platform.py:22 -#: assets/models/platform.py:68 assets/serializers/asset/common.py:86 -#: assets/serializers/platform.py:104 ops/mixin.py:20 ops/models/playbook.py:9 -#: orgs/models.py:70 perms/models/asset_permission.py:56 rbac/models/role.py:29 +#: assets/models/label.py:17 assets/models/platform.py:21 +#: assets/models/platform.py:72 assets/serializers/asset/common.py:86 +#: assets/serializers/platform.py:138 ops/mixin.py:20 ops/models/adhoc.py:24 +#: ops/models/celery.py:15 ops/models/job.py:34 ops/models/playbook.py:13 +#: orgs/models.py:70 perms/models/asset_permission.py:51 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/applet/applet.py:20 terminal/models/component/endpoint.py:11 #: terminal/models/component/endpoint.py:87 #: terminal/models/component/storage.py:25 terminal/models/component/task.py:16 -#: terminal/models/component/terminal.py:100 users/forms/profile.py:33 +#: terminal/models/component/terminal.py:82 users/forms/profile.py:33 #: users/models/group.py:15 users/models/user.py:665 #: xpack/plugins/cloud/models.py:30 msgid "Name" @@ -53,26 +54,25 @@ msgstr "1-100、低い値は最初に一致します" #: acls/models/base.py:31 authentication/models/access_key.py:15 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:74 terminal/models/session/sharing.py:28 +#: perms/models/asset_permission.py:67 terminal/models/session/sharing.py:28 #: tickets/const.py:38 msgid "Active" msgstr "アクティブ" #: acls/models/base.py:32 applications/models.py:19 assets/models/_user.py:40 -#: assets/models/asset/common.py:100 assets/models/automations/base.py:26 -#: assets/models/backup.py:30 assets/models/base.py:65 +#: assets/models/asset/common.py:100 assets/models/automations/base.py:22 +#: assets/models/backup.py:29 assets/models/base.py:58 #: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88 #: assets/models/domain.py:25 assets/models/domain.py:69 #: assets/models/group.py:23 assets/models/label.py:22 -#: assets/models/platform.py:73 ops/models/playbook.py:11 -#: ops/models/playbook.py:25 orgs/models.py:74 -#: perms/models/asset_permission.py:84 rbac/models/role.py:37 +#: assets/models/platform.py:77 orgs/models.py:74 +#: perms/models/asset_permission.py:77 rbac/models/role.py:37 #: settings/models.py:38 terminal/models/applet/applet.py:28 -#: terminal/models/applet/applet.py:61 terminal/models/applet/host.py:104 +#: terminal/models/applet/applet.py:61 terminal/models/applet/host.py:107 #: terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:97 #: terminal/models/component/storage.py:28 -#: terminal/models/component/terminal.py:114 tickets/models/comment.py:32 +#: terminal/models/component/terminal.py:93 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:288 users/models/group.py:16 #: users/models/user.py:702 xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/cloud/models.py:37 xpack/plugins/cloud/models.py:118 @@ -95,10 +95,11 @@ msgid "Login confirm" msgstr "ログイン確認" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:28 assets/models/label.py:15 audits/models.py:37 -#: audits/models.py:62 audits/models.py:87 -#: authentication/models/connection_token.py:22 -#: authentication/models/sso_token.py:15 perms/models/asset_permission.py:58 +#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:28 +#: assets/models/label.py:15 audits/models.py:29 audits/models.py:48 +#: audits/models.py:79 authentication/models/connection_token.py:22 +#: authentication/models/sso_token.py:15 perms/api/user_permission/mixin.py:80 +#: perms/models/asset_permission.py:53 perms/models/perm_token.py:12 #: rbac/builtin.py:120 rbac/models/rolebinding.py:41 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 @@ -114,14 +115,14 @@ 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:62 -#: assets/models/cmd_filter.py:81 audits/models.py:63 audits/serializers.py:49 +#: acls/serializers/login_acl.py:26 acls/serializers/login_asset_acl.py:77 +#: assets/models/cmd_filter.py:81 audits/models.py:50 audits/serializers.py:69 #: 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:86 +#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 msgid "Reviewers" msgstr "レビュー担当者" @@ -129,19 +130,25 @@ msgstr "レビュー担当者" msgid "Login acl" msgstr "ログインacl" -#: acls/models/login_asset_acl.py:21 assets/models/account.py:59 +#: acls/models/login_asset_acl.py:21 assets/models/account.py:61 +#: assets/serializers/automations/change_secret.py:88 +#: assets/serializers/automations/change_secret.py:110 #: authentication/models/connection_token.py:33 ops/models/base.py:18 -#: terminal/models/session/session.py:34 xpack/plugins/cloud/models.py:87 -#: xpack/plugins/cloud/serializers/task.py:65 +#: perms/models/perm_token.py:14 terminal/models/session/session.py:34 +#: xpack/plugins/cloud/models.py:87 xpack/plugins/cloud/serializers/task.py:65 msgid "Account" msgstr "アカウント" -#: acls/models/login_asset_acl.py:22 assets/models/account.py:49 +#: acls/models/login_asset_acl.py:22 assets/models/account.py:51 #: assets/models/asset/common.py:83 assets/models/asset/common.py:227 #: assets/models/cmd_filter.py:36 assets/models/gathered_user.py:14 -#: assets/serializers/account/account.py:58 assets/serializers/label.py:30 -#: audits/models.py:39 authentication/models/connection_token.py:26 -#: perms/models/asset_permission.py:64 terminal/backends/command/models.py:21 +#: assets/serializers/account/account.py:59 +#: assets/serializers/automations/change_secret.py:87 +#: assets/serializers/automations/change_secret.py:109 +#: assets/serializers/gathered_user.py:11 assets/serializers/label.py:30 +#: audits/models.py:33 authentication/models/connection_token.py:26 +#: perms/models/asset_permission.py:59 perms/models/perm_token.py:13 +#: terminal/backends/command/models.py:21 #: terminal/backends/command/serializers.py:14 #: terminal/models/session/session.py:32 terminal/notifications.py:90 #: xpack/plugins/change_auth_plan/models/asset.py:200 @@ -158,14 +165,14 @@ msgstr "ログインasset acl" msgid "Login asset confirm" msgstr "ログイン資産の確認" -#: acls/serializers/login_acl.py:11 acls/serializers/login_asset_acl.py:13 +#: acls/serializers/login_acl.py:16 acls/serializers/login_asset_acl.py:14 msgid "Format for comma-delimited string, with * indicating a match all. " msgstr "コンマ区切り文字列の形式。* はすべて一致することを示します。" -#: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:18 -#: acls/serializers/login_asset_acl.py:52 assets/models/_user.py:34 -#: assets/models/base.py:58 assets/models/gathered_user.py:15 -#: audits/models.py:121 authentication/forms.py:25 authentication/forms.py:27 +#: acls/serializers/login_asset_acl.py:22 +#: acls/serializers/login_asset_acl.py:64 assets/models/_user.py:34 +#: assets/models/base.py:51 assets/models/gathered_user.py:15 +#: audits/models.py:95 authentication/forms.py:25 authentication/forms.py:27 #: authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 @@ -177,7 +184,7 @@ msgstr "コンマ区切り文字列の形式。* はすべて一致すること msgid "Username" msgstr "ユーザー名" -#: acls/serializers/login_asset_acl.py:25 +#: acls/serializers/login_asset_acl.py:29 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:" @@ -187,7 +194,7 @@ 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:32 acls/serializers/rules/rules.py:33 +#: acls/serializers/login_asset_acl.py:38 acls/serializers/rules/rules.py:33 #: assets/models/asset/common.py:92 assets/models/domain.py:65 #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 @@ -196,12 +203,12 @@ msgstr "" msgid "IP" msgstr "IP" -#: acls/serializers/login_asset_acl.py:36 -#: assets/serializers/gathered_user.py:22 settings/serializers/terminal.py:7 +#: acls/serializers/login_asset_acl.py:44 +#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:7 msgid "Hostname" msgstr "ホスト名" -#: acls/serializers/login_asset_acl.py:43 +#: acls/serializers/login_asset_acl.py:51 msgid "" "Format for comma-delimited string, with * indicating a match all. Protocol " "options: {}" @@ -209,12 +216,12 @@ msgstr "" "コンマ区切り文字列の形式。* はすべて一致することを示します。プロトコルオプ" "ション: {}" -#: acls/serializers/login_asset_acl.py:84 -#: tickets/serializers/ticket/ticket.py:86 +#: acls/serializers/login_asset_acl.py:108 +#: tickets/serializers/ticket/ticket.py:67 msgid "The organization `{}` does not exist" msgstr "組織 '{}'は存在しません" -#: acls/serializers/login_asset_acl.py:89 +#: acls/serializers/login_asset_acl.py:114 msgid "None of the reviewers belong to Organization `{}`" msgstr "いずれのレビューアも組織 '{}' に属していません" @@ -242,23 +249,26 @@ msgid "Applications" msgstr "アプリケーション" #: applications/models.py:12 assets/models/label.py:20 -#: assets/models/platform.py:69 assets/serializers/asset/common.py:62 -#: assets/serializers/cagegory.py:8 assets/serializers/platform.py:76 -#: assets/serializers/platform.py:105 +#: assets/models/platform.py:73 assets/serializers/asset/common.py:62 +#: assets/serializers/cagegory.py:8 assets/serializers/platform.py:99 +#: assets/serializers/platform.py:139 perms/serializers/user_permission.py:23 #: tickets/models/ticket/apply_application.py:14 #: xpack/plugins/change_auth_plan/models/app.py:24 msgid "Category" msgstr "カテゴリ" #: applications/models.py:15 assets/models/_user.py:46 -#: assets/models/automations/base.py:24 assets/models/cmd_filter.py:74 -#: assets/models/platform.py:70 assets/serializers/asset/common.py:63 -#: assets/serializers/platform.py:75 terminal/models/applet/applet.py:24 +#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 +#: assets/models/platform.py:74 assets/serializers/asset/common.py:63 +#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:98 +#: audits/serializers.py:40 ops/models/job.py:42 +#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:24 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:142 terminal/serializers/applet.py:33 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:17 -#: tickets/models/ticket/general.py:273 +#: tickets/models/ticket/general.py:273 tickets/serializers/flow.py:53 +#: tickets/serializers/ticket/ticket.py:18 #: xpack/plugins/change_auth_plan/models/app.py:27 #: xpack/plugins/change_auth_plan/models/app.py:152 msgid "Type" @@ -277,6 +287,11 @@ msgstr "アプリケーション" msgid "Can match application" msgstr "アプリケーションを一致させることができます" +#: assets/api/automations/base.py:76 +#: xpack/plugins/change_auth_plan/api/asset.py:94 +msgid "The parameter 'action' must be [{}]" +msgstr "パラメータ 'action' は [{}] でなければなりません。" + #: assets/api/domain.py:52 msgid "Number required" msgstr "必要な数" @@ -297,13 +312,13 @@ msgstr "削除に失敗し、ノードにアセットが含まれています。 msgid "App assets" msgstr "アプリ資産" -#: assets/automations/base/manager.py:122 +#: assets/automations/base/manager.py:123 #, fuzzy #| msgid "Disabled" msgid "{} disabled" msgstr "無効" -#: assets/const/account.py:6 audits/const.py:5 +#: assets/const/account.py:6 audits/const.py:6 audits/const.py:63 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" @@ -313,19 +328,21 @@ msgstr "不明" msgid "Ok" msgstr "OK" -#: assets/const/account.py:8 audits/models.py:118 common/const/choices.py:19 +#: assets/const/account.py:8 +#: assets/serializers/automations/change_secret.py:105 +#: assets/serializers/automations/change_secret.py:133 audits/const.py:74 +#: common/const/choices.py:19 #: xpack/plugins/change_auth_plan/serializers/asset.py:190 #: xpack/plugins/cloud/const.py:33 msgid "Failed" msgstr "失敗しました" #: assets/const/account.py:12 assets/models/_user.py:35 -#: assets/models/base.py:52 assets/models/domain.py:71 -#: assets/serializers/base.py:15 audits/signal_handlers.py:50 +#: assets/models/domain.py:71 audits/signal_handlers.py:46 #: 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:46 -#: users/forms/profile.py:22 users/serializers/user.py:94 +#: users/forms/profile.py:22 users/serializers/user.py:105 #: 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 @@ -337,19 +354,18 @@ msgstr "失敗しました" msgid "Password" msgstr "パスワード" -#: assets/const/account.py:13 assets/models/base.py:53 +#: assets/const/account.py:13 #, fuzzy #| msgid "SSH Key" msgid "SSH key" msgstr "SSHキー" -#: assets/const/account.py:14 assets/models/base.py:54 -#: authentication/models/access_key.py:31 +#: assets/const/account.py:14 authentication/models/access_key.py:31 msgid "Access key" msgstr "アクセスキー" #: assets/const/account.py:15 assets/models/_user.py:38 -#: assets/models/base.py:55 authentication/models/sso_token.py:13 +#: authentication/models/sso_token.py:13 msgid "Token" msgstr "トークン" @@ -387,31 +403,31 @@ msgstr "パスワード/キーの確認" msgid "Gather accounts" msgstr "アカウントを集める" -#: assets/const/automation.py:22 +#: assets/const/automation.py:38 assets/serializers/account/base.py:26 msgid "Specific" msgstr "" -#: assets/const/automation.py:23 ops/const.py:20 +#: assets/const/automation.py:39 ops/const.py:20 #: xpack/plugins/change_auth_plan/models/base.py:28 msgid "All assets use the same random password" msgstr "すべての資産は同じランダムパスワードを使用します" -#: assets/const/automation.py:24 ops/const.py:21 +#: assets/const/automation.py:40 ops/const.py:21 #: xpack/plugins/change_auth_plan/models/base.py:29 msgid "All assets use different random password" msgstr "すべての資産は異なるランダムパスワードを使用します" -#: assets/const/automation.py:28 ops/const.py:13 +#: assets/const/automation.py:44 ops/const.py:13 #: xpack/plugins/change_auth_plan/models/asset.py:30 msgid "Append SSH KEY" msgstr "追加" -#: assets/const/automation.py:29 ops/const.py:14 +#: assets/const/automation.py:45 ops/const.py:14 #: xpack/plugins/change_auth_plan/models/asset.py:31 msgid "Empty and append SSH KEY" msgstr "すべてクリアして追加" -#: assets/const/automation.py:30 ops/const.py:15 +#: assets/const/automation.py:46 ops/const.py:15 #: xpack/plugins/change_auth_plan/models/asset.py:32 msgid "Replace (The key generated by JumpServer) " msgstr "置換(JumpServerによって生成された鍵)" @@ -439,7 +455,8 @@ msgstr "データベース" msgid "Cloud service" msgstr "クラウドセンター" -#: assets/const/category.py:15 terminal/models/applet/applet.py:18 +#: assets/const/category.py:15 audits/const.py:61 +#: terminal/models/applet/applet.py:18 msgid "Web" msgstr "" @@ -485,7 +502,6 @@ msgid "Admin user" msgstr "管理ユーザー" #: assets/models/_user.py:36 assets/models/domain.py:72 -#: assets/serializers/base.py:19 #: xpack/plugins/change_auth_plan/models/asset.py:54 #: xpack/plugins/change_auth_plan/models/asset.py:131 #: xpack/plugins/change_auth_plan/models/asset.py:207 @@ -499,11 +515,12 @@ msgstr "SSH秘密鍵" msgid "SSH public key" msgstr "SSHパブリックキー" -#: assets/models/_user.py:41 assets/models/automations/base.py:96 +#: assets/models/_user.py:41 assets/models/automations/base.py:92 #: assets/models/domain.py:26 assets/models/gathered_user.py:19 #: assets/models/group.py:22 common/db/models.py:76 common/mixins/models.py:50 -#: ops/models/base.py:53 orgs/models.py:73 perms/models/asset_permission.py:82 -#: users/models/group.py:18 users/models/user.py:927 +#: ops/models/base.py:54 ops/models/job.py:69 orgs/models.py:73 +#: perms/models/asset_permission.py:75 users/models/group.py:18 +#: users/models/user.py:927 msgid "Date created" msgstr "作成された日付" @@ -512,10 +529,10 @@ msgstr "作成された日付" msgid "Date updated" msgstr "更新日" -#: assets/models/_user.py:43 assets/models/base.py:66 +#: assets/models/_user.py:43 assets/models/base.py:59 #: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:91 #: assets/models/group.py:21 common/db/models.py:74 common/mixins/models.py:49 -#: orgs/models.py:71 perms/models/asset_permission.py:81 +#: orgs/models.py:71 perms/models/asset_permission.py:74 #: users/models/user.py:710 users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:48 msgid "Created by" @@ -526,7 +543,7 @@ msgid "Username same with user" msgstr "ユーザーと同じユーザー名" #: assets/models/_user.py:48 assets/models/domain.py:67 -#: authentication/models/connection_token.py:29 +#: authentication/models/connection_token.py:29 perms/models/perm_token.py:16 #: terminal/models/applet/applet.py:26 terminal/serializers/session.py:18 #: terminal/serializers/session.py:32 terminal/serializers/storage.py:68 msgid "Protocol" @@ -540,7 +557,7 @@ msgstr "オートプッシュ" msgid "Sudo" msgstr "すど" -#: assets/models/_user.py:51 +#: assets/models/_user.py:51 ops/models/adhoc.py:20 ops/models/job.py:30 msgid "Shell" msgstr "シェル" @@ -568,7 +585,7 @@ msgstr "ユーザースイッチ" msgid "Switch from" msgstr "から切り替え" -#: assets/models/_user.py:65 audits/models.py:40 +#: assets/models/_user.py:65 audits/models.py:34 #: terminal/backends/command/models.py:22 #: terminal/backends/command/serializers.py:36 #: xpack/plugins/change_auth_plan/models/app.py:35 @@ -580,47 +597,64 @@ msgstr "システムユーザー" msgid "Can match system user" msgstr "システムユーザーに一致できます" -#: assets/models/account.py:53 +#: assets/models/account.py:45 common/db/fields.py:222 +#: settings/serializers/terminal.py:12 +msgid "All" +msgstr "すべて" + +#: assets/models/account.py:46 +#, fuzzy +#| msgid "Manually input" +msgid "Manual input" +msgstr "手動入力" + +#: assets/models/account.py:47 +#, fuzzy +#| msgid "Dynamic code" +msgid "Dynamic user" +msgstr "動的コード" + +#: assets/models/account.py:55 #, fuzzy #| msgid "Switch from" msgid "Su from" msgstr "から切り替え" -#: assets/models/account.py:55 settings/serializers/auth/cas.py:18 +#: assets/models/account.py:57 settings/serializers/auth/cas.py:18 #: terminal/models/applet/applet.py:22 msgid "Version" msgstr "バージョン" -#: assets/models/account.py:65 +#: assets/models/account.py:67 msgid "Can view asset account secret" msgstr "資産アカウントの秘密を表示できます" -#: assets/models/account.py:66 +#: assets/models/account.py:68 msgid "Can change asset account secret" msgstr "資産口座の秘密を変更できます" -#: assets/models/account.py:67 +#: assets/models/account.py:69 msgid "Can view asset history account" msgstr "資産履歴アカウントを表示できます" -#: assets/models/account.py:68 +#: assets/models/account.py:70 msgid "Can view asset history account secret" msgstr "資産履歴アカウントパスワードを表示できます" -#: assets/models/account.py:91 assets/serializers/account/account.py:13 +#: assets/models/account.py:93 assets/serializers/account/account.py:15 #, fuzzy #| msgid "Account name" msgid "Account template" msgstr "アカウント名" #: assets/models/asset/common.py:82 assets/models/domain.py:66 -#: assets/models/platform.py:23 settings/serializers/auth/radius.py:15 +#: assets/models/platform.py:22 settings/serializers/auth/radius.py:15 #: settings/serializers/auth/sms.py:57 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "ポート" -#: assets/models/asset/common.py:93 assets/models/platform.py:104 +#: assets/models/asset/common.py:93 assets/models/platform.py:110 #: assets/serializers/asset/common.py:65 #: perms/serializers/user_permission.py:21 #: xpack/plugins/cloud/serializers/account_attrs.py:172 @@ -632,17 +666,19 @@ msgstr "プラットフォーム" msgid "Domain" msgstr "ドメイン" -#: assets/models/asset/common.py:97 assets/models/automations/base.py:19 -#: assets/serializers/asset/common.py:66 perms/models/asset_permission.py:67 +#: assets/models/asset/common.py:97 assets/models/automations/base.py:18 +#: assets/serializers/asset/common.py:66 +#: assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:62 #: xpack/plugins/change_auth_plan/models/asset.py:44 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" msgstr "ノード" -#: assets/models/asset/common.py:98 assets/models/automations/base.py:25 -#: assets/models/base.py:64 assets/models/cmd_filter.py:39 +#: assets/models/asset/common.py:98 assets/models/automations/base.py:21 +#: assets/models/base.py:57 assets/models/cmd_filter.py:39 #: assets/models/domain.py:70 assets/models/label.py:21 -#: terminal/models/applet/applet.py:25 users/serializers/user.py:147 +#: terminal/models/applet/applet.py:25 users/serializers/user.py:202 msgid "Is active" msgstr "アクティブです。" @@ -676,8 +712,8 @@ msgstr "ノードにアセットを追加する" msgid "Move asset to node" msgstr "アセットをノードに移動する" -#: assets/models/asset/web.py:9 audits/models.py:111 -#: terminal/serializers/applet_host.py:24 +#: assets/models/asset/web.py:9 audits/const.py:67 +#: terminal/serializers/applet_host.py:25 msgid "Disabled" msgstr "無効" @@ -695,97 +731,112 @@ msgstr "" msgid "Autofill" msgstr "自動" -#: assets/models/asset/web.py:14 assets/serializers/platform.py:29 +#: assets/models/asset/web.py:14 assets/serializers/platform.py:30 #, fuzzy #| msgid "Username attr" msgid "Username selector" msgstr "ユーザー名のプロパティ" -#: assets/models/asset/web.py:15 assets/serializers/platform.py:30 +#: assets/models/asset/web.py:15 assets/serializers/platform.py:33 #, fuzzy #| msgid "Password rules" msgid "Password selector" msgstr "パスワードルール" -#: assets/models/asset/web.py:16 assets/serializers/platform.py:31 +#: assets/models/asset/web.py:16 assets/serializers/platform.py:36 msgid "Submit selector" msgstr "" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:68 perms/models/asset_permission.py:70 -#: rbac/tree.py:37 +#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:65 +#: perms/serializers/permission.py:32 rbac/tree.py:37 msgid "Accounts" msgstr "アカウント" -#: assets/models/automations/base.py:22 assets/serializers/domain.py:29 -#: ops/models/base.py:17 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 assets/serializers/domain.py:29 +#: ops/models/base.py:17 ops/models/job.py:44 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 #: xpack/plugins/change_auth_plan/models/asset.py:40 msgid "Assets" msgstr "資産" -#: assets/models/automations/base.py:86 assets/models/automations/base.py:93 +#: assets/models/automations/base.py:82 assets/models/automations/base.py:89 #, fuzzy #| msgid "Automatic managed" msgid "Automation task" msgstr "自動管理" -#: assets/models/automations/base.py:97 assets/models/backup.py:77 -#: audits/models.py:44 ops/models/base.py:54 -#: perms/models/asset_permission.py:76 terminal/models/applet/host.py:102 -#: terminal/models/session/session.py:43 +#: assets/models/automations/base.py:91 audits/models.py:115 +#: audits/serializers.py:41 ops/models/base.py:49 ops/models/job.py:64 +#: terminal/models/applet/applet.py:60 terminal/models/applet/host.py:104 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 +#: tickets/models/ticket/general.py:281 tickets/serializers/ticket/ticket.py:19 +#: xpack/plugins/cloud/models.py:171 xpack/plugins/cloud/models.py:223 +msgid "Status" +msgstr "ステータス" + +#: assets/models/automations/base.py:93 assets/models/backup.py:76 +#: audits/models.py:40 ops/models/base.py:55 ops/models/celery.py:59 +#: ops/models/job.py:70 perms/models/asset_permission.py:69 +#: terminal/models/applet/host.py:105 terminal/models/session/session.py:43 #: tickets/models/ticket/apply_application.py:28 -#: tickets/models/ticket/apply_asset.py:21 +#: tickets/models/ticket/apply_asset.py:19 #: xpack/plugins/change_auth_plan/models/base.py:108 #: xpack/plugins/change_auth_plan/models/base.py:199 #: xpack/plugins/gathered_user/models.py:71 msgid "Date start" msgstr "開始日" -#: assets/models/automations/base.py:98 -#: assets/models/automations/change_secret.py:58 ops/models/base.py:55 -#: terminal/models/applet/host.py:103 +#: assets/models/automations/base.py:94 +#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 +#: ops/models/celery.py:60 ops/models/job.py:71 +#: terminal/models/applet/host.py:106 msgid "Date finished" msgstr "終了日" -#: assets/models/automations/base.py:100 +#: assets/models/automations/base.py:96 +#: assets/serializers/automations/base.py:39 #, fuzzy #| msgid "Relation snapshot" msgid "Automation snapshot" msgstr "製造オーダスナップショット" -#: assets/models/automations/base.py:104 assets/models/backup.py:88 -#: assets/serializers/account/backup.py:36 +#: assets/models/automations/base.py:100 assets/models/backup.py:87 +#: assets/serializers/account/backup.py:37 +#: assets/serializers/automations/base.py:41 #: xpack/plugins/change_auth_plan/models/base.py:121 #: xpack/plugins/change_auth_plan/serializers/base.py:78 msgid "Trigger mode" msgstr "トリガーモード" -#: assets/models/automations/base.py:108 +#: assets/models/automations/base.py:104 +#: assets/serializers/automations/change_secret.py:90 #, fuzzy #| msgid "Command execution" msgid "Automation task execution" msgstr "コマンド実行" -#: assets/models/automations/change_secret.py:15 assets/models/base.py:60 +#: assets/models/automations/change_secret.py:15 assets/models/base.py:53 +#: assets/serializers/account/account.py:95 assets/serializers/base.py:13 #, fuzzy #| msgid "Secret key" msgid "Secret type" msgstr "秘密キー" #: assets/models/automations/change_secret.py:19 +#: assets/serializers/automations/change_secret.py:25 #, fuzzy #| msgid "SSH Key strategy" msgid "Secret strategy" msgstr "SSHキー戦略" #: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:56 assets/models/base.py:62 -#: assets/serializers/account/base.py:17 -#: authentication/models/connection_token.py:34 +#: assets/models/automations/change_secret.py:57 assets/models/base.py:55 +#: assets/serializers/base.py:16 authentication/models/connection_token.py:34 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:17 +#: perms/models/perm_token.py:15 settings/serializers/auth/radius.py:17 msgid "Secret" msgstr "ひみつ" @@ -800,8 +851,9 @@ msgstr "パスワードルール" msgid "SSH key change strategy" msgstr "SSHキー戦略" -#: assets/models/automations/change_secret.py:27 assets/models/backup.py:28 -#: assets/serializers/account/backup.py:28 +#: assets/models/automations/change_secret.py:27 assets/models/backup.py:27 +#: assets/serializers/account/backup.py:30 +#: assets/serializers/automations/change_secret.py:40 #: xpack/plugins/change_auth_plan/models/app.py:40 #: xpack/plugins/change_auth_plan/models/asset.py:63 #: xpack/plugins/change_auth_plan/serializers/base.py:45 @@ -814,25 +866,25 @@ msgstr "受信者" msgid "Change secret automation" msgstr "秘密を改める" -#: assets/models/automations/change_secret.py:55 +#: assets/models/automations/change_secret.py:56 #, fuzzy #| msgid "Secret" msgid "Old secret" msgstr "ひみつ" -#: assets/models/automations/change_secret.py:57 +#: assets/models/automations/change_secret.py:58 #, fuzzy #| msgid "Date start" msgid "Date started" msgstr "開始日" -#: assets/models/automations/change_secret.py:60 common/const/choices.py:20 +#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 #, fuzzy #| msgid "WeCom Error" msgid "Error" msgstr "企業微信エラー" -#: assets/models/automations/change_secret.py:63 +#: assets/models/automations/change_secret.py:64 #, fuzzy #| msgid "Change auth" msgid "Change secret record" @@ -845,6 +897,7 @@ msgid "Discovery account automation" msgstr "パスワード/キーの確認" #: assets/models/automations/gather_accounts.py:15 +#: assets/tasks/gather_accounts.py:28 #, fuzzy #| msgid "Gather assets users" msgid "Gather asset accounts" @@ -874,24 +927,24 @@ msgstr "サービスアカウントです" msgid "Verify asset account" msgstr "パスワード/キーの確認" -#: assets/models/backup.py:38 assets/models/backup.py:96 +#: assets/models/backup.py:37 assets/models/backup.py:95 msgid "Account backup plan" msgstr "アカウントバックアップ計画" -#: assets/models/backup.py:80 +#: assets/models/backup.py:79 #: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:187 +#: notifications/notifications.py:186 #: xpack/plugins/change_auth_plan/models/base.py:111 #: xpack/plugins/change_auth_plan/models/base.py:200 #: xpack/plugins/gathered_user/models.py:74 msgid "Time" msgstr "時間" -#: assets/models/backup.py:84 +#: assets/models/backup.py:83 msgid "Account backup snapshot" msgstr "アカウントのバックアップスナップショット" -#: assets/models/backup.py:91 audits/models.py:127 +#: assets/models/backup.py:90 audits/models.py:110 #: terminal/models/session/sharing.py:108 #: xpack/plugins/change_auth_plan/models/base.py:197 #: xpack/plugins/change_auth_plan/serializers/asset.py:171 @@ -899,29 +952,32 @@ msgstr "アカウントのバックアップスナップショット" msgid "Reason" msgstr "理由" -#: assets/models/backup.py:93 terminal/serializers/session.py:36 +#: assets/models/backup.py:92 +#: assets/serializers/automations/change_secret.py:86 +#: assets/serializers/automations/change_secret.py:111 +#: terminal/serializers/session.py:36 #: xpack/plugins/change_auth_plan/models/base.py:198 #: xpack/plugins/change_auth_plan/serializers/asset.py:173 msgid "Is success" msgstr "成功は" -#: assets/models/backup.py:100 +#: assets/models/backup.py:99 msgid "Account backup execution" msgstr "アカウントバックアップの実行" -#: assets/models/base.py:29 assets/serializers/domain.py:42 +#: assets/models/base.py:28 assets/serializers/domain.py:42 msgid "Connectivity" msgstr "接続性" -#: assets/models/base.py:31 authentication/models/temp_token.py:12 +#: assets/models/base.py:30 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "確認済みの日付" -#: assets/models/base.py:63 +#: assets/models/base.py:56 msgid "Privileged" msgstr "" -#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61 +#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:56 #: users/models/group.py:31 users/models/user.py:671 msgid "User group" msgstr "ユーザーグループ" @@ -992,7 +1048,7 @@ msgstr "テストゲートウェイ" msgid "Unable to connect to port {port} on {address}" msgstr "{ip} でポート {port} に接続できません" -#: assets/models/domain.py:145 authentication/middleware.py:75 +#: assets/models/domain.py:145 authentication/middleware.py:76 #: xpack/plugins/cloud/providers/fc.py:48 msgid "Authentication failed" msgstr "認証に失敗しました" @@ -1021,7 +1077,7 @@ msgstr "収集ユーザー" msgid "Asset group" msgstr "資産グループ" -#: assets/models/group.py:34 assets/models/platform.py:20 +#: assets/models/group.py:34 assets/models/platform.py:19 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" msgstr "デフォルト" @@ -1054,7 +1110,7 @@ msgstr "新しいノード" msgid "empty" msgstr "空" -#: assets/models/node.py:552 perms/models/asset_permission.py:190 +#: assets/models/node.py:552 perms/models/perm_node.py:21 msgid "Key" msgstr "キー" @@ -1062,7 +1118,7 @@ msgstr "キー" msgid "Full value" msgstr "フルバリュー" -#: assets/models/node.py:557 perms/models/asset_permission.py:191 +#: assets/models/node.py:557 perms/models/perm_node.py:22 msgid "Parent key" msgstr "親キー" @@ -1075,60 +1131,60 @@ msgstr "ノード" msgid "Can match node" msgstr "ノードを一致させることができます" -#: assets/models/platform.py:21 +#: assets/models/platform.py:20 #, fuzzy #| msgid "MFA required" msgid "Required" msgstr "MFAが必要" -#: assets/models/platform.py:24 users/templates/users/reset_password.html:29 +#: assets/models/platform.py:23 users/templates/users/reset_password.html:29 msgid "Setting" msgstr "設定" -#: assets/models/platform.py:43 audits/models.py:112 settings/models.py:37 -#: terminal/serializers/applet_host.py:25 +#: assets/models/platform.py:42 audits/const.py:68 settings/models.py:37 +#: terminal/serializers/applet_host.py:26 msgid "Enabled" msgstr "有効化" -#: assets/models/platform.py:44 +#: assets/models/platform.py:43 msgid "Ansible config" msgstr "" -#: assets/models/platform.py:45 +#: assets/models/platform.py:44 #, fuzzy #| msgid "MFA enabled" msgid "Ping enabled" msgstr "MFA有効化" -#: assets/models/platform.py:46 +#: assets/models/platform.py:45 msgid "Ping method" msgstr "" -#: assets/models/platform.py:47 assets/models/platform.py:55 +#: assets/models/platform.py:46 assets/models/platform.py:56 #, fuzzy #| msgid "Gather assets users" msgid "Gather facts enabled" msgstr "資産ユーザーの収集" -#: assets/models/platform.py:48 assets/models/platform.py:56 +#: assets/models/platform.py:47 assets/models/platform.py:58 #, fuzzy #| msgid "Gather assets users" msgid "Gather facts method" msgstr "資産ユーザーの収集" -#: assets/models/platform.py:49 +#: assets/models/platform.py:48 #, fuzzy #| msgid "Create account successfully" msgid "Push account enabled" msgstr "アカウントを正常に作成" -#: assets/models/platform.py:50 +#: assets/models/platform.py:49 #, fuzzy #| msgid "Create account successfully" msgid "Push account method" msgstr "アカウントを正常に作成" -#: assets/models/platform.py:51 +#: assets/models/platform.py:50 #, fuzzy #| msgid "Change Password" msgid "Change password enabled" @@ -1146,47 +1202,47 @@ msgstr "パスワードの変更" msgid "Verify account enabled" msgstr "サービスアカウントキー" -#: assets/models/platform.py:54 +#: assets/models/platform.py:55 #, fuzzy #| msgid "Verify auth" msgid "Verify account method" msgstr "パスワード/キーの確認" -#: assets/models/platform.py:71 tickets/models/ticket/general.py:298 +#: assets/models/platform.py:75 tickets/models/ticket/general.py:298 msgid "Meta" msgstr "メタ" -#: assets/models/platform.py:72 +#: assets/models/platform.py:76 msgid "Internal" msgstr "内部" -#: assets/models/platform.py:75 +#: assets/models/platform.py:80 assets/serializers/platform.py:96 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:76 +#: assets/models/platform.py:82 #, fuzzy #| msgid "Domain name" msgid "Domain enabled" msgstr "ドメイン名" -#: assets/models/platform.py:77 +#: assets/models/platform.py:83 #, fuzzy #| msgid "Protocols" msgid "Protocols enabled" msgstr "プロトコル" -#: assets/models/platform.py:79 +#: assets/models/platform.py:85 #, fuzzy #| msgid "MFA enabled" msgid "Su enabled" msgstr "MFA有効化" -#: assets/models/platform.py:80 +#: assets/models/platform.py:86 msgid "SU method" msgstr "" -#: assets/models/platform.py:82 assets/serializers/platform.py:78 +#: assets/models/platform.py:88 assets/serializers/platform.py:103 #, fuzzy #| msgid "Automatic managed" msgid "Automation" @@ -1209,7 +1265,7 @@ msgstr "" "{} -アカウントバックアップの通過タスクが完了しました。詳細は添付ファイルをご" "覧ください" -#: assets/notifications.py:19 +#: assets/notifications.py:20 msgid "" "{} - The account backup passage task has been completed: the encryption " "password has not been set - please go to personal information -> file " @@ -1219,47 +1275,56 @@ msgstr "" "されていません-個人情報にアクセスしてください-> ファイル暗号化パスワードを設" "定してください暗号化パスワード" -#: assets/serializers/account/account.py:16 +#: assets/notifications.py:31 xpack/plugins/change_auth_plan/notifications.py:8 +msgid "Notification of implementation result of encryption change plan" +msgstr "暗号化変更プランの実装結果の通知" + +#: assets/notifications.py:41 +#: xpack/plugins/change_auth_plan/notifications.py:18 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください" + +#: assets/notifications.py:42 +#: 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 "" +"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" +"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" + +#: assets/serializers/account/account.py:18 msgid "Push now" msgstr "" -#: assets/serializers/account/account.py:18 +#: assets/serializers/account/account.py:20 #, fuzzy #| msgid "Secret" msgid "Has secret" msgstr "ひみつ" -#: assets/serializers/account/account.py:25 +#: assets/serializers/account/account.py:27 msgid "Account template not found" msgstr "" -#: assets/serializers/account/backup.py:27 ops/mixin.py:102 +#: assets/serializers/account/backup.py:29 +#: assets/serializers/automations/base.py:34 ops/mixin.py:102 #: settings/serializers/auth/ldap.py:65 #: xpack/plugins/change_auth_plan/serializers/base.py:43 msgid "Periodic perform" msgstr "定期的なパフォーマンス" -#: assets/serializers/account/backup.py:29 +#: assets/serializers/account/backup.py:31 +#: assets/serializers/automations/change_secret.py:41 #: xpack/plugins/change_auth_plan/serializers/base.py:46 msgid "Currently only mail sending is supported" msgstr "現在、メール送信のみがサポートされています" -#: assets/serializers/account/base.py:39 assets/serializers/base.py:34 -msgid "private key invalid or passphrase error" -msgstr "秘密鍵が無効またはpassphraseエラー" - -#: assets/serializers/account/template.py:16 common/drf/fields.py:69 -#: tickets/serializers/ticket/common.py:58 -#: xpack/plugins/change_auth_plan/serializers/asset.py:64 -#: 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:101 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "このフィールドは必須です。" - -#: assets/serializers/asset/common.py:69 assets/serializers/platform.py:77 -#: xpack/plugins/cloud/models.py:109 +#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:101 +#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:109 msgid "Protocols" msgstr "プロトコル" @@ -1345,7 +1410,37 @@ msgstr "資産番号" msgid "IP/Host" msgstr "ホスト" -#: assets/serializers/base.py:24 +#: assets/serializers/automations/change_secret.py:28 +#: xpack/plugins/change_auth_plan/models/asset.py:50 +#: xpack/plugins/change_auth_plan/serializers/asset.py:33 +msgid "SSH Key strategy" +msgstr "SSHキー戦略" + +#: assets/serializers/automations/change_secret.py:57 +#: xpack/plugins/change_auth_plan/serializers/base.py:58 +msgid "* Please enter the correct password length" +msgstr "* 正しいパスワードの長さを入力してください" + +#: assets/serializers/automations/change_secret.py:60 +#: xpack/plugins/change_auth_plan/serializers/base.py:61 +msgid "* Password length range 6-30 bits" +msgstr "* パスワードの長さの範囲6-30ビット" + +#: assets/serializers/automations/change_secret.py:104 +#: assets/serializers/automations/change_secret.py:132 audits/const.py:73 +#: audits/models.py:39 common/const/choices.py:18 +#: terminal/models/session/sharing.py:104 tickets/views/approve.py:114 +#: xpack/plugins/change_auth_plan/serializers/asset.py:189 +msgid "Success" +msgstr "成功" + +#: assets/serializers/automations/gather_accounts.py:23 +#, fuzzy +#| msgid "Executed times" +msgid "Executed amount" +msgstr "実行時間" + +#: assets/serializers/base.py:21 msgid "Key password" msgstr "キーパスワード" @@ -1360,7 +1455,6 @@ msgid "Types" msgstr "タイプ" #: assets/serializers/domain.py:14 assets/serializers/label.py:12 -#: perms/serializers/permission.py:83 msgid "Assets amount" msgstr "資産額" @@ -1368,15 +1462,10 @@ msgstr "資産額" msgid "Gateways count" msgstr "ゲートウェイ数" -#: assets/serializers/label.py:13 assets/serializers/mixin.py:7 +#: assets/serializers/label.py:13 msgid "Category display" msgstr "カテゴリ表示" -#: assets/serializers/mixin.py:10 audits/serializers.py:27 -#: tickets/serializers/flow.py:49 tickets/serializers/ticket/ticket.py:17 -msgid "Type display" -msgstr "タイプ表示" - #: assets/serializers/node.py:17 msgid "value" msgstr "値" @@ -1407,54 +1496,104 @@ msgstr "SFTPルート" msgid "Auto fill" msgstr "自動" -#: assets/serializers/platform.py:64 +#: assets/serializers/platform.py:78 msgid "Primary" msgstr "" -#: assets/serializers/utils.py:11 +#: assets/serializers/utils.py:15 msgid "Password can not contains `{{` " msgstr "パスワードには '{{' を含まない" -#: assets/serializers/utils.py:14 +#: assets/serializers/utils.py:18 msgid "Password can not contains `'` " msgstr "パスワードには `'` を含まない" -#: assets/serializers/utils.py:16 +#: assets/serializers/utils.py:20 msgid "Password can not contains `\"` " msgstr "パスワードには `\"` を含まない" -#: assets/tasks/gather_facts.py:25 +#: assets/serializers/utils.py:26 +msgid "private key invalid or passphrase error" +msgstr "秘密鍵が無効またはpassphraseエラー" + +#: assets/tasks/automation.py:11 +#, fuzzy +#| msgid "Verify auth" +msgid "Execute automation" +msgstr "パスワード/キーの確認" + +#: assets/tasks/backup.py:13 +#, fuzzy +#| msgid "Account backup plan" +msgid "Execute account backup plan" +msgstr "アカウントバックアップ計画" + +#: assets/tasks/gather_accounts.py:31 +#, fuzzy +#| msgid "Gather assets users" +msgid "Gather assets accounts" +msgstr "資産ユーザーの収集" + +#: assets/tasks/gather_facts.py:26 msgid "Update some assets hardware info. " msgstr "一部の資産ハードウェア情報を更新します。" -#: assets/tasks/gather_facts.py:48 +#: assets/tasks/gather_facts.py:44 +#, fuzzy +#| msgid "Update node asset hardware information: " +msgid "Manually update the hardware information of assets" +msgstr "ノード資産のハードウェア情報を更新します。" + +#: assets/tasks/gather_facts.py:49 msgid "Update assets hardware info: " msgstr "資産のハードウェア情報を更新する:" -#: assets/tasks/gather_facts.py:58 +#: assets/tasks/gather_facts.py:53 +msgid "Manually update the hardware information of assets under a node" +msgstr "" + +#: assets/tasks/gather_facts.py:59 msgid "Update node asset hardware information: " msgstr "ノード資産のハードウェア情報を更新します。" -#: assets/tasks/nodes_amount.py:29 +#: assets/tasks/nodes_amount.py:16 +msgid "Check the amount of assets under the node" +msgstr "" + +#: assets/tasks/nodes_amount.py:28 msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "" "セルフチェックのタスクはすでに実行されており、繰り返し開始することはできませ" "ん" -#: assets/tasks/ping.py:20 assets/tasks/ping.py:38 +#: assets/tasks/nodes_amount.py:34 +msgid "Periodic check the amount of assets under the node" +msgstr "" + +#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 #, fuzzy #| msgid "Test assets connectivity. " msgid "Test assets connectivity " msgstr "資産の接続性をテストします。" -#: assets/tasks/ping.py:48 +#: assets/tasks/ping.py:33 +#, fuzzy +#| msgid "Can test asset connectivity" +msgid "Manually test the connectivity of a asset" +msgstr "資産接続をテストできます" + +#: assets/tasks/ping.py:43 +msgid "Manually test the connectivity of assets under a node" +msgstr "" + +#: assets/tasks/ping.py:49 #, fuzzy #| msgid "Test if the assets under the node are connectable: " msgid "Test if the assets under the node are connectable " msgstr "ノードの下のアセットが接続可能かどうかをテストします。" -#: assets/tasks/push_account.py:36 +#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:31 #, fuzzy #| msgid "Create account successfully" msgid "Push accounts to assets" @@ -1476,7 +1615,13 @@ msgstr "セキュリティのために、ユーザー {} をプッシュしな msgid "No assets matched, stop task" msgstr "一致する資産がない、タスクを停止" -#: assets/tasks/verify_account.py:36 +#: assets/tasks/verify_account.py:30 +#, fuzzy +#| msgid "Verify auth" +msgid "Verify asset account availability" +msgstr "パスワード/キーの確認" + +#: assets/tasks/verify_account.py:37 #, fuzzy #| msgid "Test account connectivity: " msgid "Verify accounts connectivity" @@ -1486,278 +1631,257 @@ msgstr "テストアカウント接続:" msgid "Audits" msgstr "監査" -#: audits/models.py:27 audits/models.py:59 +#: audits/const.py:44 +msgid "Mkdir" +msgstr "Mkdir" + +#: audits/const.py:45 +msgid "Rmdir" +msgstr "Rmdir" + +#: audits/const.py:46 audits/const.py:56 #: authentication/templates/authentication/_access_key_modal.html:65 #: rbac/tree.py:226 msgid "Delete" msgstr "削除" -#: audits/models.py:28 +#: audits/const.py:47 perms/const.py:14 msgid "Upload" msgstr "アップロード" -#: audits/models.py:29 -msgid "Download" -msgstr "ダウンロード" - -#: audits/models.py:30 -msgid "Rmdir" -msgstr "Rmdir" - -#: audits/models.py:31 +#: audits/const.py:48 msgid "Rename" msgstr "名前の変更" -#: audits/models.py:32 -msgid "Mkdir" -msgstr "Mkdir" - -#: audits/models.py:33 +#: audits/const.py:49 msgid "Symlink" msgstr "Symlink" -#: audits/models.py:38 audits/models.py:66 audits/models.py:89 -#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:96 -msgid "Remote addr" -msgstr "リモートaddr" +#: audits/const.py:50 perms/const.py:15 +msgid "Download" +msgstr "ダウンロード" -#: audits/models.py:41 -msgid "Operate" -msgstr "操作" +#: audits/const.py:54 rbac/tree.py:224 +msgid "View" +msgstr "表示" -#: audits/models.py:42 -msgid "Filename" -msgstr "ファイル名" +#: audits/const.py:55 rbac/tree.py:225 templates/_csv_import_export.html:18 +#: templates/_csv_update_modal.html:6 +msgid "Update" +msgstr "更新" -#: audits/models.py:43 audits/models.py:117 common/const/choices.py:18 -#: terminal/models/session/sharing.py:104 tickets/views/approve.py:114 -#: xpack/plugins/change_auth_plan/serializers/asset.py:189 -msgid "Success" -msgstr "成功" - -#: audits/models.py:47 -msgid "File transfer log" -msgstr "ファイル転送ログ" - -#: audits/models.py:56 +#: audits/const.py:57 #: authentication/templates/authentication/_access_key_modal.html:22 #: rbac/tree.py:223 msgid "Create" msgstr "作成" -#: audits/models.py:57 rbac/tree.py:224 -msgid "View" -msgstr "表示" +#: audits/const.py:62 terminal/models/applet/host.py:24 +#: terminal/models/component/terminal.py:159 +msgid "Terminal" +msgstr "ターミナル" -#: audits/models.py:58 rbac/tree.py:225 templates/_csv_import_export.html:18 -#: templates/_csv_update_modal.html:6 -msgid "Update" -msgstr "更新" +#: audits/const.py:69 +msgid "-" +msgstr "-" -#: audits/models.py:64 audits/serializers.py:61 +#: audits/models.py:31 audits/models.py:55 audits/models.py:82 +#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:96 +msgid "Remote addr" +msgstr "リモートaddr" + +#: audits/models.py:36 audits/serializers.py:19 +msgid "Operate" +msgstr "操作" + +#: audits/models.py:38 +msgid "Filename" +msgstr "ファイル名" + +#: audits/models.py:43 +msgid "File transfer log" +msgstr "ファイル転送ログ" + +#: audits/models.py:52 audits/serializers.py:85 msgid "Resource Type" msgstr "リソースタイプ" -#: audits/models.py:65 +#: audits/models.py:53 msgid "Resource" msgstr "リソース" -#: audits/models.py:67 audits/models.py:90 +#: audits/models.py:58 audits/models.py:84 #: terminal/backends/command/serializers.py:40 msgid "Datetime" msgstr "時間" -#: audits/models.py:82 +#: audits/models.py:74 msgid "Operate log" msgstr "ログの操作" -#: audits/models.py:88 +#: audits/models.py:80 msgid "Change by" msgstr "による変更" -#: audits/models.py:96 +#: audits/models.py:90 msgid "Password change log" msgstr "パスワード変更ログ" -#: audits/models.py:113 -msgid "-" -msgstr "-" - -#: audits/models.py:122 +#: audits/models.py:97 msgid "Login type" msgstr "ログインタイプ" -#: audits/models.py:123 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:99 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "ログインIP" -#: audits/models.py:124 +#: audits/models.py:101 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "ログイン都市" -#: audits/models.py:125 audits/serializers.py:42 +#: audits/models.py:104 audits/serializers.py:62 msgid "User agent" msgstr "ユーザーエージェント" -#: audits/models.py:126 +#: audits/models.py:107 audits/serializers.py:39 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms/profile.py:65 users/models/user.py:688 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:128 ops/models/base.py:48 -#: terminal/models/applet/applet.py:60 terminal/models/applet/host.py:101 -#: terminal/models/component/status.py:33 terminal/serializers/applet.py:22 -#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:171 -#: xpack/plugins/cloud/models.py:223 -msgid "Status" -msgstr "ステータス" - -#: audits/models.py:129 +#: audits/models.py:117 msgid "Date login" msgstr "日付ログイン" -#: audits/models.py:130 audits/serializers.py:44 +#: audits/models.py:119 audits/serializers.py:64 msgid "Authentication backend" msgstr "認証バックエンド" -#: audits/models.py:169 +#: audits/models.py:160 msgid "User login log" msgstr "ユーザーログインログ" -#: audits/serializers.py:12 -msgid "Operate display" -msgstr "ディスプレイを操作する" - -#: audits/serializers.py:28 tickets/serializers/ticket/ticket.py:18 -msgid "Status display" -msgstr "ステータス表示" - -#: audits/serializers.py:29 -msgid "MFA display" -msgstr "MFAディスプレイ" - -#: audits/serializers.py:43 +#: audits/serializers.py:63 msgid "Reason display" msgstr "理由表示" -#: audits/signal_handlers.py:49 +#: audits/signal_handlers.py:45 msgid "SSH Key" msgstr "SSHキー" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:47 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:52 +#: audits/signal_handlers.py:48 msgid "Auth Token" msgstr "認証トークン" -#: audits/signal_handlers.py:53 authentication/notifications.py:73 +#: audits/signal_handlers.py:49 authentication/notifications.py:73 #: authentication/views/login.py:73 authentication/views/wecom.py:178 #: notifications/backends/__init__.py:11 users/models/user.py:724 msgid "WeCom" msgstr "企業微信" -#: audits/signal_handlers.py:54 authentication/views/feishu.py:144 +#: audits/signal_handlers.py:50 authentication/views/feishu.py:145 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 #: users/models/user.py:726 msgid "FeiShu" msgstr "本を飛ばす" -#: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179 +#: audits/signal_handlers.py:51 authentication/views/dingtalk.py:180 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 #: users/models/user.py:725 msgid "DingTalk" msgstr "DingTalk" -#: audits/signal_handlers.py:56 authentication/models/temp_token.py:16 +#: audits/signal_handlers.py:52 authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "仮パスワード" -#: audits/signal_handlers.py:68 +#: audits/signal_handlers.py:64 msgid "User and Group" msgstr "ユーザーとグループ" -#: audits/signal_handlers.py:69 +#: audits/signal_handlers.py:65 #, python-brace-format msgid "{User} JOINED {UserGroup}" msgstr "{User} に参加 {UserGroup}" -#: audits/signal_handlers.py:70 +#: audits/signal_handlers.py:66 #, python-brace-format msgid "{User} LEFT {UserGroup}" msgstr "{User} のそばを通る {UserGroup}" -#: audits/signal_handlers.py:73 +#: audits/signal_handlers.py:69 msgid "Node and Asset" msgstr "ノードと資産" -#: audits/signal_handlers.py:74 +#: audits/signal_handlers.py:70 #, python-brace-format msgid "{Node} ADD {Asset}" msgstr "{Node} 追加 {Asset}" -#: audits/signal_handlers.py:75 +#: audits/signal_handlers.py:71 #, python-brace-format msgid "{Node} REMOVE {Asset}" msgstr "{Node} 削除 {Asset}" -#: audits/signal_handlers.py:78 +#: audits/signal_handlers.py:74 msgid "User asset permissions" msgstr "ユーザー資産の権限" -#: audits/signal_handlers.py:79 +#: audits/signal_handlers.py:75 #, python-brace-format msgid "{AssetPermission} ADD {User}" msgstr "{AssetPermission} 追加 {User}" -#: audits/signal_handlers.py:80 +#: audits/signal_handlers.py:76 #, python-brace-format msgid "{AssetPermission} REMOVE {User}" msgstr "{AssetPermission} 削除 {User}" -#: audits/signal_handlers.py:83 +#: audits/signal_handlers.py:79 msgid "User group asset permissions" msgstr "ユーザーグループの資産権限" -#: audits/signal_handlers.py:84 +#: audits/signal_handlers.py:80 #, python-brace-format msgid "{AssetPermission} ADD {UserGroup}" msgstr "{AssetPermission} 追加 {UserGroup}" -#: audits/signal_handlers.py:85 +#: audits/signal_handlers.py:81 #, python-brace-format msgid "{AssetPermission} REMOVE {UserGroup}" msgstr "{AssetPermission} 削除 {UserGroup}" -#: audits/signal_handlers.py:88 perms/models/asset_permission.py:90 +#: audits/signal_handlers.py:84 perms/models/asset_permission.py:83 msgid "Asset permission" msgstr "資産権限" -#: audits/signal_handlers.py:89 +#: audits/signal_handlers.py:85 #, python-brace-format msgid "{AssetPermission} ADD {Asset}" msgstr "{AssetPermission} 追加 {Asset}" -#: audits/signal_handlers.py:90 +#: audits/signal_handlers.py:86 #, python-brace-format msgid "{AssetPermission} REMOVE {Asset}" msgstr "{AssetPermission} 削除 {Asset}" -#: audits/signal_handlers.py:93 +#: audits/signal_handlers.py:89 msgid "Node permission" msgstr "ノード権限" -#: audits/signal_handlers.py:94 +#: audits/signal_handlers.py:90 #, python-brace-format msgid "{AssetPermission} ADD {Node}" msgstr "{AssetPermission} 追加 {Node}" -#: audits/signal_handlers.py:95 +#: audits/signal_handlers.py:91 #, python-brace-format msgid "{AssetPermission} REMOVE {Node}" msgstr "{AssetPermission} 削除 {Node}" @@ -1980,12 +2104,12 @@ msgstr "企業の微信はすでにバインドされています" msgid "WeCom is not bound" msgstr "企業の微信をバインドしていません" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 -#: authentication/views/dingtalk.py:296 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 +#: authentication/views/dingtalk.py:297 msgid "DingTalk is not bound" msgstr "DingTalkはバインドされていません" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 msgid "FeiShu is not bound" msgstr "本を飛ばすは拘束されていません" @@ -2078,7 +2202,7 @@ msgstr "電話番号を設定して有効にする" msgid "Clear phone number to disable" msgstr "無効にする電話番号をクリアする" -#: authentication/middleware.py:76 settings/utils/ldap.py:652 +#: authentication/middleware.py:77 settings/utils/ldap.py:652 msgid "Authentication failed (before login check failed): {}" msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" @@ -2100,9 +2224,9 @@ msgid "Asset display" msgstr "アセット名" #: authentication/models/connection_token.py:36 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:79 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:72 #: tickets/models/ticket/apply_application.py:29 -#: tickets/models/ticket/apply_asset.py:22 users/models/user.py:707 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:707 msgid "Date expired" msgstr "期限切れの日付" @@ -2168,17 +2292,17 @@ msgstr "異なる都市ログインのリマインダー" msgid "binding reminder" msgstr "バインディングリマインダー" -#: authentication/serializers/connection_token.py:20 +#: authentication/serializers/connection_token.py:19 #: xpack/plugins/cloud/models.py:36 msgid "Validity" msgstr "有効性" -#: authentication/serializers/connection_token.py:21 +#: authentication/serializers/connection_token.py:20 msgid "Expired time" msgstr "期限切れ時間" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:60 -#: perms/serializers/permission.py:87 users/serializers/user.py:148 +#: authentication/serializers/token.py:79 perms/serializers/permission.py:30 +#: perms/serializers/permission.py:61 users/serializers/user.py:203 msgid "Is valid" msgstr "有効です" @@ -2265,7 +2389,7 @@ msgstr "コードエラー" #: authentication/templates/authentication/_msg_reset_password.html:3 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:390 ops/tasks.py:147 ops/tasks.py:153 ops/tasks.py:156 +#: jumpserver/conf.py:390 #: 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 @@ -2418,73 +2542,73 @@ msgstr "コピー成功" msgid "LAN" msgstr "ローカルエリアネットワーク" -#: authentication/views/dingtalk.py:41 +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "DingTalkエラー、システム管理者に連絡してください" -#: authentication/views/dingtalk.py:44 +#: authentication/views/dingtalk.py:45 msgid "DingTalk Error" msgstr "DingTalkエラー" -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 #: authentication/views/wecom.py:56 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "システム設定が正しくありません。管理者に連絡してください" -#: authentication/views/dingtalk.py:80 +#: authentication/views/dingtalk.py:81 msgid "DingTalk is already bound" msgstr "DingTalkはすでにバインドされています" -#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:148 +#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 msgid "Invalid user_id" msgstr "無効なuser_id" -#: authentication/views/dingtalk.py:164 +#: authentication/views/dingtalk.py:165 msgid "DingTalk query user failed" msgstr "DingTalkクエリユーザーが失敗しました" -#: authentication/views/dingtalk.py:173 +#: authentication/views/dingtalk.py:174 msgid "The DingTalk is already bound to another user" msgstr "DingTalkはすでに別のユーザーにバインドされています" -#: authentication/views/dingtalk.py:180 +#: authentication/views/dingtalk.py:181 msgid "Binding DingTalk successfully" msgstr "DingTalkのバインドに成功" -#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 +#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 msgid "Failed to get user from DingTalk" msgstr "DingTalkからユーザーを取得できませんでした" -#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 +#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 msgid "Please login with a password and then bind the DingTalk" msgstr "パスワードでログインし、DingTalkをバインドしてください" -#: authentication/views/feishu.py:39 +#: authentication/views/feishu.py:40 msgid "FeiShu Error" msgstr "FeiShuエラー" -#: authentication/views/feishu.py:87 +#: authentication/views/feishu.py:88 msgid "FeiShu is already bound" msgstr "FeiShuはすでにバインドされています" -#: authentication/views/feishu.py:129 +#: authentication/views/feishu.py:130 msgid "FeiShu query user failed" msgstr "FeiShuクエリユーザーが失敗しました" -#: authentication/views/feishu.py:138 +#: authentication/views/feishu.py:139 msgid "The FeiShu is already bound to another user" msgstr "FeiShuはすでに別のユーザーにバインドされています" -#: authentication/views/feishu.py:145 +#: authentication/views/feishu.py:146 msgid "Binding FeiShu successfully" msgstr "本を飛ばすのバインドに成功" -#: authentication/views/feishu.py:197 +#: authentication/views/feishu.py:198 msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" -#: authentication/views/feishu.py:204 +#: authentication/views/feishu.py:205 msgid "Please login with a password and then bind the FeiShu" msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" @@ -2588,31 +2712,31 @@ msgstr "キャンセル" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:80 +#: common/db/fields.py:93 msgid "Marshal dict data to char field" msgstr "チャーフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:84 +#: common/db/fields.py:97 msgid "Marshal dict data to text field" msgstr "テキストフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:96 +#: common/db/fields.py:109 msgid "Marshal list data to char field" msgstr "元帥リストデータをチャーフィールドに" -#: common/db/fields.py:100 +#: common/db/fields.py:113 msgid "Marshal list data to text field" msgstr "マーシャルリストデータをテキストフィールドに" -#: common/db/fields.py:104 +#: common/db/fields.py:117 msgid "Marshal data to char field" msgstr "チャーフィールドへのマーシャルデータ" -#: common/db/fields.py:108 +#: common/db/fields.py:121 msgid "Marshal data to text field" msgstr "テキストフィールドへのマーシャルデータ" -#: common/db/fields.py:150 +#: common/db/fields.py:163 msgid "Encrypt field using Secret Key" msgstr "Secret Keyを使用したフィールドの暗号化" @@ -2624,17 +2748,36 @@ msgstr "によって更新" msgid "Object" msgstr "オブジェクト" -#: common/drf/fields.py:70 +#: common/drf/fields.py:74 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/change_auth_plan/serializers/asset.py:64 +#: 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:101 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "このフィールドは必須です。" + +#: common/drf/fields.py:75 #, fuzzy, python-brace-format #| msgid "%s object does not exist." msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "%s オブジェクトは存在しません。" -#: common/drf/fields.py:71 +#: common/drf/fields.py:76 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" +#: common/drf/fields.py:138 +msgid "Invalid data type, should be list" +msgstr "" + +#: common/drf/fields.py:153 +#, fuzzy +#| msgid "Invalid ip" +msgid "Invalid choice: {}" +msgstr "無効なIP" + #: common/drf/parsers/base.py:17 msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "ファイルの内容がオーバーフローしました (最大長 '{}' バイト)" @@ -2694,15 +2837,15 @@ msgstr "は破棄されます" msgid "discard time" msgstr "時間を捨てる" -#: common/mixins/views.py:52 +#: common/mixins/views.py:58 msgid "Export all" msgstr "すべてエクスポート" -#: common/mixins/views.py:54 +#: common/mixins/views.py:60 msgid "Export only selected items" msgstr "選択項目のみエクスポート" -#: common/mixins/views.py:59 +#: common/mixins/views.py:65 #, python-format msgid "Export filtered: %s" msgstr "検索のエクスポート: %s" @@ -2759,6 +2902,16 @@ msgstr "確認コードが正しくありません" msgid "Please wait {} seconds before sending" msgstr "{} 秒待ってから送信してください" +#: common/tasks.py:13 +#, fuzzy +#| msgid "Send user" +msgid "Send email" +msgstr "ユーザーを送信" + +#: common/tasks.py:40 +msgid "Send email attachment" +msgstr "" + #: common/utils/ip/geoip/utils.py:26 msgid "Invalid ip" msgstr "無効なIP" @@ -2842,6 +2995,10 @@ msgstr "メール" msgid "Site message" msgstr "サイトメッセージ" +#: notifications/notifications.py:46 +msgid "Publish the station message" +msgstr "" + #: ops/ansible/inventory.py:75 #, fuzzy #| msgid "Account unavailable" @@ -2915,103 +3072,129 @@ msgstr "{} から {} までの範囲" msgid "Require periodic or regularly perform setting" msgstr "定期的または定期的に設定を行う必要があります" -#: ops/models/adhoc.py:18 +#: ops/models/adhoc.py:21 ops/models/job.py:31 +#, fuzzy +#| msgid "PowerShell" +msgid "Powershell" +msgstr "PowerShell" + +#: ops/models/adhoc.py:25 msgid "Pattern" msgstr "パターン" -#: ops/models/adhoc.py:19 +#: ops/models/adhoc.py:27 ops/models/job.py:38 msgid "Module" msgstr "" -#: ops/models/adhoc.py:20 ops/models/celery.py:45 +#: ops/models/adhoc.py:28 ops/models/celery.py:54 ops/models/job.py:36 #: terminal/models/component/task.py:17 msgid "Args" msgstr "アルグ" -#: ops/models/adhoc.py:21 ops/models/base.py:20 ops/models/playbook.py:27 -#, fuzzy -#| msgid "Command execution" -msgid "Last execution" -msgstr "コマンド実行" - -#: ops/models/adhoc.py:36 -msgid "Adhoc" -msgstr "" - -#: ops/models/adhoc.py:54 -msgid "AdHoc execution" -msgstr "アドホックエキューション" - -#: ops/models/base.py:16 ops/models/base.py:52 +#: ops/models/adhoc.py:29 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:43 ops/models/job.py:68 #: terminal/models/session/sharing.py:24 msgid "Creator" msgstr "作成者" +#: ops/models/adhoc.py:50 ops/models/job.py:21 +msgid "Adhoc" +msgstr "" + +#: ops/models/adhoc.py:68 +msgid "AdHoc execution" +msgstr "アドホックエキューション" + #: ops/models/base.py:19 #, fuzzy #| msgid "Account key" msgid "Account policy" msgstr "アカウントキー" -#: ops/models/base.py:21 +#: ops/models/base.py:20 +#, fuzzy +#| msgid "Command execution" +msgid "Last execution" +msgstr "コマンド実行" + +#: ops/models/base.py:22 #, fuzzy #| msgid "Date last sync" msgid "Date last run" msgstr "最終同期日" -#: ops/models/base.py:50 xpack/plugins/cloud/models.py:169 +#: ops/models/base.py:51 ops/models/job.py:66 xpack/plugins/cloud/models.py:169 msgid "Result" msgstr "結果" -#: ops/models/base.py:51 +#: ops/models/base.py:52 ops/models/job.py:67 msgid "Summary" msgstr "" -#: ops/models/celery.py:46 terminal/models/component/task.py:18 +#: ops/models/celery.py:55 terminal/models/component/task.py:18 msgid "Kwargs" msgstr "クワーグ" -#: ops/models/celery.py:47 tickets/models/comment.py:13 +#: ops/models/celery.py:56 tickets/models/comment.py:13 #: tickets/models/ticket/general.py:41 tickets/models/ticket/general.py:277 +#: tickets/serializers/ticket/ticket.py:20 msgid "State" msgstr "状態" -#: ops/models/celery.py:48 terminal/models/session/sharing.py:111 +#: ops/models/celery.py:57 terminal/models/session/sharing.py:111 #: tickets/const.py:25 xpack/plugins/change_auth_plan/models/base.py:188 msgid "Finished" msgstr "終了" -#: ops/models/playbook.py:10 -msgid "Path" -msgstr "" +#: ops/models/celery.py:58 +#, fuzzy +#| msgid "Date finished" +msgid "Date published" +msgstr "終了日" -#: ops/models/playbook.py:18 -msgid "Playbook template" -msgstr "" - -#: ops/models/playbook.py:23 +#: ops/models/job.py:22 ops/models/job.py:41 msgid "Playbook" msgstr "" -#: ops/models/playbook.py:24 -msgid "Owner" +#: ops/models/job.py:25 +msgid "Privileged Only" msgstr "" -#: ops/models/playbook.py:26 settings/serializers/auth/sms.py:64 -msgid "Template" -msgstr "テンプレート" +#: ops/models/job.py:26 +msgid "Privileged First" +msgstr "" -#: ops/models/playbook.py:38 ops/signal_handlers.py:63 -#: terminal/models/component/task.py:26 -#: xpack/plugins/gathered_user/models.py:68 -msgid "Task" -msgstr "タスク" +#: ops/models/job.py:27 +msgid "Skip" +msgstr "" -#: ops/models/playbook.py:39 +#: ops/models/job.py:39 +msgid "Chdir" +msgstr "" + +#: ops/models/job.py:40 +msgid "Timeout (Seconds)" +msgstr "" + +#: ops/models/job.py:45 +msgid "Runas" +msgstr "" + +#: ops/models/job.py:47 #, fuzzy -#| msgid "Run user" -msgid "Run dir" -msgstr "ユーザーの実行" +#| msgid "Account key" +msgid "Runas policy" +msgstr "アカウントキー" + +#: ops/models/job.py:48 +#, fuzzy +#| msgid "Disable" +msgid "Variables" +msgstr "無効化" + +#: ops/models/playbook.py:15 +msgid "Owner" +msgstr "" #: ops/notifications.py:17 msgid "Server performance" @@ -3041,26 +3224,48 @@ msgstr "{max_threshold}%: => {value} を超える使用メモリ" msgid "CPU load more than {max_threshold}: => {value}" msgstr "{max_threshold} を超えるCPUロード: => {value}" -#: ops/tasks.py:34 +#: ops/signal_handlers.py:63 terminal/models/applet/host.py:108 +#: terminal/models/component/task.py:26 +#: xpack/plugins/gathered_user/models.py:68 +msgid "Task" +msgstr "タスク" + +#: ops/tasks.py:27 #, fuzzy #| msgid "Run asset" msgid "Run ansible task" msgstr "アセットの実行" -#: ops/tasks.py:58 +#: ops/tasks.py:41 #, fuzzy -#| msgid "Run command" -msgid "Run ansible command" -msgstr "実行コマンド" +#| msgid "Run asset" +msgid "Run ansible task execution" +msgstr "アセットの実行" -#: ops/tasks.py:80 -msgid "Clean task history period" -msgstr "クリーンなタスク履歴期間" +#: ops/tasks.py:54 +msgid "Periodic clear celery tasks" +msgstr "" -#: ops/tasks.py:93 +#: ops/tasks.py:56 msgid "Clean celery log period" msgstr "きれいなセロリログ期間" +#: ops/tasks.py:73 +#, fuzzy +#| msgid "Clean celery log period" +msgid "Clear celery periodic tasks" +msgstr "きれいなセロリログ期間" + +#: ops/tasks.py:96 +msgid "Create or update periodic tasks" +msgstr "" + +#: ops/tasks.py:104 +#, fuzzy +#| msgid "Periodic perform" +msgid "Periodic check service performance" +msgstr "定期的なパフォーマンス" + #: ops/templates/ops/celery_task_log.html:4 msgid "Task log" msgstr "タスクログ" @@ -3091,7 +3296,7 @@ msgstr "アプリ組織" #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:88 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 -#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:72 +#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:61 msgid "Organization" msgstr "組織" @@ -3125,76 +3330,86 @@ msgstr "グローバル組織を表示できます" msgid "Can view all joined org" msgstr "参加しているすべての組織を表示できます" +#: orgs/tasks.py:9 +#, fuzzy +#| msgid "Global organization name" +msgid "Refresh organization cache" +msgstr "グローバル組織名" + #: perms/apps.py:9 msgid "App permissions" msgstr "アプリの権限" -#: perms/models/asset_permission.py:72 perms/serializers/permission.py:59 -#: perms/serializers/permission.py:85 -#: tickets/models/ticket/apply_application.py:26 -#: tickets/models/ticket/apply_asset.py:19 -msgid "Actions" -msgstr "アクション" - -#: perms/models/asset_permission.py:83 -msgid "From ticket" -msgstr "チケットから" - -#: perms/models/asset_permission.py:224 -msgid "Ungrouped" -msgstr "グループ化されていません" - -#: perms/models/asset_permission.py:226 -msgid "Favorite" -msgstr "お気に入り" - -#: perms/models/asset_permission.py:273 -msgid "Permed asset" -msgstr "許可された資産" - -#: perms/models/asset_permission.py:275 -msgid "Can view my assets" -msgstr "私の資産を見ることができます" - -#: perms/models/asset_permission.py:276 -msgid "Can view user assets" -msgstr "ユーザー資産を表示できます" - -#: perms/models/asset_permission.py:277 -msgid "Can view usergroup assets" -msgstr "ユーザーグループの資産を表示できます" - -#: perms/models/const.py:20 settings/serializers/terminal.py:12 -msgid "All" -msgstr "すべて" - -#: perms/models/const.py:21 +#: perms/const.py:13 msgid "Connect" msgstr "接続" -#: perms/models/const.py:22 -msgid "Upload file" -msgstr "ファイルのアップロード" +#: perms/const.py:16 +#, fuzzy +#| msgid "Copy link" +msgid "Copy" +msgstr "リンクのコピー" -#: perms/models/const.py:23 -msgid "Download file" -msgstr "ファイルのダウンロード" +#: perms/const.py:17 +msgid "Paste" +msgstr "" -#: perms/models/const.py:24 -msgid "Upload download" -msgstr "ダウンロードのアップロード" +#: perms/const.py:27 +msgid "Transfer" +msgstr "" -#: perms/models/const.py:25 -msgid "Clipboard copy" +#: perms/const.py:28 +#, fuzzy +#| msgid "Clipboard copy" +msgid "Clipboard" msgstr "クリップボードのコピー" -#: perms/models/const.py:26 -msgid "Clipboard paste" -msgstr "クリップボードペースト" +#: perms/models/asset_permission.py:66 perms/models/perm_token.py:18 +#: perms/serializers/permission.py:29 perms/serializers/permission.py:59 +#: tickets/models/ticket/apply_application.py:26 +#: tickets/models/ticket/apply_asset.py:18 +msgid "Actions" +msgstr "アクション" -#: perms/models/const.py:27 -msgid "Clipboard copy paste" -msgstr "クリップボードコピーペースト" +#: perms/models/asset_permission.py:76 +msgid "From ticket" +msgstr "チケットから" + +#: perms/models/perm_node.py:55 +msgid "Ungrouped" +msgstr "グループ化されていません" + +#: perms/models/perm_node.py:57 +msgid "Favorite" +msgstr "お気に入り" + +#: perms/models/perm_node.py:104 +msgid "Permed asset" +msgstr "許可された資産" + +#: perms/models/perm_node.py:106 +msgid "Can view my assets" +msgstr "私の資産を見ることができます" + +#: perms/models/perm_node.py:107 +msgid "Can view user assets" +msgstr "ユーザー資産を表示できます" + +#: perms/models/perm_node.py:108 +msgid "Can view usergroup assets" +msgstr "ユーザーグループの資産を表示できます" + +#: perms/models/perm_node.py:119 +#, fuzzy +#| msgid "Gather account" +msgid "Permed account" +msgstr "アカウントを集める" + +#: perms/models/perm_token.py:17 +#, fuzzy +#| msgid "Connect timeout" +msgid "Connect method" +msgstr "接続タイムアウト" #: perms/notifications.py:12 perms/notifications.py:44 msgid "today" @@ -3216,40 +3431,11 @@ msgstr "資産権限の有効期限が近づいています" msgid "asset permissions of organization {}" msgstr "組織 {} の資産権限" -#: perms/serializers/permission.py:48 -msgid "Users display" -msgstr "ユーザー表示" - -#: perms/serializers/permission.py:51 -msgid "User groups display" -msgstr "ユーザーグループの表示" - -#: perms/serializers/permission.py:54 -msgid "Assets display" -msgstr "資産表示" - -#: perms/serializers/permission.py:57 -msgid "Nodes display" -msgstr "ノード表示" - -#: perms/serializers/permission.py:61 perms/serializers/permission.py:86 -#: users/serializers/user.py:89 users/serializers/user.py:150 +#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: users/serializers/user.py:100 users/serializers/user.py:205 msgid "Is expired" msgstr "期限切れです" -#: perms/serializers/permission.py:81 rbac/serializers/role.py:26 -#: users/serializers/group.py:34 -msgid "Users amount" -msgstr "ユーザー数" - -#: perms/serializers/permission.py:82 -msgid "User groups amount" -msgstr "ユーザーグループの量" - -#: perms/serializers/permission.py:84 -msgid "Nodes amount" -msgstr "ノード量" - #: perms/templates/perms/_msg_item_permissions_expire.html:7 #: perms/templates/perms/_msg_permed_items_expire.html:7 #, python-format @@ -3266,7 +3452,7 @@ msgstr "" msgid "If you have any question, please contact the administrator" msgstr "質問があったら、管理者に連絡して下さい" -#: perms/utils/user_permission.py:623 rbac/tree.py:57 +#: perms/utils/user_permission.py:627 rbac/tree.py:57 msgid "My assets" msgstr "私の資産" @@ -3399,6 +3585,10 @@ msgstr "パーマ" msgid "Scope display" msgstr "スコープ表示" +#: rbac/serializers/role.py:26 users/serializers/group.py:34 +msgid "Users amount" +msgstr "ユーザー数" + #: rbac/serializers/role.py:27 terminal/models/applet/applet.py:21 msgid "Display name" msgstr "表示名" @@ -3887,6 +4077,10 @@ msgstr "元の番号(Src id)" msgid "Business type(Service id)" msgstr "ビジネス・タイプ(Service id)" +#: settings/serializers/auth/sms.py:64 +msgid "Template" +msgstr "テンプレート" + #: settings/serializers/auth/sms.py:65 #, python-brace-format msgid "" @@ -4667,8 +4861,8 @@ msgstr "期限切れです。" #, python-format msgid "" "\n" -" Your password has expired, please click this link update password.\n" +" Your password has expired, please click this link update password.\n" " " msgstr "" "\n" @@ -4689,34 +4883,34 @@ msgid "" " " msgstr "" "\n" -" クリックしてください リンク パスワードの更新\n" +" クリックしてください リンク パスワードの更新\n" " " #: templates/_message.html:43 #, python-format msgid "" "\n" -" Your information was incomplete. Please click this link to complete your information.\n" +" Your information was incomplete. Please click this link to complete your information.\n" " " msgstr "" "\n" -" あなたの情報が不完全なので、クリックしてください。 リンク 補完\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" +" Your ssh public key not set or expired. Please click this link to update\n" " " msgstr "" "\n" -" SSHキーが設定されていないか無効になっている場合は、 リンク 更新\n" +" SSHキーが設定されていないか無効になっている場合は、 リンク 更新\n" " " #: templates/_mfa_login_field.html:28 @@ -4890,7 +5084,7 @@ msgid "Timestamp" msgstr "タイムスタンプ" #: terminal/backends/command/serializers.py:41 -#: terminal/models/component/terminal.py:105 +#: terminal/models/component/terminal.py:87 msgid "Remote Address" msgstr "リモートアドレス" @@ -4932,45 +5126,41 @@ msgstr "" msgid "Hosts" msgstr "ホスト" -#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:28 +#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:27 #, fuzzy #| msgid "Apply assets" msgid "Applet" msgstr "資産の適用" -#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:36 +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 #, fuzzy #| msgid "More login options" msgid "Deploy options" msgstr "その他のログインオプション" -#: terminal/models/applet/host.py:20 +#: terminal/models/applet/host.py:19 msgid "Inited" msgstr "" -#: terminal/models/applet/host.py:21 +#: terminal/models/applet/host.py:20 #, fuzzy #| msgid "Date finished" msgid "Date inited" msgstr "終了日" -#: terminal/models/applet/host.py:22 +#: terminal/models/applet/host.py:21 #, fuzzy #| msgid "Date sync" msgid "Date synced" msgstr "日付の同期" -#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:183 -msgid "Terminal" -msgstr "ターミナル" - -#: terminal/models/applet/host.py:99 +#: terminal/models/applet/host.py:102 #, fuzzy #| msgid "Host" msgid "Hosting" msgstr "ホスト" -#: terminal/models/applet/host.py:100 +#: terminal/models/applet/host.py:103 msgid "Initial" msgstr "" @@ -4979,12 +5169,10 @@ msgid "HTTPS Port" msgstr "HTTPS ポート" #: terminal/models/component/endpoint.py:15 -#: terminal/models/component/terminal.py:107 msgid "HTTP Port" msgstr "HTTP ポート" #: terminal/models/component/endpoint.py:16 -#: terminal/models/component/terminal.py:106 msgid "SSH Port" msgstr "SSH ポート" @@ -5032,31 +5220,31 @@ msgstr "IP グループ" msgid "Endpoint rule" msgstr "エンドポイントルール" -#: terminal/models/component/status.py:18 +#: terminal/models/component/status.py:14 msgid "Session Online" msgstr "セッションオンライン" -#: terminal/models/component/status.py:19 +#: terminal/models/component/status.py:15 msgid "CPU Load" msgstr "CPUロード" -#: terminal/models/component/status.py:20 +#: terminal/models/component/status.py:16 msgid "Memory Used" msgstr "使用メモリ" -#: terminal/models/component/status.py:21 +#: terminal/models/component/status.py:17 msgid "Disk Used" msgstr "使用済みディスク" -#: terminal/models/component/status.py:22 +#: terminal/models/component/status.py:18 msgid "Connections" msgstr "接続" -#: terminal/models/component/status.py:23 +#: terminal/models/component/status.py:19 msgid "Threads" msgstr "スレッド" -#: terminal/models/component/status.py:24 +#: terminal/models/component/status.py:20 msgid "Boot Time" msgstr "ブート時間" @@ -5065,20 +5253,20 @@ msgid "Default storage" msgstr "デフォルトのストレージ" #: terminal/models/component/storage.py:136 -#: terminal/models/component/terminal.py:108 +#: terminal/models/component/terminal.py:88 msgid "Command storage" msgstr "コマンドストレージ" #: terminal/models/component/storage.py:196 -#: terminal/models/component/terminal.py:109 +#: terminal/models/component/terminal.py:89 msgid "Replay storage" msgstr "再生ストレージ" -#: terminal/models/component/terminal.py:103 +#: terminal/models/component/terminal.py:85 msgid "type" msgstr "タイプ" -#: terminal/models/component/terminal.py:185 +#: terminal/models/component/terminal.py:161 msgid "Can view terminal config" msgstr "ターミナル構成を表示できます" @@ -5216,42 +5404,46 @@ msgstr "電話が設定されていない" msgid "Icon" msgstr "" -#: terminal/serializers/applet_host.py:20 +#: terminal/serializers/applet_host.py:21 #, fuzzy #| msgid "Session" msgid "Per Session" msgstr "セッション" -#: terminal/serializers/applet_host.py:21 +#: terminal/serializers/applet_host.py:22 msgid "Per Device" msgstr "" -#: terminal/serializers/applet_host.py:27 +#: terminal/serializers/applet_host.py:28 #, fuzzy #| msgid "License" msgid "RDS Licensing" msgstr "ライセンス" -#: terminal/serializers/applet_host.py:28 +#: terminal/serializers/applet_host.py:29 msgid "RDS License Server" msgstr "" -#: terminal/serializers/applet_host.py:29 +#: terminal/serializers/applet_host.py:30 msgid "RDS Licensing Mode" msgstr "" -#: terminal/serializers/applet_host.py:30 +#: terminal/serializers/applet_host.py:32 msgid "RDS fSingleSessionPerUser" msgstr "" -#: terminal/serializers/applet_host.py:31 +#: terminal/serializers/applet_host.py:33 msgid "RDS Max Disconnection Time" msgstr "" -#: terminal/serializers/applet_host.py:32 +#: terminal/serializers/applet_host.py:34 msgid "RDS Remote App Logoff Time Limit" msgstr "" +#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +msgid "Load status" +msgstr "ロードステータス" + #: terminal/serializers/endpoint.py:12 msgid "Oracle port" msgstr "" @@ -5373,11 +5565,7 @@ msgstr "Docタイプ" msgid "Ignore Certificate Verification" msgstr "証明書の検証を無視する" -#: terminal/serializers/terminal.py:44 -msgid "Load status" -msgstr "ロードステータス" - -#: terminal/serializers/terminal.py:81 terminal/serializers/terminal.py:89 +#: terminal/serializers/terminal.py:77 terminal/serializers/terminal.py:85 msgid "Not found" msgstr "見つかりません" @@ -5498,11 +5686,11 @@ msgstr "ボディ" msgid "Approve level" msgstr "レベルを承認する" -#: tickets/models/flow.py:25 tickets/serializers/flow.py:15 +#: tickets/models/flow.py:25 tickets/serializers/flow.py:17 msgid "Approve strategy" msgstr "戦略を承認する" -#: tickets/models/flow.py:30 tickets/serializers/flow.py:16 +#: tickets/models/flow.py:30 tickets/serializers/flow.py:19 msgid "Assignees" msgstr "アシニーズ" @@ -5532,15 +5720,17 @@ msgid "Apply system users" msgstr "システムユーザーの適用" #: tickets/models/ticket/apply_asset.py:9 -#: tickets/serializers/ticket/apply_asset.py:15 +#: tickets/serializers/ticket/apply_asset.py:14 msgid "Select at least one asset or node" msgstr "少なくとも1つのアセットまたはノードを選択します。" #: tickets/models/ticket/apply_asset.py:14 +#: tickets/serializers/ticket/apply_asset.py:19 msgid "Apply nodes" msgstr "ノードの適用" #: tickets/models/ticket/apply_asset.py:16 +#: tickets/serializers/ticket/apply_asset.py:18 msgid "Apply assets" msgstr "資産の適用" @@ -5654,15 +5844,15 @@ msgstr "チケットが処理されました。プロセッサー- {}" msgid "Ticket has processed - {} ({})" msgstr "チケットが処理済み- {} ({})" -#: tickets/serializers/flow.py:17 +#: tickets/serializers/flow.py:20 msgid "Assignees display" msgstr "受付者名" -#: tickets/serializers/flow.py:43 +#: tickets/serializers/flow.py:46 msgid "Please select the Assignees" msgstr "受付をお選びください" -#: tickets/serializers/flow.py:69 +#: tickets/serializers/flow.py:74 msgid "The current organization type already exists" msgstr "現在の組織タイプは既に存在します。" @@ -5670,6 +5860,12 @@ msgstr "現在の組織タイプは既に存在します。" msgid "Processor" msgstr "プロセッサ" +#: tickets/serializers/ticket/apply_asset.py:20 +#, fuzzy +#| msgid "Apply applications" +msgid "Apply actions" +msgstr "アプリケーションの適用" + #: tickets/serializers/ticket/common.py:15 #: tickets/serializers/ticket/common.py:77 msgid "Created by ticket ({}-{})" @@ -5683,7 +5879,7 @@ msgstr "有効期限は開始日より大きくする必要があります" msgid "Permission named `{}` already exists" msgstr "'{}'という名前の権限は既に存在します" -#: tickets/serializers/ticket/ticket.py:101 +#: tickets/serializers/ticket/ticket.py:85 msgid "The ticket flow `{}` does not exist" msgstr "チケットフロー '{}'が存在しない" @@ -5858,7 +6054,7 @@ msgstr "強制有効" msgid "Local" msgstr "ローカル" -#: users/models/user.py:677 users/serializers/user.py:149 +#: users/models/user.py:677 users/serializers/user.py:204 msgid "Is service account" msgstr "サービスアカウントです" @@ -5965,105 +6161,105 @@ msgstr "新しいパスワードを最後の {} 個のパスワードにする msgid "The newly set password is inconsistent" msgstr "新しく設定されたパスワードが一致しない" -#: users/serializers/profile.py:149 users/serializers/user.py:146 +#: users/serializers/profile.py:149 users/serializers/user.py:201 msgid "Is first login" msgstr "最初のログインです" -#: users/serializers/user.py:28 +#: users/serializers/user.py:30 msgid "System roles" msgstr "システムの役割" -#: users/serializers/user.py:33 +#: users/serializers/user.py:35 msgid "Org roles" msgstr "組織ロール" -#: users/serializers/user.py:35 +#: users/serializers/user.py:38 msgid "System roles display" msgstr "システムロール表示" -#: users/serializers/user.py:36 +#: users/serializers/user.py:40 msgid "Org roles display" msgstr "組織ロール表示" -#: users/serializers/user.py:81 +#: users/serializers/user.py:90 #: 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 +#: users/serializers/user.py:92 msgid "MFA enabled" msgstr "MFA有効化" -#: users/serializers/user.py:84 +#: users/serializers/user.py:94 msgid "MFA force enabled" msgstr "MFAフォース有効化" -#: users/serializers/user.py:86 +#: users/serializers/user.py:97 msgid "MFA level display" msgstr "MFAレベル表示" -#: users/serializers/user.py:88 +#: users/serializers/user.py:99 msgid "Login blocked" msgstr "ログインブロック" -#: users/serializers/user.py:91 +#: users/serializers/user.py:102 msgid "Can public key authentication" msgstr "公開鍵認証が可能" -#: users/serializers/user.py:151 +#: users/serializers/user.py:206 msgid "Avatar url" msgstr "アバターURL" -#: users/serializers/user.py:153 +#: users/serializers/user.py:208 msgid "Groups name" msgstr "グループ名" -#: users/serializers/user.py:154 +#: users/serializers/user.py:209 msgid "Source name" msgstr "ソース名" -#: users/serializers/user.py:155 +#: users/serializers/user.py:210 msgid "Organization role name" msgstr "組織の役割名" -#: users/serializers/user.py:156 +#: users/serializers/user.py:211 msgid "Super role name" msgstr "スーパーロール名" -#: users/serializers/user.py:157 +#: users/serializers/user.py:212 msgid "Total role name" msgstr "合計ロール名" -#: users/serializers/user.py:159 +#: users/serializers/user.py:214 msgid "Is wecom bound" msgstr "企業の微信をバインドしているかどうか" -#: users/serializers/user.py:160 +#: users/serializers/user.py:215 msgid "Is dingtalk bound" msgstr "ピンをバインドしているかどうか" -#: users/serializers/user.py:161 +#: users/serializers/user.py:216 msgid "Is feishu bound" msgstr "飛本を縛ったかどうか" -#: users/serializers/user.py:162 +#: users/serializers/user.py:217 msgid "Is OTP bound" msgstr "仮想MFAがバインドされているか" -#: users/serializers/user.py:164 +#: users/serializers/user.py:219 msgid "System role name" msgstr "システムロール名" -#: users/serializers/user.py:263 +#: users/serializers/user.py:325 msgid "Select users" msgstr "ユーザーの選択" -#: users/serializers/user.py:264 +#: users/serializers/user.py:326 msgid "For security, only list several users" msgstr "セキュリティのために、複数のユーザーのみをリストします" -#: users/serializers/user.py:299 +#: users/serializers/user.py:362 msgid "name not unique" msgstr "名前が一意ではない" @@ -6317,10 +6513,6 @@ msgstr "パスワードの成功をリセットし、ログインページに戻 msgid "XPACK" msgstr "XPack" -#: xpack/plugins/change_auth_plan/api/asset.py:94 -msgid "The parameter 'action' must be [{}]" -msgstr "パラメータ 'action' は [{}] でなければなりません。" - #: xpack/plugins/change_auth_plan/meta.py:9 #: xpack/plugins/change_auth_plan/models/asset.py:124 msgid "Change auth plan" @@ -6349,11 +6541,6 @@ msgstr "改密計画タスクの適用" msgid "Password cannot be set to blank, exit. " msgstr "パスワードを空白に設定することはできません。" -#: xpack/plugins/change_auth_plan/models/asset.py:50 -#: xpack/plugins/change_auth_plan/serializers/asset.py:33 -msgid "SSH Key strategy" -msgstr "SSHキー戦略" - #: xpack/plugins/change_auth_plan/models/asset.py:68 msgid "Asset change auth plan" msgstr "資産変更のオースプラン" @@ -6407,25 +6594,6 @@ msgstr "パスワード/キーの保存" 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:30 msgid "Change Password" msgstr "パスワードの変更" @@ -6438,14 +6606,6 @@ msgstr "SSHキーの変更" 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 "秘密を変更しようとする多くの試みの後、それはまだ失敗しました" @@ -6462,6 +6622,22 @@ msgstr "ホストへの接続に失敗しました" msgid "Data could not be sent to remote" msgstr "データをリモートに送信できませんでした" +#: xpack/plugins/change_auth_plan/tasks.py:13 +#, fuzzy +#| msgid "Asset change auth plan task" +msgid "Execute change authentication task" +msgstr "資産改密計画タスク" + +#: xpack/plugins/change_auth_plan/tasks.py:24 +#, fuzzy +#| msgid "Asset change auth plan task" +msgid "Start change authentication task" +msgstr "資産改密計画タスク" + +#: xpack/plugins/change_auth_plan/tasks.py:36 +msgid "Test the validity of the change authentication plan " +msgstr "" + #: xpack/plugins/cloud/api.py:40 msgid "Test connection successful" msgstr "テスト接続成功" @@ -6997,11 +7173,11 @@ msgstr "テーマ" msgid "Interface setting" msgstr "インターフェイスの設定" -#: xpack/plugins/license/api.py:53 +#: xpack/plugins/license/api.py:50 msgid "License import successfully" msgstr "ライセンスのインポートに成功" -#: xpack/plugins/license/api.py:54 +#: xpack/plugins/license/api.py:51 msgid "License is invalid" msgstr "ライセンスが無効です" @@ -7025,10 +7201,68 @@ msgstr "究極のエディション" msgid "Community edition" msgstr "コミュニティ版" +#~ msgid "Type display" +#~ msgstr "タイプ表示" + +#~ msgid "Status display" +#~ msgstr "ステータス表示" + #, fuzzy -#~| msgid "Verify auth" -#~ msgid "Account automation" -#~ msgstr "パスワード/キーの確認" +#~| msgid "Run command" +#~ msgid "Run ansible command" +#~ msgstr "実行コマンド" + +#~ msgid "Clean task history period" +#~ msgstr "クリーンなタスク履歴期間" + +#, fuzzy +#~| msgid "WeCom Error" +#~ msgid "Hello Error" +#~ msgstr "企業微信エラー" + +#~ msgid "Operate display" +#~ msgstr "ディスプレイを操作する" + +#~ msgid "MFA display" +#~ msgstr "MFAディスプレイ" + +#, fuzzy +#~| msgid "Run user" +#~ msgid "Run dir" +#~ msgstr "ユーザーの実行" + +#~ msgid "Upload file" +#~ msgstr "ファイルのアップロード" + +#~ msgid "Download file" +#~ msgstr "ファイルのダウンロード" + +#~ msgid "Upload download" +#~ msgstr "ダウンロードのアップロード" + +#~ msgid "Clipboard paste" +#~ msgstr "クリップボードペースト" + +#~ msgid "Clipboard copy paste" +#~ msgstr "クリップボードコピーペースト" + +#~ msgid "Users display" +#~ msgstr "ユーザー表示" + +#~ msgid "User groups display" +#~ msgstr "ユーザーグループの表示" + +#~ msgid "Assets display" +#~ msgstr "資産表示" + +#~ msgid "Nodes display" +#~ msgstr "ノード表示" + +#~ msgid "User groups amount" +#~ msgstr "ユーザーグループの量" + +#~ msgid "Nodes amount" +#~ msgstr "ノード量" #~ msgid "The asset {} system platform {} does not support run Ansible tasks" #~ msgstr "" @@ -7333,66 +7567,54 @@ msgstr "コミュニティ版" #~ msgid "Asset and SystemUser" #~ msgstr "資産およびシステム・ユーザー" -#, python-brace-format #~ msgid "{Asset} ADD {SystemUser}" #~ msgstr "{Asset} 追加 {SystemUser}" -#, python-brace-format #~ msgid "{Asset} REMOVE {SystemUser}" #~ msgstr "{Asset} 削除 {SystemUser}" #~ msgid "Asset permission and SystemUser" #~ msgstr "資産権限とSystemUser" -#, python-brace-format #~ msgid "{AssetPermission} ADD {SystemUser}" #~ msgstr "{AssetPermission} 追加 {SystemUser}" -#, python-brace-format #~ msgid "{AssetPermission} REMOVE {SystemUser}" #~ msgstr "{AssetPermission} 削除 {SystemUser}" #~ msgid "User application permissions" #~ msgstr "ユーザーアプリケーションの権限" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {User}" #~ msgstr "{ApplicationPermission} 追加 {User}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {User}" #~ msgstr "{ApplicationPermission} 削除 {User}" #~ msgid "User group application permissions" #~ msgstr "ユーザーグループアプリケーションの権限" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {UserGroup}" #~ msgstr "{ApplicationPermission} 追加 {UserGroup}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {UserGroup}" #~ msgstr "{ApplicationPermission} 削除 {UserGroup}" #~ msgid "Application permission" #~ msgstr "申請許可" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {Application}" #~ msgstr "{ApplicationPermission} 追加 {Application}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {Application}" #~ msgstr "{ApplicationPermission} 削除 {Application}" #~ msgid "Application permission and SystemUser" #~ msgstr "アプリケーション権限とSystemUser" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {SystemUser}" #~ msgstr "{ApplicationPermission} 追加 {SystemUser}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {SystemUser}" #~ msgstr "{ApplicationPermission} 削除 {SystemUser}" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index bb0411aa6..4451b285c 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:314c29cb8b10aaddbb030bf49af293be23f0153ff1f1c7562946879574ce6de8 -size 102801 +oid sha256:eeaa813f4ea052a1cd85b8ae5addfde6b088fd21a0261f8724d62823835512a2 +size 104043 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 6ab6fa2be..686c70a31 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-11-03 16:00+0800\n" +"POT-Creation-Date: 2022-11-16 20:11+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -21,20 +21,21 @@ msgstr "" msgid "Acls" msgstr "访问控制" -#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:48 +#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:58 #: applications/models.py:10 assets/models/_user.py:33 #: assets/models/asset/common.py:81 assets/models/asset/common.py:91 -#: assets/models/base.py:57 assets/models/cmd_filter.py:25 +#: assets/models/base.py:50 assets/models/cmd_filter.py:25 #: assets/models/domain.py:24 assets/models/group.py:20 -#: assets/models/label.py:17 assets/models/platform.py:22 -#: assets/models/platform.py:68 assets/serializers/asset/common.py:86 -#: assets/serializers/platform.py:104 ops/mixin.py:20 ops/models/playbook.py:9 -#: orgs/models.py:70 perms/models/asset_permission.py:56 rbac/models/role.py:29 +#: assets/models/label.py:17 assets/models/platform.py:21 +#: assets/models/platform.py:72 assets/serializers/asset/common.py:86 +#: assets/serializers/platform.py:138 ops/mixin.py:20 ops/models/adhoc.py:24 +#: ops/models/celery.py:15 ops/models/job.py:34 ops/models/playbook.py:13 +#: orgs/models.py:70 perms/models/asset_permission.py:51 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/applet/applet.py:20 terminal/models/component/endpoint.py:11 #: terminal/models/component/endpoint.py:87 #: terminal/models/component/storage.py:25 terminal/models/component/task.py:16 -#: terminal/models/component/terminal.py:100 users/forms/profile.py:33 +#: terminal/models/component/terminal.py:82 users/forms/profile.py:33 #: users/models/group.py:15 users/models/user.py:665 #: xpack/plugins/cloud/models.py:30 msgid "Name" @@ -52,26 +53,25 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" #: acls/models/base.py:31 authentication/models/access_key.py:15 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:74 terminal/models/session/sharing.py:28 +#: perms/models/asset_permission.py:67 terminal/models/session/sharing.py:28 #: tickets/const.py:38 msgid "Active" msgstr "激活中" #: acls/models/base.py:32 applications/models.py:19 assets/models/_user.py:40 -#: assets/models/asset/common.py:100 assets/models/automations/base.py:26 -#: assets/models/backup.py:30 assets/models/base.py:65 +#: assets/models/asset/common.py:100 assets/models/automations/base.py:22 +#: assets/models/backup.py:29 assets/models/base.py:58 #: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88 #: assets/models/domain.py:25 assets/models/domain.py:69 #: assets/models/group.py:23 assets/models/label.py:22 -#: assets/models/platform.py:73 ops/models/playbook.py:11 -#: ops/models/playbook.py:25 orgs/models.py:74 -#: perms/models/asset_permission.py:84 rbac/models/role.py:37 +#: assets/models/platform.py:77 orgs/models.py:74 +#: perms/models/asset_permission.py:77 rbac/models/role.py:37 #: settings/models.py:38 terminal/models/applet/applet.py:28 -#: terminal/models/applet/applet.py:61 terminal/models/applet/host.py:104 +#: terminal/models/applet/applet.py:61 terminal/models/applet/host.py:107 #: terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:97 #: terminal/models/component/storage.py:28 -#: terminal/models/component/terminal.py:114 tickets/models/comment.py:32 +#: terminal/models/component/terminal.py:93 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:288 users/models/group.py:16 #: users/models/user.py:702 xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/cloud/models.py:37 xpack/plugins/cloud/models.py:118 @@ -94,10 +94,11 @@ msgid "Login confirm" msgstr "登录复核" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:28 assets/models/label.py:15 audits/models.py:37 -#: audits/models.py:62 audits/models.py:87 -#: authentication/models/connection_token.py:22 -#: authentication/models/sso_token.py:15 perms/models/asset_permission.py:58 +#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:28 +#: assets/models/label.py:15 audits/models.py:29 audits/models.py:48 +#: audits/models.py:79 authentication/models/connection_token.py:22 +#: authentication/models/sso_token.py:15 perms/api/user_permission/mixin.py:80 +#: perms/models/asset_permission.py:53 perms/models/perm_token.py:12 #: rbac/builtin.py:120 rbac/models/rolebinding.py:41 #: terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 @@ -113,14 +114,14 @@ 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:62 -#: assets/models/cmd_filter.py:81 audits/models.py:63 audits/serializers.py:49 +#: acls/serializers/login_acl.py:26 acls/serializers/login_asset_acl.py:77 +#: assets/models/cmd_filter.py:81 audits/models.py:50 audits/serializers.py:69 #: 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:86 +#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 msgid "Reviewers" msgstr "审批人" @@ -128,19 +129,25 @@ msgstr "审批人" msgid "Login acl" msgstr "登录访问控制" -#: acls/models/login_asset_acl.py:21 assets/models/account.py:59 +#: acls/models/login_asset_acl.py:21 assets/models/account.py:61 +#: assets/serializers/automations/change_secret.py:88 +#: assets/serializers/automations/change_secret.py:110 #: authentication/models/connection_token.py:33 ops/models/base.py:18 -#: terminal/models/session/session.py:34 xpack/plugins/cloud/models.py:87 -#: xpack/plugins/cloud/serializers/task.py:65 +#: perms/models/perm_token.py:14 terminal/models/session/session.py:34 +#: xpack/plugins/cloud/models.py:87 xpack/plugins/cloud/serializers/task.py:65 msgid "Account" msgstr "账号" -#: acls/models/login_asset_acl.py:22 assets/models/account.py:49 +#: acls/models/login_asset_acl.py:22 assets/models/account.py:51 #: assets/models/asset/common.py:83 assets/models/asset/common.py:227 #: assets/models/cmd_filter.py:36 assets/models/gathered_user.py:14 -#: assets/serializers/account/account.py:58 assets/serializers/label.py:30 -#: audits/models.py:39 authentication/models/connection_token.py:26 -#: perms/models/asset_permission.py:64 terminal/backends/command/models.py:21 +#: assets/serializers/account/account.py:59 +#: assets/serializers/automations/change_secret.py:87 +#: assets/serializers/automations/change_secret.py:109 +#: assets/serializers/gathered_user.py:11 assets/serializers/label.py:30 +#: audits/models.py:33 authentication/models/connection_token.py:26 +#: perms/models/asset_permission.py:59 perms/models/perm_token.py:13 +#: terminal/backends/command/models.py:21 #: terminal/backends/command/serializers.py:14 #: terminal/models/session/session.py:32 terminal/notifications.py:90 #: xpack/plugins/change_auth_plan/models/asset.py:200 @@ -157,14 +164,14 @@ msgstr "登录资产访问控制" msgid "Login asset confirm" msgstr "登录资产复核" -#: acls/serializers/login_acl.py:11 acls/serializers/login_asset_acl.py:13 +#: acls/serializers/login_acl.py:16 acls/serializers/login_asset_acl.py:14 msgid "Format for comma-delimited string, with * indicating a match all. " msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " -#: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:18 -#: acls/serializers/login_asset_acl.py:52 assets/models/_user.py:34 -#: assets/models/base.py:58 assets/models/gathered_user.py:15 -#: audits/models.py:121 authentication/forms.py:25 authentication/forms.py:27 +#: acls/serializers/login_asset_acl.py:22 +#: acls/serializers/login_asset_acl.py:64 assets/models/_user.py:34 +#: assets/models/base.py:51 assets/models/gathered_user.py:15 +#: audits/models.py:95 authentication/forms.py:25 authentication/forms.py:27 #: authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 @@ -176,7 +183,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " msgid "Username" msgstr "用户名" -#: acls/serializers/login_asset_acl.py:25 +#: acls/serializers/login_asset_acl.py:29 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:" @@ -185,7 +192,7 @@ 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:32 acls/serializers/rules/rules.py:33 +#: acls/serializers/login_asset_acl.py:38 acls/serializers/rules/rules.py:33 #: assets/models/asset/common.py:92 assets/models/domain.py:65 #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 @@ -194,23 +201,23 @@ msgstr "" msgid "IP" msgstr "IP" -#: acls/serializers/login_asset_acl.py:36 -#: assets/serializers/gathered_user.py:22 settings/serializers/terminal.py:7 +#: acls/serializers/login_asset_acl.py:44 +#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:7 msgid "Hostname" msgstr "主机名" -#: acls/serializers/login_asset_acl.py:43 +#: acls/serializers/login_asset_acl.py:51 msgid "" "Format for comma-delimited string, with * indicating a match all. Protocol " "options: {}" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" -#: acls/serializers/login_asset_acl.py:84 -#: tickets/serializers/ticket/ticket.py:86 +#: acls/serializers/login_asset_acl.py:108 +#: tickets/serializers/ticket/ticket.py:67 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/login_asset_acl.py:89 +#: acls/serializers/login_asset_acl.py:114 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" @@ -237,23 +244,26 @@ msgid "Applications" msgstr "应用管理" #: applications/models.py:12 assets/models/label.py:20 -#: assets/models/platform.py:69 assets/serializers/asset/common.py:62 -#: assets/serializers/cagegory.py:8 assets/serializers/platform.py:76 -#: assets/serializers/platform.py:105 +#: assets/models/platform.py:73 assets/serializers/asset/common.py:62 +#: assets/serializers/cagegory.py:8 assets/serializers/platform.py:99 +#: assets/serializers/platform.py:139 perms/serializers/user_permission.py:23 #: tickets/models/ticket/apply_application.py:14 #: xpack/plugins/change_auth_plan/models/app.py:24 msgid "Category" msgstr "类别" #: applications/models.py:15 assets/models/_user.py:46 -#: assets/models/automations/base.py:24 assets/models/cmd_filter.py:74 -#: assets/models/platform.py:70 assets/serializers/asset/common.py:63 -#: assets/serializers/platform.py:75 terminal/models/applet/applet.py:24 +#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 +#: assets/models/platform.py:74 assets/serializers/asset/common.py:63 +#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:98 +#: audits/serializers.py:40 ops/models/job.py:42 +#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:24 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:142 terminal/serializers/applet.py:33 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:17 -#: tickets/models/ticket/general.py:273 +#: tickets/models/ticket/general.py:273 tickets/serializers/flow.py:53 +#: tickets/serializers/ticket/ticket.py:18 #: xpack/plugins/change_auth_plan/models/app.py:27 #: xpack/plugins/change_auth_plan/models/app.py:152 msgid "Type" @@ -272,6 +282,11 @@ msgstr "应用程序" msgid "Can match application" msgstr "匹配应用" +#: assets/api/automations/base.py:76 +#: xpack/plugins/change_auth_plan/api/asset.py:94 +msgid "The parameter 'action' must be [{}]" +msgstr "参数 'action' 必须是 [{}]" + #: assets/api/domain.py:52 msgid "Number required" msgstr "需要为数字" @@ -292,11 +307,11 @@ msgstr "删除失败,节点包含资产" msgid "App assets" msgstr "资产管理" -#: assets/automations/base/manager.py:122 +#: assets/automations/base/manager.py:123 msgid "{} disabled" msgstr "{} 已禁用" -#: assets/const/account.py:6 audits/const.py:5 +#: assets/const/account.py:6 audits/const.py:6 audits/const.py:63 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" @@ -306,19 +321,21 @@ msgstr "未知" msgid "Ok" msgstr "成功" -#: assets/const/account.py:8 audits/models.py:118 common/const/choices.py:19 +#: assets/const/account.py:8 +#: assets/serializers/automations/change_secret.py:105 +#: assets/serializers/automations/change_secret.py:133 audits/const.py:74 +#: common/const/choices.py:19 #: xpack/plugins/change_auth_plan/serializers/asset.py:190 #: xpack/plugins/cloud/const.py:33 msgid "Failed" msgstr "失败" #: assets/const/account.py:12 assets/models/_user.py:35 -#: assets/models/base.py:52 assets/models/domain.py:71 -#: assets/serializers/base.py:15 audits/signal_handlers.py:50 +#: assets/models/domain.py:71 audits/signal_handlers.py:46 #: 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:46 -#: users/forms/profile.py:22 users/serializers/user.py:94 +#: users/forms/profile.py:22 users/serializers/user.py:105 #: 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 @@ -330,17 +347,16 @@ msgstr "失败" msgid "Password" msgstr "密码" -#: assets/const/account.py:13 assets/models/base.py:53 +#: assets/const/account.py:13 msgid "SSH key" msgstr "SSH 密钥" -#: assets/const/account.py:14 assets/models/base.py:54 -#: authentication/models/access_key.py:31 +#: assets/const/account.py:14 authentication/models/access_key.py:31 msgid "Access key" msgstr "Access key" #: assets/const/account.py:15 assets/models/_user.py:38 -#: assets/models/base.py:55 authentication/models/sso_token.py:13 +#: authentication/models/sso_token.py:13 msgid "Token" msgstr "Token" @@ -368,31 +384,31 @@ msgstr "验证密钥" msgid "Gather accounts" msgstr "收集账号" -#: assets/const/automation.py:22 +#: assets/const/automation.py:38 assets/serializers/account/base.py:26 msgid "Specific" msgstr "指定" -#: assets/const/automation.py:23 ops/const.py:20 +#: assets/const/automation.py:39 ops/const.py:20 #: xpack/plugins/change_auth_plan/models/base.py:28 msgid "All assets use the same random password" msgstr "使用相同的随机密码" -#: assets/const/automation.py:24 ops/const.py:21 +#: assets/const/automation.py:40 ops/const.py:21 #: xpack/plugins/change_auth_plan/models/base.py:29 msgid "All assets use different random password" msgstr "使用不同的随机密码" -#: assets/const/automation.py:28 ops/const.py:13 +#: assets/const/automation.py:44 ops/const.py:13 #: xpack/plugins/change_auth_plan/models/asset.py:30 msgid "Append SSH KEY" msgstr "追加" -#: assets/const/automation.py:29 ops/const.py:14 +#: assets/const/automation.py:45 ops/const.py:14 #: xpack/plugins/change_auth_plan/models/asset.py:31 msgid "Empty and append SSH KEY" msgstr "清空所有并添加" -#: assets/const/automation.py:30 ops/const.py:15 +#: assets/const/automation.py:46 ops/const.py:15 #: xpack/plugins/change_auth_plan/models/asset.py:32 msgid "Replace (The key generated by JumpServer) " msgstr "替换 (由 JumpServer 生成的密钥)" @@ -418,7 +434,8 @@ msgstr "数据库" msgid "Cloud service" msgstr "云服务" -#: assets/const/category.py:15 terminal/models/applet/applet.py:18 +#: assets/const/category.py:15 audits/const.py:61 +#: terminal/models/applet/applet.py:18 msgid "Web" msgstr "Web" @@ -460,7 +477,6 @@ msgid "Admin user" msgstr "特权用户" #: assets/models/_user.py:36 assets/models/domain.py:72 -#: assets/serializers/base.py:19 #: xpack/plugins/change_auth_plan/models/asset.py:54 #: xpack/plugins/change_auth_plan/models/asset.py:131 #: xpack/plugins/change_auth_plan/models/asset.py:207 @@ -474,11 +490,12 @@ msgstr "SSH 密钥" msgid "SSH public key" msgstr "SSH 公钥" -#: assets/models/_user.py:41 assets/models/automations/base.py:96 +#: assets/models/_user.py:41 assets/models/automations/base.py:92 #: assets/models/domain.py:26 assets/models/gathered_user.py:19 #: assets/models/group.py:22 common/db/models.py:76 common/mixins/models.py:50 -#: ops/models/base.py:53 orgs/models.py:73 perms/models/asset_permission.py:82 -#: users/models/group.py:18 users/models/user.py:927 +#: ops/models/base.py:54 ops/models/job.py:69 orgs/models.py:73 +#: perms/models/asset_permission.py:75 users/models/group.py:18 +#: users/models/user.py:927 msgid "Date created" msgstr "创建日期" @@ -487,10 +504,10 @@ msgstr "创建日期" msgid "Date updated" msgstr "更新日期" -#: assets/models/_user.py:43 assets/models/base.py:66 +#: assets/models/_user.py:43 assets/models/base.py:59 #: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:91 #: assets/models/group.py:21 common/db/models.py:74 common/mixins/models.py:49 -#: orgs/models.py:71 perms/models/asset_permission.py:81 +#: orgs/models.py:71 perms/models/asset_permission.py:74 #: users/models/user.py:710 users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:48 msgid "Created by" @@ -501,7 +518,7 @@ msgid "Username same with user" msgstr "用户名与用户相同" #: assets/models/_user.py:48 assets/models/domain.py:67 -#: authentication/models/connection_token.py:29 +#: authentication/models/connection_token.py:29 perms/models/perm_token.py:16 #: terminal/models/applet/applet.py:26 terminal/serializers/session.py:18 #: terminal/serializers/session.py:32 terminal/serializers/storage.py:68 msgid "Protocol" @@ -515,7 +532,7 @@ msgstr "自动推送" msgid "Sudo" msgstr "Sudo" -#: assets/models/_user.py:51 +#: assets/models/_user.py:51 ops/models/adhoc.py:20 ops/models/job.py:30 msgid "Shell" msgstr "Shell" @@ -543,7 +560,7 @@ msgstr "用户切换" msgid "Switch from" msgstr "切换自" -#: assets/models/_user.py:65 audits/models.py:40 +#: assets/models/_user.py:65 audits/models.py:34 #: terminal/backends/command/models.py:22 #: terminal/backends/command/serializers.py:36 #: xpack/plugins/change_auth_plan/models/app.py:35 @@ -555,43 +572,60 @@ msgstr "系统用户" msgid "Can match system user" msgstr "可以匹配系统用户" -#: assets/models/account.py:53 +#: assets/models/account.py:45 common/db/fields.py:222 +#: settings/serializers/terminal.py:12 +msgid "All" +msgstr "全部" + +#: assets/models/account.py:46 +#, fuzzy +#| msgid "Manually input" +msgid "Manual input" +msgstr "手动输入" + +#: assets/models/account.py:47 +#, fuzzy +#| msgid "Dynamic code" +msgid "Dynamic user" +msgstr "动态码" + +#: assets/models/account.py:55 msgid "Su from" msgstr "切换自" -#: assets/models/account.py:55 settings/serializers/auth/cas.py:18 +#: assets/models/account.py:57 settings/serializers/auth/cas.py:18 #: terminal/models/applet/applet.py:22 msgid "Version" msgstr "版本" -#: assets/models/account.py:65 +#: assets/models/account.py:67 msgid "Can view asset account secret" msgstr "可以查看资产账号密码" -#: assets/models/account.py:66 +#: assets/models/account.py:68 msgid "Can change asset account secret" msgstr "可以更改资产账号密码" -#: assets/models/account.py:67 +#: assets/models/account.py:69 msgid "Can view asset history account" msgstr "可以查看资产历史账号" -#: assets/models/account.py:68 +#: assets/models/account.py:70 msgid "Can view asset history account secret" msgstr "可以查看资产历史账号密码" -#: assets/models/account.py:91 assets/serializers/account/account.py:13 +#: assets/models/account.py:93 assets/serializers/account/account.py:15 msgid "Account template" msgstr "账号模版" #: assets/models/asset/common.py:82 assets/models/domain.py:66 -#: assets/models/platform.py:23 settings/serializers/auth/radius.py:15 +#: assets/models/platform.py:22 settings/serializers/auth/radius.py:15 #: settings/serializers/auth/sms.py:57 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" -#: assets/models/asset/common.py:93 assets/models/platform.py:104 +#: assets/models/asset/common.py:93 assets/models/platform.py:110 #: assets/serializers/asset/common.py:65 #: perms/serializers/user_permission.py:21 #: xpack/plugins/cloud/serializers/account_attrs.py:172 @@ -603,17 +637,19 @@ msgstr "资产平台" msgid "Domain" msgstr "网域" -#: assets/models/asset/common.py:97 assets/models/automations/base.py:19 -#: assets/serializers/asset/common.py:66 perms/models/asset_permission.py:67 +#: assets/models/asset/common.py:97 assets/models/automations/base.py:18 +#: assets/serializers/asset/common.py:66 +#: assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:62 #: xpack/plugins/change_auth_plan/models/asset.py:44 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" msgstr "节点" -#: assets/models/asset/common.py:98 assets/models/automations/base.py:25 -#: assets/models/base.py:64 assets/models/cmd_filter.py:39 +#: assets/models/asset/common.py:98 assets/models/automations/base.py:21 +#: assets/models/base.py:57 assets/models/cmd_filter.py:39 #: assets/models/domain.py:70 assets/models/label.py:21 -#: terminal/models/applet/applet.py:25 users/serializers/user.py:147 +#: terminal/models/applet/applet.py:25 users/serializers/user.py:202 msgid "Is active" msgstr "激活" @@ -647,8 +683,8 @@ msgstr "添加资产到节点" msgid "Move asset to node" msgstr "移动资产到节点" -#: assets/models/asset/web.py:9 audits/models.py:111 -#: terminal/serializers/applet_host.py:24 +#: assets/models/asset/web.py:9 audits/const.py:67 +#: terminal/serializers/applet_host.py:25 msgid "Disabled" msgstr "禁用" @@ -664,83 +700,98 @@ msgstr "" msgid "Autofill" msgstr "自动填充" -#: assets/models/asset/web.py:14 assets/serializers/platform.py:29 +#: assets/models/asset/web.py:14 assets/serializers/platform.py:30 msgid "Username selector" msgstr "用户名选择器" -#: assets/models/asset/web.py:15 assets/serializers/platform.py:30 +#: assets/models/asset/web.py:15 assets/serializers/platform.py:33 msgid "Password selector" msgstr "密码选择器" -#: assets/models/asset/web.py:16 assets/serializers/platform.py:31 +#: assets/models/asset/web.py:16 assets/serializers/platform.py:36 msgid "Submit selector" msgstr "提交按钮选择器" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:68 perms/models/asset_permission.py:70 -#: rbac/tree.py:37 +#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:65 +#: perms/serializers/permission.py:32 rbac/tree.py:37 msgid "Accounts" msgstr "账号管理" -#: assets/models/automations/base.py:22 assets/serializers/domain.py:29 -#: ops/models/base.py:17 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 assets/serializers/domain.py:29 +#: ops/models/base.py:17 ops/models/job.py:44 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 #: xpack/plugins/change_auth_plan/models/asset.py:40 msgid "Assets" msgstr "资产" -#: assets/models/automations/base.py:86 assets/models/automations/base.py:93 +#: assets/models/automations/base.py:82 assets/models/automations/base.py:89 msgid "Automation task" msgstr "自动化任务" -#: assets/models/automations/base.py:97 assets/models/backup.py:77 -#: audits/models.py:44 ops/models/base.py:54 -#: perms/models/asset_permission.py:76 terminal/models/applet/host.py:102 -#: terminal/models/session/session.py:43 +#: assets/models/automations/base.py:91 audits/models.py:115 +#: audits/serializers.py:41 ops/models/base.py:49 ops/models/job.py:64 +#: terminal/models/applet/applet.py:60 terminal/models/applet/host.py:104 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 +#: tickets/models/ticket/general.py:281 tickets/serializers/ticket/ticket.py:19 +#: xpack/plugins/cloud/models.py:171 xpack/plugins/cloud/models.py:223 +msgid "Status" +msgstr "状态" + +#: assets/models/automations/base.py:93 assets/models/backup.py:76 +#: audits/models.py:40 ops/models/base.py:55 ops/models/celery.py:59 +#: ops/models/job.py:70 perms/models/asset_permission.py:69 +#: terminal/models/applet/host.py:105 terminal/models/session/session.py:43 #: tickets/models/ticket/apply_application.py:28 -#: tickets/models/ticket/apply_asset.py:21 +#: tickets/models/ticket/apply_asset.py:19 #: xpack/plugins/change_auth_plan/models/base.py:108 #: xpack/plugins/change_auth_plan/models/base.py:199 #: xpack/plugins/gathered_user/models.py:71 msgid "Date start" msgstr "开始日期" -#: assets/models/automations/base.py:98 -#: assets/models/automations/change_secret.py:58 ops/models/base.py:55 -#: terminal/models/applet/host.py:103 +#: assets/models/automations/base.py:94 +#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 +#: ops/models/celery.py:60 ops/models/job.py:71 +#: terminal/models/applet/host.py:106 msgid "Date finished" msgstr "结束日期" -#: assets/models/automations/base.py:100 +#: assets/models/automations/base.py:96 +#: assets/serializers/automations/base.py:39 msgid "Automation snapshot" msgstr "自动化快照" -#: assets/models/automations/base.py:104 assets/models/backup.py:88 -#: assets/serializers/account/backup.py:36 +#: assets/models/automations/base.py:100 assets/models/backup.py:87 +#: assets/serializers/account/backup.py:37 +#: assets/serializers/automations/base.py:41 #: xpack/plugins/change_auth_plan/models/base.py:121 #: xpack/plugins/change_auth_plan/serializers/base.py:78 msgid "Trigger mode" msgstr "触发模式" -#: assets/models/automations/base.py:108 +#: assets/models/automations/base.py:104 +#: assets/serializers/automations/change_secret.py:90 msgid "Automation task execution" msgstr "自动化任务执行" -#: assets/models/automations/change_secret.py:15 assets/models/base.py:60 +#: assets/models/automations/change_secret.py:15 assets/models/base.py:53 +#: assets/serializers/account/account.py:95 assets/serializers/base.py:13 msgid "Secret type" msgstr "密文类型" #: assets/models/automations/change_secret.py:19 +#: assets/serializers/automations/change_secret.py:25 msgid "Secret strategy" msgstr "密钥策略" #: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:56 assets/models/base.py:62 -#: assets/serializers/account/base.py:17 -#: authentication/models/connection_token.py:34 +#: assets/models/automations/change_secret.py:57 assets/models/base.py:55 +#: assets/serializers/base.py:16 authentication/models/connection_token.py:34 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:17 +#: perms/models/perm_token.py:15 settings/serializers/auth/radius.py:17 msgid "Secret" msgstr "密钥" @@ -753,8 +804,9 @@ msgstr "密码规则" msgid "SSH key change strategy" msgstr "SSH 密钥策略" -#: assets/models/automations/change_secret.py:27 assets/models/backup.py:28 -#: assets/serializers/account/backup.py:28 +#: assets/models/automations/change_secret.py:27 assets/models/backup.py:27 +#: assets/serializers/account/backup.py:30 +#: assets/serializers/automations/change_secret.py:40 #: xpack/plugins/change_auth_plan/models/app.py:40 #: xpack/plugins/change_auth_plan/models/asset.py:63 #: xpack/plugins/change_auth_plan/serializers/base.py:45 @@ -765,19 +817,19 @@ msgstr "收件人" msgid "Change secret automation" msgstr "自动化改密" -#: assets/models/automations/change_secret.py:55 +#: assets/models/automations/change_secret.py:56 msgid "Old secret" msgstr "原来密码" -#: assets/models/automations/change_secret.py:57 +#: assets/models/automations/change_secret.py:58 msgid "Date started" msgstr "开始日期" -#: assets/models/automations/change_secret.py:60 common/const/choices.py:20 +#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 msgid "Error" msgstr "错误" -#: assets/models/automations/change_secret.py:63 +#: assets/models/automations/change_secret.py:64 msgid "Change secret record" msgstr "改密记录" @@ -786,10 +838,9 @@ msgid "Discovery account automation" msgstr "自动化账号发现" #: assets/models/automations/gather_accounts.py:15 -#, fuzzy -#| msgid "Gather asset facts" +#: assets/tasks/gather_accounts.py:28 msgid "Gather asset accounts" -msgstr "收集资产信息" +msgstr "收集资产账号" #: assets/models/automations/gather_facts.py:15 msgid "Gather asset facts" @@ -813,24 +864,24 @@ msgstr "服务账号" msgid "Verify asset account" msgstr "验证密钥" -#: assets/models/backup.py:38 assets/models/backup.py:96 +#: assets/models/backup.py:37 assets/models/backup.py:95 msgid "Account backup plan" msgstr "账号备份计划" -#: assets/models/backup.py:80 +#: assets/models/backup.py:79 #: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:187 +#: notifications/notifications.py:186 #: xpack/plugins/change_auth_plan/models/base.py:111 #: xpack/plugins/change_auth_plan/models/base.py:200 #: xpack/plugins/gathered_user/models.py:74 msgid "Time" msgstr "时间" -#: assets/models/backup.py:84 +#: assets/models/backup.py:83 msgid "Account backup snapshot" msgstr "账号备份快照" -#: assets/models/backup.py:91 audits/models.py:127 +#: assets/models/backup.py:90 audits/models.py:110 #: terminal/models/session/sharing.py:108 #: xpack/plugins/change_auth_plan/models/base.py:197 #: xpack/plugins/change_auth_plan/serializers/asset.py:171 @@ -838,29 +889,32 @@ msgstr "账号备份快照" msgid "Reason" msgstr "原因" -#: assets/models/backup.py:93 terminal/serializers/session.py:36 +#: assets/models/backup.py:92 +#: assets/serializers/automations/change_secret.py:86 +#: assets/serializers/automations/change_secret.py:111 +#: terminal/serializers/session.py:36 #: xpack/plugins/change_auth_plan/models/base.py:198 #: xpack/plugins/change_auth_plan/serializers/asset.py:173 msgid "Is success" msgstr "是否成功" -#: assets/models/backup.py:100 +#: assets/models/backup.py:99 msgid "Account backup execution" msgstr "账号备份执行" -#: assets/models/base.py:29 assets/serializers/domain.py:42 +#: assets/models/base.py:28 assets/serializers/domain.py:42 msgid "Connectivity" msgstr "可连接性" -#: assets/models/base.py:31 authentication/models/temp_token.py:12 +#: assets/models/base.py:30 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "校验日期" -#: assets/models/base.py:63 +#: assets/models/base.py:56 msgid "Privileged" msgstr "特权账号" -#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61 +#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:56 #: users/models/group.py:31 users/models/user.py:671 msgid "User group" msgstr "用户组" @@ -930,7 +984,7 @@ msgstr "测试网关" msgid "Unable to connect to port {port} on {address}" msgstr "无法连接到 {address} 上的端口 {port}" -#: assets/models/domain.py:145 authentication/middleware.py:75 +#: assets/models/domain.py:145 authentication/middleware.py:76 #: xpack/plugins/cloud/providers/fc.py:48 msgid "Authentication failed" msgstr "认证失败" @@ -959,7 +1013,7 @@ msgstr "收集用户" msgid "Asset group" msgstr "资产组" -#: assets/models/group.py:34 assets/models/platform.py:20 +#: assets/models/group.py:34 assets/models/platform.py:19 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" msgstr "默认" @@ -992,7 +1046,7 @@ msgstr "新节点" msgid "empty" msgstr "空" -#: assets/models/node.py:552 perms/models/asset_permission.py:190 +#: assets/models/node.py:552 perms/models/perm_node.py:21 msgid "Key" msgstr "键" @@ -1000,7 +1054,7 @@ msgstr "键" msgid "Full value" msgstr "全称" -#: assets/models/node.py:557 perms/models/asset_permission.py:191 +#: assets/models/node.py:557 perms/models/perm_node.py:22 msgid "Parent key" msgstr "ssh私钥" @@ -1013,48 +1067,48 @@ msgstr "节点" msgid "Can match node" msgstr "可以匹配节点" -#: assets/models/platform.py:21 +#: assets/models/platform.py:20 msgid "Required" msgstr "必需的" -#: assets/models/platform.py:24 users/templates/users/reset_password.html:29 +#: assets/models/platform.py:23 users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" -#: assets/models/platform.py:43 audits/models.py:112 settings/models.py:37 -#: terminal/serializers/applet_host.py:25 +#: assets/models/platform.py:42 audits/const.py:68 settings/models.py:37 +#: terminal/serializers/applet_host.py:26 msgid "Enabled" msgstr "启用" -#: assets/models/platform.py:44 +#: assets/models/platform.py:43 msgid "Ansible config" msgstr "Ansible 配置" -#: assets/models/platform.py:45 +#: assets/models/platform.py:44 msgid "Ping enabled" msgstr "探测资产" -#: assets/models/platform.py:46 +#: assets/models/platform.py:45 msgid "Ping method" msgstr "探测方式" -#: assets/models/platform.py:47 assets/models/platform.py:55 +#: assets/models/platform.py:46 assets/models/platform.py:56 msgid "Gather facts enabled" msgstr "收集资产信息" -#: assets/models/platform.py:48 assets/models/platform.py:56 +#: assets/models/platform.py:47 assets/models/platform.py:58 msgid "Gather facts method" msgstr "收集资产信息方式" -#: assets/models/platform.py:49 +#: assets/models/platform.py:48 msgid "Push account enabled" msgstr "推送账号" -#: assets/models/platform.py:50 +#: assets/models/platform.py:49 msgid "Push account method" msgstr "推送方式" -#: assets/models/platform.py:51 +#: assets/models/platform.py:50 msgid "Change password enabled" msgstr "更改密码" @@ -1066,39 +1120,39 @@ msgstr "改密方式" msgid "Verify account enabled" msgstr "校验账号" -#: assets/models/platform.py:54 +#: assets/models/platform.py:55 msgid "Verify account method" msgstr "验证z" -#: assets/models/platform.py:71 tickets/models/ticket/general.py:298 +#: assets/models/platform.py:75 tickets/models/ticket/general.py:298 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:72 +#: assets/models/platform.py:76 msgid "Internal" msgstr "内部的" -#: assets/models/platform.py:75 +#: assets/models/platform.py:80 assets/serializers/platform.py:96 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:76 +#: assets/models/platform.py:82 msgid "Domain enabled" msgstr "支持网域" -#: assets/models/platform.py:77 +#: assets/models/platform.py:83 msgid "Protocols enabled" msgstr "协议支持" -#: assets/models/platform.py:79 +#: assets/models/platform.py:85 msgid "Su enabled" msgstr "账号切换" -#: assets/models/platform.py:80 +#: assets/models/platform.py:86 msgid "SU method" msgstr "切换方式" -#: assets/models/platform.py:82 assets/serializers/platform.py:78 +#: assets/models/platform.py:88 assets/serializers/platform.py:103 msgid "Automation" msgstr "自动化" @@ -1117,7 +1171,7 @@ msgid "" "for details" msgstr "{} - 账号备份任务已完成, 详情见附件" -#: assets/notifications.py:19 +#: assets/notifications.py:20 msgid "" "{} - The account backup passage task has been completed: the encryption " "password has not been set - please go to personal information -> file " @@ -1126,45 +1180,54 @@ msgstr "" "{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" "置加密密码" -#: assets/serializers/account/account.py:16 +#: assets/notifications.py:31 xpack/plugins/change_auth_plan/notifications.py:8 +msgid "Notification of implementation result of encryption change plan" +msgstr "改密计划任务结果通知" + +#: assets/notifications.py:41 +#: xpack/plugins/change_auth_plan/notifications.py:18 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} - 改密任务已完成, 详情见附件" + +#: assets/notifications.py:42 +#: 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 "" +"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" +"密密码" + +#: assets/serializers/account/account.py:18 msgid "Push now" msgstr "立刻推送" -#: assets/serializers/account/account.py:18 +#: assets/serializers/account/account.py:20 msgid "Has secret" msgstr "存在密码" -#: assets/serializers/account/account.py:25 +#: assets/serializers/account/account.py:27 msgid "Account template not found" msgstr "账号模版没有发现" -#: assets/serializers/account/backup.py:27 ops/mixin.py:102 +#: assets/serializers/account/backup.py:29 +#: assets/serializers/automations/base.py:34 ops/mixin.py:102 #: settings/serializers/auth/ldap.py:65 #: xpack/plugins/change_auth_plan/serializers/base.py:43 msgid "Periodic perform" msgstr "定时执行" -#: assets/serializers/account/backup.py:29 +#: assets/serializers/account/backup.py:31 +#: assets/serializers/automations/change_secret.py:41 #: xpack/plugins/change_auth_plan/serializers/base.py:46 msgid "Currently only mail sending is supported" msgstr "当前只支持邮件发送" -#: assets/serializers/account/base.py:39 assets/serializers/base.py:34 -msgid "private key invalid or passphrase error" -msgstr "密钥不合法或密钥密码错误" - -#: assets/serializers/account/template.py:16 common/drf/fields.py:69 -#: tickets/serializers/ticket/common.py:58 -#: xpack/plugins/change_auth_plan/serializers/asset.py:64 -#: 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:101 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "该字段是必填项。" - -#: assets/serializers/asset/common.py:69 assets/serializers/platform.py:77 -#: xpack/plugins/cloud/models.py:109 +#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:101 +#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:109 msgid "Protocols" msgstr "协议组" @@ -1244,7 +1307,37 @@ msgstr "资产编号" msgid "IP/Host" msgstr "IP/主机名" -#: assets/serializers/base.py:24 +#: assets/serializers/automations/change_secret.py:28 +#: xpack/plugins/change_auth_plan/models/asset.py:50 +#: xpack/plugins/change_auth_plan/serializers/asset.py:33 +msgid "SSH Key strategy" +msgstr "SSH 密钥策略" + +#: assets/serializers/automations/change_secret.py:57 +#: xpack/plugins/change_auth_plan/serializers/base.py:58 +msgid "* Please enter the correct password length" +msgstr "* 请输入正确的密码长度" + +#: assets/serializers/automations/change_secret.py:60 +#: xpack/plugins/change_auth_plan/serializers/base.py:61 +msgid "* Password length range 6-30 bits" +msgstr "* 密码长度范围 6-30 位" + +#: assets/serializers/automations/change_secret.py:104 +#: assets/serializers/automations/change_secret.py:132 audits/const.py:73 +#: audits/models.py:39 common/const/choices.py:18 +#: terminal/models/session/sharing.py:104 tickets/views/approve.py:114 +#: xpack/plugins/change_auth_plan/serializers/asset.py:189 +msgid "Success" +msgstr "成功" + +#: assets/serializers/automations/gather_accounts.py:23 +#, fuzzy +#| msgid "Executed times" +msgid "Executed amount" +msgstr "执行次数" + +#: assets/serializers/base.py:21 msgid "Key password" msgstr "密钥密码" @@ -1257,7 +1350,6 @@ msgid "Types" msgstr "类型" #: assets/serializers/domain.py:14 assets/serializers/label.py:12 -#: perms/serializers/permission.py:83 msgid "Assets amount" msgstr "资产数量" @@ -1265,15 +1357,10 @@ msgstr "资产数量" msgid "Gateways count" msgstr "网关数量" -#: assets/serializers/label.py:13 assets/serializers/mixin.py:7 +#: assets/serializers/label.py:13 msgid "Category display" msgstr "类别名称" -#: assets/serializers/mixin.py:10 audits/serializers.py:27 -#: tickets/serializers/flow.py:49 tickets/serializers/ticket/ticket.py:17 -msgid "Type display" -msgstr "类型名称" - #: assets/serializers/node.py:17 msgid "value" msgstr "值" @@ -1298,56 +1385,92 @@ msgstr "SFTP 根路径" msgid "Auto fill" msgstr "自动填充" -#: assets/serializers/platform.py:64 +#: assets/serializers/platform.py:78 msgid "Primary" msgstr "主要的" -#: assets/serializers/utils.py:11 +#: assets/serializers/utils.py:15 msgid "Password can not contains `{{` " msgstr "密码不能包含 `{{` 字符" -#: assets/serializers/utils.py:14 +#: assets/serializers/utils.py:18 msgid "Password can not contains `'` " msgstr "密码不能包含 `'` 字符" -#: assets/serializers/utils.py:16 +#: assets/serializers/utils.py:20 msgid "Password can not contains `\"` " msgstr "密码不能包含 `\"` 字符" -#: assets/tasks/gather_facts.py:25 +#: assets/serializers/utils.py:26 +msgid "private key invalid or passphrase error" +msgstr "密钥不合法或密钥密码错误" + +#: assets/tasks/automation.py:11 +msgid "Execute automation" +msgstr "执行自动化" + +#: assets/tasks/backup.py:13 +msgid "Execute account backup plan" +msgstr "执行账号备份计划" + +#: assets/tasks/gather_accounts.py:31 +msgid "Gather assets accounts" +msgstr "收集资产账号" + +#: assets/tasks/gather_facts.py:26 msgid "Update some assets hardware info. " msgstr "更新资产硬件信息. " -#: assets/tasks/gather_facts.py:48 +#: assets/tasks/gather_facts.py:44 +msgid "Manually update the hardware information of assets" +msgstr "手动更新节点资产硬件信息: " + +#: assets/tasks/gather_facts.py:49 msgid "Update assets hardware info: " msgstr "更新资产硬件信息: " -#: assets/tasks/gather_facts.py:58 -msgid "Update node asset hardware information: " -msgstr "更新节点资产硬件信息: " +#: assets/tasks/gather_facts.py:53 +msgid "Manually update the hardware information of assets under a node" +msgstr "手动更新节点下的资产硬件信息" -#: assets/tasks/nodes_amount.py:29 +#: assets/tasks/gather_facts.py:59 +#, fuzzy +#| msgid "Update node asset hardware information" +msgid "Update node asset hardware information: " +msgstr "更新节点资产硬件信息" + +#: assets/tasks/nodes_amount.py:16 +msgid "Check the amount of assets under the node" +msgstr "校准节点下的资产数量" + +#: assets/tasks/nodes_amount.py:28 msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "自检程序已经在运行,不能重复启动" -#: assets/tasks/ping.py:20 assets/tasks/ping.py:38 -#, fuzzy -#| msgid "Test assets connectivity. " +#: assets/tasks/nodes_amount.py:34 +msgid "Periodic check the amount of assets under the node" +msgstr "定时校准节点下的资产数量" + +#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 msgid "Test assets connectivity " msgstr "测试资产可连接性. " -#: assets/tasks/ping.py:48 -#, fuzzy -#| msgid "Test if the assets under the node are connectable: " +#: assets/tasks/ping.py:33 +msgid "Manually test the connectivity of a asset" +msgstr "手动测试资产连接性" + +#: assets/tasks/ping.py:43 +msgid "Manually test the connectivity of assets under a node" +msgstr "手动测试节点下的资产可连接性" + +#: assets/tasks/ping.py:49 msgid "Test if the assets under the node are connectable " msgstr "测试节点下资产是否可连接: " -#: assets/tasks/push_account.py:36 -#, fuzzy -#| msgid "Push account method" +#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:31 msgid "Push accounts to assets" -msgstr "推送方式" +msgstr "推送账号到资产" #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" @@ -1365,9 +1488,11 @@ msgstr "为了安全,禁止推送用户 {}" msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks/verify_account.py:36 -#, fuzzy -#| msgid "Test account connectivity: " +#: assets/tasks/verify_account.py:30 +msgid "Verify asset account availability" +msgstr "验证资产账号的有效性" + +#: assets/tasks/verify_account.py:37 msgid "Verify accounts connectivity" msgstr "测试账号可连接性: " @@ -1375,278 +1500,257 @@ msgstr "测试账号可连接性: " msgid "Audits" msgstr "日志审计" -#: audits/models.py:27 audits/models.py:59 +#: audits/const.py:44 +msgid "Mkdir" +msgstr "创建目录" + +#: audits/const.py:45 +msgid "Rmdir" +msgstr "删除目录" + +#: audits/const.py:46 audits/const.py:56 #: authentication/templates/authentication/_access_key_modal.html:65 #: rbac/tree.py:226 msgid "Delete" msgstr "删除" -#: audits/models.py:28 +#: audits/const.py:47 perms/const.py:14 msgid "Upload" msgstr "上传文件" -#: audits/models.py:29 -msgid "Download" -msgstr "下载文件" - -#: audits/models.py:30 -msgid "Rmdir" -msgstr "删除目录" - -#: audits/models.py:31 +#: audits/const.py:48 msgid "Rename" msgstr "重命名" -#: audits/models.py:32 -msgid "Mkdir" -msgstr "创建目录" - -#: audits/models.py:33 +#: audits/const.py:49 msgid "Symlink" msgstr "建立软链接" -#: audits/models.py:38 audits/models.py:66 audits/models.py:89 -#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:96 -msgid "Remote addr" -msgstr "远端地址" +#: audits/const.py:50 perms/const.py:15 +msgid "Download" +msgstr "下载文件" -#: audits/models.py:41 -msgid "Operate" -msgstr "操作" +#: audits/const.py:54 rbac/tree.py:224 +msgid "View" +msgstr "查看" -#: audits/models.py:42 -msgid "Filename" -msgstr "文件名" +#: audits/const.py:55 rbac/tree.py:225 templates/_csv_import_export.html:18 +#: templates/_csv_update_modal.html:6 +msgid "Update" +msgstr "更新" -#: audits/models.py:43 audits/models.py:117 common/const/choices.py:18 -#: terminal/models/session/sharing.py:104 tickets/views/approve.py:114 -#: xpack/plugins/change_auth_plan/serializers/asset.py:189 -msgid "Success" -msgstr "成功" - -#: audits/models.py:47 -msgid "File transfer log" -msgstr "文件管理" - -#: audits/models.py:56 +#: audits/const.py:57 #: authentication/templates/authentication/_access_key_modal.html:22 #: rbac/tree.py:223 msgid "Create" msgstr "创建" -#: audits/models.py:57 rbac/tree.py:224 -msgid "View" -msgstr "查看" +#: audits/const.py:62 terminal/models/applet/host.py:24 +#: terminal/models/component/terminal.py:159 +msgid "Terminal" +msgstr "终端" -#: audits/models.py:58 rbac/tree.py:225 templates/_csv_import_export.html:18 -#: templates/_csv_update_modal.html:6 -msgid "Update" -msgstr "更新" +#: audits/const.py:69 +msgid "-" +msgstr "-" -#: audits/models.py:64 audits/serializers.py:61 +#: audits/models.py:31 audits/models.py:55 audits/models.py:82 +#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:96 +msgid "Remote addr" +msgstr "远端地址" + +#: audits/models.py:36 audits/serializers.py:19 +msgid "Operate" +msgstr "操作" + +#: audits/models.py:38 +msgid "Filename" +msgstr "文件名" + +#: audits/models.py:43 +msgid "File transfer log" +msgstr "文件管理" + +#: audits/models.py:52 audits/serializers.py:85 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:65 +#: audits/models.py:53 msgid "Resource" msgstr "资源" -#: audits/models.py:67 audits/models.py:90 +#: audits/models.py:58 audits/models.py:84 #: terminal/backends/command/serializers.py:40 msgid "Datetime" msgstr "日期" -#: audits/models.py:82 +#: audits/models.py:74 msgid "Operate log" msgstr "操作日志" -#: audits/models.py:88 +#: audits/models.py:80 msgid "Change by" msgstr "修改者" -#: audits/models.py:96 +#: audits/models.py:90 msgid "Password change log" msgstr "改密日志" -#: audits/models.py:113 -msgid "-" -msgstr "-" - -#: audits/models.py:122 +#: audits/models.py:97 msgid "Login type" msgstr "登录方式" -#: audits/models.py:123 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:99 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:124 +#: audits/models.py:101 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "登录城市" -#: audits/models.py:125 audits/serializers.py:42 +#: audits/models.py:104 audits/serializers.py:62 msgid "User agent" msgstr "用户代理" -#: audits/models.py:126 +#: audits/models.py:107 audits/serializers.py:39 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms/profile.py:65 users/models/user.py:688 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:128 ops/models/base.py:48 -#: terminal/models/applet/applet.py:60 terminal/models/applet/host.py:101 -#: terminal/models/component/status.py:33 terminal/serializers/applet.py:22 -#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:171 -#: xpack/plugins/cloud/models.py:223 -msgid "Status" -msgstr "状态" - -#: audits/models.py:129 +#: audits/models.py:117 msgid "Date login" msgstr "登录日期" -#: audits/models.py:130 audits/serializers.py:44 +#: audits/models.py:119 audits/serializers.py:64 msgid "Authentication backend" msgstr "认证方式" -#: audits/models.py:169 +#: audits/models.py:160 msgid "User login log" msgstr "用户登录日志" -#: audits/serializers.py:12 -msgid "Operate display" -msgstr "操作名称" - -#: audits/serializers.py:28 tickets/serializers/ticket/ticket.py:18 -msgid "Status display" -msgstr "状态名称" - -#: audits/serializers.py:29 -msgid "MFA display" -msgstr "MFA名称" - -#: audits/serializers.py:43 +#: audits/serializers.py:63 msgid "Reason display" msgstr "原因描述" -#: audits/signal_handlers.py:49 +#: audits/signal_handlers.py:45 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:47 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:52 +#: audits/signal_handlers.py:48 msgid "Auth Token" msgstr "认证令牌" -#: audits/signal_handlers.py:53 authentication/notifications.py:73 +#: audits/signal_handlers.py:49 authentication/notifications.py:73 #: authentication/views/login.py:73 authentication/views/wecom.py:178 #: notifications/backends/__init__.py:11 users/models/user.py:724 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers.py:54 authentication/views/feishu.py:144 +#: audits/signal_handlers.py:50 authentication/views/feishu.py:145 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 #: users/models/user.py:726 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179 +#: audits/signal_handlers.py:51 authentication/views/dingtalk.py:180 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 #: users/models/user.py:725 msgid "DingTalk" msgstr "钉钉" -#: audits/signal_handlers.py:56 authentication/models/temp_token.py:16 +#: audits/signal_handlers.py:52 authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "临时密码" -#: audits/signal_handlers.py:68 +#: audits/signal_handlers.py:64 msgid "User and Group" msgstr "用户与用户组" -#: audits/signal_handlers.py:69 +#: audits/signal_handlers.py:65 #, python-brace-format msgid "{User} JOINED {UserGroup}" msgstr "{User} 加入 {UserGroup}" -#: audits/signal_handlers.py:70 +#: audits/signal_handlers.py:66 #, python-brace-format msgid "{User} LEFT {UserGroup}" msgstr "{User} 离开 {UserGroup}" -#: audits/signal_handlers.py:73 +#: audits/signal_handlers.py:69 msgid "Node and Asset" msgstr "节点与资产" -#: audits/signal_handlers.py:74 +#: audits/signal_handlers.py:70 #, python-brace-format msgid "{Node} ADD {Asset}" msgstr "{Node} 添加 {Asset}" -#: audits/signal_handlers.py:75 +#: audits/signal_handlers.py:71 #, python-brace-format msgid "{Node} REMOVE {Asset}" msgstr "{Node} 移除 {Asset}" -#: audits/signal_handlers.py:78 +#: audits/signal_handlers.py:74 msgid "User asset permissions" msgstr "用户资产授权" -#: audits/signal_handlers.py:79 +#: audits/signal_handlers.py:75 #, python-brace-format msgid "{AssetPermission} ADD {User}" msgstr "{AssetPermission} 添加 {User}" -#: audits/signal_handlers.py:80 +#: audits/signal_handlers.py:76 #, python-brace-format msgid "{AssetPermission} REMOVE {User}" msgstr "{AssetPermission} 移除 {User}" -#: audits/signal_handlers.py:83 +#: audits/signal_handlers.py:79 msgid "User group asset permissions" msgstr "用户组资产授权" -#: audits/signal_handlers.py:84 +#: audits/signal_handlers.py:80 #, python-brace-format msgid "{AssetPermission} ADD {UserGroup}" msgstr "{AssetPermission} 添加 {UserGroup}" -#: audits/signal_handlers.py:85 +#: audits/signal_handlers.py:81 #, python-brace-format msgid "{AssetPermission} REMOVE {UserGroup}" msgstr "{AssetPermission} 移除 {UserGroup}" -#: audits/signal_handlers.py:88 perms/models/asset_permission.py:90 +#: audits/signal_handlers.py:84 perms/models/asset_permission.py:83 msgid "Asset permission" msgstr "资产授权" -#: audits/signal_handlers.py:89 +#: audits/signal_handlers.py:85 #, python-brace-format msgid "{AssetPermission} ADD {Asset}" msgstr "{AssetPermission} 添加 {Asset}" -#: audits/signal_handlers.py:90 +#: audits/signal_handlers.py:86 #, python-brace-format msgid "{AssetPermission} REMOVE {Asset}" msgstr "{AssetPermission} 移除 {Asset}" -#: audits/signal_handlers.py:93 +#: audits/signal_handlers.py:89 msgid "Node permission" msgstr "节点授权" -#: audits/signal_handlers.py:94 +#: audits/signal_handlers.py:90 #, python-brace-format msgid "{AssetPermission} ADD {Node}" msgstr "{AssetPermission} 添加 {Node}" -#: audits/signal_handlers.py:95 +#: audits/signal_handlers.py:91 #, python-brace-format msgid "{AssetPermission} REMOVE {Node}" msgstr "{AssetPermission} 移除 {Node}" @@ -1857,12 +1961,12 @@ msgstr "企业微信已经绑定" msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 -#: authentication/views/dingtalk.py:296 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 +#: authentication/views/dingtalk.py:297 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 msgid "FeiShu is not bound" msgstr "没有绑定飞书" @@ -1954,7 +2058,7 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" -#: authentication/middleware.py:76 settings/utils/ldap.py:652 +#: authentication/middleware.py:77 settings/utils/ldap.py:652 msgid "Authentication failed (before login check failed): {}" msgstr "认证失败(登录前检查失败): {}" @@ -1976,9 +2080,9 @@ msgid "Asset display" msgstr "资产名称" #: authentication/models/connection_token.py:36 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:79 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:72 #: tickets/models/ticket/apply_application.py:29 -#: tickets/models/ticket/apply_asset.py:22 users/models/user.py:707 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:707 msgid "Date expired" msgstr "失效日期" @@ -2042,17 +2146,17 @@ msgstr "异地登录提醒" msgid "binding reminder" msgstr "绑定提醒" -#: authentication/serializers/connection_token.py:20 +#: authentication/serializers/connection_token.py:19 #: xpack/plugins/cloud/models.py:36 msgid "Validity" msgstr "有效" -#: authentication/serializers/connection_token.py:21 +#: authentication/serializers/connection_token.py:20 msgid "Expired time" msgstr "过期时间" -#: authentication/serializers/token.py:79 perms/serializers/permission.py:60 -#: perms/serializers/permission.py:87 users/serializers/user.py:148 +#: authentication/serializers/token.py:79 perms/serializers/permission.py:30 +#: perms/serializers/permission.py:61 users/serializers/user.py:203 msgid "Is valid" msgstr "账号是否有效" @@ -2139,7 +2243,7 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_reset_password.html:3 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:390 ops/tasks.py:147 ops/tasks.py:153 ops/tasks.py:156 +#: jumpserver/conf.py:390 #: 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 @@ -2283,73 +2387,73 @@ msgstr "复制成功" msgid "LAN" msgstr "局域网" -#: authentication/views/dingtalk.py:41 +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:44 +#: authentication/views/dingtalk.py:45 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 #: authentication/views/wecom.py:56 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" -#: authentication/views/dingtalk.py:80 +#: authentication/views/dingtalk.py:81 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:148 +#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:164 +#: authentication/views/dingtalk.py:165 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:173 +#: authentication/views/dingtalk.py:174 msgid "The DingTalk is already bound to another user" msgstr "该钉钉已经绑定其他用户" -#: authentication/views/dingtalk.py:180 +#: authentication/views/dingtalk.py:181 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 +#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 +#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:39 +#: authentication/views/feishu.py:40 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:87 +#: authentication/views/feishu.py:88 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:129 +#: authentication/views/feishu.py:130 msgid "FeiShu query user failed" msgstr "飞书查询用户失败" -#: authentication/views/feishu.py:138 +#: authentication/views/feishu.py:139 msgid "The FeiShu is already bound to another user" msgstr "该飞书已经绑定其他用户" -#: authentication/views/feishu.py:145 +#: authentication/views/feishu.py:146 msgid "Binding FeiShu successfully" msgstr "绑定 飞书 成功" -#: authentication/views/feishu.py:197 +#: authentication/views/feishu.py:198 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/feishu.py:204 +#: authentication/views/feishu.py:205 msgid "Please login with a password and then bind the FeiShu" msgstr "请使用密码登录,然后绑定飞书" @@ -2453,31 +2557,31 @@ msgstr "取消" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:80 +#: common/db/fields.py:93 msgid "Marshal dict data to char field" msgstr "编码 dict 为 char" -#: common/db/fields.py:84 +#: common/db/fields.py:97 msgid "Marshal dict data to text field" msgstr "编码 dict 为 text" -#: common/db/fields.py:96 +#: common/db/fields.py:109 msgid "Marshal list data to char field" msgstr "编码 list 为 char" -#: common/db/fields.py:100 +#: common/db/fields.py:113 msgid "Marshal list data to text field" msgstr "编码 list 为 text" -#: common/db/fields.py:104 +#: common/db/fields.py:117 msgid "Marshal data to char field" msgstr "编码数据为 char" -#: common/db/fields.py:108 +#: common/db/fields.py:121 msgid "Marshal data to text field" msgstr "编码数据为 text" -#: common/db/fields.py:150 +#: common/db/fields.py:163 msgid "Encrypt field using Secret Key" msgstr "加密的字段" @@ -2489,16 +2593,35 @@ msgstr "更新人" msgid "Object" msgstr "对象" -#: common/drf/fields.py:70 +#: common/drf/fields.py:74 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/change_auth_plan/serializers/asset.py:64 +#: 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:101 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "该字段是必填项。" + +#: common/drf/fields.py:75 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "{pk_value} 对象不存在" -#: common/drf/fields.py:71 +#: common/drf/fields.py:76 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "不正确的类型。期望 pk 值,收到 {data_type} 类型。" +#: common/drf/fields.py:138 +msgid "Invalid data type, should be list" +msgstr "" + +#: common/drf/fields.py:153 +#, fuzzy +#| msgid "Invalid ip" +msgid "Invalid choice: {}" +msgstr "无效IP" + #: common/drf/parsers/base.py:17 msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "文件内容太大 (最大长度 `{}` 字节)" @@ -2558,15 +2681,15 @@ msgstr "忽略的" msgid "discard time" msgstr "忽略时间" -#: common/mixins/views.py:52 +#: common/mixins/views.py:58 msgid "Export all" msgstr "导出所有" -#: common/mixins/views.py:54 +#: common/mixins/views.py:60 msgid "Export only selected items" msgstr "仅导出选择项" -#: common/mixins/views.py:59 +#: common/mixins/views.py:65 #, python-format msgid "Export filtered: %s" msgstr "导出搜素: %s" @@ -2623,6 +2746,14 @@ msgstr "验证码错误" msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" +#: common/tasks.py:13 +msgid "Send email" +msgstr "发送邮件" + +#: common/tasks.py:40 +msgid "Send email attachment" +msgstr "发送邮件附件" + #: common/utils/ip/geoip/utils.py:26 msgid "Invalid ip" msgstr "无效IP" @@ -2699,6 +2830,10 @@ msgstr "邮件" msgid "Site message" msgstr "站内信" +#: notifications/notifications.py:46 +msgid "Publish the station message" +msgstr "发布站内信" + #: ops/ansible/inventory.py:75 msgid "No account available" msgstr "没有账号可以使用" @@ -2764,96 +2899,126 @@ msgstr "输入在 {} - {} 范围之间" msgid "Require periodic or regularly perform setting" msgstr "需要周期或定期设置" -#: ops/models/adhoc.py:18 +#: ops/models/adhoc.py:21 ops/models/job.py:31 +#, fuzzy +#| msgid "PowerShell" +msgid "Powershell" +msgstr "PowerShell" + +#: ops/models/adhoc.py:25 msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:19 +#: ops/models/adhoc.py:27 ops/models/job.py:38 msgid "Module" msgstr "" -#: ops/models/adhoc.py:20 ops/models/celery.py:45 +#: ops/models/adhoc.py:28 ops/models/celery.py:54 ops/models/job.py:36 #: terminal/models/component/task.py:17 msgid "Args" msgstr "参数" -#: ops/models/adhoc.py:21 ops/models/base.py:20 ops/models/playbook.py:27 -msgid "Last execution" -msgstr "最后执行" - -#: ops/models/adhoc.py:36 -msgid "Adhoc" -msgstr "" - -#: ops/models/adhoc.py:54 -msgid "AdHoc execution" -msgstr "任务执行" - -#: ops/models/base.py:16 ops/models/base.py:52 +#: ops/models/adhoc.py:29 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:43 ops/models/job.py:68 #: terminal/models/session/sharing.py:24 msgid "Creator" msgstr "创建者" +#: ops/models/adhoc.py:50 ops/models/job.py:21 +msgid "Adhoc" +msgstr "" + +#: ops/models/adhoc.py:68 +msgid "AdHoc execution" +msgstr "任务执行" + #: ops/models/base.py:19 msgid "Account policy" msgstr "账号策略" -#: ops/models/base.py:21 +#: ops/models/base.py:20 +msgid "Last execution" +msgstr "最后执行" + +#: ops/models/base.py:22 msgid "Date last run" msgstr "最后执行日期" -#: ops/models/base.py:50 xpack/plugins/cloud/models.py:169 +#: ops/models/base.py:51 ops/models/job.py:66 xpack/plugins/cloud/models.py:169 msgid "Result" msgstr "结果" -#: ops/models/base.py:51 +#: ops/models/base.py:52 ops/models/job.py:67 msgid "Summary" msgstr "汇总" -#: ops/models/celery.py:46 terminal/models/component/task.py:18 +#: ops/models/celery.py:55 terminal/models/component/task.py:18 msgid "Kwargs" msgstr "其它参数" -#: ops/models/celery.py:47 tickets/models/comment.py:13 +#: ops/models/celery.py:56 tickets/models/comment.py:13 #: tickets/models/ticket/general.py:41 tickets/models/ticket/general.py:277 +#: tickets/serializers/ticket/ticket.py:20 msgid "State" msgstr "状态" -#: ops/models/celery.py:48 terminal/models/session/sharing.py:111 +#: ops/models/celery.py:57 terminal/models/session/sharing.py:111 #: tickets/const.py:25 xpack/plugins/change_auth_plan/models/base.py:188 msgid "Finished" msgstr "结束" -#: ops/models/playbook.py:10 -msgid "Path" -msgstr "路径" +#: ops/models/celery.py:58 +msgid "Date published" +msgstr "发布日期" -#: ops/models/playbook.py:18 -msgid "Playbook template" -msgstr "Playbook 模版" - -#: ops/models/playbook.py:23 +#: ops/models/job.py:22 ops/models/job.py:41 msgid "Playbook" msgstr "Playbook" -#: ops/models/playbook.py:24 +#: ops/models/job.py:25 +#, fuzzy +#| msgid "Privileged" +msgid "Privileged Only" +msgstr "特权账号" + +#: ops/models/job.py:26 +#, fuzzy +#| msgid "Privileged" +msgid "Privileged First" +msgstr "特权账号" + +#: ops/models/job.py:27 +msgid "Skip" +msgstr "" + +#: ops/models/job.py:39 +msgid "Chdir" +msgstr "执行路径" + +#: ops/models/job.py:40 +msgid "Timeout (Seconds)" +msgstr "超时时间(秒)" + +#: ops/models/job.py:45 +msgid "Runas" +msgstr "" + +#: ops/models/job.py:47 +#, fuzzy +#| msgid "Account policy" +msgid "Runas policy" +msgstr "账号策略" + +#: ops/models/job.py:48 +#, fuzzy +#| msgid "Disable" +msgid "Variables" +msgstr "禁用" + +#: ops/models/playbook.py:15 msgid "Owner" msgstr "Owner" -#: ops/models/playbook.py:26 settings/serializers/auth/sms.py:64 -msgid "Template" -msgstr "模板" - -#: ops/models/playbook.py:38 ops/signal_handlers.py:63 -#: terminal/models/component/task.py:26 -#: xpack/plugins/gathered_user/models.py:68 -msgid "Task" -msgstr "任务" - -#: ops/models/playbook.py:39 -msgid "Run dir" -msgstr "运行目录" - #: ops/notifications.py:17 msgid "Server performance" msgstr "监控告警" @@ -2882,21 +3047,39 @@ msgstr "内存使用率超过 {max_threshold}%: => {value}" msgid "CPU load more than {max_threshold}: => {value}" msgstr "CPU 使用率超过 {max_threshold}: => {value}" -#: ops/tasks.py:34 +#: ops/signal_handlers.py:63 terminal/models/applet/host.py:108 +#: terminal/models/component/task.py:26 +#: xpack/plugins/gathered_user/models.py:68 +msgid "Task" +msgstr "任务" + +#: ops/tasks.py:27 msgid "Run ansible task" msgstr "运行 ansible 任务" -#: ops/tasks.py:58 -msgid "Run ansible command" -msgstr "运行 ansible 命令" +#: ops/tasks.py:41 +msgid "Run ansible task execution" +msgstr "运行 ansible 任务" -#: ops/tasks.py:80 -msgid "Clean task history period" -msgstr "定期清除任务历史" +#: ops/tasks.py:54 +msgid "Periodic clear celery tasks" +msgstr "定时清理 Celery 任务" -#: ops/tasks.py:93 +#: ops/tasks.py:56 msgid "Clean celery log period" -msgstr "定期清除Celery日志" +msgstr "定期清理 Celery 日志" + +#: ops/tasks.py:73 +msgid "Clear celery periodic tasks" +msgstr "清理 Celery 定时任务" + +#: ops/tasks.py:96 +msgid "Create or update periodic tasks" +msgstr "创建或更新定时任务" + +#: ops/tasks.py:104 +msgid "Periodic check service performance" +msgstr "定时检查服务性能" #: ops/templates/ops/celery_task_log.html:4 msgid "Task log" @@ -2927,7 +3110,7 @@ msgstr "组织管理" #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:88 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 -#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:72 +#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:61 msgid "Organization" msgstr "组织" @@ -2961,76 +3144,84 @@ msgstr "可以查看全局组织" msgid "Can view all joined org" msgstr "可以查看所有加入的组织" +#: orgs/tasks.py:9 +msgid "Refresh organization cache" +msgstr "刷新组织缓存" + #: perms/apps.py:9 msgid "App permissions" msgstr "授权管理" -#: perms/models/asset_permission.py:72 perms/serializers/permission.py:59 -#: perms/serializers/permission.py:85 -#: tickets/models/ticket/apply_application.py:26 -#: tickets/models/ticket/apply_asset.py:19 -msgid "Actions" -msgstr "动作" - -#: perms/models/asset_permission.py:83 -msgid "From ticket" -msgstr "来自工单" - -#: perms/models/asset_permission.py:224 -msgid "Ungrouped" -msgstr "未分组" - -#: perms/models/asset_permission.py:226 -msgid "Favorite" -msgstr "收藏夹" - -#: perms/models/asset_permission.py:273 -msgid "Permed asset" -msgstr "授权的资产" - -#: perms/models/asset_permission.py:275 -msgid "Can view my assets" -msgstr "可以查看我的资产" - -#: perms/models/asset_permission.py:276 -msgid "Can view user assets" -msgstr "可以查看用户授权的资产" - -#: perms/models/asset_permission.py:277 -msgid "Can view usergroup assets" -msgstr "可以查看用户组授权的资产" - -#: perms/models/const.py:20 settings/serializers/terminal.py:12 -msgid "All" -msgstr "全部" - -#: perms/models/const.py:21 +#: perms/const.py:13 msgid "Connect" msgstr "连接" -#: perms/models/const.py:22 -msgid "Upload file" -msgstr "上传文件" +#: perms/const.py:16 +#, fuzzy +#| msgid "Copy link" +msgid "Copy" +msgstr "复制链接" -#: perms/models/const.py:23 -msgid "Download file" -msgstr "下载文件" +#: perms/const.py:17 +msgid "Paste" +msgstr "" -#: perms/models/const.py:24 -msgid "Upload download" -msgstr "上传下载" +#: perms/const.py:27 +msgid "Transfer" +msgstr "" -#: perms/models/const.py:25 -msgid "Clipboard copy" +#: perms/const.py:28 +#, fuzzy +#| msgid "Clipboard copy" +msgid "Clipboard" msgstr "剪贴板复制" -#: perms/models/const.py:26 -msgid "Clipboard paste" -msgstr "剪贴板粘贴" +#: perms/models/asset_permission.py:66 perms/models/perm_token.py:18 +#: perms/serializers/permission.py:29 perms/serializers/permission.py:59 +#: tickets/models/ticket/apply_application.py:26 +#: tickets/models/ticket/apply_asset.py:18 +msgid "Actions" +msgstr "动作" -#: perms/models/const.py:27 -msgid "Clipboard copy paste" -msgstr "剪贴板复制粘贴" +#: perms/models/asset_permission.py:76 +msgid "From ticket" +msgstr "来自工单" + +#: perms/models/perm_node.py:55 +msgid "Ungrouped" +msgstr "未分组" + +#: perms/models/perm_node.py:57 +msgid "Favorite" +msgstr "收藏夹" + +#: perms/models/perm_node.py:104 +msgid "Permed asset" +msgstr "授权的资产" + +#: perms/models/perm_node.py:106 +msgid "Can view my assets" +msgstr "可以查看我的资产" + +#: perms/models/perm_node.py:107 +msgid "Can view user assets" +msgstr "可以查看用户授权的资产" + +#: perms/models/perm_node.py:108 +msgid "Can view usergroup assets" +msgstr "可以查看用户组授权的资产" + +#: perms/models/perm_node.py:119 +#, fuzzy +#| msgid "Create account" +msgid "Permed account" +msgstr "收集账号" + +#: perms/models/perm_token.py:17 +#, fuzzy +#| msgid "Connect timeout" +msgid "Connect method" +msgstr "连接超时时间" #: perms/notifications.py:12 perms/notifications.py:44 msgid "today" @@ -3052,40 +3243,11 @@ msgstr "资产授权规则将要过期" msgid "asset permissions of organization {}" msgstr "组织 ({}) 的资产授权" -#: perms/serializers/permission.py:48 -msgid "Users display" -msgstr "用户名称" - -#: perms/serializers/permission.py:51 -msgid "User groups display" -msgstr "用户组名称" - -#: perms/serializers/permission.py:54 -msgid "Assets display" -msgstr "资产名称" - -#: perms/serializers/permission.py:57 -msgid "Nodes display" -msgstr "节点名称" - -#: perms/serializers/permission.py:61 perms/serializers/permission.py:86 -#: users/serializers/user.py:89 users/serializers/user.py:150 +#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: users/serializers/user.py:100 users/serializers/user.py:205 msgid "Is expired" msgstr "已过期" -#: perms/serializers/permission.py:81 rbac/serializers/role.py:26 -#: users/serializers/group.py:34 -msgid "Users amount" -msgstr "用户数量" - -#: perms/serializers/permission.py:82 -msgid "User groups amount" -msgstr "用户组数量" - -#: perms/serializers/permission.py:84 -msgid "Nodes amount" -msgstr "节点数量" - #: perms/templates/perms/_msg_item_permissions_expire.html:7 #: perms/templates/perms/_msg_permed_items_expire.html:7 #, python-format @@ -3102,7 +3264,7 @@ msgstr "" msgid "If you have any question, please contact the administrator" msgstr "如果有疑问或需求,请联系系统管理员" -#: perms/utils/user_permission.py:623 rbac/tree.py:57 +#: perms/utils/user_permission.py:627 rbac/tree.py:57 msgid "My assets" msgstr "我的资产" @@ -3234,6 +3396,10 @@ msgstr "权限" msgid "Scope display" msgstr "范围名称" +#: rbac/serializers/role.py:26 users/serializers/group.py:34 +msgid "Users amount" +msgstr "用户数量" + #: rbac/serializers/role.py:27 terminal/models/applet/applet.py:21 msgid "Display name" msgstr "显示名称" @@ -3722,6 +3888,10 @@ msgstr "原始号码(Src id)" msgid "Business type(Service id)" msgstr "业务类型(Service id)" +#: settings/serializers/auth/sms.py:64 +msgid "Template" +msgstr "模板" + #: settings/serializers/auth/sms.py:65 #, python-brace-format msgid "" @@ -4473,13 +4643,13 @@ msgstr "过期。" #, python-format msgid "" "\n" -" Your password has expired, please click this link update password.\n" +" Your password has expired, please click this link update password.\n" " " msgstr "" "\n" -" 您的密码已经过期,请点击 链接 更新密码\n" +" 您的密码已经过期,请点击 链接 更新密码\n" " " #: templates/_message.html:30 @@ -4503,8 +4673,8 @@ msgstr "" #, python-format msgid "" "\n" -" Your information was incomplete. Please click this link to complete your information.\n" +" Your information was incomplete. Please click this link to complete your information.\n" " " msgstr "" "\n" @@ -4516,13 +4686,13 @@ msgstr "" #, python-format msgid "" "\n" -" Your ssh public key not set or expired. Please click this link to update\n" +" Your ssh public key not set or expired. Please click this link to update\n" " " msgstr "" "\n" -" 您的SSH密钥没有设置或已失效,请点击 链接 更新\n" +" 您的SSH密钥没有设置或已失效,请点击 链接 更新\n" " " #: templates/_mfa_login_field.html:28 @@ -4691,7 +4861,7 @@ msgid "Timestamp" msgstr "时间戳" #: terminal/backends/command/serializers.py:41 -#: terminal/models/component/terminal.py:105 +#: terminal/models/component/terminal.py:87 msgid "Remote Address" msgstr "远端地址" @@ -4731,39 +4901,35 @@ msgstr "标签" msgid "Hosts" msgstr "主机" -#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:28 +#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:27 msgid "Applet" msgstr "远程应用" -#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:36 +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 #, fuzzy #| msgid "More login options" msgid "Deploy options" msgstr "其他方式登录" -#: terminal/models/applet/host.py:20 +#: terminal/models/applet/host.py:19 msgid "Inited" msgstr "" -#: terminal/models/applet/host.py:21 +#: terminal/models/applet/host.py:20 #, fuzzy #| msgid "Date finished" msgid "Date inited" msgstr "结束日期" -#: terminal/models/applet/host.py:22 +#: terminal/models/applet/host.py:21 msgid "Date synced" msgstr "最后同步日期" -#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:183 -msgid "Terminal" -msgstr "终端" - -#: terminal/models/applet/host.py:99 +#: terminal/models/applet/host.py:102 msgid "Hosting" msgstr "主机" -#: terminal/models/applet/host.py:100 +#: terminal/models/applet/host.py:103 msgid "Initial" msgstr "" @@ -4772,12 +4938,10 @@ msgid "HTTPS Port" msgstr "HTTPS 端口" #: terminal/models/component/endpoint.py:15 -#: terminal/models/component/terminal.py:107 msgid "HTTP Port" msgstr "HTTP 端口" #: terminal/models/component/endpoint.py:16 -#: terminal/models/component/terminal.py:106 msgid "SSH Port" msgstr "SSH 端口" @@ -4825,31 +4989,31 @@ msgstr "IP 组" msgid "Endpoint rule" msgstr "端点规则" -#: terminal/models/component/status.py:18 +#: terminal/models/component/status.py:14 msgid "Session Online" msgstr "在线会话" -#: terminal/models/component/status.py:19 +#: terminal/models/component/status.py:15 msgid "CPU Load" msgstr "CPU负载" -#: terminal/models/component/status.py:20 +#: terminal/models/component/status.py:16 msgid "Memory Used" msgstr "内存使用" -#: terminal/models/component/status.py:21 +#: terminal/models/component/status.py:17 msgid "Disk Used" msgstr "磁盘使用" -#: terminal/models/component/status.py:22 +#: terminal/models/component/status.py:18 msgid "Connections" msgstr "连接数" -#: terminal/models/component/status.py:23 +#: terminal/models/component/status.py:19 msgid "Threads" msgstr "线程数" -#: terminal/models/component/status.py:24 +#: terminal/models/component/status.py:20 msgid "Boot Time" msgstr "运行时间" @@ -4858,20 +5022,20 @@ msgid "Default storage" msgstr "默认存储" #: terminal/models/component/storage.py:136 -#: terminal/models/component/terminal.py:108 +#: terminal/models/component/terminal.py:88 msgid "Command storage" msgstr "命令存储" #: terminal/models/component/storage.py:196 -#: terminal/models/component/terminal.py:109 +#: terminal/models/component/terminal.py:89 msgid "Replay storage" msgstr "录像存储" -#: terminal/models/component/terminal.py:103 +#: terminal/models/component/terminal.py:85 msgid "type" msgstr "类型" -#: terminal/models/component/terminal.py:185 +#: terminal/models/component/terminal.py:161 msgid "Can view terminal config" msgstr "可以查看终端配置" @@ -5003,38 +5167,42 @@ msgstr "不匹配" msgid "Icon" msgstr "图标" -#: terminal/serializers/applet_host.py:20 +#: terminal/serializers/applet_host.py:21 msgid "Per Session" msgstr "按会话" -#: terminal/serializers/applet_host.py:21 +#: terminal/serializers/applet_host.py:22 msgid "Per Device" msgstr "按设备" -#: terminal/serializers/applet_host.py:27 +#: terminal/serializers/applet_host.py:28 msgid "RDS Licensing" msgstr "部署 RDS 许可服务" -#: terminal/serializers/applet_host.py:28 +#: terminal/serializers/applet_host.py:29 msgid "RDS License Server" msgstr "RDS 许可服务主机" -#: terminal/serializers/applet_host.py:29 +#: terminal/serializers/applet_host.py:30 msgid "RDS Licensing Mode" msgstr "RDS 许可模式" -#: terminal/serializers/applet_host.py:30 +#: terminal/serializers/applet_host.py:32 msgid "RDS fSingleSessionPerUser" msgstr "RDS 会话用户数" -#: terminal/serializers/applet_host.py:31 +#: terminal/serializers/applet_host.py:33 msgid "RDS Max Disconnection Time" msgstr "RDS 会话断开时间" -#: terminal/serializers/applet_host.py:32 +#: terminal/serializers/applet_host.py:34 msgid "RDS Remote App Logoff Time Limit" msgstr "RDS 远程应用注销时间" +#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +msgid "Load status" +msgstr "负载状态" + #: terminal/serializers/endpoint.py:12 msgid "Oracle port" msgstr "" @@ -5154,11 +5322,7 @@ msgstr "文档类型" msgid "Ignore Certificate Verification" msgstr "忽略证书认证" -#: terminal/serializers/terminal.py:44 -msgid "Load status" -msgstr "负载状态" - -#: terminal/serializers/terminal.py:81 terminal/serializers/terminal.py:89 +#: terminal/serializers/terminal.py:77 terminal/serializers/terminal.py:85 msgid "Not found" msgstr "没有发现" @@ -5278,11 +5442,11 @@ msgstr "内容" msgid "Approve level" msgstr "审批级别" -#: tickets/models/flow.py:25 tickets/serializers/flow.py:15 +#: tickets/models/flow.py:25 tickets/serializers/flow.py:17 msgid "Approve strategy" msgstr "审批策略" -#: tickets/models/flow.py:30 tickets/serializers/flow.py:16 +#: tickets/models/flow.py:30 tickets/serializers/flow.py:19 msgid "Assignees" msgstr "受理人" @@ -5312,15 +5476,17 @@ msgid "Apply system users" msgstr "申请的系统用户" #: tickets/models/ticket/apply_asset.py:9 -#: tickets/serializers/ticket/apply_asset.py:15 +#: tickets/serializers/ticket/apply_asset.py:14 msgid "Select at least one asset or node" msgstr "资产或者节点至少选择一项" #: tickets/models/ticket/apply_asset.py:14 +#: tickets/serializers/ticket/apply_asset.py:19 msgid "Apply nodes" msgstr "申请节点" #: tickets/models/ticket/apply_asset.py:16 +#: tickets/serializers/ticket/apply_asset.py:18 msgid "Apply assets" msgstr "申请资产" @@ -5428,15 +5594,15 @@ msgstr "你的工单已被处理, 处理人 - {}" msgid "Ticket has processed - {} ({})" msgstr "你的工单已被处理, 处理人 - {} ({})" -#: tickets/serializers/flow.py:17 +#: tickets/serializers/flow.py:20 msgid "Assignees display" msgstr "受理人名称" -#: tickets/serializers/flow.py:43 +#: tickets/serializers/flow.py:46 msgid "Please select the Assignees" msgstr "请选择受理人" -#: tickets/serializers/flow.py:69 +#: tickets/serializers/flow.py:74 msgid "The current organization type already exists" msgstr "当前组织已存在该类型" @@ -5444,6 +5610,12 @@ msgstr "当前组织已存在该类型" msgid "Processor" msgstr "处理人" +#: tickets/serializers/ticket/apply_asset.py:20 +#, fuzzy +#| msgid "Apply applications" +msgid "Apply actions" +msgstr "申请应用" + #: tickets/serializers/ticket/common.py:15 #: tickets/serializers/ticket/common.py:77 msgid "Created by ticket ({}-{})" @@ -5457,7 +5629,7 @@ msgstr "过期时间要大于开始时间" msgid "Permission named `{}` already exists" msgstr "授权名称 `{}` 已存在" -#: tickets/serializers/ticket/ticket.py:101 +#: tickets/serializers/ticket/ticket.py:85 msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" @@ -5630,7 +5802,7 @@ msgstr "强制启用" msgid "Local" msgstr "数据库" -#: users/models/user.py:677 users/serializers/user.py:149 +#: users/models/user.py:677 users/serializers/user.py:204 msgid "Is service account" msgstr "服务账号" @@ -5737,105 +5909,105 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:149 users/serializers/user.py:146 +#: users/serializers/profile.py:149 users/serializers/user.py:201 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:28 +#: users/serializers/user.py:30 msgid "System roles" msgstr "系统角色" -#: users/serializers/user.py:33 +#: users/serializers/user.py:35 msgid "Org roles" msgstr "组织角色" -#: users/serializers/user.py:35 +#: users/serializers/user.py:38 msgid "System roles display" msgstr "系统角色显示" -#: users/serializers/user.py:36 +#: users/serializers/user.py:40 msgid "Org roles display" msgstr "组织角色显示" -#: users/serializers/user.py:81 +#: users/serializers/user.py:90 #: 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 +#: users/serializers/user.py:92 msgid "MFA enabled" msgstr "MFA 已启用" -#: users/serializers/user.py:84 +#: users/serializers/user.py:94 msgid "MFA force enabled" msgstr "强制 MFA" -#: users/serializers/user.py:86 +#: users/serializers/user.py:97 msgid "MFA level display" msgstr "MFA 等级名称" -#: users/serializers/user.py:88 +#: users/serializers/user.py:99 msgid "Login blocked" msgstr "登录被阻塞" -#: users/serializers/user.py:91 +#: users/serializers/user.py:102 msgid "Can public key authentication" msgstr "能否公钥认证" -#: users/serializers/user.py:151 +#: users/serializers/user.py:206 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:153 +#: users/serializers/user.py:208 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:154 +#: users/serializers/user.py:209 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:155 +#: users/serializers/user.py:210 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:156 +#: users/serializers/user.py:211 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:157 +#: users/serializers/user.py:212 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:159 +#: users/serializers/user.py:214 msgid "Is wecom bound" msgstr "是否绑定了企业微信" -#: users/serializers/user.py:160 +#: users/serializers/user.py:215 msgid "Is dingtalk bound" msgstr "是否绑定了钉钉" -#: users/serializers/user.py:161 +#: users/serializers/user.py:216 msgid "Is feishu bound" msgstr "是否绑定了飞书" -#: users/serializers/user.py:162 +#: users/serializers/user.py:217 msgid "Is OTP bound" msgstr "是否绑定了虚拟 MFA" -#: users/serializers/user.py:164 +#: users/serializers/user.py:219 msgid "System role name" msgstr "系统角色名称" -#: users/serializers/user.py:263 +#: users/serializers/user.py:325 msgid "Select users" msgstr "选择用户" -#: users/serializers/user.py:264 +#: users/serializers/user.py:326 msgid "For security, only list several users" msgstr "为了安全,仅列出几个用户" -#: users/serializers/user.py:299 +#: users/serializers/user.py:362 msgid "name not unique" msgstr "名称重复" @@ -6076,10 +6248,6 @@ msgstr "重置密码成功,返回到登录页面" msgid "XPACK" msgstr "XPack" -#: xpack/plugins/change_auth_plan/api/asset.py:94 -msgid "The parameter 'action' must be [{}]" -msgstr "参数 'action' 必须是 [{}]" - #: xpack/plugins/change_auth_plan/meta.py:9 #: xpack/plugins/change_auth_plan/models/asset.py:124 msgid "Change auth plan" @@ -6108,11 +6276,6 @@ msgstr "应用改密计划任务" msgid "Password cannot be set to blank, exit. " msgstr "密码不能设置为空, 退出. " -#: xpack/plugins/change_auth_plan/models/asset.py:50 -#: xpack/plugins/change_auth_plan/serializers/asset.py:33 -msgid "SSH Key strategy" -msgstr "SSH 密钥策略" - #: xpack/plugins/change_auth_plan/models/asset.py:68 msgid "Asset change auth plan" msgstr "资产改密计划" @@ -6166,25 +6329,6 @@ msgstr "保存密码/密钥" 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:30 msgid "Change Password" msgstr "更改密码" @@ -6197,14 +6341,6 @@ msgstr "修改 SSH Key" 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 "多次尝试改密后, 依然失败" @@ -6221,6 +6357,18 @@ msgstr "连接主机失败" msgid "Data could not be sent to remote" msgstr "无法将数据发送到远程" +#: xpack/plugins/change_auth_plan/tasks.py:13 +msgid "Execute change authentication task" +msgstr "执行资产改密计划任务" + +#: xpack/plugins/change_auth_plan/tasks.py:24 +msgid "Start change authentication task" +msgstr "开始资产改密计划任务" + +#: xpack/plugins/change_auth_plan/tasks.py:36 +msgid "Test the validity of the change authentication plan " +msgstr "测试资产改密结果" + #: xpack/plugins/cloud/api.py:40 msgid "Test connection successful" msgstr "测试成功" @@ -6753,11 +6901,11 @@ msgstr "主题" msgid "Interface setting" msgstr "界面设置" -#: xpack/plugins/license/api.py:53 +#: xpack/plugins/license/api.py:50 msgid "License import successfully" msgstr "许可证导入成功" -#: xpack/plugins/license/api.py:54 +#: xpack/plugins/license/api.py:51 msgid "License is invalid" msgstr "无效的许可证" @@ -6781,8 +6929,70 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" -#~ msgid "Account automation" -#~ msgstr "账号自动化" +#~ msgid "Type display" +#~ msgstr "类型名称" + +#~ msgid "Status display" +#~ msgstr "状态名称" + +#~ msgid "Run ansible command" +#~ msgstr "运行 ansible 命令" + +#~ msgid "Clean task history period" +#~ msgstr "定期清除任务历史" + +#, fuzzy +#~| msgid "WeCom Error" +#~ msgid "Hello Error" +#~ msgstr "企业微信错误" + +#~ msgid "Operate display" +#~ msgstr "操作名称" + +#~ msgid "MFA display" +#~ msgstr "MFA名称" + +#~ msgid "Path" +#~ msgstr "路径" + +#~ msgid "Playbook template" +#~ msgstr "Playbook 模版" + +#~ msgid "Run dir" +#~ msgstr "运行目录" + +#~ msgid "Upload file" +#~ msgstr "上传文件" + +#~ msgid "Download file" +#~ msgstr "下载文件" + +#~ msgid "Upload download" +#~ msgstr "上传下载" + +#~ msgid "Clipboard paste" +#~ msgstr "剪贴板粘贴" + +#~ msgid "Clipboard copy paste" +#~ msgstr "剪贴板复制粘贴" + +#~ msgid "Users display" +#~ msgstr "用户名称" + +#~ msgid "User groups display" +#~ msgstr "用户组名称" + +#~ msgid "Assets display" +#~ msgstr "资产名称" + +#~ msgid "Nodes display" +#~ msgstr "节点名称" + +#~ msgid "User groups amount" +#~ msgstr "用户组数量" + +#~ msgid "Nodes amount" +#~ msgstr "节点数量" #~ msgid "The asset {} system platform {} does not support run Ansible tasks" #~ msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" @@ -7069,66 +7279,54 @@ msgstr "社区版" #~ msgid "Asset and SystemUser" #~ msgstr "资产与系统用户" -#, python-brace-format #~ msgid "{Asset} ADD {SystemUser}" #~ msgstr "{Asset} 添加 {SystemUser}" -#, python-brace-format #~ msgid "{Asset} REMOVE {SystemUser}" #~ msgstr "{Asset} 移除 {SystemUser}" #~ msgid "Asset permission and SystemUser" #~ msgstr "资产授权与系统用户" -#, python-brace-format #~ msgid "{AssetPermission} ADD {SystemUser}" #~ msgstr "{AssetPermission} 添加 {SystemUser}" -#, python-brace-format #~ msgid "{AssetPermission} REMOVE {SystemUser}" #~ msgstr "{AssetPermission} 移除 {SystemUser}" #~ msgid "User application permissions" #~ msgstr "用户应用授权" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {User}" #~ msgstr "{ApplicationPermission} 添加 {User}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {User}" #~ msgstr "{ApplicationPermission} 移除 {User}" #~ msgid "User group application permissions" #~ msgstr "用户组应用授权" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {UserGroup}" #~ msgstr "{ApplicationPermission} 添加 {UserGroup}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {UserGroup}" #~ msgstr "{ApplicationPermission} 移除 {UserGroup}" #~ msgid "Application permission" #~ msgstr "应用授权" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {Application}" #~ msgstr "{ApplicationPermission} 添加 {Application}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {Application}" #~ msgstr "{ApplicationPermission} 移除 {Application}" #~ msgid "Application permission and SystemUser" #~ msgstr "应用授权与系统用户" -#, python-brace-format #~ msgid "{ApplicationPermission} ADD {SystemUser}" #~ msgstr "{ApplicationPermission} 添加 {SystemUser}" -#, python-brace-format #~ msgid "{ApplicationPermission} REMOVE {SystemUser}" #~ msgstr "{ApplicationPermission} 移除 {SystemUser}" 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 3f794a194..7d2b1f39d 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -5,6 +5,7 @@ class DefaultCallback: STATUS_MAPPER = { 'successful': 'success', 'failure': 'failed', + 'failed': 'failed', 'running': 'running', 'pending': 'pending', 'unknown': 'unknown' diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index fbf3245ae..13d56bd00 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -13,7 +13,7 @@ class AdHocRunner: "reboot", 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' ] - def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/'): + def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars={}): self.id = uuid.uuid4() self.inventory = inventory self.pattern = pattern @@ -22,6 +22,7 @@ class AdHocRunner: self.project_dir = project_dir self.cb = DefaultCallback() self.runner = None + self.extra_vars = extra_vars def check_module(self): if self.module not in self.cmd_modules_choices: @@ -38,6 +39,7 @@ class AdHocRunner: os.mkdir(self.project_dir, 0o755) ansible_runner.run( + extravars=self.extra_vars, host_pattern=self.pattern, private_data_dir=self.project_dir, inventory=self.inventory, 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 8eb5356e4..e82f3fb2e 100644 --- a/apps/ops/api/__init__.py +++ b/apps/ops/api/__init__.py @@ -2,3 +2,5 @@ # from .adhoc import * from .celery import * +from .job import * +from .playbook import * diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 71e818fbb..9889349df 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -1,52 +1,17 @@ # -*- coding: utf-8 -*- # -from django.shortcuts import get_object_or_404 -from rest_framework import viewsets, generics -from rest_framework.views import Response - -from common.drf.serializers import CeleryTaskExecutionSerializer -from ..models import AdHoc, AdHocExecution +from rest_framework import viewsets +from ..models import AdHoc from ..serializers import ( - AdHocSerializer, - AdHocExecutionSerializer, - AdHocDetailSerializer, + AdHocSerializer ) __all__ = [ - 'AdHocViewSet', 'AdHocExecutionViewSet' + 'AdHocViewSet' ] class AdHocViewSet(viewsets.ModelViewSet): queryset = AdHoc.objects.all() serializer_class = AdHocSerializer - - def get_serializer_class(self): - if self.action == 'retrieve': - return AdHocDetailSerializer - return super().get_serializer_class() - - -class AdHocExecutionViewSet(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(AdHoc, 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 - - - - - diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 61d13db17..8d58c1981 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -98,20 +98,27 @@ class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet): return queryset +class CelerySummaryAPIView(generics.RetrieveAPIView): + def get(self, request, *args, **kwargs): + pass + + class CeleryTaskViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet): - queryset = CeleryTask.objects.all() 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.kwargs.get("task_pk") + task_id = self.request.query_params.get('task_id') if task_id: - task = CeleryTask.objects.get(pk=task_id) - return CeleryTaskExecution.objects.filter(name=task.name) - else: - return CeleryTaskExecution.objects.none() + 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/job.py b/apps/ops/api/job.py new file mode 100644 index 000000000..86a52f373 --- /dev/null +++ b/apps/ops/api/job.py @@ -0,0 +1,43 @@ +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, run_ops_job_executions +from orgs.mixins.api import OrgBulkModelViewSet + + +class JobViewSet(OrgBulkModelViewSet): + serializer_class = JobSerializer + model = Job + permission_classes = () + + def get_queryset(self): + query_set = super().get_queryset() + return query_set.filter(instant=False) + + def perform_create(self, serializer): + instance = serializer.save() + if instance.instant: + run_ops_job.delay(instance.id) + + +class JobExecutionViewSet(OrgBulkModelViewSet): + serializer_class = JobExecutionSerializer + http_method_names = ('get', 'post', 'head', 'options',) + # filter_fields = ('type',) + permission_classes = () + model = JobExecution + + def perform_create(self, serializer): + instance = serializer.save() + run_ops_job_executions.delay(instance.id) + + def get_queryset(self): + query_set = super().get_queryset() + job_id = self.request.query_params.get('job_id') + if job_id: + self.queryset = 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..2cb846c0e --- /dev/null +++ b/apps/ops/api/playbook.py @@ -0,0 +1,28 @@ +import os +import zipfile + +from django.conf import settings +from rest_framework import viewsets +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(viewsets.ModelViewSet): + queryset = Playbook.objects.all() + serializer_class = PlaybookSerializer + + 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__()) + if os.path.exists(dest_path): + os.makedirs(dest_path) + unzip_playbook(src_path, dest_path) diff --git a/apps/ops/migrations/0029_auto_20221111_1919.py b/apps/ops/migrations/0029_auto_20221111_1919.py new file mode 100644 index 000000000..072e59867 --- /dev/null +++ b/apps/ops/migrations/0029_auto_20221111_1919.py @@ -0,0 +1,171 @@ +# Generated by Django 3.2.14 on 2022-11-11 11:19 + +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', '0111_alter_automationexecution_status'), + ('ops', '0028_celerytask_last_published_time'), + ] + + operations = [ + 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')), + ('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')), + ('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')), + ('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={ + 'abstract': False, + }, + ), + 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')), + ('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')), + ('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={ + 'abstract': False, + }, + ), + migrations.AlterUniqueTogether( + name='playbooktemplate', + unique_together=None, + ), + 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='adhoc', + name='org_id', + ), + 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='comment', + ), + 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='org_id', + ), + migrations.RemoveField( + model_name='playbook', + name='template', + ), + 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='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='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/migrations/0030_auto_20221116_1811.py b/apps/ops/migrations/0030_auto_20221116_1811.py new file mode 100644 index 000000000..3118f26ca --- /dev/null +++ b/apps/ops/migrations/0030_auto_20221116_1811.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.14 on 2022-11-16 10:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0029_auto_20221111_1919'), + ] + + operations = [ + migrations.AlterModelOptions( + name='celerytask', + options={'ordering': ('name',)}, + ), + migrations.AddField( + model_name='job', + name='variables', + field=models.JSONField(default=dict, verbose_name='Variables'), + ), + migrations.AlterField( + model_name='celerytask', + name='name', + field=models.CharField(max_length=1024, verbose_name='Name'), + ), + migrations.AlterField( + model_name='celerytaskexecution', + name='date_finished', + field=models.DateTimeField(null=True, verbose_name='Date finished'), + ), + migrations.AlterField( + model_name='celerytaskexecution', + name='date_published', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date published'), + ), + migrations.AlterField( + model_name='celerytaskexecution', + name='date_start', + field=models.DateTimeField(null=True, verbose_name='Date start'), + ), + ] diff --git a/apps/ops/migrations/0031_auto_20221116_2024.py b/apps/ops/migrations/0031_auto_20221116_2024.py new file mode 100644 index 000000000..5c132e974 --- /dev/null +++ b/apps/ops/migrations/0031_auto_20221116_2024.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.14 on 2022-11-16 12:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0030_auto_20221116_1811'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='chdir', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir'), + ), + migrations.AddField( + model_name='job', + name='comment', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), + ), + migrations.AddField( + model_name='job', + name='timeout', + field=models.IntegerField(default=60, verbose_name='Timeout (Seconds)'), + ), + ] diff --git a/apps/ops/migrations/0032_auto_20221117_1848.py b/apps/ops/migrations/0032_auto_20221117_1848.py new file mode 100644 index 000000000..ae18c5280 --- /dev/null +++ b/apps/ops/migrations/0032_auto_20221117_1848.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.14 on 2022-11-17 10:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0031_auto_20221116_2024'), + ] + + operations = [ + migrations.RemoveField( + model_name='job', + name='variables', + ), + migrations.AddField( + model_name='job', + name='parameters_define', + field=models.JSONField(default=dict, verbose_name='Parameters define'), + ), + migrations.AddField( + model_name='jobexecution', + name='parameters', + field=models.JSONField(default=dict, verbose_name='Parameters'), + ), + ] diff --git a/apps/ops/migrations/0033_auto_20221118_1431.py b/apps/ops/migrations/0033_auto_20221118_1431.py new file mode 100644 index 000000000..70518eee6 --- /dev/null +++ b/apps/ops/migrations/0033_auto_20221118_1431.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.14 on 2022-11-18 06:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0032_auto_20221117_1848'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='crontab', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform'), + ), + migrations.AddField( + model_name='job', + name='interval', + field=models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform'), + ), + migrations.AddField( + model_name='job', + name='is_periodic', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/ops/migrations/0034_job_org_id.py b/apps/ops/migrations/0034_job_org_id.py new file mode 100644 index 000000000..07926cec3 --- /dev/null +++ b/apps/ops/migrations/0034_job_org_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-23 09:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0033_auto_20221118_1431'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/migrations/0035_jobexecution_org_id.py b/apps/ops/migrations/0035_jobexecution_org_id.py new file mode 100644 index 000000000..1161d10e3 --- /dev/null +++ b/apps/ops/migrations/0035_jobexecution_org_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-23 10:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0034_job_org_id'), + ] + + operations = [ + migrations.AddField( + model_name='jobexecution', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/models/__init__.py b/apps/ops/models/__init__.py index 93b630dd6..b6edb768d 100644 --- a/apps/ops/models/__init__.py +++ b/apps/ops/models/__init__.py @@ -4,3 +4,4 @@ from .adhoc import * from .celery import * from .playbook import * +from .job import * diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 22fd0f054..e94223fb9 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -1,29 +1,43 @@ # ~*~ coding: utf-8 ~*~ import os.path +import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ +from common.db.models import BaseCreateUpdateModel from common.utils import get_logger from .base import BaseAnsibleJob, BaseAnsibleExecution from ..ansible import AdHocRunner __all__ = ["AdHoc", "AdHocExecution"] - logger = get_logger(__file__) -class AdHoc(BaseAnsibleJob): - pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all') - module = models.CharField(max_length=128, default='shell', verbose_name=_('Module')) - args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) - last_execution = models.ForeignKey('AdHocExecution', verbose_name=_("Last execution"), - on_delete=models.SET_NULL, null=True, blank=True) +class AdHoc(BaseCreateUpdateModel): + class Modules(models.TextChoices): + shell = 'shell', _('Shell') + winshell = 'win_shell', _('Powershell') - def get_register_task(self): - from ops.tasks import run_adhoc - return "run_adhoc_{}".format(self.id), run_adhoc, (str(self.id),), {} + 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')) + owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + + @property + def row_count(self): + if len(self.args) == 0: + return 0 + count = str(self.args).count('\n') + return count + 1 + + @property + def size(self): + return len(self.args) def __str__(self): return "{}: {}".format(self.module, self.args) diff --git a/apps/ops/models/base.py b/apps/ops/models/base.py index a37982673..3d6d1438d 100644 --- a/apps/ops/models/base.py +++ b/apps/ops/models/base.py @@ -17,7 +17,8 @@ class BaseAnsibleJob(PeriodTaskModelMixin, JMSOrgBaseModel): 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) + 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: @@ -118,12 +119,6 @@ class BaseAnsibleExecution(models.Model): def is_success(self): return self.status == 'success' - @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 short_id(self): return str(self.id).split('-')[-1] @@ -134,4 +129,8 @@ class BaseAnsibleExecution(models.Model): 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 55f05129f..6e53f868d 100644 --- a/apps/ops/models/celery.py +++ b/apps/ops/models/celery.py @@ -12,18 +12,24 @@ from ops.celery import app class CeleryTask(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) - name = models.CharField(max_length=1024) + name = models.CharField(max_length=1024, verbose_name=_('Name')) last_published_time = models.DateTimeField(null=True) @property def meta(self): task = app.tasks.get(self.name, None) return { - "verbose_name": getattr(task, 'verbose_name', None), - "comment": getattr(task, 'comment', None), + "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] @@ -37,6 +43,9 @@ class CeleryTask(models.Model): return "yellow" return "green" + class Meta: + ordering = ('name',) + class CeleryTaskExecution(models.Model): LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery') @@ -46,9 +55,21 @@ class CeleryTaskExecution(models.Model): 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) - date_start = models.DateTimeField(null=True) - date_finished = models.DateTimeField(null=True) + 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 def __str__(self): return "{}: {}".format(self.name, self.id) 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..f2e7eaa4b --- /dev/null +++ b/apps/ops/models/job.py @@ -0,0 +1,200 @@ +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')) + 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 + name = "run_ops_job_period_{}".format(str(self.id)[:8]) + task = run_ops_job.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 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")) + + 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': + runner = AdHocRunner( + self.inventory_path, self.job.module, module_args=self.job.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.work_path + ) + 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) diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index a0c11db3b..10be7bd06 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -1,39 +1,19 @@ +import os.path +import uuid + +from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ -from orgs.mixins.models import JMSOrgBaseModel -from .base import BaseAnsibleExecution, BaseAnsibleJob +from common.db.models import BaseCreateUpdateModel -class PlaybookTemplate(JMSOrgBaseModel): - name = models.CharField(max_length=128, verbose_name=_("Name")) - path = models.FilePathField(verbose_name=_("Path")) - comment = models.TextField(verbose_name=_("Comment"), blank=True) - - def __str__(self): - return self.name - - class Meta: - ordering = ['name'] - verbose_name = _("Playbook template") - unique_together = [('org_id', 'name')] - - -class Playbook(BaseAnsibleJob): - path = models.FilePathField(max_length=1024, verbose_name=_("Playbook")) +class Playbook(BaseCreateUpdateModel): + 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/') owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True) - comment = models.TextField(blank=True, verbose_name=_("Comment")) - template = models.ForeignKey('PlaybookTemplate', verbose_name=_("Template"), on_delete=models.SET_NULL, null=True) - last_execution = models.ForeignKey('PlaybookExecution', verbose_name=_("Last execution"), on_delete=models.SET_NULL, null=True, blank=True) - def get_register_task(self): - name = "automation_strategy_period_{}".format(str(self.id)[:8]) - task = execute_automation_strategy.name - args = (str(self.id), Trigger.timing) - kwargs = {} - return name, task, args, kwargs - - -class PlaybookExecution(BaseAnsibleExecution): - task = models.ForeignKey('Playbook', verbose_name=_("Task"), on_delete=models.CASCADE) - path = models.FilePathField(max_length=1024, verbose_name=_("Run dir")) + @property + def work_path(self): + return os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__(), "main.yaml") diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 5df047bfa..f5d8d4780 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -1,11 +1,24 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -from rest_framework import serializers -from django.shortcuts import reverse +import datetime + +from rest_framework import serializers + +from common.drf.fields import ReadableHiddenField from ..models import AdHoc, AdHocExecution +class AdHocSerializer(serializers.ModelSerializer): + owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) + row_count = serializers.IntegerField(read_only=True) + size = serializers.IntegerField(read_only=True) + + class Meta: + model = AdHoc + fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"] + + class AdHocExecutionSerializer(serializers.ModelSerializer): stat = serializers.SerializerMethodField() last_success = serializers.ListField(source='success_hosts') @@ -49,26 +62,6 @@ class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer): ] -class AdHocSerializer(serializers.ModelSerializer): - tasks = serializers.ListField() - - class Meta: - model = AdHoc - fields_mini = ['id'] - fields_small = fields_mini + [ - 'tasks', "pattern", "args", "date_created", - ] - fields_fk = ["last_execution"] - fields_m2m = ["assets"] - 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') @@ -80,38 +73,3 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer): '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', '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_m2m = ['hosts'] -# fields = fields_small + 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}) - diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py index 351d63213..8d75d227f 100644 --- a/apps/ops/serializers/celery.py +++ b/apps/ops/serializers/celery.py @@ -30,14 +30,15 @@ class CeleryPeriodTaskSerializer(serializers.ModelSerializer): class CeleryTaskSerializer(serializers.ModelSerializer): class Meta: model = CeleryTask - fields = [ - 'id', 'name', 'meta', 'state', 'last_published_time', - ] + read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'last_published_time'] + fields = read_only_fields class CeleryTaskExecutionSerializer(serializers.ModelSerializer): class Meta: model = CeleryTaskExecution fields = [ - "id", "name", "args", "kwargs", "state", "is_finished", "date_published", "date_start", "date_finished" + "id", "name", "args", "kwargs", "time_cost", "timedelta", "state", "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..e5d76f85b --- /dev/null +++ b/apps/ops/serializers/job.py @@ -0,0 +1,34 @@ +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 + +_all_ = [] + + +class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): + owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = Job + read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"] + fields = read_only_fields + [ + "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner", + "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'] + 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..7ca165501 --- /dev/null +++ b/apps/ops/serializers/playbook.py @@ -0,0 +1,28 @@ +import os + +from rest_framework import serializers + +from common.drf.fields import ReadableHiddenField +from ops.models import Playbook + + +def parse_playbook_name(path): + file_name = os.path.split(path)[-1] + return file_name.split(".")[-2] + + +class PlaybookSerializer(serializers.ModelSerializer): + owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) + + 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 + fields = [ + "id", "name", "path", "date_created", "owner", "date_updated" + ] diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py index dd49a4d94..965bd494c 100644 --- a/apps/ops/signal_handlers.py +++ b/apps/ops/signal_handlers.py @@ -1,14 +1,16 @@ import ast +from celery import signals from django.db import transaction +from django.core.cache import cache 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 django.core.cache import cache -from celery import signals, current_app -from common.db.utils import close_old_connections, get_logger 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 @@ -23,15 +25,15 @@ def sync_registered_tasks(*args, **kwargs): with transaction.atomic(): try: db_tasks = CeleryTask.objects.all() - except Exception as e: - return - celery_task_names = [key for key in app.tasks] - db_task_names = db_tasks.values_list('name', flat=True) + 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) + 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 @@ -45,7 +47,7 @@ def before_task_publish(headers=None, **kwargs): @signals.task_prerun.connect def on_celery_task_pre_run(task_id='', **kwargs): # 更新状态 - CeleryTaskExecution.objects.filter(id=task_id)\ + CeleryTaskExecution.objects.filter(id=task_id) \ .update(state='RUNNING', date_start=timezone.now()) # 关闭之前的数据库连接 close_old_connections() diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 97868a884..51541dd32 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -1,18 +1,16 @@ # coding: utf-8 import os -import random import subprocess from django.conf import settings -from celery import shared_task, subtask -from celery import signals +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 @@ -21,32 +19,19 @@ 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 CeleryTaskExecution, AdHoc, Playbook +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_adhoc(tid, **kwargs): - """ - :param tid: is the tasks serialized data - :param callback: callback function name - :return: - """ - with tmp_to_root_org(): - task = get_object_or_none(AdHoc, id=tid) - if not task: - logger.error("No task found") - return - with tmp_to_org(task.org): - execution = task.create_execution() +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() try: - execution.start(**kwargs) + execution.start() except SoftTimeLimitExceeded: execution.set_error('Run timeout') logger.error("Run adhoc timeout") @@ -55,40 +40,21 @@ def run_adhoc(tid, **kwargs): logger.error("Start adhoc execution error: {}".format(e)) -@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible command")) -def run_playbook(pid, **kwargs): - with tmp_to_root_org(): - task = get_object_or_none(Playbook, id=pid) - if not task: - logger.error("No task found") - return - - with tmp_to_org(task.org): - execution = task.create_execution() +@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution")) +def run_ops_job_executions(execution_id, **kwargs): + execution = get_object_or_none(JobExecution, id=execution_id) + with tmp_to_org(execution.org): try: - execution.start(**kwargs) + execution.start() except SoftTimeLimitExceeded: execution.set_error('Run timeout') - logger.error("Run playbook timeout") + logger.error("Run adhoc timeout") except Exception as e: execution.set_error(e) - logger.error("Run playbook execution error: {}".format(e)) + logger.error("Start adhoc execution error: {}".format(e)) -@shared_task -@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 +@shared_task(verbose_name=_('Periodic clear celery tasks')) @after_app_shutdown_clean_periodic @register_as_period_task(interval=3600 * 24, description=_("Clean celery log period")) def clean_celery_tasks_period(): @@ -107,7 +73,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定时任务""" @@ -130,7 +96,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 @@ -138,37 +104,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(verbose_name=_("Hello"), comment="an test shared task") -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(verbose_name="Hello Error", comment="an test shared task error") -def hello_error(): - raise Exception("must be error") - - -@shared_task(verbose_name="Hello Random", comment="some time error and some time success") -def hello_random(): - i = random.randint(0, 1) - if i == 1: - raise Exception("must be error") - - -@shared_task -def hello_callback(result): - print(result) - print("Hello callback") diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 1edbd16e7..a8b71734f 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.urls import path from rest_framework.routers import DefaultRouter from rest_framework_bulk.routes import BulkRouter -from rest_framework_nested import routers from .. import api @@ -13,23 +12,25 @@ app_name = "ops" router = DefaultRouter() bulk_router = BulkRouter() -router.register(r'adhoc', api.AdHocViewSet, 'adhoc') -router.register(r'adhoc-executions', api.AdHocExecutionViewSet, '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') router.register(r'tasks', api.CeleryTaskViewSet, 'task') - -task_router = routers.NestedDefaultRouter(router, r'tasks', lookup='task') -task_router.register(r'executions', api.CeleryTaskExecutionViewSet, 'task-execution') +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-execution//log/', api.AnsibleTaskLogApi.as_view(), name='ansible-task-log'), ] -urlpatterns += (router.urls + bulk_router.urls + task_router.urls) +urlpatterns += (router.urls + bulk_router.urls) diff --git a/apps/orgs/api.py b/apps/orgs/api.py index 16fde4d69..da9b1530f 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -14,7 +14,7 @@ from .serializers import ( ) from users.models import User, UserGroup from assets.models import ( - Asset, Domain, Label, Node, Gateway, + Asset, Domain, Label, Node, CommandFilter, CommandFilterRule, GatheredUser ) from perms.models import AssetPermission @@ -27,7 +27,7 @@ logger = get_logger(__file__) # 部分 org 相关的 model,需要清空这些数据之后才能删除该组织 org_related_models = [ - User, UserGroup, Asset, Label, Domain, Gateway, Node, Label, + User, UserGroup, Asset, Label, Domain, Node, Label, CommandFilter, CommandFilterRule, GatheredUser, AssetPermission, ] diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 5df387c91..a17cd832b 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -6,7 +6,7 @@ from orgs.utils import current_org, tmp_to_org from common.cache import Cache, IntegerField from common.utils import get_logger from users.models import UserGroup, User -from assets.models import Node, Domain, Gateway, Asset, Account +from assets.models import Node, Domain, Asset, Account from terminal.models import Session from perms.models import AssetPermission @@ -54,7 +54,7 @@ class OrgResourceStatisticsCache(OrgRelatedCache): nodes_amount = IntegerField(queryset=Node.objects) accounts_amount = IntegerField(queryset=Account.objects) domains_amount = IntegerField(queryset=Domain.objects) - gateways_amount = IntegerField(queryset=Gateway.objects) + # gateways_amount = IntegerField(queryset=Gateway.objects) asset_perms_amount = IntegerField(queryset=AssetPermission.objects) total_count_online_users = IntegerField() diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 1e3343931..b3e06362d 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -8,7 +8,7 @@ from users.models import UserGroup, User from users.signals import pre_user_leave_org from terminal.models import Session from rbac.models import OrgRoleBinding, SystemRoleBinding, RoleBinding -from assets.models import Asset, 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 @@ -75,7 +75,6 @@ def on_user_delete_refresh_cache(sender, instance, **kwargs): class OrgResourceStatisticsRefreshUtil: model_cache_field_mapper = { AssetPermission: ['asset_perms_amount'], - Gateway: ['gateways_amount'], Domain: ['domains_amount'], Node: ['nodes_amount'], Asset: ['assets_amount'], diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py index 2136b28a0..666f68fa1 100644 --- a/apps/orgs/signal_handlers/common.py +++ b/apps/orgs/signal_handlers/common.py @@ -7,6 +7,7 @@ from functools import partial import django.db.utils from django.dispatch import receiver from django.conf import settings +from django.db.utils import ProgrammingError, OperationalError from django.utils.functional import LazyObject from django.db.models.signals import post_save, pre_delete, m2m_changed @@ -48,7 +49,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs): if settings.DEBUG: try: set_to_default_org() - except django.db.utils.OperationalError: + except (ProgrammingError, OperationalError): pass def keep_subscribe_org_mapping(): diff --git a/apps/orgs/tasks.py b/apps/orgs/tasks.py index 6b6ec9e0d..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 +@shared_task(verbose_name=_("Refresh organization cache")) def refresh_org_cache_task(*fields): from .caches import OrgResourceStatisticsCache OrgResourceStatisticsCache.refresh(*fields) diff --git a/apps/perms/api/asset_permission.py b/apps/perms/api/asset_permission.py index afadc456c..de15a6c6f 100644 --- a/apps/perms/api/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/perm_token.py b/apps/perms/api/perm_token.py new file mode 100644 index 000000000..63cf08062 --- /dev/null +++ b/apps/perms/api/perm_token.py @@ -0,0 +1,5 @@ +from rest_framework.viewsets import ModelViewSet + + +class PermTokenViewSet(ModelViewSet): + pass diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py index dedd90a3c..48f94f6f7 100644 --- a/apps/perms/api/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -19,7 +19,6 @@ __all__ = [ 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedNodeAssetsApi', 'UserGroupGrantedNodeChildrenAsTreeApi', - 'UserGroupGrantedAssetAccountsApi', ] @@ -191,17 +190,3 @@ class UserGroupGrantedNodeChildrenAsTreeApi(SerializeToTreeNodeMixin, ListAPIVie nodes = self.get_nodes() nodes = self.serialize_nodes(nodes) return Response(data=nodes) - - -class UserGroupGrantedAssetAccountsApi(uapi.UserGrantedAssetAccountsApi): - - @lazyproperty - def user_group(self): - group_id = self.kwargs.get('pk') - return UserGroup.objects.get(id=group_id) - - def get_queryset(self): - accounts = PermAccountUtil().get_perm_accounts_for_user_group_asset( - self.user_group, self.asset, with_actions=True - ) - return accounts diff --git a/apps/perms/api/user_permission/accounts.py b/apps/perms/api/user_permission/accounts.py index 70973d988..96c09667f 100644 --- a/apps/perms/api/user_permission/accounts.py +++ b/apps/perms/api/user_permission/accounts.py @@ -1,52 +1,21 @@ from django.shortcuts import get_object_or_404 from rest_framework.generics import ListAPIView, get_object_or_404 -from common.permissions import IsValidUser from common.utils import get_logger, lazyproperty -from assets.serializers import AccountSerializer -from perms.hands import User, Asset, Account from perms import serializers -from perms.models import Action +from perms.hands import Asset from perms.utils import PermAccountUtil -from .mixin import RoleAdminMixin, RoleUserMixin +from .mixin import SelfOrPKUserMixin logger = get_logger(__name__) - __all__ = [ - 'UserAllGrantedAccountsApi', - 'MyAllGrantedAccountsApi', - 'UserGrantedAssetAccountsApi', - 'MyGrantedAssetAccountsApi', - 'UserGrantedAssetSpecialAccountsApi', - 'MyGrantedAssetSpecialAccountsApi', + 'UserPermedAssetAccountsApi', ] -class UserAllGrantedAccountsApi(RoleAdminMixin, ListAPIView): - """ 授权给用户的所有账号列表 """ - serializer_class = AccountSerializer - filterset_fields = ("name", "username", "privileged", "version") - search_fields = filterset_fields - - def get_queryset(self): - util = PermAccountUtil() - accounts = util.get_perm_accounts_for_user(self.user) - return accounts - - -class MyAllGrantedAccountsApi(RoleUserMixin, UserAllGrantedAccountsApi): - """ 授权给我的所有账号列表 """ - pass - - -class UserGrantedAssetAccountsApi(ListAPIView): - serializer_class = serializers.AccountsGrantedSerializer - - @lazyproperty - def user(self) -> User: - user_id = self.kwargs.get('pk') - return User.objects.get(id=user_id) +class UserPermedAssetAccountsApi(SelfOrPKUserMixin, ListAPIView): + serializer_class = serializers.AccountsPermedSerializer @lazyproperty def asset(self): @@ -56,41 +25,6 @@ class UserGrantedAssetAccountsApi(ListAPIView): return asset def get_queryset(self): - accounts = PermAccountUtil().get_perm_accounts_for_user_asset( - self.user, self.asset, with_actions=True - ) + util = PermAccountUtil() + accounts = util.get_permed_accounts_for_user(self.user, self.asset) return accounts - - -class MyGrantedAssetAccountsApi(UserGrantedAssetAccountsApi): - permission_classes = (IsValidUser,) - - @lazyproperty - def user(self): - return self.request.user - - -class UserGrantedAssetSpecialAccountsApi(ListAPIView): - serializer_class = serializers.AccountsGrantedSerializer - - @lazyproperty - def user(self): - return self.request.user - - def get_queryset(self): - # 构造默认包含的账号,如: @INPUT @USER - accounts = [ - Account.get_input_account(), - Account.get_user_account(self.user.username) - ] - for account in accounts: - account.actions = Action.ALL - return accounts - - -class MyGrantedAssetSpecialAccountsApi(UserGrantedAssetSpecialAccountsApi): - permission_classes = (IsValidUser,) - - @lazyproperty - def user(self): - return self.request.user diff --git a/apps/perms/api/user_permission/assets/api.py b/apps/perms/api/user_permission/assets/api.py index 9c616adc3..0fc3047fd 100644 --- a/apps/perms/api/user_permission/assets/api.py +++ b/apps/perms/api/user_permission/assets/api.py @@ -1,12 +1,12 @@ -from rest_framework.generics import ListAPIView from django.conf import settings +from rest_framework.generics import ListAPIView from common.utils import get_logger -from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin from .mixin import ( UserAllGrantedAssetsQuerysetMixin, UserDirectGrantedAssetsQuerysetMixin, UserFavoriteGrantedAssetsMixin, UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, AssetsTreeFormatMixin, ) +from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin __all__ = [ 'UserDirectGrantedAssetsApi', 'MyDirectGrantedAssetsApi', @@ -22,8 +22,8 @@ logger = get_logger(__name__) class UserDirectGrantedAssetsApi( - AssetRoleAdminMixin, - UserDirectGrantedAssetsQuerysetMixin, AssetsSerializerFormatMixin, ListAPIView + AssetRoleAdminMixin, UserDirectGrantedAssetsQuerysetMixin, + AssetsSerializerFormatMixin, ListAPIView ): """ 直接授权给用户的资产 """ pass @@ -35,8 +35,8 @@ class MyDirectGrantedAssetsApi(AssetRoleUserMixin, UserDirectGrantedAssetsApi): class UserFavoriteGrantedAssetsApi( - AssetRoleAdminMixin, - UserFavoriteGrantedAssetsMixin, AssetsSerializerFormatMixin, ListAPIView + AssetRoleAdminMixin, UserFavoriteGrantedAssetsMixin, + AssetsSerializerFormatMixin, ListAPIView ): """ 用户收藏的授权资产 """ pass @@ -63,8 +63,8 @@ class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTree class UserAllGrantedAssetsApi( - AssetRoleAdminMixin, - UserAllGrantedAssetsQuerysetMixin, AssetsSerializerFormatMixin, ListAPIView + AssetRoleAdminMixin, UserAllGrantedAssetsQuerysetMixin, + AssetsSerializerFormatMixin, ListAPIView ): """ 授权给用户的所有资产 """ pass @@ -81,7 +81,8 @@ class MyAllAssetsAsTreeApi(AssetsTreeFormatMixin, MyAllGrantedAssetsApi): class UserGrantedNodeAssetsApi( - AssetRoleAdminMixin, UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, ListAPIView + AssetRoleAdminMixin, UserGrantedNodeAssetsMixin, + AssetsSerializerFormatMixin, ListAPIView ): """ 授权给用户的节点资产 """ pass diff --git a/apps/perms/api/user_permission/assets/mixin.py b/apps/perms/api/user_permission/assets/mixin.py index d82e39faa..e7a584ef4 100644 --- a/apps/perms/api/user_permission/assets/mixin.py +++ b/apps/perms/api/user_permission/assets/mixin.py @@ -1,11 +1,11 @@ from rest_framework.response import Response from rest_framework.request import Request +from common.utils import get_logger 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.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination from perms import serializers from perms.utils.user_permission import UserGrantedAssetsQueryUtils @@ -21,8 +21,7 @@ class UserDirectGrantedAssetsQuerysetMixin: def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return Asset.objects.none() - user = self.user - assets = UserGrantedAssetsQueryUtils(user) \ + assets = UserGrantedAssetsQueryUtils(self.user) \ .get_direct_granted_assets() \ .prefetch_related('platform') \ .only(*self.only_fields) @@ -32,7 +31,7 @@ class UserDirectGrantedAssetsQuerysetMixin: class UserAllGrantedAssetsQuerysetMixin: only_fields = serializers.AssetGrantedSerializer.Meta.only_fields pagination_class = AllGrantedAssetPagination - ordering_fields = ("name", "address", "port", "cpu_cores") + ordering_fields = ("name", "address") ordering = ('name', ) user: User diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index da9691f38..9ff8ed0f1 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- # +from django.shortcuts import get_object_or_404 from rest_framework.request import Request +from django.utils.translation import ugettext_lazy as _ from common.http import is_true -from common.mixins.api import RoleAdminMixin -from common.mixins.api import RoleUserMixin -from orgs.utils import tmp_to_root_org -from users.models import User +from common.utils import is_uuid +from common.exceptions import JMSObjectDoesNotExist +from common.mixins.api import RoleAdminMixin, RoleUserMixin from perms.utils.user_permission import UserGrantedTreeRefreshController +from rbac.permissions import RBACPermission +from users.models import User class RebuildTreeMixin: @@ -36,3 +39,46 @@ class AssetRoleUserMixin(RebuildTreeMixin, RoleUserMixin): ('get_tree', 'perms.view_myassets'), ('GET', 'perms.view_myassets'), ) + + +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'), + ) + + @property + def admin_rbac_perms(self): + return ( + ('list', 'perms.view_userassets'), + ('retrieve', 'perms.view_userassets'), + ('get_tree', 'perms.view_userassets'), + ('GET', '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')) + else: + raise JMSObjectDoesNotExist(object_name=_('User')) + return user + + def request_user_is_self(self): + return self.kwargs.get('user') in ['my', 'self'] 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/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 df8b46cde..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 perms.models 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/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 9cb0efc76..9041990f2 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -2,4 +2,5 @@ # from .asset_permission import * -from .const import * +from .perm_node import * +from .perm_token import * diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 20186527d..6f48c05d1 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,23 +1,19 @@ -import uuid 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 import models -from django.db.models import F, Q, TextChoices -from common.utils import lazyproperty, date_expired_default -from common.db.models import BaseCreateUpdateModel, UnionQuerySet -from assets.models import Asset, Node, FamilyMixin, Account -from orgs.mixins.models import OrgModelMixin +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 .const import Action, SpecialAccount +from orgs.mixins.models import OrgModelMixin +from perms.const import ActionChoices -__all__ = [ - 'AssetPermission', 'PermNode', - 'UserAssetGrantedTreeNodeRelation', - 'Action' -] +__all__ = ['AssetPermission', 'ActionChoices'] # 使用场景 logger = logging.getLogger(__name__) @@ -41,7 +37,7 @@ class AssetPermissionQuerySet(models.QuerySet): def filter_by_accounts(self, accounts): q = Q(accounts__contains=list(accounts)) | \ - Q(accounts__contains=SpecialAccount.ALL.value) + Q(accounts__contains=Account.AliasAccount.ALL.value) return self.filter(q) @@ -67,20 +63,16 @@ class AssetPermission(OrgModelMixin): ) # 特殊的账号: @ALL, @INPUT @USER 默认包含,将来在全局设置中进行控制. accounts = models.JSONField(default=list, verbose_name=_("Accounts")) - 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") - ) + 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') ) - 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')) - from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket')) 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)() @@ -133,145 +125,9 @@ class AssetPermission(OrgModelMixin): """ asset_ids = self.get_all_assets(flat=True) q = Q(asset_id__in=asset_ids) - if not self.is_perm_all_accounts: + if Account.AliasAccount.ALL 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) - - @property - def is_perm_all_accounts(self): - return SpecialAccount.ALL in self.accounts - - @lazyproperty - def users_amount(self): - return self.users.count() - - @lazyproperty - def user_groups_amount(self): - return self.user_groups.count() - - @lazyproperty - def assets_amount(self): - return self.assets.count() - - @lazyproperty - def nodes_amount(self): - return self.nodes.count() - - 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.name for asset in self.assets.all()] - return names - - def nodes_display(self): - names = [node.full_value for node in self.nodes.all()] - return names - - -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')), - ] diff --git a/apps/perms/models/const.py b/apps/perms/models/const.py deleted file mode 100644 index 6128418b0..000000000 --- a/apps/perms/models/const.py +++ /dev/null @@ -1,48 +0,0 @@ -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from common.db.models import BitOperationChoice - - -__all__ = ['Action', 'SpecialAccount'] - - -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 SpecialAccount(models.TextChoices): - ALL = '@ALL', 'All' 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/serializers/permission.py b/apps/perms/serializers/permission.py index ff19b9dd6..fc2bf1cf3 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -1,75 +1,64 @@ # -*- coding: utf-8 -*- # -from rest_framework import serializers -from rest_framework.fields import empty -from django.utils.translation import ugettext_lazy as _ from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers -from common.drf.fields import ObjectRelatedField -from orgs.mixins.serializers import BulkOrgResourceModelSerializer 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 -from perms.models import AssetPermission, Action -__all__ = ['AssetPermissionSerializer', 'ActionsField'] +__all__ = ["AssetPermissionSerializer", "ActionChoicesField"] -class ActionsField(serializers.MultipleChoiceField): +class ActionChoicesField(BitChoicesField): def __init__(self, **kwargs): - kwargs['choices'] = Action.CHOICES - super().__init__(**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] + 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) + 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 = ActionsField(required=False, allow_null=True, label=_("Actions")) + 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')) + 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_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' + "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', + "users", + "user_groups", + "assets", + "nodes", ] fields = fields_small + fields_m2m - read_only_fields = ['created_by', 'date_created', 'from_ticket'] + read_only_fields = ["created_by", "date_created", "from_ticket"] extra_kwargs = { - 'actions': {'label': _('Actions')}, - 'is_expired': {'label': _('Is expired')}, - 'is_valid': {'label': _('Is valid')}, + "actions": {"label": _("Actions")}, + "is_expired": {"label": _("Is expired")}, + "is_valid": {"label": _("Is valid")}, } def __init__(self, *args, **kwargs): @@ -77,7 +66,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): self.set_actions_field() def set_actions_field(self): - actions = self.fields.get('actions') + actions = self.fields.get("actions") if not actions: return choices = actions._choices @@ -86,9 +75,12 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ + """Perform necessary eager loading of data.""" queryset = queryset.prefetch_related( - 'users', 'user_groups', 'assets', 'nodes', + "users", + "user_groups", + "assets", + "nodes", ) return queryset @@ -96,35 +88,34 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): 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')) + 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') + 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')) + 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') + 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', '') + "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/user_permission.py b/apps/perms/serializers/user_permission.py index 9b7ac091c..1d795d650 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -1,33 +1,36 @@ # -*- coding: utf-8 -*- # -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from assets.const import Category, AllTypes +from assets.serializers.asset.common import AssetProtocolsSerializer from assets.models import Node, Asset, Platform, Account -from perms.serializers.permission import ActionsField +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from perms.serializers.permission import ActionChoicesField __all__ = [ - 'NodeGrantedSerializer', - 'AssetGrantedSerializer', - 'ActionsSerializer', - 'AccountsGrantedSerializer' + 'NodeGrantedSerializer', 'AssetGrantedSerializer', + 'ActionsSerializer', 'AccountsPermedSerializer' ] class AssetGrantedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 """ - platform = serializers.SlugRelatedField( - slug_field='name', queryset=Platform.objects.all(), label=_("Platform") - ) + 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", "protocols", 'domain', - "platform", "comment", "org_id", "is_active" + "id", "name", "address", + 'domain', 'platform', + "comment", "org_id", "is_active", ] - fields = only_fields + ['org_name'] + fields = only_fields + ['protocols', 'category', 'type'] + ['org_name'] read_only_fields = fields @@ -41,17 +44,13 @@ class NodeGrantedSerializer(serializers.ModelSerializer): class ActionsSerializer(serializers.Serializer): - actions = ActionsField(read_only=True) + actions = ActionChoicesField(read_only=True) -class AccountsGrantedSerializer(serializers.ModelSerializer): - """ 授权的账号序列类 """ - - # Todo: 添加前端登录逻辑中需要的一些字段,比如:是否需要手动输入密码 - # need_manual = serializers.BooleanField(label=_('Need manual input')) - actions = ActionsField(read_only=True) +class AccountsPermedSerializer(serializers.ModelSerializer): + actions = ActionChoicesField(read_only=True) class Meta: model = Account - fields = ['id', 'name', 'username', 'actions'] + fields = ['id', 'name', 'username', 'secret_type', 'has_secret', 'actions'] read_only_fields = fields diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py index 568c226ee..9a4b3f10a 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -1,8 +1,9 @@ # coding:utf-8 from .asset_permission import asset_permission_urlpatterns +from .user_permission import user_permission_urlpatterns app_name = 'perms' -urlpatterns = [] -urlpatterns += asset_permission_urlpatterns +urlpatterns = asset_permission_urlpatterns \ + + user_permission_urlpatterns diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index 99605372d..41ffe444a 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -7,81 +7,14 @@ 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') - -user_permission_urlpatterns = [ - # 以 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.UserDirectGrantedAssetsAsTreeApi.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.UserGrantedNodesApi.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.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), - path('nodes//assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'), - - # 未分组的资产 - path('/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), name='user-ungrouped-assets'), - path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'), - - # 收藏的资产 - path('/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'), - path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), name='my-ungrouped-assets'), - - # 获取授权给用户的所有账号 - path('/accounts/', api.UserAllGrantedAccountsApi.as_view(), name='user-accounts'), - path('accounts/', api.MyAllGrantedAccountsApi.as_view(), name='my-accounts'), - - # 获取授权给用户某个资产的所有账号 - path('/assets//accounts/', api.UserGrantedAssetAccountsApi.as_view(), name='user-asset-accounts'), - path('assets//accounts/', api.MyGrantedAssetAccountsApi.as_view(), name='my-asset-accounts'), - # 用户登录资产的特殊账号, @INPUT, @USER 等 - path('/assets/special-accounts/', api.UserGrantedAssetSpecialAccountsApi.as_view(), name='user-special-accounts'), - path('assets/special-accounts/', api.MyGrantedAssetSpecialAccountsApi.as_view(), name='my-special-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'), - - # 获取所有和资产-用户组关联的账号列表 - path('/assets//accounts/', api.UserGroupGrantedAssetAccountsApi.as_view(), name='user-group-asset-accounts'), -] +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 = [ # 授权规则中授权的资产 @@ -92,8 +25,6 @@ permission_urlpatterns = [ 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/user_permission.py b/apps/perms/urls/user_permission.py new file mode 100644 index 000000000..c7413dbbc --- /dev/null +++ b/apps/perms/urls/user_permission.py @@ -0,0 +1,77 @@ +from django.urls import path, include + +from .. import api + +user_permission_urlpatterns = [ + # 以 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.UserDirectGrantedAssetsAsTreeApi.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.UserGrantedNodesApi.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.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), + path('nodes//assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'), + + # 未分组的资产 + path('/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), name='user-ungrouped-assets'), + path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'), + + # 收藏的资产 + path('/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'), + path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), + name='my-ungrouped-assets'), + + # 获取授权给用户某个资产的所有账号 + # user params: ['my', 'self'] or user.id + 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/account.py b/apps/perms/utils/account.py index 8d8f5e743..b394bfbc4 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,5 +1,5 @@ -import time from collections import defaultdict + from assets.models import Account from .permission import AssetPermissionUtil @@ -9,53 +9,69 @@ __all__ = ['PermAccountUtil'] class PermAccountUtil(AssetPermissionUtil): """ 资产授权账号相关的工具 """ - def get_perm_accounts_for_user(self, user, with_actions=False): - """ 获取授权给用户的所有账号 """ - perms = self.get_permissions_for_user(user) - accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) - return accounts + def validate_permission(self, user, asset, account_username): + """ 校验用户有某个资产下某个账号名的权限 + :param user: User + :param asset: Asset + :param account_username: 可能是 @USER @INPUT 字符串 + """ + permed_accounts = self.get_permed_accounts_for_user(user, asset) + accounts_mapper = {account.username: account for account in permed_accounts} + account = accounts_mapper.get(account_username) + return account - def get_perm_accounts_for_user_asset(self, user, asset, with_actions=False, with_perms=False): + def get_permed_accounts_for_user(self, user, asset): """ 获取授权给用户某个资产的账号 """ perms = self.get_permissions_for_user_asset(user, asset) - accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) - if with_perms: - return perms, accounts - return accounts - - def get_perm_accounts_for_user_group_asset(self, user_group, asset, with_actions=False): - """ 获取授权给用户组某个资产的账号 """ - perms = self.get_permissions_for_user_group_asset(user_group, asset) - accounts = self.get_perm_accounts_for_permissions(perms, with_actions=with_actions) - return accounts + permed_accounts = self.get_permed_accounts_from_perms(perms, user, asset) + return permed_accounts @staticmethod - def get_perm_accounts_for_permissions(permissions, with_actions=False): - """ 获取授权规则包含的账号 """ - aid_actions_map = defaultdict(int) - for perm in permissions: - account_ids = perm.get_all_accounts(flat=True) - actions = perm.actions - for aid in account_ids: - aid_actions_map[str(aid)] |= actions - account_ids = list(aid_actions_map.keys()) - accounts = Account.objects.filter(id__in=account_ids).order_by( - 'asset__name', 'name', 'username' - ) - if with_actions: - for account in accounts: - account.actions = aid_actions_map.get(str(account.id)) - return accounts + 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) - def validate_permission(self, user, asset, account_username): - """ 校验用户有某个资产下某个账号名的权限 """ - perms, accounts = self.get_perm_accounts_for_user_asset( - user, asset, with_actions=True, with_perms=True - ) - perm = perms.first() - actions = [] - for account in accounts: - if account.username == account_username: - actions = account.actions - expire_at = perm.date_expired.timestamp() if perm else time.time() - return actions, expire_at + 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/permission.py b/apps/perms/utils/permission.py index fd0ea593b..8e4cd9199 100644 --- a/apps/perms/utils/permission.py +++ b/apps/perms/utils/permission.py @@ -1,12 +1,6 @@ -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, Node -from perms.utils.user_permission import get_user_all_asset_perm_ids +from perms.models import AssetPermission logger = get_logger(__file__) diff --git a/apps/perms/utils/user_permission.py b/apps/perms/utils/user_permission.py index 81a2b9a10..92a42a401 100644 --- a/apps/perms/utils/user_permission.py +++ b/apps/perms/utils/user_permission.py @@ -1,30 +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.models import ( + Asset, FavoriteAsset, AssetQuerySet, NodeQuerySet +) from assets.utils import NodeAssetsUtil -from common.utils import get_logger +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 assets.models import ( - Asset, FavoriteAsset, AssetQuerySet, NodeQuerySet -) -from orgs.models import Organization +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') @@ -120,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): @@ -206,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: @@ -428,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() @@ -605,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): @@ -650,11 +654,9 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase): :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/terminal/api/applet/host.py b/apps/terminal/api/applet/host.py index e5fb1c754..4542f3b8d 100644 --- a/apps/terminal/api/applet/host.py +++ b/apps/terminal/api/applet/host.py @@ -2,16 +2,15 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from common.permissions import IsServiceAccount 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 + AppletHostStartupSerializer, AppletHostDeployAppletSerializer ) -from terminal.models import AppletHost, AppletHostDeployment -from terminal.tasks import run_applet_host_deployment - +from terminal.tasks import run_applet_host_deployment, run_applet_host_deployment_install_applet __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet'] @@ -41,11 +40,24 @@ class AppletHostViewSet(JMSModelViewSet): 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/component/endpoint.py b/apps/terminal/api/component/endpoint.py index 364cd803e..f5252c85f 100644 --- a/apps/terminal/api/component/endpoint.py +++ b/apps/terminal/api/component/endpoint.py @@ -1,17 +1,16 @@ -from rest_framework.decorators import action -from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +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 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 terminal.models import Session, Endpoint, EndpointRule from terminal import serializers - +from terminal.models import Session, Endpoint, EndpointRule __all__ = ['EndpointViewSet', 'EndpointRuleViewSet'] @@ -24,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,12 +56,12 @@ class SmartEndpointViewMixin: asset_id = request.GET.get('asset_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 and token.asset: asset_id = token.asset.id - if asset_id: pk, model = asset_id, Asset elif session_id: @@ -78,8 +76,6 @@ class SmartEndpointViewMixin: def get_target_protocol(self): protocol = None - if isinstance(self.target_instance, Application) and self.target_instance.is_type(Application.APP_TYPE.oracle): - protocol = self.target_instance.get_target_protocol_for_oracle() if not protocol: protocol = self.request.GET.get('protocol') return protocol diff --git a/apps/terminal/api/component/terminal.py b/apps/terminal/api/component/terminal.py index 3133db6eb..e3aa4afb3 100644 --- a/apps/terminal/api/component/terminal.py +++ b/apps/terminal/api/component/terminal.py @@ -1,26 +1,24 @@ # -*- 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 terminal.models import Terminal from terminal import serializers -from terminal import exceptions +from terminal.const import TerminalType +from terminal.models import Terminal __all__ = [ - 'TerminalViewSet', 'TerminalConfig', - 'TerminalRegistrationApi', + 'TerminalViewSet', 'TerminalConfig', + 'TerminalRegistrationApi', 'ConnectMethodListApi' ] logger = logging.getLogger(__file__) @@ -72,3 +70,22 @@ 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): + user_agent = self.request.META['HTTP_USER_AGENT'].lower() + if 'macintosh' in user_agent: + os = 'macos' + elif 'windows' in user_agent: + os = 'windows' + else: + os = 'linux' + 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/automations/deploy_applet_host/__init__.py b/apps/terminal/automations/deploy_applet_host/__init__.py index 8b415ece4..c5e903f35 100644 --- a/apps/terminal/automations/deploy_applet_host/__init__.py +++ b/apps/terminal/automations/deploy_applet_host/__init__.py @@ -1,77 +1,126 @@ -import os import datetime -import shutil +import os import yaml -from django.utils import timezone from django.conf import settings +from django.utils import timezone -from common.utils import get_logger 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): + 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') + 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 generate_playbook(self): - playbook_src = os.path.join(CURRENT_DIR, 'playbook.yml') - base_site_url = settings.BASE_SITE_URL + 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 base_site_url: - base_site_url = "localhost:8080" - with open(playbook_src) as f: - plays = yaml.safe_load(f) - for play in plays: - play['vars'].update(self.deployment.host.deploy_options) - play['vars']['DownloadHost'] = base_site_url + '/download/' - play['vars']['CORE_HOST'] = base_site_url - play['vars']['BOOTSTRAP_TOKEN'] = bootstrap_token - play['vars']['HOST_ID'] = host_id - play['vars']['HOST_NAME'] = self.deployment.host.name + 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("/") - 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 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 = 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 _run(self, **kwargs): + 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 = self.generate_playbook() + playbook = generate_playbook() runner = PlaybookRunner( inventory=inventory, playbook=playbook, project_dir=self.run_dir ) return runner.run(**kwargs) - def run(self, **kwargs): + def _run(self, cb_func: callable, **kwargs): try: self.deployment.date_start = timezone.now() - cb = self._run(**kwargs) + cb = cb_func(**kwargs) self.deployment.status = cb.status except Exception as e: logger.error("Error: {}".format(e)) - self.deployment.status = 'error' + self.deployment.status = "error" finally: self.deployment.date_finished = timezone.now() with safe_db_connection(): 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 index d8b040583..3d86ea52a 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -2,8 +2,7 @@ - hosts: all vars: - DownloadHost: https://demo.jumpserver.org/download - Initial: 0 + APPLET_DOWNLOAD_HOST: https://demo.jumpserver.org HOST_NAME: test HOST_ID: 00000000-0000-0000-0000-000000000000 CORE_HOST: https://demo.jumpserver.org @@ -17,166 +16,166 @@ 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-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: 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: "{{ DownloadHost }}/{{ TinkerInstaller }}" + - 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: 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: 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: "{{ DownloadHost }}/python-3.10.8-amd64.exe" - dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + - 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: 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: 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 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 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 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 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: 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: "{{ DownloadHost }}/pip_packages_v0.0.1.zip" - dest: "{{ ansible_env.TEMP }}\\pip_packages_v0.0.1.zip" + - 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_v0.0.1.zip" - dest: "{{ ansible_env.TEMP }}" + - 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_v0.0.1\requirements.txt' - --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages_v0.0.1' + - 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: "{{ DownloadHost }}/chromedriver_win32.107.zip" - dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip" + - 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.107.zip" - dest: C:\Program Files\JumpServer\drivers + - 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: 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: "{{ DownloadHost }}/googlechromestandaloneenterprise64.msi" - dest: "{{ ansible_env.TEMP }}\\googlechromestandaloneenterprise64.msi" + - 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: 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 }} + - 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: Install tinkerd service + ansible.windows.win_shell: + "tinkerd service install" - - name: Start tinkerd service - ansible.windows.win_shell: - "tinkerd service start" + - 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: 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" - - name: Sync all remote applets - ansible.windows.win_shell: > - echo "TODO: Sync all remote applets" - when: Initial diff --git a/apps/terminal/const.py b/apps/terminal/const.py index acea15238..3f653aaf7 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -1,9 +1,13 @@ # -*- 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 # -------------------------------- @@ -40,6 +44,96 @@ class ComponentLoad(TextChoices): return set(dict(cls.choices).keys()) +class HttpMethod(TextChoices): + web_gui = 'web_gui', 'Web GUI' + web_cli = 'web_cli', 'Web CLI' + + +class NativeClient(TextChoices): + # Koko + ssh = 'ssh', 'SSH' + putty = 'putty', 'PuTTY' + xshell = 'xshell', 'Xshell' + + # Magnus + mysql = 'mysql', 'mysql' + psql = 'psql', 'psql' + sqlplus = 'sqlplus', 'sqlplus' + redis = 'redis-cli', 'redis-cli' + mongodb = 'mongo', 'mongo' + + # Razor + mstsc = 'mstsc', 'Remote Desktop' + + @classmethod + def get_native_clients(cls): + 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_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, os='windows'): + commands = { + 'ssh': 'ssh {username}@{hostname} -p {port}', + 'putty': 'putty -ssh {username}@{hostname} -P {port}', + 'xshell': '-url ssh://root:passwd@192.168.10.100', + 'mysql': 'mysql -h {hostname} -P {port} -u {username} -p', + 'psql': { + 'default': 'psql -h {hostname} -p {port} -U {username} -W', + 'windows': 'psql /h {hostname} /p {port} /U {username} -W', + }, + 'sqlplus': 'sqlplus {username}/{password}@{hostname}:{port}', + 'redis': 'redis-cli -h {hostname} -p {port} -a {password}', + 'mstsc': 'mstsc /v:{hostname}:{port}', + } + 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' @@ -48,11 +142,94 @@ class TerminalType(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_method': HttpMethod.web_cli, + '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_method': HttpMethod.web_gui, + 'listen': [Protocol.http], + 'support': [ + Protocol.mysql, Protocol.postgresql, Protocol.oracle, + Protocol.sqlserver, Protocol.mariadb + ], + 'match': 'm2m' + }, + cls.lion: { + 'web_method': HttpMethod.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_protocols_connect_methods(cls, os): + methods = defaultdict(list) + 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: + if listen_protocol == Protocol.http: + web_protocol = component_protocol['web_method'] + methods[protocol.value].append({ + 'value': web_protocol.value, + 'label': web_protocol.label, + 'type': 'web', + 'component': component.value, + }) + + # Native method + methods[protocol.value].extend([ + {'component': component.value, 'type': 'native', **method} + for method in native_methods[listen_protocol] + ]) + + for protocol, applet_methods in applet_methods.items(): + for method in applet_methods: + method['type'] = 'applet' + method['component'] = cls.tinker.value + methods[protocol].extend(applet_methods) + return methods 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/models/applet/host.py b/apps/terminal/models/applet/host.py index 5a85cbaf8..020c4d98a 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -2,14 +2,14 @@ import os from collections import defaultdict from django.db import models -from django.utils.translation import gettext_lazy as _ 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 -from assets.models import Host __all__ = ['AppletHost', 'AppletHostDeployment'] @@ -44,13 +44,11 @@ class AppletHost(Host): raise ValidationError('Request user has no terminal') self.date_synced = timezone.now() - if not self.terminal: + if self.terminal == request_terminal: + self.save(update_fields=['date_synced']) + else: self.terminal = request_terminal self.save(update_fields=['terminal', 'date_synced']) - elif self.terminal and self.terminal != request_terminal: - raise ValidationError('Terminal has been set') - else: - self.save(update_fields=['date_synced']) def check_applets_state(self, applets_value_list): applets = self.applets.all() @@ -107,8 +105,26 @@ class AppletHostDeployment(JMSBaseModel): 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/terminal.py b/apps/terminal/models/component/terminal.py index 11a2a9a61..1d65ad32a 100644 --- a/apps/terminal/models/component/terminal.py +++ b/apps/terminal/models/component/terminal.py @@ -1,19 +1,16 @@ -import uuid import time +import uuid -from django.utils import timezone -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, lazyproperty -from users.models import User from orgs.utils import tmp_to_root_org -from terminal.const import TerminalType as TypeChoices, ComponentLoad as StatusChoice +from terminal.const import TerminalType as TypeChoices +from users.models import User from ..session import Session - logger = get_logger(__file__) @@ -87,7 +84,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) 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) + user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, + on_delete=models.CASCADE) is_deleted = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) comment = models.TextField(blank=True, verbose_name=_('Comment')) @@ -160,4 +158,3 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): permissions = ( ('view_terminalconfig', _('Can view terminal config')), ) - diff --git a/apps/terminal/serializers/applet_host.py b/apps/terminal/serializers/applet_host.py index b94d615c8..c81258892 100644 --- a/apps/terminal/serializers/applet_host.py +++ b/apps/terminal/serializers/applet_host.py @@ -1,19 +1,18 @@ -from rest_framework import serializers from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers -from common.validators import ProjectUniqueValidator -from common.drf.fields import ObjectRelatedField, LabeledChoiceField from assets.models import Platform, Account from assets.serializers import HostSerializer -from ..models import AppletHost, AppletHostDeployment, Applet +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', + 'AppletHostStartupSerializer', 'AppletHostDeployAppletSerializer' ] @@ -29,7 +28,8 @@ class DeployOptionsSerializer(serializers.Serializer): 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_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")) @@ -86,7 +86,7 @@ class HostAppletSerializer(AppletSerializer): class AppletHostDeploymentSerializer(serializers.ModelSerializer): class Meta: model = AppletHostDeployment - fields_mini = ['id', 'host', 'status'] + fields_mini = ['id', 'host', 'status', 'task'] read_only_fields = [ 'status', 'date_created', 'date_updated', 'date_start', 'date_finished' @@ -94,6 +94,18 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer): 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 diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 4b2e3614e..df32d89c2 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -1,8 +1,8 @@ -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers -from common.drf.serializers import BulkModelSerializer 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 .. import const @@ -133,3 +133,9 @@ 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) + group = serializers.CharField(max_length=128) diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 396369d16..4e67d1fc7 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -1,26 +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 ) -from .models import ( - Status, Session, Command, Task, AppletHost, - AppletHostDeployment -) 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 @@ -57,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(): @@ -114,3 +113,10 @@ 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 3e39b55ce..fb7087850 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -31,7 +31,6 @@ 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') - urlpatterns = [ path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'), path('terminal-registrations/', api.TerminalRegistrationApi.as_view(), name='terminal-registration'), @@ -44,10 +43,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'), + path('components/connect-methods/', api.ConnectMethodListApi.as_view(), name='connect-methods'), ] old_version_urlpatterns = [ @@ -55,6 +57,3 @@ old_version_urlpatterns = [ ] urlpatterns += router.urls + old_version_urlpatterns - - - 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/assignee.py b/apps/tickets/api/assignee.py deleted file mode 100644 index e69de29bb..000000000 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 12b1cab1b..8cf58bd6e 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -5,19 +5,17 @@ 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, ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket ) +from tickets.permissions.ticket import IsAssignee, IsApplicant __all__ = [ 'TicketViewSet', 'ApplyAssetTicketViewSet', @@ -27,10 +25,8 @@ __all__ = [ 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 @@ -40,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 = { @@ -98,28 +94,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 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 f52c74930..ccd044bbe 100644 --- a/apps/tickets/const.py +++ b/apps/tickets/const.py @@ -6,18 +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') - 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): @@ -27,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): @@ -52,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 536f06b56..e7cd5944c 100644 --- a/apps/tickets/filters.py +++ b/apps/tickets/filters.py @@ -1,5 +1,5 @@ -from django_filters import rest_framework as filters from django.db.models import Subquery, OuterRef +from django_filters import rest_framework as filters from common.drf.filters import BaseFilterSet diff --git a/apps/tickets/handlers/apply_asset.py b/apps/tickets/handlers/apply_asset.py index 3f2051120..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 perms.models import AssetPermission from orgs.utils import tmp_to_org +from perms.models import AssetPermission 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): @@ -27,6 +26,7 @@ class Handler(BaseHandler): 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( @@ -46,19 +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.users.add(self.ticket.applicant) return asset_permission 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/0017_auto_20220623_1027.py b/apps/tickets/migrations/0017_auto_20220623_1027.py index 87752a469..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+)\)') 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/apply_application.py b/apps/tickets/models/ticket/apply_application.py index 115d45a9f..3c1f4b6f7 100644 --- a/apps/tickets/models/ticket/apply_application.py +++ b/apps/tickets/models/ticket/apply_application.py @@ -1,7 +1,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from perms.models import Action from .general import Ticket __all__ = ['ApplyApplicationTicket'] @@ -23,14 +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_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 c0ec46cc0..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'] @@ -15,15 +15,9 @@ class ApplyAssetTicket(Ticket): # 申请信息 apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets')) apply_accounts = models.JSONField(default=list, verbose_name=_('Apply accounts')) - apply_actions = models.IntegerField( - choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions') - ) + 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 601102ba3..ac70eaadb 100644 --- a/apps/tickets/models/ticket/command_confirm.py +++ b/apps/tickets/models/ticket/command_confirm.py @@ -10,8 +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_account = models.CharField(max_length=128, default='', verbose_name=_('Run account')) 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 2b97cd7a2..8761bc7fe 100644 --- a/apps/tickets/models/ticket/login_asset_confirm.py +++ b/apps/tickets/models/ticket/login_asset_confirm.py @@ -8,12 +8,10 @@ __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_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 e949fa8d6..f48e16501 100644 --- a/apps/tickets/serializers/flow.py +++ b/apps/tickets/serializers/flow.py @@ -1,6 +1,7 @@ +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 diff --git a/apps/tickets/serializers/ticket/__init__.py b/apps/tickets/serializers/ticket/__init__.py index 7b1bdfe13..73fc3b122 100644 --- a/apps/tickets/serializers/ticket/__init__.py +++ b/apps/tickets/serializers/ticket/__init__.py @@ -1,6 +1,6 @@ +from .common import * from .ticket import * from .apply_asset 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_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index a2cb94179..cc4b6aa9a 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -1,40 +1,36 @@ -from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ -from perms.serializers.permission 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_accounts', 'apply_actions', 'org_id', 'comment', - 'apply_date_start', 'apply_date_expired' + 'apply_nodes', 'apply_assets', 'apply_accounts', + 'apply_actions', 'apply_date_start', 'apply_date_expired' ] - 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_accounts': {'required': False, 'allow_empty': True}, + 'apply_accounts': {'required': False}, } extra_kwargs.update(ticket_extra_kwargs) @@ -45,6 +41,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') @@ -56,29 +53,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() - - 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) + 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 fced49976..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_account', - '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 f38f97a43..1af361693 100644 --- a/apps/tickets/serializers/ticket/common.py +++ b/apps/tickets/serializers/ticket/common.py @@ -1,12 +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 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): @@ -34,7 +34,7 @@ class DefaultPermissionName(object): return self.default -class BaseApplyAssetApplicationSerializer(serializers.Serializer): +class BaseApplyAssetSerializer(serializers.Serializer): permission_model: Model @property @@ -75,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 43d54d327..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_account' - ] + 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 461e61870..dbdf89cc0 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -1,33 +1,36 @@ # -*- 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 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) @@ -41,43 +44,20 @@ 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 class TicketApplySerializer(TicketSerializer): - org_id = serializers.CharField( - required=True, max_length=36, - allow_blank=True, label=_("Organization") - ) - - class Meta: - model = Ticket - fields = TicketSerializer.Meta.fields - extra_kwargs = { - 'type': {'required': True} - } + org_id = serializers.CharField(required=True, max_length=36, allow_blank=True, label=_("Organization")) @staticmethod def validate_org_id(org_id): @@ -93,11 +73,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)\ + 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) - return attrs diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index 6602e207c..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,16 +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-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 d742437f8..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.const import TicketType +from tickets.errors import AlreadyClosed from tickets.models import ( Ticket, ApplyAssetTicket, ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket ) -from tickets.const import TicketType -from tickets.errors import AlreadyClosed from common.utils import get_logger, FlashMessageUtil logger = get_logger(__name__) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 833752727..d84a7baa0 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -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/entrypoint.sh b/entrypoint.sh index fe81b1470..58ed0b104 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,6 +11,9 @@ action="${1-start}" service="${2-all}" trap cleanup EXIT + +rm -f /opt/jumpserver/tmp/*.pid + if [[ "$action" == "bash" || "$action" == "sh" ]];then bash elif [[ "$action" == "sleep" ]];then @@ -19,4 +22,3 @@ elif [[ "$action" == "sleep" ]];then else python jms "${action}" "${service}" fi -