diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index e50aae839..25f21a6f4 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -20,6 +20,8 @@ jobs: run: | TAG=$(basename ${GITHUB_REF}) VERSION=${TAG/v/} + wget https://raw.githubusercontent.com/jumpserver/installer/v${VERSION}/quick_start.sh + sed -i "s@Version=.*@Version=v${VERSION}@g" quick_start.sh echo "::set-output name=TAG::$TAG" echo "::set-output name=VERSION::$VERSION" - name: Create Release @@ -31,6 +33,16 @@ jobs: config-name: release-config.yml version: ${{ steps.get_version.outputs.TAG }} tag: ${{ steps.get_version.outputs.TAG }} + - name: Upload Quick Start Script + id: upload-release-quick-start-shell + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./quick_start.sh + asset_name: quick_start.sh + asset_content_type: application/text build-and-release: needs: create-realese @@ -43,4 +55,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ needs.create-realese.outputs.upload_url }} \ No newline at end of file + upload_url: ${{ needs.create-realese.outputs.upload_url }} diff --git a/.github/workflows/sync-gitee.yml b/.github/workflows/sync-gitee.yml new file mode 100644 index 000000000..55871042a --- /dev/null +++ b/.github/workflows/sync-gitee.yml @@ -0,0 +1,23 @@ +name: 🔀 Sync mirror to Gitee + +on: + push: + branches: + - master + - dev + create: + +jobs: + mirror: + runs-on: ubuntu-latest + if: github.repository == 'jumpserver/jumpserver' + steps: + - name: mirror + continue-on-error: true + if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag') + uses: wearerequired/git-mirror-action@v1 + env: + SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} + with: + source-repo: 'git@github.com:jumpserver/jumpserver.git' + destination-repo: 'git@gitee.com:jumpserver/jumpserver.git' diff --git a/Dockerfile b/Dockerfile index fd551c2e3..a013c074d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # 编译代码 -FROM python:3.8.6-slim as stage-build +FROM python:3.8-slim as stage-build MAINTAINER JumpServer Team ARG VERSION ENV VERSION=$VERSION @@ -9,7 +9,7 @@ ADD . . RUN cd utils && bash -ixeu build.sh # 构建运行时环境 -FROM python:3.8.6-slim +FROM python:3.8-slim ARG PIP_MIRROR=https://pypi.douban.com/simple ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple diff --git a/README.md b/README.md index 879be93a2..3fdcb7fde 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju ### License & Copyright -Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved. +Copyright (c) 2014-2022 飞致云 FIT2CLOUD, All rights reserved. Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/README_EN.md b/README_EN.md index 4e9bf0e6c..bb2b15097 100644 --- a/README_EN.md +++ b/README_EN.md @@ -85,7 +85,7 @@ If you find a security problem, please contact us directly: - 400-052-0755 ### License & Copyright -Copyright (c) 2014-2021 FIT2CLOUD Tech, Inc., All rights reserved. +Copyright (c) 2014-2022 FIT2CLOUD Tech, Inc., All rights reserved. Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 91c0b5b49..f45f4f2ff 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -60,7 +60,8 @@ class LoginAssetCheckAPI(CreateAPIView): 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'ticket_detail_url': ticket_detail_url, - 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees] + 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees], + 'ticket_id': str(ticket.id) } return data diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index 9428df39f..ef00fe8c1 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -1,6 +1,6 @@ # coding: utf-8 # - +from django.shortcuts import get_object_or_404 from orgs.mixins.api import OrgBulkModelViewSet from rest_framework.decorators import action from rest_framework.response import Response diff --git a/apps/applications/api/mixin.py b/apps/applications/api/mixin.py index 59bc51467..e17755ea8 100644 --- a/apps/applications/api/mixin.py +++ b/apps/applications/api/mixin.py @@ -1,13 +1,21 @@ +from urllib.parse import urlencode, parse_qsl + from django.utils.translation import ugettext as _ +from rest_framework.generics import get_object_or_404 from common.tree import TreeNode from orgs.models import Organization +from assets.models import SystemUser +from applications.utils import KubernetesClient, KubernetesTree +from perms.utils.application.permission import get_application_system_user_ids + from ..models import Application __all__ = ['SerializeApplicationToTreeNodeMixin'] class SerializeApplicationToTreeNodeMixin: + @staticmethod def filter_organizations(applications): organization_ids = set(applications.values_list('org_id', flat=True)) @@ -31,25 +39,47 @@ class SerializeApplicationToTreeNodeMixin: }) return node - def serialize_applications_with_org(self, applications): + def serialize_applications_with_org(self, applications, tree_id, parent_info, user): + tree_nodes = [] if not applications: - return [] - root_node = self.create_root_node() - tree_nodes = [root_node] - organizations = self.filter_organizations(applications) + return tree_nodes - for i, org in enumerate(organizations): - # 组织节点 - org_node = org.as_tree_node(pid=root_node.id) - tree_nodes.append(org_node) - org_applications = applications.filter(org_id=org.id) - count = org_applications.count() - org_node.name += '({})'.format(count) + if not tree_id: + root_node = self.create_root_node() + tree_nodes.append(root_node) + organizations = self.filter_organizations(applications) + for i, org in enumerate(organizations): + tree_id = urlencode({'org_id': str(org.id)}) + apps = applications.filter(org_id=org.id) + # 组织节点 + org_node = org.as_tree_node(oid=tree_id, pid=root_node.id) + org_node.name += '({})'.format(apps.count()) + tree_nodes.append(org_node) + category_type_nodes = Application.create_category_type_tree_nodes( + apps, tree_id, show_empty=False + ) + tree_nodes += category_type_nodes - # 各应用节点 - apps_nodes = Application.create_tree_nodes( - queryset=org_applications, root_node=org_node, - show_empty=False - ) - tree_nodes += apps_nodes + for app in apps: + app_node = app.as_tree_node(tree_id, is_luna=True) + tree_nodes.append(app_node) + return tree_nodes + + parent_info = dict(parse_qsl(parent_info)) + pod_name = parent_info.get('pod') + app_id = parent_info.get('app_id') + namespace = parent_info.get('namespace') + system_user_id = parent_info.get('system_user_id') + if app_id and not any([pod_name, namespace, system_user_id]): + app = get_object_or_404(Application, id=app_id) + system_user_ids = get_application_system_user_ids(user, app) + system_users = SystemUser.objects.filter(id__in=system_user_ids).order_by('priority') + for system_user in system_users: + system_user_node = KubernetesTree(tree_id).as_system_user_tree_node( + system_user, parent_info + ) + tree_nodes.append(system_user_node) + return tree_nodes + + tree_nodes = KubernetesTree(tree_id).async_tree_node(parent_info) return tree_nodes diff --git a/apps/applications/const.py b/apps/applications/const.py index dde571fbc..79a76b593 100644 --- a/apps/applications/const.py +++ b/apps/applications/const.py @@ -17,6 +17,7 @@ class AppCategory(TextChoices): class AppType(TextChoices): # db category mysql = 'mysql', 'MySQL' + redis = 'redis', 'Redis' oracle = 'oracle', 'Oracle' pgsql = 'postgresql', 'PostgreSQL' mariadb = 'mariadb', 'MariaDB' @@ -34,7 +35,9 @@ class AppType(TextChoices): @classmethod def category_types_mapper(cls): return { - AppCategory.db: [cls.mysql, cls.oracle, cls.pgsql, cls.mariadb, cls.sqlserver], + AppCategory.db: [ + cls.mysql, cls.oracle, cls.redis, cls.pgsql, cls.mariadb, cls.sqlserver + ], AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom], AppCategory.cloud: [cls.k8s] } @@ -62,7 +65,3 @@ class AppType(TextChoices): @classmethod def cloud_types(cls): return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]] - - - - diff --git a/apps/applications/migrations/0015_auto_20220112_2035.py b/apps/applications/migrations/0015_auto_20220112_2035.py new file mode 100644 index 000000000..3f91878df --- /dev/null +++ b/apps/applications/migrations/0015_auto_20220112_2035.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2022-01-12 12:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0014_auto_20211105_1605'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='type', + field=models.CharField(choices=[('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index 5df8e3dfd..725736aeb 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -1,4 +1,5 @@ from collections import defaultdict +from urllib.parse import urlencode, parse_qsl from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -7,6 +8,8 @@ from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin from common.tree import TreeNode from assets.models import Asset, SystemUser + +from ..utils import KubernetesTree from .. import const @@ -16,6 +19,13 @@ class ApplicationTreeNodeMixin: type: str category: str + @staticmethod + def create_tree_id(pid, type, v): + i = dict(parse_qsl(pid)) + i[type] = v + tree_id = urlencode(i) + return tree_id + @classmethod def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None, show_empty=True, show_count=True): @@ -65,13 +75,13 @@ class ApplicationTreeNodeMixin: return node @classmethod - def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True): + def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True): nodes = [] categories = const.AppType.category_types_mapper().keys() for category in categories: - i = root_node.id + '_' + category.value + i = cls.create_tree_id(pid, 'category', category.value) node = cls.create_choice_node( - category, i, pid=root_node.id, tp='category', + category, i, pid=pid, tp='category', counts=counts, opened=False, show_empty=show_empty, show_count=show_count ) @@ -81,17 +91,20 @@ class ApplicationTreeNodeMixin: return nodes @classmethod - def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True): + def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True): nodes = [] + temp_pid = pid type_category_mapper = const.AppType.type_category_mapper() - for tp in const.AppType.type_category_mapper().keys(): + types = const.AppType.type_category_mapper().keys() + for tp in types: category = type_category_mapper.get(tp) - pid = root_node.id + '_' + category.value - i = root_node.id + '_' + tp.value + pid = cls.create_tree_id(pid, 'category', category.value) + i = cls.create_tree_id(pid, 'type', tp.value) node = cls.create_choice_node( tp, i, pid, tp='type', counts=counts, opened=False, show_empty=show_empty, show_count=show_count ) + pid = temp_pid if not node: continue nodes.append(node) @@ -109,40 +122,63 @@ class ApplicationTreeNodeMixin: return counts @classmethod - def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True): + def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True): counts = cls.get_tree_node_counts(queryset) tree_nodes = [] + # 类别的节点 + tree_nodes += cls.create_category_tree_nodes( + pid, counts, show_empty=show_empty, + show_count=show_count + ) + + # 类型的节点 + tree_nodes += cls.create_types_tree_nodes( + pid, counts, show_empty=show_empty, + show_count=show_count + ) + + return tree_nodes + + @classmethod + def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True): + tree_nodes = [] + # 根节点有可能是组织名称 if root_node is None: root_node = cls.create_root_tree_node(queryset, show_count=show_count) tree_nodes.append(root_node) - # 类别的节点 - tree_nodes += cls.create_category_tree_nodes( - root_node, counts, show_empty=show_empty, - show_count=show_count - ) - - # 类型的节点 - tree_nodes += cls.create_types_tree_nodes( - root_node, counts, show_empty=show_empty, - show_count=show_count + tree_nodes += cls.create_category_type_tree_nodes( + queryset, root_node.id, show_empty=show_empty, show_count=show_count ) # 应用的节点 for app in queryset: - pid = root_node.id + '_' + app.type - tree_nodes.append(app.as_tree_node(pid)) + node = app.as_tree_node(root_node.id) + tree_nodes.append(node) return tree_nodes - def as_tree_node(self, pid): + def create_app_tree_pid(self, root_id): + pid = self.create_tree_id(root_id, 'category', self.category) + pid = self.create_tree_id(pid, 'type', self.type) + return pid + + def as_tree_node(self, pid, is_luna=False): + if is_luna and self.type == const.AppType.k8s: + node = KubernetesTree(pid).as_tree_node(self) + else: + node = self._as_tree_node(pid) + return node + + def _as_tree_node(self, pid): icon_skin_category_mapper = { 'remote_app': 'chrome', 'db': 'database', 'cloud': 'cloud' } icon_skin = icon_skin_category_mapper.get(self.category, 'file') + pid = self.create_app_tree_pid(pid) node = TreeNode(**{ 'id': str(self.id), 'name': self.name, diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 6fecd3a67..2d2273dc7 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -96,7 +96,7 @@ class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = models.Account fields_mini = ['id', 'username', 'version'] - fields_write_only = ['password', 'private_key'] + fields_write_only = ['password', 'private_key', 'passphrase'] fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display'] fields = fields_mini + fields_fk + fields_write_only + [ 'type', 'type_display', 'category', 'category_display', diff --git a/apps/applications/serializers/attrs/application_type/__init__.py b/apps/applications/serializers/attrs/application_type/__init__.py index 8881dedc0..94a6f921d 100644 --- a/apps/applications/serializers/attrs/application_type/__init__.py +++ b/apps/applications/serializers/attrs/application_type/__init__.py @@ -1,5 +1,6 @@ from .mysql import * +from .redis import * from .mariadb import * from .oracle import * from .pgsql import * diff --git a/apps/applications/serializers/attrs/application_type/redis.py b/apps/applications/serializers/attrs/application_type/redis.py new file mode 100644 index 000000000..3ba9ea715 --- /dev/null +++ b/apps/applications/serializers/attrs/application_type/redis.py @@ -0,0 +1,15 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from ..application_category import DBSerializer + + +__all__ = ['RedisSerializer'] + + +class RedisSerializer(DBSerializer): + port = serializers.IntegerField(default=6379, label=_('Port'), allow_null=True) + + + + diff --git a/apps/applications/serializers/attrs/attrs.py b/apps/applications/serializers/attrs/attrs.py index 4bd3f81eb..f249d7816 100644 --- a/apps/applications/serializers/attrs/attrs.py +++ b/apps/applications/serializers/attrs/attrs.py @@ -25,6 +25,7 @@ category_serializer_classes_mapping = { type_serializer_classes_mapping = { # db const.AppType.mysql.value: application_type.MySQLSerializer, + const.AppType.redis.value: application_type.RedisSerializer, const.AppType.mariadb.value: application_type.MariaDBSerializer, const.AppType.oracle.value: application_type.OracleSerializer, const.AppType.pgsql.value: application_type.PostgreSerializer, diff --git a/apps/applications/utils/__init__.py b/apps/applications/utils/__init__.py new file mode 100644 index 000000000..5efec40b2 --- /dev/null +++ b/apps/applications/utils/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# + +from .kubernetes_util import * diff --git a/apps/applications/utils/kubernetes_util.py b/apps/applications/utils/kubernetes_util.py new file mode 100644 index 000000000..e95922d48 --- /dev/null +++ b/apps/applications/utils/kubernetes_util.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +from urllib3.exceptions import MaxRetryError +from urllib.parse import urlencode + +from kubernetes.client import api_client +from kubernetes.client.api import core_v1_api +from kubernetes import client +from kubernetes.client.exceptions import ApiException + +from rest_framework.generics import get_object_or_404 + +from common.utils import get_logger +from common.tree import TreeNode +from assets.models import SystemUser + +from .. import const + +logger = get_logger(__file__) + + +class KubernetesClient: + def __init__(self, url, token): + self.url = url + self.token = token + + def get_api(self): + configuration = client.Configuration() + configuration.host = self.url + configuration.verify_ssl = False + configuration.api_key = {"authorization": "Bearer " + self.token} + c = api_client.ApiClient(configuration=configuration) + api = core_v1_api.CoreV1Api(c) + return api + + def get_namespace_list(self): + api = self.get_api() + namespace_list = [] + for ns in api.list_namespace().items: + namespace_list.append(ns.metadata.name) + return namespace_list + + def get_services(self): + api = self.get_api() + ret = api.list_service_for_all_namespaces(watch=False) + for i in ret.items: + print("%s \t%s \t%s \t%s \t%s \n" % ( + i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports)) + + def get_pod_info(self, namespace, pod): + api = self.get_api() + resp = api.read_namespaced_pod(namespace=namespace, name=pod) + return resp + + def get_pod_logs(self, namespace, pod): + api = self.get_api() + log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200) + return log_content + + def get_pods(self): + api = self.get_api() + try: + ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3)) + except MaxRetryError: + logger.warning('Kubernetes connection timed out') + return + except ApiException as e: + if e.status == 401: + logger.warning('Kubernetes User not authenticated') + else: + logger.warning(e) + return + data = {} + for i in ret.items: + namespace = i.metadata.namespace + pod_info = { + 'pod_name': i.metadata.name, + 'containers': [j.name for j in i.spec.containers] + } + if namespace in data: + data[namespace].append(pod_info) + else: + data[namespace] = [pod_info, ] + return data + + @staticmethod + def get_kubernetes_data(app_id, system_user_id): + from ..models import Application + app = get_object_or_404(Application, id=app_id) + system_user = get_object_or_404(SystemUser, id=system_user_id) + k8s = KubernetesClient(app.attrs['cluster'], system_user.token) + return k8s.get_pods() + + +class KubernetesTree: + def __init__(self, tree_id): + self.tree_id = tree_id + + def as_tree_node(self, app): + pid = app.create_app_tree_pid(self.tree_id) + app_id = str(app.id) + parent_info = {'app_id': app_id} + node = self.create_tree_node( + app_id, pid, app.name, 'k8s', parent_info + ) + return node + + def as_system_user_tree_node(self, system_user, parent_info): + from ..models import ApplicationTreeNodeMixin + system_user_id = str(system_user.id) + username = system_user.username + username = username if username else '*' + name = f'{system_user.name}({username})' + pid = urlencode({'app_id': self.tree_id}) + i = ApplicationTreeNodeMixin.create_tree_id(pid, 'system_user_id', system_user_id) + parent_info.update({'system_user_id': system_user_id}) + node = self.create_tree_node( + i, pid, name, 'system_user', parent_info, icon='user-tie' + ) + return node + + def as_namespace_pod_tree_node(self, name, meta, type, counts=0, is_container=False): + from ..models import ApplicationTreeNodeMixin + i = ApplicationTreeNodeMixin.create_tree_id(self.tree_id, type, name) + meta.update({type: name}) + name = name if is_container else f'{name}({counts})' + node = self.create_tree_node( + i, self.tree_id, name, type, meta, icon='cloud', is_container=is_container + ) + return node + + @staticmethod + def create_tree_node(id_, pid, name, identity, parent_info, icon='', is_container=False): + node = TreeNode(**{ + 'id': id_, + 'name': name, + 'title': name, + 'pId': pid, + 'isParent': not is_container, + 'open': False, + 'iconSkin': icon, + 'parentInfo': urlencode(parent_info), + 'meta': { + 'type': 'application', + 'data': { + 'category': const.AppCategory.cloud, + 'type': const.AppType.k8s, + 'identity': identity + } + } + }) + return node + + def async_tree_node(self, parent_info): + pod_name = parent_info.get('pod') + app_id = parent_info.get('app_id') + namespace = parent_info.get('namespace') + system_user_id = parent_info.get('system_user_id') + + tree_nodes = [] + data = KubernetesClient.get_kubernetes_data(app_id, system_user_id) + if not data: + return tree_nodes + + if pod_name: + for container in next( + filter( + lambda x: x['pod_name'] == pod_name, data[namespace] + ) + )['containers']: + container_node = self.as_namespace_pod_tree_node( + container, parent_info, 'container', is_container=True + ) + tree_nodes.append(container_node) + elif namespace: + for pod in data[namespace]: + pod_nodes = self.as_namespace_pod_tree_node( + pod['pod_name'], parent_info, 'pod', len(pod['containers']) + ) + tree_nodes.append(pod_nodes) + elif system_user_id: + for namespace, pods in data.items(): + namespace_node = self.as_namespace_pod_tree_node( + namespace, parent_info, 'namespace', len(pods) + ) + tree_nodes.append(namespace_node) + return tree_nodes diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 3178c8e3f..45afd4379 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -10,3 +10,4 @@ from .domain import * from .cmd_filter import * from .gathered_user import * from .favorite_asset import * +from .backup import * diff --git a/apps/assets/api/backup.py b/apps/assets/api/backup.py new file mode 100644 index 000000000..cdd26b406 --- /dev/null +++ b/apps/assets/api/backup.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import status, mixins, viewsets +from rest_framework.response import Response + +from common.permissions import IsOrgAdmin +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import serializers +from ..tasks import execute_account_backup_plan +from ..models import ( + AccountBackupPlan, AccountBackupPlanExecution +) + +__all__ = [ + 'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet' +] + + +class AccountBackupPlanViewSet(OrgBulkModelViewSet): + model = AccountBackupPlan + filter_fields = ('name',) + search_fields = filter_fields + ordering_fields = ('name',) + ordering = ('name',) + serializer_class = serializers.AccountBackupPlanSerializer + permission_classes = (IsOrgAdmin,) + + +class AccountBackupPlanExecutionViewSet( + mixins.CreateModelMixin, mixins.ListModelMixin, + mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + serializer_class = serializers.AccountBackupPlanExecutionSerializer + search_fields = ('trigger', 'plan_id') + filterset_fields = search_fields + permission_classes = (IsOrgAdmin,) + + def get_queryset(self): + queryset = AccountBackupPlanExecution.objects.all() + return queryset + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + pid = serializer.data.get('plan') + task = execute_account_backup_plan.delay( + pid=pid, trigger=AccountBackupPlanExecution.Trigger.manual + ) + return Response({'task': task.id}, status=status.HTTP_201_CREATED) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = queryset.order_by('-date_start') + return queryset diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 7e4ff61aa..c7f32b306 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -17,7 +17,7 @@ from common.mixins.api import SuggestionMixin from assets.models import Asset from common.utils import get_logger, get_object_or_none from common.tree import TreeNodeSerializer -from orgs.mixins.api import OrgModelViewSet +from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics from orgs.utils import current_org from ..hands import IsOrgAdmin @@ -42,19 +42,13 @@ __all__ = [ ] -class NodeViewSet(SuggestionMixin, OrgModelViewSet): +class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet): model = Node filterset_fields = ('value', 'key', 'id') search_fields = ('value', ) permission_classes = (IsOrgAdmin,) serializer_class = serializers.NodeSerializer - # 仅支持根节点指直接创建,子节点下的节点需要通过children接口创建 - def perform_create(self, serializer): - child_key = Node.org_root().get_next_child_key() - serializer.validated_data["key"] = child_key - serializer.save() - @action(methods=[POST], detail=False, url_path='check_assets_amount_task') def check_assets_amount_task(self, request): task = check_node_assets_amount_task.delay(current_org.id) diff --git a/apps/assets/migrations/0084_auto_20220112_1959.py b/apps/assets/migrations/0084_auto_20220112_1959.py new file mode 100644 index 000000000..23183e71d --- /dev/null +++ b/apps/assets/migrations/0084_auto_20220112_1959.py @@ -0,0 +1,62 @@ +# Generated by Django 3.1.13 on 2022-01-12 11:59 + +import common.db.encoder +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', '0083_auto_20211215_1436'), + ] + + operations = [ + migrations.CreateModel( + name='AccountBackupPlan', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_periodic', models.BooleanField(default=False)), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('types', models.IntegerField(choices=[(255, 'All'), (1, 'Asset'), (2, 'Application')], default=255, verbose_name='Type')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('recipients', models.ManyToManyField(blank=True, related_name='recipient_escape_route_plans', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Account backup plan', + 'ordering': ['name'], + 'unique_together': {('name', 'org_id')}, + }, + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + migrations.CreateModel( + name='AccountBackupPlanExecution', + 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)), + ('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')), + ('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')), + ('plan_snapshot', models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, verbose_name='Account backup snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')), + ('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')), + ('is_success', models.BooleanField(default=False, verbose_name='Is success')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', to='assets.accountbackupplan', verbose_name='Account backup plan')), + ], + options={ + 'verbose_name': 'Account backup execution', + }, + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 0f6aec901..d2dd03885 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -12,3 +12,4 @@ from .utils import * from .authbook import * from .gathered_user import * from .favorite_asset import * +from .backup import * diff --git a/apps/assets/models/backup.py b/apps/assets/models/backup.py new file mode 100644 index 000000000..437e91dbd --- /dev/null +++ b/apps/assets/models/backup.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +import uuid + +from celery import current_task +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins.models import OrgModelMixin +from ops.mixin import PeriodTaskModelMixin +from common.utils import get_logger +from common.db.encoder import ModelJSONFieldEncoder +from common.db.models import BitOperationChoice +from common.mixins.models import CommonModelMixin + +__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution', 'Type'] + +logger = get_logger(__file__) + + +class Type(BitOperationChoice): + NONE = 0 + ALL = 0xff + + Asset = 0b1 + App = 0b1 << 1 + + DB_CHOICES = ( + (ALL, _('All')), + (Asset, _('Asset')), + (App, _('Application')) + ) + + NAME_MAP = { + ALL: "all", + Asset: "asset", + App: "application" + } + + NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} + CHOICES = [] + for i, j in DB_CHOICES: + CHOICES.append((NAME_MAP[i], j)) + + +class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + types = models.IntegerField(choices=Type.DB_CHOICES, default=Type.ALL, verbose_name=_('Type')) + recipients = models.ManyToManyField( + 'users.User', related_name='recipient_escape_route_plans', blank=True, + verbose_name=_("Recipient") + ) + comment = models.TextField(blank=True, verbose_name=_('Comment')) + + def __str__(self): + return f'{self.name}({self.org_id})' + + class Meta: + ordering = ['name'] + unique_together = [('name', 'org_id')] + verbose_name = _('Account backup plan') + + def get_register_task(self): + from ..tasks import execute_account_backup_plan + name = "account_backup_plan_period_{}".format(str(self.id)[:8]) + task = execute_account_backup_plan.name + args = (str(self.id), AccountBackupPlanExecution.Trigger.timing) + kwargs = {} + return name, task, args, kwargs + + def to_attr_json(self): + return { + 'name': self.name, + 'is_periodic': self.is_periodic, + 'interval': self.interval, + 'crontab': self.crontab, + 'org_id': self.org_id, + 'created_by': self.created_by, + 'types': Type.value_to_choices(self.types), + 'recipients': { + str(recipient.id): (str(recipient), bool(recipient.secret_key)) + for recipient in self.recipients.all() + } + } + + def execute(self, trigger): + try: + hid = current_task.request.id + except AttributeError: + hid = str(uuid.uuid4()) + execution = AccountBackupPlanExecution.objects.create( + id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger + ) + return execution.start() + + +class AccountBackupPlanExecution(OrgModelMixin): + class Trigger(models.TextChoices): + manual = 'manual', _('Manual trigger') + timing = 'timing', _('Timing trigger') + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + date_start = models.DateTimeField( + auto_now_add=True, verbose_name=_('Date start') + ) + timedelta = models.FloatField( + default=0.0, verbose_name=_('Time'), null=True + ) + plan_snapshot = models.JSONField( + encoder=ModelJSONFieldEncoder, default=dict, + blank=True, null=True, verbose_name=_('Account backup snapshot') + ) + trigger = models.CharField( + max_length=128, default=Trigger.manual, choices=Trigger.choices, + verbose_name=_('Trigger mode') + ) + reason = models.CharField( + max_length=1024, blank=True, null=True, verbose_name=_('Reason') + ) + is_success = models.BooleanField(default=False, verbose_name=_('Is success')) + plan = models.ForeignKey( + 'AccountBackupPlan', related_name='execution', on_delete=models.CASCADE, + verbose_name=_('Account backup plan') + ) + + class Meta: + verbose_name = _('Account backup execution') + + @property + def types(self): + types = self.plan_snapshot.get('types') + return types + + @property + def recipients(self): + recipients = self.plan_snapshot.get('recipients') + if not recipients: + return [] + return recipients.values() + + def start(self): + from ..task_handlers import ExecutionManager + manager = ExecutionManager(execution=self) + return manager.run() diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 7677c3f08..0059785f7 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -29,6 +29,7 @@ class ProtocolMixin: telnet = 'telnet', 'Telnet' vnc = 'vnc', 'VNC' mysql = 'mysql', 'MySQL' + redis = 'redis', 'Redis' oracle = 'oracle', 'Oracle' mariadb = 'mariadb', 'MariaDB' postgresql = 'postgresql', 'PostgreSQL' @@ -44,7 +45,8 @@ class ProtocolMixin: Protocol.rdp ] APPLICATION_CATEGORY_DB_PROTOCOLS = [ - Protocol.mysql, Protocol.oracle, Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver + Protocol.mysql, Protocol.redis, Protocol.oracle, + Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver ] APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [ Protocol.k8s diff --git a/apps/assets/notifications.py b/apps/assets/notifications.py new file mode 100644 index 000000000..58c02686c --- /dev/null +++ b/apps/assets/notifications.py @@ -0,0 +1,25 @@ +from django.utils.translation import ugettext_lazy as _ + +from users.models import User +from common.tasks import send_mail_attachment_async + + +class AccountBackupExecutionTaskMsg(object): + subject = _('Notification of account backup route task results') + + def __init__(self, name: str, user: User): + self.name = name + self.user = user + + @property + def message(self): + name = self.name + if self.user.secret_key: + return _('{} - The account backup passage task has been completed. See the attachment for details').format(name) + return _("{} - The account backup passage task has been completed: the encryption password has not been set - " + "please go to personal information -> file encryption password to set the encryption password").format(name) + + def publish(self, attachment_list=None): + send_mail_attachment_async.delay( + self.subject, self.message, [self.user.email], attachment_list + ) diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 55437f609..f48552c9d 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -11,3 +11,4 @@ from .cmd_filter import * from .gathered_user import * from .favorite_asset import * from .account import * +from .backup import * diff --git a/apps/assets/serializers/account.py b/apps/assets/serializers/account.py index acca74ce1..2f80ab936 100644 --- a/apps/assets/serializers/account.py +++ b/apps/assets/serializers/account.py @@ -15,7 +15,7 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = AuthBook fields_mini = ['id', 'username', 'ip', 'hostname', 'version'] - fields_write_only = ['password', 'private_key', "public_key"] + fields_write_only = ['password', 'private_key', "public_key", 'passphrase'] fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment'] fields_small = fields_mini + fields_write_only + fields_other fields_fk = ['asset', 'systemuser', 'systemuser_display'] diff --git a/apps/assets/serializers/backup.py b/apps/assets/serializers/backup.py new file mode 100644 index 000000000..c95d0f394 --- /dev/null +++ b/apps/assets/serializers/backup.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from ops.mixin import PeriodTaskSerializerMixin +from common.utils import get_logger + +from .base import TypesField + +from ..models import AccountBackupPlan, AccountBackupPlanExecution + +logger = get_logger(__file__) + +__all__ = ['AccountBackupPlanSerializer', 'AccountBackupPlanExecutionSerializer'] + + +class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): + types = TypesField(required=False, allow_null=True, label=_("Actions")) + + class Meta: + model = AccountBackupPlan + fields = [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created', + 'date_updated', 'created_by', 'periodic_display', 'comment', + 'recipients', 'types' + ] + extra_kwargs = { + 'name': {'required': True}, + 'periodic_display': {'label': _('Periodic perform')}, + 'recipients': {'label': _('Recipient'), 'help_text': _( + 'Currently only mail sending is supported' + )} + } + + +class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): + trigger_display = serializers.ReadOnlyField( + source='get_trigger_display', label=_('Trigger mode') + ) + + class Meta: + model = AccountBackupPlanExecution + fields = [ + 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', + 'is_success', 'plan', 'org_id', 'recipients', 'trigger_display' + ] + read_only_fields = ( + 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', + 'is_success', 'org_id', 'recipients' + ) diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index fa63dc41d..9eabc41a9 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +from io import StringIO from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.utils import ssh_pubkey_gen, validate_ssh_private_key +from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key +from assets.models import Type class AuthSerializer(serializers.ModelSerializer): @@ -28,17 +30,27 @@ class AuthSerializer(serializers.ModelSerializer): return self.instance -class AuthSerializerMixin: +class AuthSerializerMixin(serializers.ModelSerializer): + passphrase = serializers.CharField( + allow_blank=True, allow_null=True, required=False, max_length=512, + write_only=True, label=_('Key password') + ) + def validate_password(self, password): return password def validate_private_key(self, private_key): if not private_key: return - password = self.initial_data.get("password") - valid = validate_ssh_private_key(private_key, password) + passphrase = self.initial_data.get('passphrase') + valid = validate_ssh_private_key(private_key, password=passphrase) if not valid: - raise serializers.ValidationError(_("private key invalid")) + raise serializers.ValidationError(_("private key invalid or passphrase error")) + + private_key = ssh_private_key_gen(private_key, password=passphrase) + string_io = StringIO() + private_key.write_private_key(string_io) + private_key = string_io.getvalue() return private_key def validate_public_key(self, public_key): @@ -50,6 +62,7 @@ class AuthSerializerMixin: value = validated_data.get(field) if not value: validated_data.pop(field, None) + validated_data.pop('passphrase', None) def create(self, validated_data): self.clean_auth_fields(validated_data) @@ -58,3 +71,24 @@ class AuthSerializerMixin: def update(self, instance, validated_data): self.clean_auth_fields(validated_data) return super().update(instance, validated_data) + + +class TypesField(serializers.MultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['choices'] = Type.CHOICES + super().__init__(*args, **kwargs) + + def to_representation(self, value): + return Type.value_to_choices(value) + + def to_internal_value(self, data): + if data is None: + return data + return Type.choices_to_value(data) + + +class ActionsDisplayField(TypesField): + def to_representation(self, value): + values = super().to_representation(value) + choices = dict(Type.CHOICES) + return [choices.get(i) for i in values] diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 172448d44..b86938c43 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -49,7 +49,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): model = Gateway fields_mini = ['id', 'name'] fields_write_only = [ - 'password', 'private_key', 'public_key', + 'password', 'private_key', 'public_key', 'passphrase' ] fields_small = fields_mini + fields_write_only + [ 'username', 'ip', 'port', 'protocol', diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index fd04486fe..bb09e7580 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -17,6 +17,9 @@ class NodeSerializer(BulkOrgResourceModelSerializer): value = serializers.CharField( required=False, allow_blank=True, allow_null=True, label=_("value") ) + full_value = serializers.CharField( + required=False, allow_blank=True, allow_null=True, label=_("Full value") + ) class Meta: model = Node @@ -40,6 +43,19 @@ class NodeSerializer(BulkOrgResourceModelSerializer): ) return data + def create(self, validated_data): + full_value = validated_data.get('full_value') + value = validated_data.get('value') + + # 直接多层级创建 + if full_value: + node = Node.create_node_by_full_value(full_value) + # 根据 value 在 root 下创建 + else: + key = Node.org_root().get_next_child_key() + node = Node.objects.create(key=key, value=value) + return node + class NodeAssetsSerializer(BulkOrgResourceModelSerializer): assets = serializers.PrimaryKeyRelatedField( diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 9a085fb27..4c01d46bb 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -4,7 +4,7 @@ from django.db.models import Count from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen -from common.validators import alphanumeric_re, alphanumeric_cn_re +from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import SystemUser, Asset from .utils import validate_password_contains_left_double_curly_bracket @@ -33,7 +33,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = SystemUser fields_mini = ['id', 'name', 'username'] - fields_write_only = ['password', 'public_key', 'private_key'] + fields_write_only = ['password', 'public_key', 'private_key', 'passphrase'] fields_small = fields_mini + fields_write_only + [ 'token', 'ssh_key_fingerprint', 'type', 'type_display', 'protocol', 'is_asset_protocol', @@ -107,9 +107,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): def validate_username(self, username): protocol = self.get_initial_value("protocol") if username: - regx = alphanumeric_re if protocol == SystemUser.Protocol.telnet: regx = alphanumeric_cn_re + elif protocol == SystemUser.Protocol.rdp: + regx = alphanumeric_win_re + else: + regx = alphanumeric_re if not regx.match(username): raise serializers.ValidationError(_('Special char not allowed')) return username @@ -119,7 +122,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): return '' login_mode = self.get_initial_value("login_mode") - if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc: + if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc \ + and protocol != SystemUser.Protocol.redis: msg = _('* Automatic login mode must fill in the username.') raise serializers.ValidationError(msg) return username @@ -141,9 +145,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): def validate_password(self, password): super().validate_password(password) - auto_gen_key = self.get_initial_value("auto_generate_key", False) - private_key = self.get_initial_value("private_key") - login_mode = self.get_initial_value("login_mode") + auto_gen_key = self.get_initial_value('auto_generate_key', False) + private_key = self.get_initial_value('private_key') + login_mode = self.get_initial_value('login_mode') if not self.instance and not auto_gen_key and not password and \ not private_key and login_mode == SystemUser.LOGIN_AUTO: @@ -187,9 +191,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): return attrs def _validate_gen_key(self, attrs): - username = attrs.get("username", "manual") - auto_gen_key = attrs.pop("auto_generate_key", False) - protocol = attrs.get("protocol") + username = attrs.get('username', 'manual') + auto_gen_key = attrs.pop('auto_generate_key', False) + protocol = attrs.get('protocol') if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: return attrs @@ -197,17 +201,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): # 自动生成 if auto_gen_key and not self.instance: password = SystemUser.gen_password() - attrs["password"] = password + attrs['password'] = password if protocol == SystemUser.Protocol.ssh: private_key, public_key = SystemUser.gen_key(username) - attrs["private_key"] = private_key - attrs["public_key"] = public_key + attrs['private_key'] = private_key + attrs['public_key'] = public_key # 如果设置了private key,没有设置public key则生成 - elif attrs.get("private_key", None): - private_key = attrs["private_key"] - password = attrs.get("password") + elif attrs.get('private_key'): + private_key = attrs['private_key'] + password = attrs.get('password') public_key = ssh_pubkey_gen(private_key, password=password, username=username) - attrs["public_key"] = public_key + attrs['public_key'] = public_key return attrs def _validate_login_mode(self, attrs): @@ -232,7 +236,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset\ + queryset = queryset \ .annotate(assets_amount=Count("assets")) \ .prefetch_related('nodes', 'cmd_filters') return queryset diff --git a/apps/assets/task_handlers/__init__.py b/apps/assets/task_handlers/__init__.py new file mode 100644 index 000000000..d557c449e --- /dev/null +++ b/apps/assets/task_handlers/__init__.py @@ -0,0 +1 @@ +from .endpoint import * diff --git a/apps/assets/task_handlers/backup/__init__.py b/apps/assets/task_handlers/backup/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/task_handlers/backup/handlers.py b/apps/assets/task_handlers/backup/handlers.py new file mode 100644 index 000000000..c1b62a730 --- /dev/null +++ b/apps/assets/task_handlers/backup/handlers.py @@ -0,0 +1,203 @@ +import os +import time +import pandas as pd +from collections import defaultdict + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from assets.models import AuthBook, Asset, BaseUser, ProtocolsMixin +from assets.notifications import AccountBackupExecutionTaskMsg +from applications.models import Account, Application +from applications.const import AppType +from users.models import User +from common.utils import get_logger +from common.utils.timezone import local_now_display +from common.utils.file import encrypt_and_compress_zip_file + +logger = get_logger(__file__) + +PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') + + +class AssetAccountHandler: + @staticmethod + def get_filename(plan_name): + filename = os.path.join( + PATH, f'{plan_name}-{_("Asset")}-{local_now_display()}-{time.time()}.xlsx' + ) + return filename + + @staticmethod + def create_df(): + df_dict = defaultdict(list) + label_key = AuthBook._meta.verbose_name + accounts = AuthBook.objects.all().prefetch_related('systemuser', 'asset') + for account in accounts: + account.load_auth() + protocol = account.asset.protocol + protocol_label = getattr(ProtocolsMixin.Protocol, protocol).label + row = { + getattr(Asset, 'hostname').field.verbose_name: account.asset.hostname, + getattr(Asset, 'ip').field.verbose_name: account.asset.ip, + } + secret_row = AccountBackupHandler.create_secret_row(account) + row.update(secret_row) + row.update({ + getattr(Asset, 'protocol').field.verbose_name: protocol_label, + getattr(AuthBook, 'version').field.verbose_name: account.version + }) + df_dict[label_key].append(row) + for k, v in df_dict.items(): + df_dict[k] = pd.DataFrame(v) + return df_dict + + +class AppAccountHandler: + @staticmethod + def get_filename(plan_name): + filename = os.path.join( + PATH, f'{plan_name}-{_("Application")}-{local_now_display()}-{time.time()}.xlsx' + ) + return filename + + @staticmethod + def create_df(): + df_dict = defaultdict(list) + accounts = Account.objects.all().prefetch_related('systemuser', 'app') + for account in accounts: + account.load_auth() + app_type = account.app.type + if app_type == 'postgresql': + label_key = getattr(AppType, 'pgsql').label + else: + label_key = getattr(AppType, app_type).label + row = { + getattr(Application, 'name').field.verbose_name: account.app.name, + getattr(Application, 'attrs').field.verbose_name: account.app.attrs + } + secret_row = AccountBackupHandler.create_secret_row(account) + row.update(secret_row) + row.update({ + getattr(Account, 'version').field.verbose_name: account.version + }) + df_dict[label_key].append(row) + for k, v in df_dict.items(): + df_dict[k] = pd.DataFrame(v) + return df_dict + + +HANDLER_MAP = { + 'asset': AssetAccountHandler, + 'application': AppAccountHandler +} + + +class AccountBackupHandler: + def __init__(self, execution): + self.execution = execution + self.plan_name = self.execution.plan.name + self.is_frozen = False # 任务状态冻结标志 + + def create_excel(self): + logger.info( + '\n' + '\033[32m>>> 正在生成资产及应用相关备份信息文件\033[0m' + '' + ) + # Print task start date + time_start = time.time() + info = {} + for account_type in self.execution.types: + if account_type in HANDLER_MAP: + account_handler = HANDLER_MAP[account_type] + df = account_handler.create_df() + filename = account_handler.get_filename(self.plan_name) + info[filename] = df + for filename, df_dict in info.items(): + with pd.ExcelWriter(filename) as w: + for sheet, df in df_dict.items(): + sheet = sheet.replace(' ', '-') + getattr(df, 'to_excel')(w, sheet_name=sheet, index=False) + timedelta = round((time.time() - time_start), 2) + logger.info('步骤完成: 用时 {}s'.format(timedelta)) + return list(info.keys()) + + def send_backup_mail(self, files): + recipients = self.execution.plan_snapshot.get('recipients') + if not recipients: + return + recipients = User.objects.filter(id__in=list(recipients)) + logger.info( + '\n' + '\033[32m>>> 发送备份邮件\033[0m' + '' + ) + plan_name = self.plan_name + for user in recipients: + if not user.secret_key: + attachment_list = [] + else: + password = user.secret_key.encode('utf8') + attachment = os.path.join(PATH, f'{plan_name}-{local_now_display()}-{time.time()}.zip') + encrypt_and_compress_zip_file(attachment, password, files) + attachment_list = [attachment, ] + AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list) + logger.info('邮件已发送至{}({})'.format(user, user.email)) + for file in files: + os.remove(file) + + def step_perform_task_update(self, is_success, reason): + self.execution.reason = reason[:1024] + self.execution.is_success = is_success + self.execution.save() + logger.info('已完成对任务状态的更新') + + def step_finished(self, is_success): + if is_success: + logger.info('任务执行成功') + else: + logger.error('任务执行失败') + + def _run(self): + is_success = False + error = '-' + try: + files = self.create_excel() + self.send_backup_mail(files) + except Exception as e: + self.is_frozen = True + logger.error('任务执行被异常中断') + logger.info('下面打印发生异常的 Traceback 信息 : ') + logger.error(e, exc_info=True) + error = str(e) + else: + is_success = True + finally: + reason = error + self.step_perform_task_update(is_success, reason) + self.step_finished(is_success) + + def run(self): + logger.info('任务开始: {}'.format(local_now_display())) + time_start = time.time() + try: + self._run() + except Exception as e: + logger.error('任务运行出现异常') + logger.error('下面显示异常 Traceback 信息: ') + logger.error(e, exc_info=True) + finally: + logger.info('\n任务结束: {}'.format(local_now_display())) + timedelta = round((time.time() - time_start), 2) + logger.info('用时: {}'.format(timedelta)) + + @staticmethod + def create_secret_row(instance): + row = { + getattr(BaseUser, 'username').field.verbose_name: instance.username, + getattr(BaseUser, 'password').field.verbose_name: instance.password, + getattr(BaseUser, 'private_key').field.verbose_name: instance.private_key, + getattr(BaseUser, 'public_key').field.verbose_name: instance.public_key + } + return row diff --git a/apps/assets/task_handlers/backup/manager.py b/apps/assets/task_handlers/backup/manager.py new file mode 100644 index 000000000..c9558fea0 --- /dev/null +++ b/apps/assets/task_handlers/backup/manager.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +import time + +from django.utils import timezone + +from common.utils import get_logger +from common.utils.timezone import local_now_display + +from .handlers import AccountBackupHandler + +logger = get_logger(__name__) + + +class AccountBackupExecutionManager: + def __init__(self, execution): + self.execution = execution + self.date_start = timezone.now() + self.time_start = time.time() + self.date_end = None + self.time_end = None + self.timedelta = 0 + + def do_run(self): + execution = self.execution + logger.info('\n\033[33m# 账号备份计划正在执行\033[0m') + handler = AccountBackupHandler(execution) + handler.run() + + def pre_run(self): + self.execution.date_start = self.date_start + self.execution.save() + + def post_run(self): + self.time_end = time.time() + self.date_end = timezone.now() + + logger.info('\n\n' + '-' * 80) + logger.info('计划执行结束 {}\n'.format(local_now_display())) + self.timedelta = self.time_end - self.time_start + logger.info('用时: {}s'.format(self.timedelta)) + self.execution.timedelta = self.timedelta + self.execution.save() + + def run(self): + self.pre_run() + self.do_run() + self.post_run() diff --git a/apps/assets/task_handlers/endpoint.py b/apps/assets/task_handlers/endpoint.py new file mode 100644 index 000000000..729fc8648 --- /dev/null +++ b/apps/assets/task_handlers/endpoint.py @@ -0,0 +1,10 @@ +from .backup.manager import AccountBackupExecutionManager + + +class ExecutionManager: + manager_type = { + 'backup': AccountBackupExecutionManager + } + + def __new__(cls, execution): + return AccountBackupExecutionManager(execution) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index 7aaed0efd..22ccbf503 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -9,3 +9,4 @@ from .gather_asset_hardware_info import * from .push_system_user import * from .system_user_connectivity import * from .nodes_amount import * +from .backup import * diff --git a/apps/assets/tasks/backup.py b/apps/assets/tasks/backup.py new file mode 100644 index 000000000..5d4e91011 --- /dev/null +++ b/apps/assets/tasks/backup.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +from celery import shared_task + +from common.utils import get_object_or_none, get_logger +from orgs.utils import tmp_to_org, tmp_to_root_org +from assets.models import AccountBackupPlan + +logger = get_logger(__file__) + + +@shared_task +def execute_account_backup_plan(pid, trigger): + with tmp_to_root_org(): + plan = get_object_or_none(AccountBackupPlan, pk=pid) + if not plan: + logger.error("No account backup route plan found: {}".format(pid)) + return + with tmp_to_org(plan.org): + plan.execute(trigger) diff --git a/apps/assets/urls/__init__.py b/apps/assets/urls/__init__.py index 8b1378917..e69de29bb 100644 --- a/apps/assets/urls/__init__.py +++ b/apps/assets/urls/__init__.py @@ -1 +0,0 @@ - diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 8d00e6543..b92903d02 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -26,6 +26,8 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation') +router.register(r'backup', api.AccountBackupPlanViewSet, 'backup') +router.register(r'backup-execution', api.AccountBackupPlanExecutionViewSet, 'backup-execution') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 4adaa1942..05297fbbd 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -28,7 +28,7 @@ from common.utils.common import get_file_by_arch from orgs.mixins.api import RootOrgViewMixin from common.http import is_true from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user -from perms.models.asset_permission import Action +from perms.models.base import Action from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, @@ -231,7 +231,7 @@ class SecretDetailMixin: @staticmethod def _get_application_secret_detail(application): - from perms.models import Action + from perms.models.base import Action gateway = None if not application.category_remote_app: @@ -391,10 +391,10 @@ class UserConnectionTokenViewSet( asset = get_object_or_404(Asset, id=value.get('asset')) if not asset.is_active: raise serializers.ValidationError("Asset disabled") - has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect') + has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user) else: app = get_object_or_404(Application, id=value.get('application')) - has_perm, expired_at = app_validate_permission(user, app, system_user) + has_perm, actions, expired_at = app_validate_permission(user, app, system_user) if not has_perm: raise serializers.ValidationError('Permission expired or invalid') diff --git a/apps/authentication/migrations/0006_auto_20211227_1059.py b/apps/authentication/migrations/0006_auto_20211227_1059.py new file mode 100644 index 000000000..945c5ff5c --- /dev/null +++ b/apps/authentication/migrations/0006_auto_20211227_1059.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2021-12-27 02:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('authentication', '0005_delete_loginconfirmsetting'), + ] + + operations = [ + migrations.AlterField( + model_name='ssotoken', + name='user', + field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 9322fd7f9..58c80ee68 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -44,4 +44,4 @@ class SSOToken(models.JMSBaseModel): """ authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) expired = models.BooleanField(default=False, verbose_name=_('Expired')) - user = models.ForeignKey('users.User', on_delete=models.PROTECT, verbose_name=_('User'), db_constraint=False) + user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False) diff --git a/apps/authentication/notifications.py b/apps/authentication/notifications.py index 5c5f8de2f..364ac20dd 100644 --- a/apps/authentication/notifications.py +++ b/apps/authentication/notifications.py @@ -1,9 +1,9 @@ -from django.utils import timezone from django.utils.translation import ugettext as _ from django.template.loader import render_to_string from notifications.notifications import UserMessage from common.utils import get_logger +from common.utils.timezone import local_now_display logger = get_logger(__file__) @@ -15,8 +15,7 @@ class DifferentCityLoginMessage(UserMessage): super().__init__(user) def get_html_msg(self) -> dict: - now_local = timezone.localtime(timezone.now()) - now = now_local.strftime("%Y-%m-%d %H:%M:%S") + now = local_now_display() subject = _('Different city login reminder') context = dict( subject=subject, @@ -39,3 +38,36 @@ class DifferentCityLoginMessage(UserMessage): ip = '8.8.8.8' city = '洛杉矶' return cls(user, ip, city) + + +class OAuthBindMessage(UserMessage): + def __init__(self, user, ip, oauth_name, oauth_id): + super().__init__(user) + self.ip = ip + self.oauth_name = oauth_name + self.oauth_id = oauth_id + + def get_html_msg(self) -> dict: + now = local_now_display() + subject = self.oauth_name + ' ' + _('binding reminder') + context = dict( + subject=subject, + name=self.user.name, + username=self.user.username, + ip=self.ip, + time=now, + oauth_name=self.oauth_name, + oauth_id=self.oauth_id + ) + message = render_to_string('authentication/_msg_oauth_bind.html', context) + return { + 'subject': subject, + 'message': message + } + + @classmethod + def gen_test_msg(cls): + from users.models import User + user = User.objects.first() + ip = '8.8.8.8' + return cls(user, ip, _('WeCom'), '000000') diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 44fea3242..3c02dea99 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -9,7 +9,7 @@ from assets.models import Asset, SystemUser, Gateway from applications.models import Application from users.serializers import UserProfileSerializer from assets.serializers import ProtocolsField -from perms.serializers.asset.permission import ActionsField +from perms.serializers.base import ActionsField from .models import AccessKey __all__ = [ diff --git a/apps/authentication/templates/authentication/_msg_oauth_bind.html b/apps/authentication/templates/authentication/_msg_oauth_bind.html new file mode 100644 index 000000000..7a37b9218 --- /dev/null +++ b/apps/authentication/templates/authentication/_msg_oauth_bind.html @@ -0,0 +1,18 @@ +{% load i18n %} +

+ {% trans 'Hello' %} {{ name }}, +

+

+ {% trans 'Your account has just been bound to' %} {{ oauth_name }} +

+

+ {% trans 'Username' %}: {{ username }}
+ {{ oauth_name }}: {{ oauth_id }}
+ {% trans 'Time' %}: {{ time }}
+ {% trans 'IP' %}: {{ ip }} +

+ +- +

+ {% trans 'If the operation is not your own, unbind and change the password.' %} +

diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 70bb9211d..e42e7ff81 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -19,6 +19,8 @@ from common.mixins.views import PermissionsMixin from authentication import errors from authentication.mixins import AuthMixin from common.sdk.im.dingtalk import DingTalk +from common.utils.common import get_request_ip +from authentication.notifications import OAuthBindMessage logger = get_logger(__file__) @@ -154,6 +156,8 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View): return response raise e + ip = get_request_ip(request) + OAuthBindMessage(user, ip, _('DingTalk'), user_id).publish_async() msg = _('Binding DingTalk successfully') response = self.get_success_response(redirect_url, msg, msg) return response diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 597b83e46..88ba6636e 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -16,8 +16,10 @@ from common.utils.random import random_string from common.utils.django import reverse, get_object_or_none from common.mixins.views import PermissionsMixin from common.sdk.im.feishu import FeiShu, URL +from common.utils.common import get_request_ip from authentication import errors from authentication.mixins import AuthMixin +from authentication.notifications import OAuthBindMessage logger = get_logger(__file__) @@ -142,6 +144,8 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View): return response raise e + ip = get_request_ip(request) + OAuthBindMessage(user, ip, _('FeiShu'), user_id).publish_async() msg = _('Binding FeiShu successfully') response = self.get_success_response(redirect_url, msg, msg) return response diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index ce7cd24f9..545da111f 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -157,7 +157,7 @@ class UserLoginView(mixins.AuthMixin, FormView): 'name': 'SAML2', 'enabled': settings.AUTH_SAML2, 'url': reverse('authentication:saml2:saml2-login'), - 'logo': static('img/login_cas_logo.png'), + 'logo': static('img/login_saml2_logo.png'), 'auto_redirect': True }, { diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 793479ccc..3ac1df686 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -17,8 +17,10 @@ from common.utils.django import reverse, get_object_or_none from common.sdk.im.wecom import URL from common.sdk.im.wecom import WeCom from common.mixins.views import PermissionsMixin +from common.utils.common import get_request_ip from authentication import errors from authentication.mixins import AuthMixin +from authentication.notifications import OAuthBindMessage logger = get_logger(__file__) @@ -152,6 +154,8 @@ class WeComQRBindCallbackView(WeComQRMixin, View): return response raise e + ip = get_request_ip(request) + OAuthBindMessage(user, ip, _('WeCom'), wecom_userid).publish_async() msg = _('Binding WeCom successfully') response = self.get_success_response(redirect_url, msg, msg) return response diff --git a/apps/common/db/models.py b/apps/common/db/models.py index 77a49a683..2989f734e 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -66,6 +66,47 @@ class ChoiceSet(metaclass=ChoiceSetType): choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 +class BitOperationChoice: + NONE = 0 + NAME_MAP: dict + DB_CHOICES: tuple + NAME_MAP_REVERSE: dict + + @classmethod + def value_to_choices(cls, value): + if isinstance(value, list): + return value + value = int(value) + choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] + return choices + + @classmethod + def value_to_choices_display(cls, value): + choices = cls.value_to_choices(value) + return [str(dict(cls.choices())[i]) for i in choices] + + @classmethod + def choices_to_value(cls, value): + if not isinstance(value, list): + return cls.NONE + db_value = [ + cls.NAME_MAP_REVERSE[v] for v in value + if v in cls.NAME_MAP_REVERSE.keys() + ] + if not db_value: + return cls.NONE + + def to_choices(x, y): + return x | y + + result = reduce(to_choices, db_value) + return result + + @classmethod + def choices(cls): + return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] + + class JMSBaseModel(Model): created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by')) diff --git a/apps/common/tree.py b/apps/common/tree.py index fbade582e..8d5e40e5b 100644 --- a/apps/common/tree.py +++ b/apps/common/tree.py @@ -13,6 +13,7 @@ class TreeNode: pId = "" open = False iconSkin = "" + parentInfo = '' meta = {} _tree = None @@ -95,6 +96,7 @@ class TreeNodeSerializer(serializers.Serializer): name = serializers.CharField(max_length=128) title = serializers.CharField(max_length=128) pId = serializers.CharField(max_length=128) + parentInfo = serializers.CharField(max_length=4096, allow_blank=True) isParent = serializers.BooleanField(default=False) open = serializers.BooleanField(default=False) iconSkin = serializers.CharField(max_length=128, allow_blank=True) diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index dc68c9ba8..ee35b5c76 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -22,7 +22,6 @@ from django.db.models.fields.files import FileField from .http import http_date - UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') @@ -41,6 +40,7 @@ class Singleton(type): class Signer(metaclass=Singleton): """用来加密,解密,和基于时间戳的方式验证token""" + def __init__(self, secret_key=None): self.secret_key = secret_key @@ -88,11 +88,16 @@ def ssh_key_string_to_obj(text, password=None): return key -def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None): +def ssh_private_key_gen(private_key, password=None): if isinstance(private_key, bytes): private_key = private_key.decode("utf-8") if isinstance(private_key, string_types): private_key = ssh_key_string_to_obj(private_key, password=password) + return private_key + + +def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None): + private_key = ssh_private_key_gen(private_key, password=password) if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)): raise IOError('Invalid private key') @@ -230,4 +235,3 @@ def model_to_json(instance, sort_keys=True, indent=2, cls=None): if cls is None: cls = DjangoJSONEncoder return json.dumps(data, sort_keys=sort_keys, indent=indent, cls=cls) - diff --git a/apps/common/utils/file.py b/apps/common/utils/file.py index cb883ed55..c00293de5 100644 --- a/apps/common/utils/file.py +++ b/apps/common/utils/file.py @@ -10,10 +10,11 @@ def create_csv_file(filename, headers, rows, ): w.writerows(rows) -def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filename): +def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filenames): with pyzipper.AESZipFile( filename, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES ) as zf: zf.setpassword(secret_password) - with open(encrypted_filename, 'rb') as f: - zf.writestr(os.path.basename(encrypted_filename), f.read()) + for encrypted_filename in encrypted_filenames: + with open(encrypted_filename, 'rb') as f: + zf.writestr(os.path.basename(encrypted_filename), f.read()) diff --git a/apps/common/validators.py b/apps/common/validators.py index 8e7e504ed..352482a1b 100644 --- a/apps/common/validators.py +++ b/apps/common/validators.py @@ -17,6 +17,8 @@ alphanumeric_re = re.compile(r'^[0-9a-zA-Z_@\-\.]*$') alphanumeric_cn_re = re.compile(r'^[0-9a-zA-Z_@\-\.\u4E00-\u9FA5]*$') +alphanumeric_win_re = re.compile(r'^[0-9a-zA-Z_@#%&~\^\$\-\.\u4E00-\u9FA5]*$') + class ProjectUniqueValidator(UniqueTogetherValidator): def __call__(self, attrs, serializer): diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 51a077cca..f714759f0 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -31,7 +31,7 @@ def jumpserver_processor(request): context = default_context context.update({ 'VERSION': settings.VERSION, - 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021', + 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2022', 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, 'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL, 'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME, diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 3b62528ee..1633ceafe 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:2bf3340b7ca0dc3e698160fe8df980df07ebdecfd9312c5aca230d18a05fea18 -size 94929 +oid sha256:942e981be66e5d0c32efb59583a377503ee3dc285e2794da40c312694c4a9dc2 +size 96378 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index fcc84607b..47e01f362 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: 2021-12-15 15:41+0800\n" +"POT-Creation-Date: 2022-01-12 20:51+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -18,11 +18,11 @@ msgstr "" "X-Generator: Poedit 2.4.3\n" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 -#: applications/models/application.py:166 assets/models/asset.py:139 +#: applications/models/application.py:202 assets/models/asset.py:139 #: assets/models/base.py:175 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:23 assets/models/domain.py:24 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 -#: orgs/models.py:24 perms/models/base.py:44 settings/models.py:29 +#: orgs/models.py:24 perms/models/base.py:83 settings/models.py:29 #: settings/serializers/sms.py:6 terminal/models/storage.py:23 #: terminal/models/task.py:16 terminal/models/terminal.py:100 #: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:549 @@ -35,32 +35,33 @@ msgid "Name" msgstr "名称" #: acls/models/base.py:27 assets/models/cmd_filter.py:77 -#: assets/models/user.py:209 +#: assets/models/user.py:211 msgid "Priority" msgstr "优先级" #: acls/models/base.py:28 assets/models/cmd_filter.py:77 -#: assets/models/user.py:209 +#: assets/models/user.py:211 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" #: acls/models/base.py:31 authentication/models.py:17 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/base.py:48 terminal/models/sharing.py:24 +#: perms/models/base.py:88 terminal/models/sharing.py:24 #: users/templates/users/_select_user_modal.html:18 msgid "Active" msgstr "激活中" -#: acls/models/base.py:32 applications/models/application.py:179 +#: acls/models/base.py:32 applications/models/application.py:215 #: assets/models/asset.py:144 assets/models/asset.py:232 -#: assets/models/base.py:180 assets/models/cluster.py:29 -#: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:87 -#: assets/models/domain.py:25 assets/models/domain.py:65 -#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 -#: orgs/models.py:27 perms/models/base.py:53 settings/models.py:34 -#: terminal/models/storage.py:26 terminal/models/terminal.py:114 -#: tickets/models/ticket.py:71 users/models/group.py:16 -#: users/models/user.py:585 xpack/plugins/change_auth_plan/models/base.py:41 +#: assets/models/backup.py:54 assets/models/base.py:180 +#: assets/models/cluster.py:29 assets/models/cmd_filter.py:44 +#: assets/models/cmd_filter.py:87 assets/models/domain.py:25 +#: assets/models/domain.py:65 assets/models/group.py:23 +#: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:27 +#: perms/models/base.py:93 settings/models.py:34 terminal/models/storage.py:26 +#: terminal/models/terminal.py:114 tickets/models/ticket.py:75 +#: users/models/group.py:16 users/models/user.py:585 +#: xpack/plugins/change_auth_plan/models/base.py:41 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:113 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -83,9 +84,9 @@ msgstr "登录复核" #: assets/models/cmd_filter.py:26 assets/models/label.py:15 audits/models.py:36 #: audits/models.py:56 audits/models.py:74 audits/serializers.py:94 #: authentication/models.py:47 orgs/models.py:19 orgs/models.py:433 -#: perms/models/base.py:45 templates/index.html:78 -#: terminal/backends/command/models.py:18 -#: terminal/backends/command/serializers.py:12 terminal/models/session.py:39 +#: perms/models/base.py:84 templates/index.html:78 +#: terminal/backends/command/models.py:19 +#: terminal/backends/command/serializers.py:12 terminal/models/session.py:40 #: terminal/notifications.py:90 terminal/notifications.py:138 #: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:169 #: users/models/user.py:756 users/models/user.py:782 @@ -130,15 +131,15 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:37 #: assets/models/asset.py:356 assets/models/authbook.py:18 -#: assets/models/cmd_filter.py:34 assets/models/gathered_user.py:14 -#: assets/serializers/system_user.py:260 audits/models.py:38 -#: perms/models/asset_permission.py:99 templates/index.html:82 -#: terminal/backends/command/models.py:19 -#: terminal/backends/command/serializers.py:13 terminal/models/session.py:41 +#: assets/models/backup.py:31 assets/models/cmd_filter.py:34 +#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:264 +#: audits/models.py:38 perms/models/asset_permission.py:24 +#: templates/index.html:82 terminal/backends/command/models.py:20 +#: terminal/backends/command/serializers.py:13 terminal/models/session.py:42 #: terminal/notifications.py:89 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 -#: xpack/plugins/change_auth_plan/models/asset.py:200 +#: xpack/plugins/change_auth_plan/models/asset.py:199 #: xpack/plugins/cloud/models.py:217 msgid "Asset" msgstr "资产" @@ -164,11 +165,12 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: assets/models/base.py:176 assets/models/gathered_user.py:15 #: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17 #: authentication/templates/authentication/_msg_different_city.html:9 +#: authentication/templates/authentication/_msg_oauth_bind.html:9 #: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:547 #: users/templates/users/_msg_user_created.html:12 #: users/templates/users/_select_user_modal.html:14 -#: xpack/plugins/change_auth_plan/models/asset.py:35 -#: xpack/plugins/change_auth_plan/models/asset.py:196 +#: xpack/plugins/change_auth_plan/models/asset.py:34 +#: xpack/plugins/change_auth_plan/models/asset.py:195 #: xpack/plugins/cloud/serializers/account_attrs.py:22 msgid "Username" msgstr "用户名" @@ -186,6 +188,7 @@ msgstr "" #: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: assets/models/asset.py:211 assets/models/domain.py:61 #: assets/serializers/account.py:12 +#: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 #: settings/serializers/terminal.py:8 @@ -209,7 +212,7 @@ msgid "" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:214 -#: assets/models/domain.py:63 assets/models/user.py:210 +#: assets/models/domain.py:63 assets/models/user.py:212 #: terminal/serializers/session.py:30 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -245,7 +248,7 @@ msgstr "" msgid "Time Period" msgstr "时段" -#: applications/api/mixin.py:20 templates/_nav_user.html:10 +#: applications/api/mixin.py:28 templates/_nav_user.html:10 msgid "My applications" msgstr "我的应用" @@ -260,16 +263,16 @@ msgstr "数据库" msgid "Remote app" msgstr "远程应用" -#: applications/const.py:29 +#: applications/const.py:30 msgid "Custom" msgstr "自定义" #: applications/models/account.py:11 assets/models/authbook.py:19 -#: assets/models/cmd_filter.py:38 assets/models/user.py:300 audits/models.py:39 +#: assets/models/cmd_filter.py:38 assets/models/user.py:302 audits/models.py:39 #: perms/models/application_permission.py:32 -#: perms/models/asset_permission.py:101 templates/_nav.html:45 -#: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:43 +#: perms/models/asset_permission.py:26 templates/_nav.html:45 +#: terminal/backends/command/models.py:21 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:44 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 @@ -291,11 +294,11 @@ msgstr "版本" msgid "Account" msgstr "账户" -#: applications/models/application.py:50 templates/_nav.html:60 +#: applications/models/application.py:60 templates/_nav.html:60 msgid "Applications" msgstr "应用管理" -#: applications/models/application.py:168 +#: applications/models/application.py:204 #: applications/serializers/application.py:88 assets/models/label.py:21 #: perms/models/application_permission.py:20 #: perms/serializers/application/user_permission.py:33 @@ -304,35 +307,37 @@ msgstr "应用管理" msgid "Category" msgstr "类别" -#: applications/models/application.py:171 -#: applications/serializers/application.py:90 assets/models/cmd_filter.py:76 -#: assets/models/user.py:208 perms/models/application_permission.py:23 +#: applications/models/application.py:207 +#: applications/serializers/application.py:90 assets/models/backup.py:49 +#: assets/models/cmd_filter.py:76 assets/models/user.py:210 +#: perms/models/application_permission.py:23 #: perms/serializers/application/user_permission.py:34 #: terminal/models/storage.py:55 terminal/models/storage.py:116 -#: tickets/models/flow.py:51 tickets/models/ticket.py:48 +#: tickets/models/flow.py:55 tickets/models/ticket.py:52 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:29 #: xpack/plugins/change_auth_plan/models/app.py:28 #: xpack/plugins/change_auth_plan/models/app.py:153 msgid "Type" msgstr "类型" -#: applications/models/application.py:175 assets/models/asset.py:218 +#: applications/models/application.py:211 assets/models/asset.py:218 #: assets/models/domain.py:30 assets/models/domain.py:64 msgid "Domain" msgstr "网域" -#: applications/models/application.py:177 xpack/plugins/cloud/models.py:33 +#: applications/models/application.py:213 xpack/plugins/cloud/models.py:33 msgid "Attrs" -msgstr "" +msgstr "属性" -#: applications/models/application.py:183 assets/models/cmd_filter.py:41 -#: perms/models/application_permission.py:27 users/models/user.py:170 +#: applications/models/application.py:219 assets/models/backup.py:32 +#: assets/models/cmd_filter.py:41 perms/models/application_permission.py:27 +#: users/models/user.py:170 msgid "Application" msgstr "应用程序" #: applications/serializers/application.py:59 #: applications/serializers/application.py:89 assets/serializers/label.py:13 -#: perms/serializers/application/permission.py:16 +#: perms/serializers/application/permission.py:18 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:26 msgid "Category display" msgstr "类别名称" @@ -340,10 +345,10 @@ msgstr "类别名称" #: applications/serializers/application.py:60 #: applications/serializers/application.py:91 #: assets/serializers/system_user.py:27 audits/serializers.py:29 -#: perms/serializers/application/permission.py:17 +#: perms/serializers/application/permission.py:19 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:33 -#: tickets/serializers/ticket/ticket.py:22 -#: tickets/serializers/ticket/ticket.py:169 +#: tickets/serializers/ticket/ticket.py:21 +#: tickets/serializers/ticket/ticket.py:173 msgid "Type display" msgstr "类型名称" @@ -368,6 +373,7 @@ msgstr "主机" #: applications/serializers/attrs/application_type/mysql_workbench.py:22 #: applications/serializers/attrs/application_type/oracle.py:11 #: applications/serializers/attrs/application_type/pgsql.py:11 +#: applications/serializers/attrs/application_type/redis.py:11 #: applications/serializers/attrs/application_type/sqlserver.py:11 #: assets/models/asset.py:215 assets/models/domain.py:62 #: settings/serializers/auth/radius.py:15 @@ -383,11 +389,11 @@ msgid "Application path" msgstr "应用路径" #: applications/serializers/attrs/application_category/remote_app.py:45 -#: assets/serializers/system_user.py:159 -#: 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 +#: assets/serializers/system_user.py:163 +#: xpack/plugins/change_auth_plan/serializers/asset.py:65 +#: xpack/plugins/change_auth_plan/serializers/asset.py:68 +#: xpack/plugins/change_auth_plan/serializers/asset.py:71 +#: xpack/plugins/change_auth_plan/serializers/asset.py:102 #: xpack/plugins/cloud/serializers/account_attrs.py:52 msgid "This field is required." msgstr "该字段是必填项。" @@ -427,15 +433,15 @@ msgstr "目标URL" msgid "Number required" msgstr "需要为数字" -#: assets/api/node.py:66 +#: assets/api/node.py:60 msgid "You can't update the root node name" msgstr "不能修改根节点名称" -#: assets/api/node.py:73 +#: assets/api/node.py:67 msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" -#: assets/api/node.py:76 +#: assets/api/node.py:70 msgid "Deletion failed and the node contains assets" msgstr "删除失败,节点包含资产" @@ -448,7 +454,7 @@ msgid "Charset" msgstr "编码" #: assets/models/asset.py:142 assets/serializers/asset.py:178 -#: tickets/models/ticket.py:50 +#: tickets/models/ticket.py:54 msgid "Meta" msgstr "元数据" @@ -469,7 +475,7 @@ msgstr "制造商" msgid "Model" msgstr "型号" -#: assets/models/asset.py:171 +#: assets/models/asset.py:171 tickets/models/ticket.py:80 msgid "Serial number" msgstr "序列号" @@ -523,9 +529,9 @@ msgstr "主机名原始" msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:219 assets/models/user.py:200 -#: perms/models/asset_permission.py:100 -#: xpack/plugins/change_auth_plan/models/asset.py:44 +#: assets/models/asset.py:219 assets/models/user.py:202 +#: perms/models/asset_permission.py:25 +#: xpack/plugins/change_auth_plan/models/asset.py:43 #: xpack/plugins/gathered_user/models.py:24 msgid "Nodes" msgstr "节点" @@ -536,7 +542,7 @@ msgid "Is active" msgstr "激活" #: assets/models/asset.py:223 assets/models/cluster.py:19 -#: assets/models/user.py:197 assets/models/user.py:349 templates/_nav.html:44 +#: assets/models/user.py:199 assets/models/user.py:351 templates/_nav.html:44 msgid "Admin user" msgstr "特权用户" @@ -555,8 +561,8 @@ msgstr "标签管理" #: assets/models/asset.py:230 assets/models/base.py:183 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:48 #: assets/models/cmd_filter.py:90 assets/models/group.py:21 -#: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25 -#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:593 +#: common/db/models.py:111 common/mixins/models.py:49 orgs/models.py:25 +#: orgs/models.py:437 perms/models/base.py:91 users/models/user.py:593 #: users/serializers/group.py:33 #: xpack/plugins/change_auth_plan/models/base.py:45 #: xpack/plugins/cloud/models.py:119 xpack/plugins/gathered_user/models.py:30 @@ -566,9 +572,9 @@ msgstr "创建者" #: assets/models/asset.py:231 assets/models/base.py:181 #: assets/models/cluster.py:26 assets/models/domain.py:27 #: assets/models/gathered_user.py:19 assets/models/group.py:22 -#: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 +#: assets/models/label.py:25 common/db/models.py:113 common/mixins/models.py:50 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:26 -#: orgs/models.py:435 perms/models/base.py:52 users/models/group.py:18 +#: orgs/models.py:435 perms/models/base.py:92 users/models/group.py:18 #: users/models/user.py:783 xpack/plugins/cloud/models.py:122 msgid "Date created" msgstr "创建日期" @@ -577,6 +583,79 @@ msgstr "创建日期" msgid "AuthBook" msgstr "账号" +#: assets/models/backup.py:30 perms/models/base.py:54 +#: settings/serializers/terminal.py:12 +msgid "All" +msgstr "全部" + +#: assets/models/backup.py:52 assets/serializers/backup.py:32 +#: xpack/plugins/change_auth_plan/models/app.py:41 +#: xpack/plugins/change_auth_plan/models/asset.py:62 +#: xpack/plugins/change_auth_plan/serializers/base.py:44 +msgid "Recipient" +msgstr "收件人" + +#: assets/models/backup.py:62 assets/models/backup.py:124 +msgid "Account backup plan" +msgstr "账户备份计划" + +#: assets/models/backup.py:100 +#: xpack/plugins/change_auth_plan/models/base.py:104 +msgid "Manual trigger" +msgstr "手动触发" + +#: assets/models/backup.py:101 +#: xpack/plugins/change_auth_plan/models/base.py:105 +msgid "Timing trigger" +msgstr "定时触发" + +#: assets/models/backup.py:105 audits/models.py:43 ops/models/command.py:30 +#: perms/models/base.py:89 terminal/models/session.py:54 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:55 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57 +#: xpack/plugins/change_auth_plan/models/base.py:109 +#: xpack/plugins/change_auth_plan/models/base.py:200 +#: xpack/plugins/gathered_user/models.py:76 +msgid "Date start" +msgstr "开始日期" + +#: assets/models/backup.py:108 +#: authentication/templates/authentication/_msg_oauth_bind.html:11 +#: notifications/notifications.py:187 ops/models/adhoc.py:246 +#: xpack/plugins/change_auth_plan/models/base.py:112 +#: xpack/plugins/change_auth_plan/models/base.py:201 +#: xpack/plugins/gathered_user/models.py:79 +msgid "Time" +msgstr "时间" + +#: assets/models/backup.py:112 +msgid "Account backup snapshot" +msgstr "账号备份快照" + +#: assets/models/backup.py:116 assets/serializers/backup.py:40 +#: xpack/plugins/change_auth_plan/models/base.py:122 +#: xpack/plugins/change_auth_plan/serializers/base.py:73 +msgid "Trigger mode" +msgstr "触发模式" + +#: assets/models/backup.py:119 audits/models.py:111 +#: terminal/models/sharing.py:88 +#: xpack/plugins/change_auth_plan/models/base.py:198 +#: xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "原因" + +#: assets/models/backup.py:121 audits/serializers.py:76 +#: audits/serializers.py:91 ops/models/adhoc.py:248 +#: terminal/serializers/session.py:35 +#: xpack/plugins/change_auth_plan/models/base.py:199 +msgid "Is success" +msgstr "是否成功" + +#: assets/models/backup.py:128 +msgid "Account backup execution" +msgstr "账号备份执行" + #: assets/models/base.py:30 assets/tasks/const.py:51 audits/const.py:5 msgid "Unknown" msgstr "未知" @@ -599,20 +678,20 @@ msgstr "可连接性" msgid "Date verified" msgstr "校验日期" -#: assets/models/base.py:178 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 +#: assets/models/base.py:178 xpack/plugins/change_auth_plan/models/asset.py:53 +#: xpack/plugins/change_auth_plan/models/asset.py:130 +#: xpack/plugins/change_auth_plan/models/asset.py:206 msgid "SSH private key" msgstr "SSH密钥" -#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models/asset.py:57 -#: xpack/plugins/change_auth_plan/models/asset.py:127 -#: xpack/plugins/change_auth_plan/models/asset.py:203 +#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models/asset.py:56 +#: xpack/plugins/change_auth_plan/models/asset.py:126 +#: xpack/plugins/change_auth_plan/models/asset.py:202 msgid "SSH public key" msgstr "SSH公钥" #: assets/models/base.py:182 assets/models/gathered_user.py:20 -#: common/db/models.py:73 common/mixins/models.py:51 ops/models/adhoc.py:39 +#: common/db/models.py:114 common/mixins/models.py:51 ops/models/adhoc.py:39 #: orgs/models.py:436 msgid "Date updated" msgstr "更新日期" @@ -659,7 +738,7 @@ msgstr "系统" msgid "Default Cluster" msgstr "默认Cluster" -#: assets/models/cmd_filter.py:30 perms/models/base.py:47 +#: assets/models/cmd_filter.py:30 perms/models/base.py:86 #: templates/_nav.html:21 users/models/group.py:31 users/models/user.py:555 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 @@ -678,7 +757,7 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:64 ops/models/command.py:25 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:50 +#: terminal/backends/command/serializers.py:15 terminal/models/session.py:51 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 msgid "Command" @@ -770,19 +849,19 @@ msgstr "新节点" msgid "empty" msgstr "空" -#: assets/models/node.py:545 perms/models/asset_permission.py:176 +#: assets/models/node.py:545 perms/models/asset_permission.py:100 msgid "Key" msgstr "键" -#: assets/models/node.py:547 +#: assets/models/node.py:547 assets/serializers/node.py:21 msgid "Full value" msgstr "全称" -#: assets/models/node.py:550 perms/models/asset_permission.py:177 +#: assets/models/node.py:550 perms/models/asset_permission.py:101 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:559 assets/serializers/system_user.py:259 +#: assets/models/node.py:559 assets/serializers/system_user.py:263 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -790,75 +869,75 @@ msgstr "ssh私钥" msgid "Node" msgstr "节点" -#: assets/models/user.py:191 +#: assets/models/user.py:193 msgid "Automatic managed" msgstr "托管密码" -#: assets/models/user.py:192 +#: assets/models/user.py:194 msgid "Manually input" msgstr "手动输入" -#: assets/models/user.py:196 +#: assets/models/user.py:198 msgid "Common user" msgstr "普通用户" -#: assets/models/user.py:199 +#: assets/models/user.py:201 msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/user.py:202 assets/serializers/domain.py:29 +#: assets/models/user.py:204 assets/serializers/domain.py:29 #: templates/_nav.html:39 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 -#: xpack/plugins/change_auth_plan/models/asset.py:40 +#: xpack/plugins/change_auth_plan/models/asset.py:39 msgid "Assets" msgstr "资产" -#: assets/models/user.py:206 templates/_nav.html:17 +#: assets/models/user.py:208 templates/_nav.html:17 #: users/views/profile/pubkey.py:37 msgid "Users" msgstr "用户管理" -#: assets/models/user.py:207 +#: assets/models/user.py:209 msgid "User groups" msgstr "用户组" -#: assets/models/user.py:211 +#: assets/models/user.py:213 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:212 +#: assets/models/user.py:214 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:213 +#: assets/models/user.py:215 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:214 +#: assets/models/user.py:216 msgid "Login mode" msgstr "认证方式" -#: assets/models/user.py:215 +#: assets/models/user.py:217 msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:216 authentication/models.py:45 +#: assets/models/user.py:218 authentication/models.py:45 msgid "Token" msgstr "" -#: assets/models/user.py:217 +#: assets/models/user.py:219 msgid "Home" msgstr "家目录" -#: assets/models/user.py:218 +#: assets/models/user.py:220 msgid "System groups" msgstr "用户组" -#: assets/models/user.py:221 +#: assets/models/user.py:223 msgid "User switch" msgstr "用户切换" -#: assets/models/user.py:222 +#: assets/models/user.py:224 msgid "Switch from" msgstr "切换自" @@ -867,6 +946,25 @@ msgstr "切换自" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" +#: assets/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "账号备份任务结果通知" + +#: assets/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "{} - 账号备份任务已完成, 详情见附件" + +#: assets/notifications.py:19 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" +"置加密密码" + #: assets/serializers/account.py:31 assets/serializers/account.py:52 msgid "System user display" msgstr "系统用户名称" @@ -903,18 +1001,40 @@ msgstr "特权用户名称" msgid "CPU info" msgstr "CPU信息" -#: assets/serializers/base.py:41 -msgid "private key invalid" -msgstr "密钥不合法" +#: assets/serializers/backup.py:20 perms/models/base.py:87 +#: perms/serializers/application/permission.py:17 +#: perms/serializers/application/permission.py:42 +#: perms/serializers/asset/permission.py:18 +#: perms/serializers/asset/permission.py:46 +msgid "Actions" +msgstr "动作" + +#: assets/serializers/backup.py:31 ops/mixin.py:106 ops/mixin.py:147 +#: xpack/plugins/change_auth_plan/serializers/base.py:42 +msgid "Periodic perform" +msgstr "定时执行" + +#: assets/serializers/backup.py:33 +#: xpack/plugins/change_auth_plan/serializers/base.py:45 +msgid "Currently only mail sending is supported" +msgstr "当前只支持邮件发送" + +#: assets/serializers/base.py:36 +msgid "Key password" +msgstr "密钥密码" + +#: assets/serializers/base.py:48 +msgid "private key invalid or passphrase error" +msgstr "密钥不合法或密钥密码错误" #: assets/serializers/domain.py:13 assets/serializers/label.py:12 #: assets/serializers/system_user.py:59 -#: perms/serializers/asset/permission.py:72 +#: perms/serializers/asset/permission.py:49 msgid "Assets amount" msgstr "资产数量" #: assets/serializers/domain.py:14 -#: perms/serializers/application/permission.py:43 +#: perms/serializers/application/permission.py:46 msgid "Applications amount" msgstr "应用数量" @@ -926,11 +1046,11 @@ msgstr "网关数量" msgid "value" msgstr "值" -#: assets/serializers/node.py:29 +#: assets/serializers/node.py:32 msgid "Can't contains: /" msgstr "不能包含: /" -#: assets/serializers/node.py:39 +#: assets/serializers/node.py:42 msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" @@ -943,11 +1063,11 @@ msgid "Apps amount" msgstr "应用数量" #: assets/serializers/system_user.py:58 -#: perms/serializers/asset/permission.py:73 +#: perms/serializers/asset/permission.py:50 msgid "Nodes amount" msgstr "节点数量" -#: assets/serializers/system_user.py:60 assets/serializers/system_user.py:261 +#: assets/serializers/system_user.py:60 assets/serializers/system_user.py:265 msgid "Login mode display" msgstr "认证方式名称" @@ -967,43 +1087,43 @@ msgstr "仅支持ssh协议和自动登录的系统用户" msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:114 common/validators.py:14 +#: assets/serializers/system_user.py:117 common/validators.py:14 msgid "Special char not allowed" msgstr "不能包含特殊字符" -#: assets/serializers/system_user.py:123 +#: assets/serializers/system_user.py:127 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:138 +#: assets/serializers/system_user.py:142 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:150 +#: assets/serializers/system_user.py:154 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" -#: assets/serializers/system_user.py:164 +#: assets/serializers/system_user.py:168 msgid "Only ssh protocol system users are allowed" msgstr "仅允许ssh协议的系统用户" -#: assets/serializers/system_user.py:168 +#: assets/serializers/system_user.py:172 msgid "The protocol must be consistent with the current user: {}" msgstr "协议必须和当前用户保持一致: {}" -#: assets/serializers/system_user.py:172 +#: assets/serializers/system_user.py:176 msgid "Only system users with automatic login are allowed" msgstr "仅允许自动登录的系统用户" -#: assets/serializers/system_user.py:277 +#: assets/serializers/system_user.py:281 msgid "System user name" msgstr "系统用户名称" -#: assets/serializers/system_user.py:278 orgs/mixins/serializers.py:26 +#: assets/serializers/system_user.py:282 orgs/mixins/serializers.py:26 msgid "Org name" msgstr "组织名称" -#: assets/serializers/system_user.py:287 +#: assets/serializers/system_user.py:291 msgid "Asset hostname" msgstr "资产主机名" @@ -1157,7 +1277,7 @@ msgid "Symlink" msgstr "建立软链接" #: audits/models.py:37 audits/models.py:60 audits/models.py:76 -#: terminal/models/session.py:46 terminal/models/sharing.py:76 +#: terminal/models/session.py:47 terminal/models/sharing.py:76 msgid "Remote addr" msgstr "远端地址" @@ -1174,16 +1294,6 @@ msgstr "文件名" msgid "Success" msgstr "成功" -#: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:49 -#: terminal/models/session.py:53 -#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:55 -#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57 -#: xpack/plugins/change_auth_plan/models/base.py:109 -#: xpack/plugins/change_auth_plan/models/base.py:200 -#: xpack/plugins/gathered_user/models.py:76 -msgid "Date start" -msgstr "开始日期" - #: audits/models.py:51 #: authentication/templates/authentication/_access_key_modal.html:22 msgid "Create" @@ -1250,13 +1360,7 @@ msgstr "用户代理" msgid "MFA" msgstr "MFA" -#: audits/models.py:111 terminal/models/sharing.py:88 -#: xpack/plugins/change_auth_plan/models/base.py:198 -#: xpack/plugins/cloud/models.py:176 -msgid "Reason" -msgstr "原因" - -#: audits/models.py:112 tickets/models/ticket.py:57 +#: audits/models.py:112 tickets/models/ticket.py:61 #: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:221 msgid "Status" msgstr "状态" @@ -1273,7 +1377,7 @@ msgstr "认证方式" msgid "Operate display" msgstr "操作名称" -#: audits/serializers.py:30 tickets/serializers/ticket/ticket.py:23 +#: audits/serializers.py:30 tickets/serializers/ticket/ticket.py:22 msgid "Status display" msgstr "状态名称" @@ -1285,12 +1389,6 @@ msgstr "MFA名称" msgid "Reason display" msgstr "原因描述" -#: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:248 -#: terminal/serializers/session.py:35 -#: xpack/plugins/change_auth_plan/models/base.py:199 -msgid "Is success" -msgstr "是否成功" - #: audits/serializers.py:78 msgid "Hosts display" msgstr "主机名称" @@ -1328,7 +1426,9 @@ msgstr "" msgid "Auth Token" msgstr "认证令牌" -#: audits/signals_handler.py:68 authentication/views/login.py:164 +#: audits/signals_handler.py:68 authentication/notifications.py:73 +#: authentication/views/dingtalk.py:160 authentication/views/feishu.py:148 +#: authentication/views/login.py:164 authentication/views/wecom.py:158 #: notifications/backends/__init__.py:11 users/models/user.py:607 msgid "WeCom" msgstr "企业微信" @@ -1422,7 +1522,7 @@ msgstr "{AssetPermission} 添加 {UserGroup}" msgid "{AssetPermission} REMOVE {UserGroup}" msgstr "{AssetPermission} 移除 {UserGroup}" -#: audits/signals_handler.py:132 perms/models/asset_permission.py:106 +#: audits/signals_handler.py:132 perms/models/asset_permission.py:30 #: templates/_nav.html:78 users/templates/users/_user_detail_nav_header.html:31 msgid "Asset permission" msgstr "资产授权" @@ -1810,10 +1910,14 @@ msgstr "SSH密钥" msgid "Expired" msgstr "过期时间" -#: authentication/notifications.py:20 +#: authentication/notifications.py:19 msgid "Different city login reminder" msgstr "异地登录提醒" +#: authentication/notifications.py:52 +msgid "binding reminder" +msgstr "定时提醒" + #: authentication/templates/authentication/_access_key_modal.html:6 msgid "API key list" msgstr "API Key列表" @@ -1898,6 +2002,7 @@ msgid "Code error" msgstr "代码错误" #: authentication/templates/authentication/_msg_different_city.html:3 +#: authentication/templates/authentication/_msg_oauth_bind.html:3 #: authentication/templates/authentication/_msg_reset_password.html:3 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 @@ -1919,12 +2024,20 @@ msgstr "你的账号存在异地登录行为,请关注。" msgid "Login time" msgstr "登录日期" -#: authentication/templates/authentication/_msg_different_city.html:15 +#: authentication/templates/authentication/_msg_different_city.html:16 msgid "" "If you suspect that the login behavior is abnormal, please modify the " "account password in time." msgstr "若怀疑此次登录行为异常,请及时修改账号密码" +#: authentication/templates/authentication/_msg_oauth_bind.html:6 +msgid "Your account has just been bound to" +msgstr "您的帐户刚刚绑定到" + +#: authentication/templates/authentication/_msg_oauth_bind.html:17 +msgid "If the operation is not your own, unbind and change the password." +msgstr "" + #: authentication/templates/authentication/_msg_reset_password.html:6 msgid "" "Please click the link below to reset your password, if not your request, " @@ -1935,13 +2048,13 @@ msgstr "请点击下面链接重置密码, 如果不是您申请的,请关 msgid "Click here reset password" msgstr "点击这里重置密码" -#: authentication/templates/authentication/_msg_reset_password.html:15 -#: users/templates/users/_msg_user_created.html:19 +#: authentication/templates/authentication/_msg_reset_password.html:16 +#: users/templates/users/_msg_user_created.html:22 msgid "This link is valid for 1 hour. After it expires" msgstr "这个链接有效期1小时, 超过时间您可以" #: authentication/templates/authentication/_msg_reset_password.html:17 -#: users/templates/users/_msg_user_created.html:20 +#: users/templates/users/_msg_user_created.html:23 msgid "request new one" msgstr "重新申请" @@ -1954,14 +2067,14 @@ msgstr "你的密码刚刚成功更新" msgid "Browser" msgstr "浏览器" -#: authentication/templates/authentication/_msg_rest_password_success.html:12 +#: authentication/templates/authentication/_msg_rest_password_success.html:13 msgid "" "If the password update was not initiated by you, your account may have " "security issues" msgstr "如果这次密码更新不是由你发起的,那么你的账号可能存在安全问题" -#: authentication/templates/authentication/_msg_rest_password_success.html:13 -#: authentication/templates/authentication/_msg_rest_public_key_success.html:13 +#: authentication/templates/authentication/_msg_rest_password_success.html:14 +#: authentication/templates/authentication/_msg_rest_public_key_success.html:14 msgid "If you have any questions, you can contact the administrator" msgstr "如果有疑问或需求,请联系系统管理员" @@ -1969,7 +2082,7 @@ msgstr "如果有疑问或需求,请联系系统管理员" msgid "Your public key has just been successfully updated" msgstr "你的公钥刚刚成功更新" -#: authentication/templates/authentication/_msg_rest_public_key_success.html:12 +#: authentication/templates/authentication/_msg_rest_public_key_success.html:13 msgid "" "If the public key update was not initiated by you, your account may have " "security issues" @@ -2026,86 +2139,86 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/dingtalk.py:37 +#: authentication/views/dingtalk.py:39 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:40 +#: authentication/views/dingtalk.py:42 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:52 authentication/views/feishu.py:48 -#: authentication/views/wecom.py:52 +#: authentication/views/dingtalk.py:54 authentication/views/feishu.py:50 +#: authentication/views/wecom.py:54 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" -#: authentication/views/dingtalk.py:90 +#: authentication/views/dingtalk.py:92 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:103 authentication/views/feishu.py:97 -#: authentication/views/wecom.py:102 +#: authentication/views/dingtalk.py:105 authentication/views/feishu.py:99 +#: authentication/views/wecom.py:104 msgid "Please verify your password first" msgstr "请检查密码" -#: authentication/views/dingtalk.py:127 authentication/views/wecom.py:126 +#: authentication/views/dingtalk.py:129 authentication/views/wecom.py:128 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:143 +#: authentication/views/dingtalk.py:145 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:152 +#: authentication/views/dingtalk.py:154 msgid "The DingTalk is already bound to another user" msgstr "该钉钉已经绑定其他用户" -#: authentication/views/dingtalk.py:157 +#: authentication/views/dingtalk.py:161 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:209 +#: authentication/views/dingtalk.py:213 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:215 +#: authentication/views/dingtalk.py:219 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/views/dingtalk.py:216 +#: authentication/views/dingtalk.py:220 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:36 +#: authentication/views/feishu.py:38 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:84 +#: authentication/views/feishu.py:86 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:131 +#: authentication/views/feishu.py:133 msgid "FeiShu query user failed" msgstr "飞书查询用户失败" -#: authentication/views/feishu.py:140 +#: authentication/views/feishu.py:142 msgid "The FeiShu is already bound to another user" msgstr "该飞书已经绑定其他用户" -#: authentication/views/feishu.py:145 +#: authentication/views/feishu.py:149 msgid "Binding FeiShu successfully" msgstr "绑定 飞书 成功" -#: authentication/views/feishu.py:196 +#: authentication/views/feishu.py:200 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/feishu.py:202 +#: authentication/views/feishu.py:206 msgid "FeiShu is not bound" msgstr "没有绑定飞书" -#: authentication/views/feishu.py:203 +#: authentication/views/feishu.py:207 msgid "Please login with a password and then bind the FeiShu" msgstr "请使用密码登录,然后绑定飞书" @@ -2146,39 +2259,39 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:37 +#: authentication/views/wecom.py:39 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" -#: authentication/views/wecom.py:40 +#: authentication/views/wecom.py:42 msgid "WeCom Error" msgstr "企业微信错误" -#: authentication/views/wecom.py:89 +#: authentication/views/wecom.py:91 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/views/wecom.py:141 +#: authentication/views/wecom.py:143 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:150 +#: authentication/views/wecom.py:152 msgid "The WeCom is already bound to another user" msgstr "该企业微信已经绑定其他用户" -#: authentication/views/wecom.py:155 +#: authentication/views/wecom.py:159 msgid "Binding WeCom successfully" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:204 +#: authentication/views/wecom.py:208 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:210 +#: authentication/views/wecom.py:214 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/views/wecom.py:211 +#: authentication/views/wecom.py:215 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" @@ -2196,7 +2309,7 @@ msgstr "%(name)s 更新成功" msgid "ugettext_lazy" msgstr "" -#: common/db/models.py:71 +#: common/db/models.py:112 msgid "Updated by" msgstr "更新人" @@ -2329,15 +2442,15 @@ msgstr "" msgid "Unknown ip" msgstr "未知ip" -#: common/validators.py:30 +#: common/validators.py:32 msgid "This field must be unique." msgstr "字段必须唯一" -#: common/validators.py:38 +#: common/validators.py:40 msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: common/validators.py:44 +#: common/validators.py:46 msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" @@ -2389,13 +2502,6 @@ msgstr "邮件" msgid "Site message" msgstr "站内信" -#: notifications/notifications.py:172 ops/models/adhoc.py:246 -#: xpack/plugins/change_auth_plan/models/base.py:112 -#: xpack/plugins/change_auth_plan/models/base.py:201 -#: xpack/plugins/gathered_user/models.py:79 -msgid "Time" -msgstr "时间" - #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -2417,11 +2523,6 @@ msgstr "周期执行" msgid "Regularly perform" msgstr "定期执行" -#: ops/mixin.py:106 ops/mixin.py:147 -#: xpack/plugins/change_auth_plan/serializers/base.py:42 -msgid "Periodic perform" -msgstr "定时执行" - #: ops/mixin.py:112 settings/serializers/auth/ldap.py:61 msgid "Interval" msgstr "间隔" @@ -2609,61 +2710,50 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/models/asset_permission.py:37 settings/serializers/terminal.py:12 -msgid "All" -msgstr "全部" - -#: perms/models/asset_permission.py:38 -msgid "Connect" -msgstr "连接" - -#: perms/models/asset_permission.py:39 -msgid "Upload file" -msgstr "上传文件" - -#: perms/models/asset_permission.py:40 -msgid "Download file" -msgstr "下载文件" - -#: perms/models/asset_permission.py:41 -msgid "Upload download" -msgstr "上传下载" - -#: perms/models/asset_permission.py:42 -msgid "Clipboard copy" -msgstr "剪贴板复制" - -#: perms/models/asset_permission.py:43 -msgid "Clipboard paste" -msgstr "剪贴板粘贴" - -#: perms/models/asset_permission.py:44 -msgid "Clipboard copy paste" -msgstr "剪贴板复制粘贴" - -#: perms/models/asset_permission.py:102 -#: perms/serializers/application/permission.py:39 -#: perms/serializers/asset/permission.py:41 -#: perms/serializers/asset/permission.py:69 -msgid "Actions" -msgstr "动作" - -#: perms/models/asset_permission.py:209 +#: perms/models/asset_permission.py:133 msgid "Ungrouped" msgstr "未分组" -#: perms/models/asset_permission.py:211 +#: perms/models/asset_permission.py:135 msgid "Favorite" msgstr "收藏夹" -#: perms/models/base.py:50 +#: perms/models/base.py:55 +msgid "Connect" +msgstr "连接" + +#: perms/models/base.py:56 +msgid "Upload file" +msgstr "上传文件" + +#: perms/models/base.py:57 +msgid "Download file" +msgstr "下载文件" + +#: perms/models/base.py:58 +msgid "Upload download" +msgstr "上传下载" + +#: perms/models/base.py:59 +msgid "Clipboard copy" +msgstr "剪贴板复制" + +#: perms/models/base.py:60 +msgid "Clipboard paste" +msgstr "剪贴板粘贴" + +#: perms/models/base.py:61 +msgid "Clipboard copy paste" +msgstr "剪贴板复制粘贴" + +#: perms/models/base.py:90 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:58 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:60 #: users/models/user.py:590 msgid "Date expired" msgstr "失效日期" -#: perms/models/base.py:54 +#: perms/models/base.py:94 msgid "From ticket" msgstr "来自工单" @@ -2699,59 +2789,59 @@ msgstr "应用授权规则即将过期" msgid "application permissions of organization {}" msgstr "组织 ({}) 的应用授权" -#: perms/serializers/application/permission.py:18 -#: perms/serializers/application/permission.py:38 -#: perms/serializers/asset/permission.py:42 -#: perms/serializers/asset/permission.py:68 users/serializers/user.py:79 +#: perms/serializers/application/permission.py:20 +#: perms/serializers/application/permission.py:41 +#: perms/serializers/asset/permission.py:19 +#: perms/serializers/asset/permission.py:45 users/serializers/user.py:79 msgid "Is valid" msgstr "账户是否有效" -#: perms/serializers/application/permission.py:19 -#: perms/serializers/application/permission.py:37 -#: perms/serializers/asset/permission.py:43 -#: perms/serializers/asset/permission.py:67 users/serializers/user.py:28 +#: perms/serializers/application/permission.py:21 +#: perms/serializers/application/permission.py:40 +#: perms/serializers/asset/permission.py:20 +#: perms/serializers/asset/permission.py:44 users/serializers/user.py:28 #: users/serializers/user.py:80 msgid "Is expired" msgstr "已过期" -#: perms/serializers/application/permission.py:40 -#: perms/serializers/asset/permission.py:70 users/serializers/group.py:34 +#: perms/serializers/application/permission.py:43 +#: perms/serializers/asset/permission.py:47 users/serializers/group.py:34 msgid "Users amount" msgstr "用户数量" -#: perms/serializers/application/permission.py:41 -#: perms/serializers/asset/permission.py:71 +#: perms/serializers/application/permission.py:44 +#: perms/serializers/asset/permission.py:48 msgid "User groups amount" msgstr "用户组数量" -#: perms/serializers/application/permission.py:42 -#: perms/serializers/asset/permission.py:74 +#: perms/serializers/application/permission.py:45 +#: perms/serializers/asset/permission.py:51 msgid "System users amount" msgstr "系统用户数量" -#: perms/serializers/application/permission.py:66 +#: perms/serializers/application/permission.py:88 msgid "" "The application list contains applications that are different from the " "permission type. ({})" msgstr "应用列表中包含与授权类型不同的应用。({})" -#: perms/serializers/asset/permission.py:44 +#: perms/serializers/asset/permission.py:21 msgid "Users display" msgstr "用户名称" -#: perms/serializers/asset/permission.py:45 +#: perms/serializers/asset/permission.py:22 msgid "User groups display" msgstr "用户名称" -#: perms/serializers/asset/permission.py:46 +#: perms/serializers/asset/permission.py:23 msgid "Assets display" msgstr "资产名称" -#: perms/serializers/asset/permission.py:47 +#: perms/serializers/asset/permission.py:24 msgid "Nodes display" msgstr "节点名称" -#: perms/serializers/asset/permission.py:48 +#: perms/serializers/asset/permission.py:25 msgid "System users display" msgstr "系统用户名称" @@ -2767,7 +2857,7 @@ msgstr "" " 以下 %(item_type)s 即将在 3 天后过期\n" " " -#: perms/templates/perms/_msg_permed_items_expire.html:20 +#: perms/templates/perms/_msg_permed_items_expire.html:21 msgid "If you have any question, please contact the administrator" msgstr "如果有疑问或需求,请联系系统管理员" @@ -4212,23 +4302,33 @@ msgid "" "Windows" msgstr "macOS 需要下载客户端来连接 RDP 资产,Windows 系统默认安装了该程序" +#: templates/resource_download.html:41 +msgid "Windows Remote application publisher tools" +msgstr "Windows 远程应用发布服务器工具" + +#: templates/resource_download.html:42 +msgid "" +"Jmservisor is the program used to pull up remote applications in Windows " +"Remote Application publisher" +msgstr "Jmservisor 是在 windows 远程应用发布服务器中用来拉起远程应用的程序" + #: templates/rest_framework/base.html:128 msgid "Filters" msgstr "过滤" -#: terminal/api/session.py:189 +#: terminal/api/session.py:190 msgid "Session does not exist: {}" msgstr "会话不存在: {}" -#: terminal/api/session.py:192 +#: terminal/api/session.py:193 msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session.py:197 +#: terminal/api/session.py:198 msgid "User does not exist: {}" msgstr "用户不存在: {}" -#: terminal/api/session.py:201 +#: terminal/api/session.py:205 msgid "User does not have permission" msgstr "用户没有权限" @@ -4272,30 +4372,30 @@ msgstr "有在线会话" msgid "Invalid elasticsearch config" msgstr "无效的 Elasticsearch 配置" -#: terminal/backends/command/models.py:14 +#: terminal/backends/command/models.py:15 msgid "Ordinary" msgstr "普通" -#: terminal/backends/command/models.py:15 +#: terminal/backends/command/models.py:16 msgid "Dangerous" msgstr "危险" -#: terminal/backends/command/models.py:21 +#: terminal/backends/command/models.py:22 msgid "Input" msgstr "输入" -#: terminal/backends/command/models.py:22 +#: terminal/backends/command/models.py:23 #: terminal/backends/command/serializers.py:16 msgid "Output" msgstr "输出" -#: terminal/backends/command/models.py:23 terminal/models/sharing.py:15 +#: terminal/backends/command/models.py:24 terminal/models/sharing.py:15 #: terminal/models/sharing.py:58 #: terminal/templates/terminal/_msg_command_alert.html:10 msgid "Session" msgstr "会话" -#: terminal/backends/command/models.py:24 +#: terminal/backends/command/models.py:25 #: terminal/backends/command/serializers.py:18 msgid "Risk level" msgstr "风险等级" @@ -4312,6 +4412,10 @@ msgstr "风险等级名称" msgid "Timestamp" msgstr "时间戳" +#: terminal/backends/command/serializers.py:22 terminal/models/terminal.py:105 +msgid "Remote Address" +msgstr "远端地址" + #: terminal/const.py:32 msgid "Critical" msgstr "严重" @@ -4337,15 +4441,15 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/session.py:45 terminal/models/sharing.py:81 +#: terminal/models/session.py:46 terminal/models/sharing.py:81 msgid "Login from" msgstr "登录来源" -#: terminal/models/session.py:49 +#: terminal/models/session.py:50 msgid "Replay" msgstr "回放" -#: terminal/models/session.py:54 +#: terminal/models/session.py:55 msgid "Date end" msgstr "结束日期" @@ -4438,10 +4542,6 @@ msgstr "" msgid "type" msgstr "类型" -#: terminal/models/terminal.py:105 -msgid "Remote Address" -msgstr "远端地址" - #: terminal/models/terminal.py:106 msgid "SSH Port" msgstr "SSH端口" @@ -4785,63 +4885,63 @@ msgstr "用户显示名称" msgid "Body" msgstr "内容" -#: tickets/models/flow.py:20 tickets/models/flow.py:56 -#: tickets/models/ticket.py:25 +#: tickets/models/flow.py:18 tickets/models/flow.py:60 +#: tickets/models/ticket.py:29 msgid "Approve level" msgstr "审批级别" -#: tickets/models/flow.py:25 tickets/serializers/ticket/ticket.py:141 +#: tickets/models/flow.py:23 tickets/serializers/ticket/ticket.py:141 msgid "Approve strategy" msgstr "审批策略" -#: tickets/models/flow.py:30 tickets/serializers/ticket/ticket.py:142 +#: tickets/models/flow.py:28 tickets/serializers/ticket/ticket.py:142 msgid "Assignees" msgstr "受理人" -#: tickets/models/flow.py:34 -msgid "Assignees display" -msgstr "受理人名称" - -#: tickets/models/flow.py:38 +#: tickets/models/flow.py:32 msgid "Ticket flow approval rule" msgstr "工单批准信息" -#: tickets/models/flow.py:61 +#: tickets/models/flow.py:65 msgid "Ticket flow" msgstr "工单流程" -#: tickets/models/ticket.py:38 +#: tickets/models/ticket.py:42 msgid "Ticket assignee" msgstr "工单受理人" -#: tickets/models/ticket.py:45 +#: tickets/models/ticket.py:49 msgid "Title" msgstr "标题" -#: tickets/models/ticket.py:53 +#: tickets/models/ticket.py:57 msgid "State" msgstr "状态" -#: tickets/models/ticket.py:61 +#: tickets/models/ticket.py:65 msgid "Approval step" msgstr "审批步骤" -#: tickets/models/ticket.py:66 +#: tickets/models/ticket.py:70 msgid "Applicant" msgstr "申请人" -#: tickets/models/ticket.py:68 +#: tickets/models/ticket.py:72 msgid "Applicant display" msgstr "申请人名称" -#: tickets/models/ticket.py:69 +#: tickets/models/ticket.py:73 msgid "Process" msgstr "流程" -#: tickets/models/ticket.py:74 +#: tickets/models/ticket.py:78 msgid "TicketFlow" msgstr "工单流程" +#: tickets/models/ticket.py:296 +msgid "Please try again" +msgstr "请再次尝试" + #: tickets/notifications.py:57 msgid "Your has a new ticket, applicant - {}" msgstr "你有一个新的工单, 申请人 - {}" @@ -4972,11 +5072,15 @@ msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" -#: tickets/serializers/ticket/ticket.py:163 +#: tickets/serializers/ticket/ticket.py:143 +msgid "Assignees display" +msgstr "受理人名称" + +#: tickets/serializers/ticket/ticket.py:167 msgid "Please select the Assignees" msgstr "请选择受理人" -#: tickets/serializers/ticket/ticket.py:189 +#: tickets/serializers/ticket/ticket.py:193 msgid "The current organization type already exists" msgstr "当前组织已存在该类型" @@ -5597,17 +5701,11 @@ msgid "The parameter 'action' must be [{}]" msgstr "参数 'action' 必须是 [{}]" #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models/asset.py:68 -#: xpack/plugins/change_auth_plan/models/asset.py:124 +#: xpack/plugins/change_auth_plan/models/asset.py:67 +#: xpack/plugins/change_auth_plan/models/asset.py:123 msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models/app.py:41 -#: xpack/plugins/change_auth_plan/models/asset.py:63 -#: xpack/plugins/change_auth_plan/serializers/base.py:44 -msgid "Recipient" -msgstr "收件人" - #: xpack/plugins/change_auth_plan/models/app.py:46 #: xpack/plugins/change_auth_plan/models/app.py:95 msgid "Application change auth plan" @@ -5626,29 +5724,29 @@ msgstr "应用" msgid "Application change auth plan task" msgstr "用用改密计划任务" -#: xpack/plugins/change_auth_plan/models/asset.py:30 +#: xpack/plugins/change_auth_plan/models/asset.py:29 msgid "Append SSH KEY" msgstr "追加" -#: xpack/plugins/change_auth_plan/models/asset.py:31 +#: xpack/plugins/change_auth_plan/models/asset.py:30 msgid "Empty and append SSH KEY" msgstr "清空所有并添加" -#: xpack/plugins/change_auth_plan/models/asset.py:32 +#: xpack/plugins/change_auth_plan/models/asset.py:31 msgid "Replace (The key generated by JumpServer) " msgstr "替换 (由 JumpServer 生成的密钥)" -#: xpack/plugins/change_auth_plan/models/asset.py:50 -#: xpack/plugins/change_auth_plan/serializers/asset.py:33 +#: xpack/plugins/change_auth_plan/models/asset.py:49 +#: xpack/plugins/change_auth_plan/serializers/asset.py:34 msgid "SSH Key strategy" msgstr "SSH 密钥策略" -#: xpack/plugins/change_auth_plan/models/asset.py:135 -#: xpack/plugins/change_auth_plan/models/asset.py:211 +#: xpack/plugins/change_auth_plan/models/asset.py:134 +#: xpack/plugins/change_auth_plan/models/asset.py:210 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models/asset.py:218 +#: xpack/plugins/change_auth_plan/models/asset.py:217 msgid "Change auth plan task" msgstr "改密计划任务" @@ -5668,23 +5766,10 @@ msgstr "使用不同的随机密码" msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models/base.py:104 -msgid "Manual trigger" -msgstr "手动触发" - -#: xpack/plugins/change_auth_plan/models/base.py:105 -msgid "Timing trigger" -msgstr "定时触发" - #: xpack/plugins/change_auth_plan/models/base.py:115 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models/base.py:122 -#: xpack/plugins/change_auth_plan/serializers/base.py:73 -msgid "Trigger mode" -msgstr "触发模式" - #: xpack/plugins/change_auth_plan/models/base.py:184 msgid "Ready" msgstr "准备" @@ -5728,11 +5813,11 @@ msgstr "" "{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" "密密码" -#: xpack/plugins/change_auth_plan/serializers/asset.py:30 +#: xpack/plugins/change_auth_plan/serializers/asset.py:31 msgid "Change Password" msgstr "更改密码" -#: xpack/plugins/change_auth_plan/serializers/asset.py:31 +#: xpack/plugins/change_auth_plan/serializers/asset.py:32 msgid "Change SSH Key" msgstr "修改 SSH Key" @@ -5740,10 +5825,6 @@ msgstr "修改 SSH Key" msgid "Run times" msgstr "执行次数" -#: xpack/plugins/change_auth_plan/serializers/base.py:45 -msgid "Currently only mail sending is supported" -msgstr "当前只支持邮件发送" - #: xpack/plugins/change_auth_plan/serializers/base.py:57 msgid "* Please enter the correct password length" msgstr "* 请输入正确的密码长度" @@ -6230,11 +6311,17 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" -#~ msgid "Please enter the password of" -#~ msgstr "请输入" +#~ msgid "Account backup plan execution" +#~ msgstr "改密计划执行" -#~ msgid "account" -#~ msgstr "账户" +#~ msgid "Account backup plan task" +#~ msgstr "改密计划任务" -#~ msgid "to complete the binding operation" -#~ msgstr "的密码完成绑定操作" +#~ msgid "Escape route plan" +#~ msgstr "改密计划" + +#~ msgid "Escape route execution" +#~ msgstr "改密计划执行" + +#~ msgid "Escape route plan task" +#~ msgstr "改密计划任务" diff --git a/apps/orgs/api.py b/apps/orgs/api.py index 283e71469..4a5c24fdb 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -19,7 +19,7 @@ from .serializers import ( ) from users.models import User, UserGroup from assets.models import ( - Asset, Domain, AdminUser, SystemUser, Label, Node, Gateway, + Asset, Domain, SystemUser, Label, Node, Gateway, CommandFilter, CommandFilterRule, GatheredUser ) from applications.models import Application @@ -35,7 +35,7 @@ logger = get_logger(__file__) # 部分 org 相关的 model,需要清空这些数据之后才能删除该组织 org_related_models = [ - User, UserGroup, Asset, Label, Domain, Gateway, Node, AdminUser, SystemUser, Label, + User, UserGroup, Asset, Label, Domain, Gateway, Node, SystemUser, Label, CommandFilter, CommandFilterRule, GatheredUser, AssetPermission, ApplicationPermission, Application, diff --git a/apps/orgs/models.py b/apps/orgs/models.py index efbee20b2..c8baec8fb 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -234,9 +234,9 @@ class Organization(models.Model): with tmp_to_org(self): return resource_model.objects.all().count() - def as_tree_node(self, pid, opened=True): + def as_tree_node(self, oid, pid, opened=True): node = TreeNode(**{ - 'id': str(self.id), + 'id': oid, 'name': self.name, 'title': self.name, 'pId': pid, diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py index 2a64d122d..707e2ca72 100644 --- a/apps/perms/api/application/user_permission/common.py +++ b/apps/perms/api/application/user_permission/common.py @@ -13,7 +13,6 @@ from rest_framework.generics import ( from orgs.utils import tmp_to_root_org from applications.models import Application from perms.utils.application.permission import ( - has_application_system_permission, get_application_system_user_ids, validate_permission, ) @@ -64,13 +63,22 @@ class ValidateUserApplicationPermissionApi(APIView): application_id = request.query_params.get('application_id', '') system_user_id = request.query_params.get('system_user_id', '') + data = { + 'has_permission': False, + 'expire_at': int(time.time()), + 'actions': [] + } if not all((user_id, application_id, system_user_id)): - return Response({'has_permission': False, 'expire_at': int(time.time())}) + return Response(data) user = User.objects.get(id=user_id) application = Application.objects.get(id=application_id) system_user = SystemUser.objects.get(id=system_user_id) - - has_permission, expire_at = validate_permission(user, application, system_user) - status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN - return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) + has_perm, actions, expire_at = validate_permission(user, application, system_user) + status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN + data = { + 'has_permission': has_perm, + 'expire_at': int(expire_at), + 'actions': actions + } + return Response(data, status=status_code) diff --git a/apps/perms/api/application/user_permission/user_permission_applications.py b/apps/perms/api/application/user_permission/user_permission_applications.py index be5fc6745..b9ddf1850 100644 --- a/apps/perms/api/application/user_permission/user_permission_applications.py +++ b/apps/perms/api/application/user_permission/user_permission_applications.py @@ -54,10 +54,15 @@ class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin): 将应用序列化成树的结构返回 """ serializer_class = TreeNodeSerializer + user: None def list(self, request, *args, **kwargs): + tree_id = request.query_params.get('tree_id', None) + parent_info = request.query_params.get('parentInfo', None) queryset = self.filter_queryset(self.get_queryset()) - tree_nodes = self.serialize_applications_with_org(queryset) + tree_nodes = self.serialize_applications_with_org( + queryset, tree_id, parent_info, self.user + ) serializer = self.get_serializer(tree_nodes, many=True) return Response(data=serializer.data) diff --git a/apps/perms/api/asset/user_permission/common.py b/apps/perms/api/asset/user_permission/common.py index 1d09274c8..e874d591f 100644 --- a/apps/perms/api/asset/user_permission/common.py +++ b/apps/perms/api/asset/user_permission/common.py @@ -72,16 +72,27 @@ class ValidateUserAssetPermissionApi(APIView): system_id = request.query_params.get('system_user_id', '') action_name = request.query_params.get('action_name', '') + data = { + 'has_permission': False, + 'expire_at': int(time.time()), + 'actions': [] + } + if not all((user_id, asset_id, system_id, action_name)): - return Response({'has_permission': False, 'expire_at': int(time.time())}) + return Response(data) user = User.objects.get(id=user_id) asset = Asset.objects.valid().get(id=asset_id) system_user = SystemUser.objects.get(id=system_id) - has_permission, expire_at = validate_permission(user, asset, system_user, action_name) - status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN - return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) + has_perm, actions, expire_at = validate_permission(user, asset, system_user, action_name) + status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN + data = { + 'has_permission': has_perm, + 'actions': actions, + 'expire_at': int(expire_at) + } + return Response(data, status=status_code) # TODO 删除 diff --git a/apps/perms/migrations/0011_auto_20200721_1739.py b/apps/perms/migrations/0011_auto_20200721_1739.py index 7e6b37188..352bd6b79 100644 --- a/apps/perms/migrations/0011_auto_20200721_1739.py +++ b/apps/perms/migrations/0011_auto_20200721_1739.py @@ -3,7 +3,7 @@ from django.db import migrations, models from django.db.models import F -from ..models.asset_permission import Action +from ..models.base import Action def migrate_asset_permission(apps, schema_editor): diff --git a/apps/perms/migrations/0022_applicationpermission_actions.py b/apps/perms/migrations/0022_applicationpermission_actions.py new file mode 100644 index 000000000..3026b1326 --- /dev/null +++ b/apps/perms/migrations/0022_applicationpermission_actions.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.13 on 2021-12-20 06:55 + +from django.db import migrations, models + +ACTION_CONNECT = 1 + + +def migrate_app_perms_actions(apps, schema_editor): + perm_model = apps.get_model("perms", "ApplicationPermission") + perm_model.objects.all().update(actions=ACTION_CONNECT) + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0021_auto_20211105_1605'), + ] + + operations = [ + migrations.AddField( + model_name='applicationpermission', + name='actions', + field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'), + ), + migrations.RunPython(migrate_app_perms_actions) + ] diff --git a/apps/perms/migrations/0023_auto_20220112_2035.py b/apps/perms/migrations/0023_auto_20220112_2035.py new file mode 100644 index 000000000..0cfa0e0cc --- /dev/null +++ b/apps/perms/migrations/0023_auto_20220112_2035.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.13 on 2022-01-12 12:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0022_applicationpermission_actions'), + ] + + operations = [ + migrations.AlterField( + model_name='applicationpermission', + name='type', + field=models.CharField(choices=[('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/perms/models/__init__.py b/apps/perms/models/__init__.py index 31a1344eb..e2ac0c1d6 100644 --- a/apps/perms/models/__init__.py +++ b/apps/perms/models/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .application_permission import * +from .base import * diff --git a/apps/perms/models/application_permission.py b/apps/perms/models/application_permission.py index 40ac61ed8..bec5f3da5 100644 --- a/apps/perms/models/application_permission.py +++ b/apps/perms/models/application_permission.py @@ -6,7 +6,7 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty -from .base import BasePermission +from .base import BasePermission, Action from users.models import User from applications.const import AppCategory, AppType @@ -72,3 +72,31 @@ class ApplicationPermission(BasePermission): Q(id__in=user_ids) | Q(groups__id__in=user_group_ids) ) return users + + @classmethod + def get_include_actions_choices(cls, category=None): + actions = {Action.ALL, Action.CONNECT} + if category == AppCategory.db: + _actions = [Action.UPLOAD, Action.DOWNLOAD] + elif category == AppCategory.remote_app: + _actions = [ + Action.UPLOAD, Action.DOWNLOAD, + Action.CLIPBOARD_COPY, Action.CLIPBOARD_PASTE + ] + else: + _actions = [] + actions.update(_actions) + + if (Action.UPLOAD in actions) or (Action.DOWNLOAD in actions): + actions.update([Action.UPDOWNLOAD]) + if (Action.CLIPBOARD_COPY in actions) or (Action.CLIPBOARD_PASTE in actions): + actions.update([Action.CLIPBOARD_COPY_PASTE]) + + choices = [Action.NAME_MAP[action] for action in actions] + return choices + + @classmethod + def get_exclude_actions_choices(cls, category=None): + include_choices = cls.get_include_actions_choices(category) + exclude_choices = set(Action.NAME_MAP.values()) - set(include_choices) + return exclude_choices diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 78e6b9b5b..7947449e7 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,5 +1,4 @@ import logging -from functools import reduce from django.utils.translation import ugettext_lazy as _ from django.db.models import F @@ -14,92 +13,17 @@ from .base import BasePermission __all__ = [ - 'AssetPermission', 'Action', 'PermNode', 'UserAssetGrantedTreeNodeRelation', + 'AssetPermission', 'PermNode', 'UserAssetGrantedTreeNodeRelation', ] # 使用场景 logger = logging.getLogger(__name__) -class Action: - NONE = 0 - - CONNECT = 0b1 - UPLOAD = 0b1 << 1 - DOWNLOAD = 0b1 << 2 - CLIPBOARD_COPY = 0b1 << 3 - CLIPBOARD_PASTE = 0b1 << 4 - ALL = 0xff - 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)) - - @classmethod - def value_to_choices(cls, value): - if isinstance(value, list): - return value - value = int(value) - choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] - return choices - - @classmethod - def value_to_choices_display(cls, value): - choices = cls.value_to_choices(value) - return [str(dict(cls.choices())[i]) for i in choices] - - @classmethod - def choices_to_value(cls, value): - if not isinstance(value, list): - return cls.NONE - db_value = [ - cls.NAME_MAP_REVERSE[v] for v in value - if v in cls.NAME_MAP_REVERSE.keys() - ] - if not db_value: - return cls.NONE - - def to_choices(x, y): - return x | y - - result = reduce(to_choices, db_value) - return result - - @classmethod - def choices(cls): - return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] - - class AssetPermission(BasePermission): assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', blank=True, verbose_name=_("System user")) - actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) class Meta: unique_together = [('org_id', 'name')] diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index 2e62a84bf..b2d388717 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -8,12 +8,12 @@ from django.db.models import Q from django.utils import timezone from orgs.mixins.models import OrgModelMixin -from common.db.models import UnionQuerySet +from common.db.models import UnionQuerySet, BitOperationChoice from common.utils import date_expired_default, lazyproperty from orgs.mixins.models import OrgManager __all__ = [ - 'BasePermission', 'BasePermissionQuerySet' + 'BasePermission', 'BasePermissionQuerySet', 'Action' ] @@ -39,12 +39,52 @@ class BasePermissionManager(OrgManager): return self.get_queryset().valid() +class Action(BitOperationChoice): + ALL = 0xff + + CONNECT = 0b1 + UPLOAD = 0b1 << 1 + DOWNLOAD = 0b1 << 2 + CLIPBOARD_COPY = 0b1 << 3 + CLIPBOARD_PASTE = 0b1 << 4 + UPDOWNLOAD = UPLOAD | DOWNLOAD + CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE + + DB_CHOICES = ( + (ALL, _('All')), + (CONNECT, _('Connect')), + (UPLOAD, _('Upload file')), + (DOWNLOAD, _('Download file')), + (UPDOWNLOAD, _("Upload download")), + (CLIPBOARD_COPY, _('Clipboard copy')), + (CLIPBOARD_PASTE, _('Clipboard paste')), + (CLIPBOARD_COPY_PASTE, _('Clipboard copy paste')) + ) + + NAME_MAP = { + ALL: "all", + CONNECT: "connect", + UPLOAD: "upload_file", + DOWNLOAD: "download_file", + UPDOWNLOAD: "updownload", + CLIPBOARD_COPY: 'clipboard_copy', + CLIPBOARD_PASTE: 'clipboard_paste', + CLIPBOARD_COPY_PASTE: 'clipboard_copy_paste' + } + + NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} + CHOICES = [] + for i, j in DB_CHOICES: + CHOICES.append((NAME_MAP[i], j)) + + class BasePermission(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss') user_groups = models.ManyToManyField( 'users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss') + actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) is_active = models.BooleanField(default=True, verbose_name=_('Active')) date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired')) diff --git a/apps/perms/serializers/__init__.py b/apps/perms/serializers/__init__.py index 9fb257580..a4a701773 100644 --- a/apps/perms/serializers/__init__.py +++ b/apps/perms/serializers/__init__.py @@ -1,5 +1,6 @@ # coding: utf-8 # +from .base import * from .asset import * from .application import * from .system_user_permission import * diff --git a/apps/perms/serializers/application/permission.py b/apps/perms/serializers/application/permission.py index e636eec8c..1df5b1eca 100644 --- a/apps/perms/serializers/application/permission.py +++ b/apps/perms/serializers/application/permission.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import ApplicationPermission +from ..base import ActionsField __all__ = [ 'ApplicationPermissionSerializer' @@ -13,6 +14,7 @@ __all__ = [ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): + actions = ActionsField(required=False, allow_null=True, label=_("Actions")) category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) is_valid = serializers.BooleanField(read_only=True, label=_('Is valid')) @@ -23,6 +25,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): fields_mini = ['id', 'name'] fields_small = fields_mini + [ 'category', 'category_display', 'type', 'type_display', + 'actions', 'is_active', 'is_expired', 'is_valid', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket' ] @@ -43,6 +46,25 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): 'applications_amount': {'label': _('Applications amount')}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_actions_choices() + + def set_actions_choices(self): + actions = self.fields.get('actions') + if not actions: + return + choices = actions._choices + if request := self.context.get('request'): + category = request.query_params.get('category') + else: + category = None + exclude_choices = ApplicationPermission.get_exclude_actions_choices(category=category) + for choice in exclude_choices: + choices.pop(choice, None) + actions._choices = choices + actions.default = list(choices.keys()) + @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index 3df2c1173..0b0d5aaa6 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -9,32 +9,9 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action from assets.models import Asset, Node, SystemUser from users.models import User, UserGroup +from ..base import ActionsField -__all__ = [ - 'AssetPermissionSerializer', - 'ActionsField', -] - - -class ActionsField(serializers.MultipleChoiceField): - def __init__(self, *args, **kwargs): - kwargs['choices'] = Action.CHOICES - super().__init__(*args, **kwargs) - - def to_representation(self, value): - return Action.value_to_choices(value) - - def to_internal_value(self, data): - if data is None: - 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] +__all__ = ['AssetPermissionSerializer'] class AssetPermissionSerializer(BulkOrgResourceModelSerializer): diff --git a/apps/perms/serializers/asset/user_permission.py b/apps/perms/serializers/asset/user_permission.py index 0603a0e08..d2ad97894 100644 --- a/apps/perms/serializers/asset/user_permission.py +++ b/apps/perms/serializers/asset/user_permission.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from assets.models import Node, SystemUser, Asset, Platform from assets.serializers import ProtocolsField -from perms.serializers.asset.permission import ActionsField +from perms.serializers.base import ActionsField __all__ = [ 'NodeGrantedSerializer', diff --git a/apps/perms/serializers/base.py b/apps/perms/serializers/base.py new file mode 100644 index 000000000..7e8e1b63f --- /dev/null +++ b/apps/perms/serializers/base.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from perms.models import Action + +__all__ = ['ActionsDisplayField', 'ActionsField'] + + +class ActionsField(serializers.MultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['choices'] = Action.CHOICES + super().__init__(*args, **kwargs) + + def to_representation(self, value): + return Action.value_to_choices(value) + + def to_internal_value(self, data): + if data is None: + 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] + diff --git a/apps/perms/utils/application/permission.py b/apps/perms/utils/application/permission.py index c4ebb5bdb..3f3dc8018 100644 --- a/apps/perms/utils/application/permission.py +++ b/apps/perms/utils/application/permission.py @@ -3,7 +3,7 @@ import time from django.db.models import Q from common.utils import get_logger -from perms.models import ApplicationPermission +from perms.models import ApplicationPermission, Action logger = get_logger(__file__) @@ -33,31 +33,38 @@ def get_user_all_app_perm_ids(user) -> set: return app_perm_ids -def validate_permission(user, application, system_user): +def validate_permission(user, application, system_user, action='connect'): app_perm_ids = get_user_all_app_perm_ids(user) app_perm_ids = ApplicationPermission.applications.through.objects.filter( applicationpermission_id__in=app_perm_ids, application_id=application.id ).values_list('applicationpermission_id', flat=True) - app_perm_ids = set(app_perm_ids) - app_perm_ids = ApplicationPermission.system_users.through.objects.filter( applicationpermission_id__in=app_perm_ids, systemuser_id=system_user.id ).values_list('applicationpermission_id', flat=True) - app_perm_ids = set(app_perm_ids) - - app_perm = ApplicationPermission.objects.filter( + app_perms = ApplicationPermission.objects.filter( id__in=app_perm_ids - ).order_by('-date_expired').first() + ).order_by('-date_expired') - app_perm: ApplicationPermission - if app_perm: - return True, app_perm.date_expired.timestamp() + if app_perms: + actions = set() + actions_values = app_perms.values_list('actions', flat=True) + for value in actions_values: + _actions = Action.value_to_choices(value) + actions.update(_actions) + actions = list(actions) + app_perm: ApplicationPermission = app_perms.first() + expire_at = app_perm.date_expired.timestamp() else: - return False, time.time() + actions = [] + expire_at = time.time() + + # TODO: 组件改造API完成后统一通过actions判断has_perm + has_perm = action in actions + return has_perm, actions, expire_at def get_application_system_user_ids(user, application): diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py index 349040b99..e749e630b 100644 --- a/apps/perms/utils/asset/permission.py +++ b/apps/perms/utils/asset/permission.py @@ -11,7 +11,7 @@ from perms.utils.asset.user_permission import get_user_all_asset_perm_ids logger = get_logger(__file__) -def validate_permission(user, asset, system_user, action_name): +def validate_permission(user, asset, system_user, action='connect'): if not system_user.protocol in asset.protocols_as_dict.keys(): return False, time.time() @@ -50,10 +50,22 @@ def validate_permission(user, asset, system_user, action_name): id__in=asset_perm_ids ).order_by('-date_expired') - for asset_perm in asset_perms: - if action_name in Action.value_to_choices(asset_perm.actions): - return True, asset_perm.date_expired.timestamp() - return False, time.time() + if asset_perms: + actions = set() + actions_values = asset_perms.values_list('actions', flat=True) + for value in actions_values: + _actions = Action.value_to_choices(value) + actions.update(_actions) + asset_perm: AssetPermission = asset_perms.first() + actions = list(actions) + expire_at = asset_perm.date_expired.timestamp() + else: + actions = [] + expire_at = time.time() + + # TODO: 组件改造API完成后统一通过actions判断has_perm + has_perm = action in actions + return has_perm, actions, expire_at def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset): diff --git a/apps/static/img/login_saml2_logo.png b/apps/static/img/login_saml2_logo.png new file mode 100644 index 000000000..d7e8c03db Binary files /dev/null and b/apps/static/img/login_saml2_logo.png differ diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html index f03b20686..479408161 100644 --- a/apps/templates/resource_download.html +++ b/apps/templates/resource_download.html @@ -35,6 +35,16 @@ p {
  • Microsoft_Remote_Desktop_10.6.7_installer.pkg
  • + + {% if XPACK_ENABLED %} +
    +

    {% trans 'Windows Remote application publisher tools' %}

    +

    {% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}

    + +
    + {% endif %}