Merge pull request #7487 from jumpserver/dev

v2.18.0-rc1
pull/7561/head
Jiangjie.Bai 2022-01-12 20:56:33 +08:00 committed by GitHub
commit 35722a8466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 2280 additions and 650 deletions

View File

@ -20,6 +20,8 @@ jobs:
run: | run: |
TAG=$(basename ${GITHUB_REF}) TAG=$(basename ${GITHUB_REF})
VERSION=${TAG/v/} 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=TAG::$TAG"
echo "::set-output name=VERSION::$VERSION" echo "::set-output name=VERSION::$VERSION"
- name: Create Release - name: Create Release
@ -31,6 +33,16 @@ jobs:
config-name: release-config.yml config-name: release-config.yml
version: ${{ steps.get_version.outputs.TAG }} version: ${{ steps.get_version.outputs.TAG }}
tag: ${{ 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: build-and-release:
needs: create-realese needs: create-realese

23
.github/workflows/sync-gitee.yml vendored Normal file
View File

@ -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'

View File

@ -1,5 +1,5 @@
# 编译代码 # 编译代码
FROM python:3.8.6-slim as stage-build FROM python:3.8-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com> MAINTAINER JumpServer Team <ibuler@qq.com>
ARG VERSION ARG VERSION
ENV VERSION=$VERSION ENV VERSION=$VERSION
@ -9,7 +9,7 @@ ADD . .
RUN cd utils && bash -ixeu build.sh 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 ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple

View File

@ -124,7 +124,7 @@ JumpServer是一款安全产品请参考 [基本安全建议](https://docs.ju
### License & Copyright ### 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 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

View File

@ -85,7 +85,7 @@ If you find a security problem, please contact us directly
- 400-052-0755 - 400-052-0755
### License & Copyright ### 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 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

View File

@ -60,7 +60,8 @@ class LoginAssetCheckAPI(CreateAPIView):
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
'ticket_detail_url': ticket_detail_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 return data

View File

@ -1,6 +1,6 @@
# coding: utf-8 # coding: utf-8
# #
from django.shortcuts import get_object_or_404
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response

View File

@ -1,13 +1,21 @@
from urllib.parse import urlencode, parse_qsl
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework.generics import get_object_or_404
from common.tree import TreeNode from common.tree import TreeNode
from orgs.models import Organization 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 from ..models import Application
__all__ = ['SerializeApplicationToTreeNodeMixin'] __all__ = ['SerializeApplicationToTreeNodeMixin']
class SerializeApplicationToTreeNodeMixin: class SerializeApplicationToTreeNodeMixin:
@staticmethod @staticmethod
def filter_organizations(applications): def filter_organizations(applications):
organization_ids = set(applications.values_list('org_id', flat=True)) organization_ids = set(applications.values_list('org_id', flat=True))
@ -31,25 +39,47 @@ class SerializeApplicationToTreeNodeMixin:
}) })
return node 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: if not applications:
return [] return tree_nodes
root_node = self.create_root_node()
tree_nodes = [root_node] if not tree_id:
organizations = self.filter_organizations(applications) root_node = self.create_root_node()
tree_nodes.append(root_node)
for i, org in enumerate(organizations): organizations = self.filter_organizations(applications)
# 组织节点 for i, org in enumerate(organizations):
org_node = org.as_tree_node(pid=root_node.id) tree_id = urlencode({'org_id': str(org.id)})
tree_nodes.append(org_node) apps = applications.filter(org_id=org.id)
org_applications = applications.filter(org_id=org.id) # 组织节点
count = org_applications.count() org_node = org.as_tree_node(oid=tree_id, pid=root_node.id)
org_node.name += '({})'.format(count) org_node.name += '({})'.format(apps.count())
tree_nodes.append(org_node)
# 各应用节点 category_type_nodes = Application.create_category_type_tree_nodes(
apps_nodes = Application.create_tree_nodes( apps, tree_id, show_empty=False
queryset=org_applications, root_node=org_node, )
show_empty=False tree_nodes += category_type_nodes
)
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 return tree_nodes

View File

@ -17,6 +17,7 @@ class AppCategory(TextChoices):
class AppType(TextChoices): class AppType(TextChoices):
# db category # db category
mysql = 'mysql', 'MySQL' mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
oracle = 'oracle', 'Oracle' oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL' pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB' mariadb = 'mariadb', 'MariaDB'
@ -34,7 +35,9 @@ class AppType(TextChoices):
@classmethod @classmethod
def category_types_mapper(cls): def category_types_mapper(cls):
return { 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.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
AppCategory.cloud: [cls.k8s] AppCategory.cloud: [cls.k8s]
} }
@ -62,7 +65,3 @@ class AppType(TextChoices):
@classmethod @classmethod
def cloud_types(cls): def cloud_types(cls):
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]] return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]

View File

@ -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'),
),
]

View File

@ -1,4 +1,5 @@
from collections import defaultdict from collections import defaultdict
from urllib.parse import urlencode, parse_qsl
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ 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.mixins import CommonModelMixin
from common.tree import TreeNode from common.tree import TreeNode
from assets.models import Asset, SystemUser from assets.models import Asset, SystemUser
from ..utils import KubernetesTree
from .. import const from .. import const
@ -16,6 +19,13 @@ class ApplicationTreeNodeMixin:
type: str type: str
category: 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 @classmethod
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None, def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
show_empty=True, show_count=True): show_empty=True, show_count=True):
@ -65,13 +75,13 @@ class ApplicationTreeNodeMixin:
return node return node
@classmethod @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 = [] nodes = []
categories = const.AppType.category_types_mapper().keys() categories = const.AppType.category_types_mapper().keys()
for category in categories: for category in categories:
i = root_node.id + '_' + category.value i = cls.create_tree_id(pid, 'category', category.value)
node = cls.create_choice_node( 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, counts=counts, opened=False, show_empty=show_empty,
show_count=show_count show_count=show_count
) )
@ -81,17 +91,20 @@ class ApplicationTreeNodeMixin:
return nodes return nodes
@classmethod @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 = [] nodes = []
temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper() 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) category = type_category_mapper.get(tp)
pid = root_node.id + '_' + category.value pid = cls.create_tree_id(pid, 'category', category.value)
i = root_node.id + '_' + tp.value i = cls.create_tree_id(pid, 'type', tp.value)
node = cls.create_choice_node( node = cls.create_choice_node(
tp, i, pid, tp='type', counts=counts, opened=False, tp, i, pid, tp='type', counts=counts, opened=False,
show_empty=show_empty, show_count=show_count show_empty=show_empty, show_count=show_count
) )
pid = temp_pid
if not node: if not node:
continue continue
nodes.append(node) nodes.append(node)
@ -109,40 +122,63 @@ class ApplicationTreeNodeMixin:
return counts return counts
@classmethod @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) counts = cls.get_tree_node_counts(queryset)
tree_nodes = [] 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: if root_node is None:
root_node = cls.create_root_tree_node(queryset, show_count=show_count) root_node = cls.create_root_tree_node(queryset, show_count=show_count)
tree_nodes.append(root_node) tree_nodes.append(root_node)
# 类别的节点 tree_nodes += cls.create_category_type_tree_nodes(
tree_nodes += cls.create_category_tree_nodes( queryset, root_node.id, show_empty=show_empty, show_count=show_count
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
) )
# 应用的节点 # 应用的节点
for app in queryset: for app in queryset:
pid = root_node.id + '_' + app.type node = app.as_tree_node(root_node.id)
tree_nodes.append(app.as_tree_node(pid)) tree_nodes.append(node)
return tree_nodes 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 = { icon_skin_category_mapper = {
'remote_app': 'chrome', 'remote_app': 'chrome',
'db': 'database', 'db': 'database',
'cloud': 'cloud' 'cloud': 'cloud'
} }
icon_skin = icon_skin_category_mapper.get(self.category, 'file') icon_skin = icon_skin_category_mapper.get(self.category, 'file')
pid = self.create_app_tree_pid(pid)
node = TreeNode(**{ node = TreeNode(**{
'id': str(self.id), 'id': str(self.id),
'name': self.name, 'name': self.name,

View File

@ -96,7 +96,7 @@ class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = models.Account model = models.Account
fields_mini = ['id', 'username', 'version'] 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_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
fields = fields_mini + fields_fk + fields_write_only + [ fields = fields_mini + fields_fk + fields_write_only + [
'type', 'type_display', 'category', 'category_display', 'type', 'type_display', 'category', 'category_display',

View File

@ -1,5 +1,6 @@
from .mysql import * from .mysql import *
from .redis import *
from .mariadb import * from .mariadb import *
from .oracle import * from .oracle import *
from .pgsql import * from .pgsql import *

View File

@ -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)

View File

@ -25,6 +25,7 @@ category_serializer_classes_mapping = {
type_serializer_classes_mapping = { type_serializer_classes_mapping = {
# db # db
const.AppType.mysql.value: application_type.MySQLSerializer, const.AppType.mysql.value: application_type.MySQLSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mariadb.value: application_type.MariaDBSerializer, const.AppType.mariadb.value: application_type.MariaDBSerializer,
const.AppType.oracle.value: application_type.OracleSerializer, const.AppType.oracle.value: application_type.OracleSerializer,
const.AppType.pgsql.value: application_type.PostgreSerializer, const.AppType.pgsql.value: application_type.PostgreSerializer,

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from .kubernetes_util import *

View File

@ -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

View File

@ -10,3 +10,4 @@ from .domain import *
from .cmd_filter import * from .cmd_filter import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .backup import *

55
apps/assets/api/backup.py Normal file
View File

@ -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

View File

@ -17,7 +17,7 @@ from common.mixins.api import SuggestionMixin
from assets.models import Asset from assets.models import Asset
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer from common.tree import TreeNodeSerializer
from orgs.mixins.api import OrgModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from orgs.utils import current_org from orgs.utils import current_org
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
@ -42,19 +42,13 @@ __all__ = [
] ]
class NodeViewSet(SuggestionMixin, OrgModelViewSet): class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Node model = Node
filterset_fields = ('value', 'key', 'id') filterset_fields = ('value', 'key', 'id')
search_fields = ('value', ) search_fields = ('value', )
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer 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') @action(methods=[POST], detail=False, url_path='check_assets_amount_task')
def check_assets_amount_task(self, request): def check_assets_amount_task(self, request):
task = check_node_assets_amount_task.delay(current_org.id) task = check_node_assets_amount_task.delay(current_org.id)

View File

@ -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',
},
),
]

View File

@ -12,3 +12,4 @@ from .utils import *
from .authbook import * from .authbook import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .backup import *

View File

@ -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()

View File

@ -29,6 +29,7 @@ class ProtocolMixin:
telnet = 'telnet', 'Telnet' telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC' vnc = 'vnc', 'VNC'
mysql = 'mysql', 'MySQL' mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
oracle = 'oracle', 'Oracle' oracle = 'oracle', 'Oracle'
mariadb = 'mariadb', 'MariaDB' mariadb = 'mariadb', 'MariaDB'
postgresql = 'postgresql', 'PostgreSQL' postgresql = 'postgresql', 'PostgreSQL'
@ -44,7 +45,8 @@ class ProtocolMixin:
Protocol.rdp Protocol.rdp
] ]
APPLICATION_CATEGORY_DB_PROTOCOLS = [ 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 = [ APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
Protocol.k8s Protocol.k8s

View File

@ -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
)

View File

@ -11,3 +11,4 @@ from .cmd_filter import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .account import * from .account import *
from .backup import *

View File

@ -15,7 +15,7 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = AuthBook model = AuthBook
fields_mini = ['id', 'username', 'ip', 'hostname', 'version'] 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_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
fields_small = fields_mini + fields_write_only + fields_other fields_small = fields_mini + fields_write_only + fields_other
fields_fk = ['asset', 'systemuser', 'systemuser_display'] fields_fk = ['asset', 'systemuser', 'systemuser_display']

View File

@ -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'
)

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from io import StringIO
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import serializers 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): class AuthSerializer(serializers.ModelSerializer):
@ -28,17 +30,27 @@ class AuthSerializer(serializers.ModelSerializer):
return self.instance 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): def validate_password(self, password):
return password return password
def validate_private_key(self, private_key): def validate_private_key(self, private_key):
if not private_key: if not private_key:
return return
password = self.initial_data.get("password") passphrase = self.initial_data.get('passphrase')
valid = validate_ssh_private_key(private_key, password) valid = validate_ssh_private_key(private_key, password=passphrase)
if not valid: 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 return private_key
def validate_public_key(self, public_key): def validate_public_key(self, public_key):
@ -50,6 +62,7 @@ class AuthSerializerMixin:
value = validated_data.get(field) value = validated_data.get(field)
if not value: if not value:
validated_data.pop(field, None) validated_data.pop(field, None)
validated_data.pop('passphrase', None)
def create(self, validated_data): def create(self, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
@ -58,3 +71,24 @@ class AuthSerializerMixin:
def update(self, instance, validated_data): def update(self, instance, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
return super().update(instance, 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]

View File

@ -49,7 +49,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
model = Gateway model = Gateway
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_write_only = [ fields_write_only = [
'password', 'private_key', 'public_key', 'password', 'private_key', 'public_key', 'passphrase'
] ]
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
'username', 'ip', 'port', 'protocol', 'username', 'ip', 'port', 'protocol',

View File

@ -17,6 +17,9 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
value = serializers.CharField( value = serializers.CharField(
required=False, allow_blank=True, allow_null=True, label=_("value") 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: class Meta:
model = Node model = Node
@ -40,6 +43,19 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
) )
return data 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): class NodeAssetsSerializer(BulkOrgResourceModelSerializer):
assets = serializers.PrimaryKeyRelatedField( assets = serializers.PrimaryKeyRelatedField(

View File

@ -4,7 +4,7 @@ from django.db.models import Count
from common.mixins.serializers import BulkSerializerMixin from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen 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 orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .utils import validate_password_contains_left_double_curly_bracket from .utils import validate_password_contains_left_double_curly_bracket
@ -33,7 +33,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = SystemUser model = SystemUser
fields_mini = ['id', 'name', 'username'] 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 + [ fields_small = fields_mini + fields_write_only + [
'token', 'ssh_key_fingerprint', 'token', 'ssh_key_fingerprint',
'type', 'type_display', 'protocol', 'is_asset_protocol', 'type', 'type_display', 'protocol', 'is_asset_protocol',
@ -107,9 +107,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def validate_username(self, username): def validate_username(self, username):
protocol = self.get_initial_value("protocol") protocol = self.get_initial_value("protocol")
if username: if username:
regx = alphanumeric_re
if protocol == SystemUser.Protocol.telnet: if protocol == SystemUser.Protocol.telnet:
regx = alphanumeric_cn_re regx = alphanumeric_cn_re
elif protocol == SystemUser.Protocol.rdp:
regx = alphanumeric_win_re
else:
regx = alphanumeric_re
if not regx.match(username): if not regx.match(username):
raise serializers.ValidationError(_('Special char not allowed')) raise serializers.ValidationError(_('Special char not allowed'))
return username return username
@ -119,7 +122,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return '' return ''
login_mode = self.get_initial_value("login_mode") 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.') msg = _('* Automatic login mode must fill in the username.')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
return username return username
@ -141,9 +145,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def validate_password(self, password): def validate_password(self, password):
super().validate_password(password) super().validate_password(password)
auto_gen_key = self.get_initial_value("auto_generate_key", False) auto_gen_key = self.get_initial_value('auto_generate_key', False)
private_key = self.get_initial_value("private_key") private_key = self.get_initial_value('private_key')
login_mode = self.get_initial_value("login_mode") login_mode = self.get_initial_value('login_mode')
if not self.instance and not auto_gen_key and not password and \ if not self.instance and not auto_gen_key and not password and \
not private_key and login_mode == SystemUser.LOGIN_AUTO: not private_key and login_mode == SystemUser.LOGIN_AUTO:
@ -187,9 +191,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return attrs return attrs
def _validate_gen_key(self, attrs): def _validate_gen_key(self, attrs):
username = attrs.get("username", "manual") username = attrs.get('username', 'manual')
auto_gen_key = attrs.pop("auto_generate_key", False) auto_gen_key = attrs.pop('auto_generate_key', False)
protocol = attrs.get("protocol") protocol = attrs.get('protocol')
if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
return attrs return attrs
@ -197,17 +201,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
# 自动生成 # 自动生成
if auto_gen_key and not self.instance: if auto_gen_key and not self.instance:
password = SystemUser.gen_password() password = SystemUser.gen_password()
attrs["password"] = password attrs['password'] = password
if protocol == SystemUser.Protocol.ssh: if protocol == SystemUser.Protocol.ssh:
private_key, public_key = SystemUser.gen_key(username) private_key, public_key = SystemUser.gen_key(username)
attrs["private_key"] = private_key attrs['private_key'] = private_key
attrs["public_key"] = public_key attrs['public_key'] = public_key
# 如果设置了private key没有设置public key则生成 # 如果设置了private key没有设置public key则生成
elif attrs.get("private_key", None): elif attrs.get('private_key'):
private_key = attrs["private_key"] private_key = attrs['private_key']
password = attrs.get("password") password = attrs.get('password')
public_key = ssh_pubkey_gen(private_key, password=password, username=username) public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs["public_key"] = public_key attrs['public_key'] = public_key
return attrs return attrs
def _validate_login_mode(self, attrs): def _validate_login_mode(self, attrs):

View File

@ -0,0 +1 @@
from .endpoint import *

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,10 @@
from .backup.manager import AccountBackupExecutionManager
class ExecutionManager:
manager_type = {
'backup': AccountBackupExecutionManager
}
def __new__(cls, execution):
return AccountBackupExecutionManager(execution)

View File

@ -9,3 +9,4 @@ from .gather_asset_hardware_info import *
from .push_system_user import * from .push_system_user import *
from .system_user_connectivity import * from .system_user_connectivity import *
from .nodes_amount import * from .nodes_amount import *
from .backup import *

View File

@ -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)

View File

@ -1 +0,0 @@

View File

@ -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-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-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-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 = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')

View File

@ -28,7 +28,7 @@ from common.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true from common.http import is_true
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user 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 ( from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@ -231,7 +231,7 @@ class SecretDetailMixin:
@staticmethod @staticmethod
def _get_application_secret_detail(application): def _get_application_secret_detail(application):
from perms.models import Action from perms.models.base import Action
gateway = None gateway = None
if not application.category_remote_app: if not application.category_remote_app:
@ -391,10 +391,10 @@ class UserConnectionTokenViewSet(
asset = get_object_or_404(Asset, id=value.get('asset')) asset = get_object_or_404(Asset, id=value.get('asset'))
if not asset.is_active: if not asset.is_active:
raise serializers.ValidationError("Asset disabled") 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: else:
app = get_object_or_404(Application, id=value.get('application')) 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: if not has_perm:
raise serializers.ValidationError('Permission expired or invalid') raise serializers.ValidationError('Permission expired or invalid')

View File

@ -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'),
),
]

View File

@ -44,4 +44,4 @@ class SSOToken(models.JMSBaseModel):
""" """
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token')) authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
expired = models.BooleanField(default=False, verbose_name=_('Expired')) 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)

View File

@ -1,9 +1,9 @@
from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.template.loader import render_to_string from django.template.loader import render_to_string
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from common.utils import get_logger from common.utils import get_logger
from common.utils.timezone import local_now_display
logger = get_logger(__file__) logger = get_logger(__file__)
@ -15,8 +15,7 @@ class DifferentCityLoginMessage(UserMessage):
super().__init__(user) super().__init__(user)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
now_local = timezone.localtime(timezone.now()) now = local_now_display()
now = now_local.strftime("%Y-%m-%d %H:%M:%S")
subject = _('Different city login reminder') subject = _('Different city login reminder')
context = dict( context = dict(
subject=subject, subject=subject,
@ -39,3 +38,36 @@ class DifferentCityLoginMessage(UserMessage):
ip = '8.8.8.8' ip = '8.8.8.8'
city = '洛杉矶' city = '洛杉矶'
return cls(user, ip, 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')

View File

@ -9,7 +9,7 @@ from assets.models import Asset, SystemUser, Gateway
from applications.models import Application from applications.models import Application
from users.serializers import UserProfileSerializer from users.serializers import UserProfileSerializer
from assets.serializers import ProtocolsField from assets.serializers import ProtocolsField
from perms.serializers.asset.permission import ActionsField from perms.serializers.base import ActionsField
from .models import AccessKey from .models import AccessKey
__all__ = [ __all__ = [

View File

@ -0,0 +1,18 @@
{% load i18n %}
<p>
{% trans 'Hello' %} {{ name }},
</p>
<p>
{% trans 'Your account has just been bound to' %} {{ oauth_name }}
</p>
<p>
<b>{% trans 'Username' %}:</b> {{ username }}<br>
<b>{{ oauth_name }}:</b> {{ oauth_id }}<br>
<b>{% trans 'Time' %}:</b> {{ time }}<br>
<b>{% trans 'IP' %}:</b> {{ ip }}
</p>
-
<p>
{% trans 'If the operation is not your own, unbind and change the password.' %}
</p>

View File

@ -19,6 +19,8 @@ from common.mixins.views import PermissionsMixin
from authentication import errors from authentication import errors
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from common.sdk.im.dingtalk import DingTalk from common.sdk.im.dingtalk import DingTalk
from common.utils.common import get_request_ip
from authentication.notifications import OAuthBindMessage
logger = get_logger(__file__) logger = get_logger(__file__)
@ -154,6 +156,8 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
return response return response
raise e raise e
ip = get_request_ip(request)
OAuthBindMessage(user, ip, _('DingTalk'), user_id).publish_async()
msg = _('Binding DingTalk successfully') msg = _('Binding DingTalk successfully')
response = self.get_success_response(redirect_url, msg, msg) response = self.get_success_response(redirect_url, msg, msg)
return response return response

View File

@ -16,8 +16,10 @@ from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none from common.utils.django import reverse, get_object_or_none
from common.mixins.views import PermissionsMixin from common.mixins.views import PermissionsMixin
from common.sdk.im.feishu import FeiShu, URL from common.sdk.im.feishu import FeiShu, URL
from common.utils.common import get_request_ip
from authentication import errors from authentication import errors
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
logger = get_logger(__file__) logger = get_logger(__file__)
@ -142,6 +144,8 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View):
return response return response
raise e raise e
ip = get_request_ip(request)
OAuthBindMessage(user, ip, _('FeiShu'), user_id).publish_async()
msg = _('Binding FeiShu successfully') msg = _('Binding FeiShu successfully')
response = self.get_success_response(redirect_url, msg, msg) response = self.get_success_response(redirect_url, msg, msg)
return response return response

View File

@ -157,7 +157,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
'name': 'SAML2', 'name': 'SAML2',
'enabled': settings.AUTH_SAML2, 'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'), 'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_cas_logo.png'), 'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True 'auto_redirect': True
}, },
{ {

View File

@ -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 URL
from common.sdk.im.wecom import WeCom from common.sdk.im.wecom import WeCom
from common.mixins.views import PermissionsMixin from common.mixins.views import PermissionsMixin
from common.utils.common import get_request_ip
from authentication import errors from authentication import errors
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
logger = get_logger(__file__) logger = get_logger(__file__)
@ -152,6 +154,8 @@ class WeComQRBindCallbackView(WeComQRMixin, View):
return response return response
raise e raise e
ip = get_request_ip(request)
OAuthBindMessage(user, ip, _('WeCom'), wecom_userid).publish_async()
msg = _('Binding WeCom successfully') msg = _('Binding WeCom successfully')
response = self.get_success_response(redirect_url, msg, msg) response = self.get_success_response(redirect_url, msg, msg)
return response return response

View File

@ -66,6 +66,47 @@ class ChoiceSet(metaclass=ChoiceSetType):
choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 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): class JMSBaseModel(Model):
created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) 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')) updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))

View File

@ -13,6 +13,7 @@ class TreeNode:
pId = "" pId = ""
open = False open = False
iconSkin = "" iconSkin = ""
parentInfo = ''
meta = {} meta = {}
_tree = None _tree = None
@ -95,6 +96,7 @@ class TreeNodeSerializer(serializers.Serializer):
name = serializers.CharField(max_length=128) name = serializers.CharField(max_length=128)
title = serializers.CharField(max_length=128) title = serializers.CharField(max_length=128)
pId = 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) isParent = serializers.BooleanField(default=False)
open = serializers.BooleanField(default=False) open = serializers.BooleanField(default=False)
iconSkin = serializers.CharField(max_length=128, allow_blank=True) iconSkin = serializers.CharField(max_length=128, allow_blank=True)

View File

@ -22,7 +22,6 @@ from django.db.models.fields.files import FileField
from .http import http_date from .http import http_date
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
@ -41,6 +40,7 @@ class Singleton(type):
class Signer(metaclass=Singleton): class Signer(metaclass=Singleton):
"""用来加密,解密,和基于时间戳的方式验证token""" """用来加密,解密,和基于时间戳的方式验证token"""
def __init__(self, secret_key=None): def __init__(self, secret_key=None):
self.secret_key = secret_key self.secret_key = secret_key
@ -88,11 +88,16 @@ def ssh_key_string_to_obj(text, password=None):
return key 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): if isinstance(private_key, bytes):
private_key = private_key.decode("utf-8") private_key = private_key.decode("utf-8")
if isinstance(private_key, string_types): if isinstance(private_key, string_types):
private_key = ssh_key_string_to_obj(private_key, password=password) 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)): if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
raise IOError('Invalid private key') 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: if cls is None:
cls = DjangoJSONEncoder cls = DjangoJSONEncoder
return json.dumps(data, sort_keys=sort_keys, indent=indent, cls=cls) return json.dumps(data, sort_keys=sort_keys, indent=indent, cls=cls)

View File

@ -10,10 +10,11 @@ def create_csv_file(filename, headers, rows, ):
w.writerows(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( with pyzipper.AESZipFile(
filename, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES filename, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES
) as zf: ) as zf:
zf.setpassword(secret_password) zf.setpassword(secret_password)
for encrypted_filename in encrypted_filenames:
with open(encrypted_filename, 'rb') as f: with open(encrypted_filename, 'rb') as f:
zf.writestr(os.path.basename(encrypted_filename), f.read()) zf.writestr(os.path.basename(encrypted_filename), f.read())

View File

@ -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_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): class ProjectUniqueValidator(UniqueTogetherValidator):
def __call__(self, attrs, serializer): def __call__(self, attrs, serializer):

View File

@ -31,7 +31,7 @@ def jumpserver_processor(request):
context = default_context context = default_context
context.update({ context.update({
'VERSION': settings.VERSION, 'VERSION': settings.VERSION,
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021', 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2022',
'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION,
'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL, 'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL,
'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME, 'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME,

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2bf3340b7ca0dc3e698160fe8df980df07ebdecfd9312c5aca230d18a05fea18 oid sha256:942e981be66e5d0c32efb59583a377503ee3dc285e2794da40c312694c4a9dc2
size 94929 size 96378

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ from .serializers import (
) )
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import ( from assets.models import (
Asset, Domain, AdminUser, SystemUser, Label, Node, Gateway, Asset, Domain, SystemUser, Label, Node, Gateway,
CommandFilter, CommandFilterRule, GatheredUser CommandFilter, CommandFilterRule, GatheredUser
) )
from applications.models import Application from applications.models import Application
@ -35,7 +35,7 @@ logger = get_logger(__file__)
# 部分 org 相关的 model需要清空这些数据之后才能删除该组织 # 部分 org 相关的 model需要清空这些数据之后才能删除该组织
org_related_models = [ 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, CommandFilter, CommandFilterRule, GatheredUser,
AssetPermission, ApplicationPermission, AssetPermission, ApplicationPermission,
Application, Application,

View File

@ -234,9 +234,9 @@ class Organization(models.Model):
with tmp_to_org(self): with tmp_to_org(self):
return resource_model.objects.all().count() 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(**{ node = TreeNode(**{
'id': str(self.id), 'id': oid,
'name': self.name, 'name': self.name,
'title': self.name, 'title': self.name,
'pId': pid, 'pId': pid,

View File

@ -13,7 +13,6 @@ from rest_framework.generics import (
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from applications.models import Application from applications.models import Application
from perms.utils.application.permission import ( from perms.utils.application.permission import (
has_application_system_permission,
get_application_system_user_ids, get_application_system_user_ids,
validate_permission, validate_permission,
) )
@ -64,13 +63,22 @@ class ValidateUserApplicationPermissionApi(APIView):
application_id = request.query_params.get('application_id', '') application_id = request.query_params.get('application_id', '')
system_user_id = request.query_params.get('system_user_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)): 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) user = User.objects.get(id=user_id)
application = Application.objects.get(id=application_id) application = Application.objects.get(id=application_id)
system_user = SystemUser.objects.get(id=system_user_id) system_user = SystemUser.objects.get(id=system_user_id)
has_perm, actions, expire_at = validate_permission(user, application, system_user)
has_permission, expire_at = validate_permission(user, application, system_user) status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN
status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN data = {
return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) 'has_permission': has_perm,
'expire_at': int(expire_at),
'actions': actions
}
return Response(data, status=status_code)

View File

@ -54,10 +54,15 @@ class ApplicationsAsTreeMixin(SerializeApplicationToTreeNodeMixin):
将应用序列化成树的结构返回 将应用序列化成树的结构返回
""" """
serializer_class = TreeNodeSerializer serializer_class = TreeNodeSerializer
user: None
def list(self, request, *args, **kwargs): 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()) 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) serializer = self.get_serializer(tree_nodes, many=True)
return Response(data=serializer.data) return Response(data=serializer.data)

View File

@ -72,16 +72,27 @@ class ValidateUserAssetPermissionApi(APIView):
system_id = request.query_params.get('system_user_id', '') system_id = request.query_params.get('system_user_id', '')
action_name = request.query_params.get('action_name', '') 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)): 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) user = User.objects.get(id=user_id)
asset = Asset.objects.valid().get(id=asset_id) asset = Asset.objects.valid().get(id=asset_id)
system_user = SystemUser.objects.get(id=system_id) system_user = SystemUser.objects.get(id=system_id)
has_permission, expire_at = validate_permission(user, asset, system_user, action_name) has_perm, actions, expire_at = validate_permission(user, asset, system_user, action_name)
status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN
return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) data = {
'has_permission': has_perm,
'actions': actions,
'expire_at': int(expire_at)
}
return Response(data, status=status_code)
# TODO 删除 # TODO 删除

View File

@ -3,7 +3,7 @@
from django.db import migrations, models from django.db import migrations, models
from django.db.models import F from django.db.models import F
from ..models.asset_permission import Action from ..models.base import Action
def migrate_asset_permission(apps, schema_editor): def migrate_asset_permission(apps, schema_editor):

View File

@ -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)
]

View File

@ -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'),
),
]

View File

@ -3,3 +3,4 @@
from .asset_permission import * from .asset_permission import *
from .application_permission import * from .application_permission import *
from .base import *

View File

@ -6,7 +6,7 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty from common.utils import lazyproperty
from .base import BasePermission from .base import BasePermission, Action
from users.models import User from users.models import User
from applications.const import AppCategory, AppType 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) Q(id__in=user_ids) | Q(groups__id__in=user_group_ids)
) )
return users 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

View File

@ -1,5 +1,4 @@
import logging import logging
from functools import reduce
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import F from django.db.models import F
@ -14,92 +13,17 @@ from .base import BasePermission
__all__ = [ __all__ = [
'AssetPermission', 'Action', 'PermNode', 'UserAssetGrantedTreeNodeRelation', 'AssetPermission', 'PermNode', 'UserAssetGrantedTreeNodeRelation',
] ]
# 使用场景 # 使用场景
logger = logging.getLogger(__name__) 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): class AssetPermission(BasePermission):
assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) 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")) 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")) 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: class Meta:
unique_together = [('org_id', 'name')] unique_together = [('org_id', 'name')]

View File

@ -8,12 +8,12 @@ from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from orgs.mixins.models import OrgModelMixin 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 common.utils import date_expired_default, lazyproperty
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
__all__ = [ __all__ = [
'BasePermission', 'BasePermissionQuerySet' 'BasePermission', 'BasePermissionQuerySet', 'Action'
] ]
@ -39,12 +39,52 @@ class BasePermissionManager(OrgManager):
return self.get_queryset().valid() 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): class BasePermission(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss') users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss')
user_groups = models.ManyToManyField( user_groups = models.ManyToManyField(
'users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss') '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')) is_active = models.BooleanField(default=True, verbose_name=_('Active'))
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) 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')) date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired'))

View File

@ -1,5 +1,6 @@
# coding: utf-8 # coding: utf-8
# #
from .base import *
from .asset import * from .asset import *
from .application import * from .application import *
from .system_user_permission import * from .system_user_permission import *

View File

@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from perms.models import ApplicationPermission from perms.models import ApplicationPermission
from ..base import ActionsField
__all__ = [ __all__ = [
'ApplicationPermissionSerializer' 'ApplicationPermissionSerializer'
@ -13,6 +14,7 @@ __all__ = [
class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
is_valid = serializers.BooleanField(read_only=True, label=_('Is valid')) is_valid = serializers.BooleanField(read_only=True, label=_('Is valid'))
@ -23,6 +25,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + [
'category', 'category_display', 'type', 'type_display', 'category', 'category_display', 'type', 'type_display',
'actions',
'is_active', 'is_expired', 'is_valid', 'is_active', 'is_expired', 'is_valid',
'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket' 'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket'
] ]
@ -43,6 +46,25 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
'applications_amount': {'label': _('Applications amount')}, '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 @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """

View File

@ -9,32 +9,9 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from perms.models import AssetPermission, Action from perms.models import AssetPermission, Action
from assets.models import Asset, Node, SystemUser from assets.models import Asset, Node, SystemUser
from users.models import User, UserGroup from users.models import User, UserGroup
from ..base import ActionsField
__all__ = [ __all__ = ['AssetPermissionSerializer']
'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]
class AssetPermissionSerializer(BulkOrgResourceModelSerializer): class AssetPermissionSerializer(BulkOrgResourceModelSerializer):

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from assets.models import Node, SystemUser, Asset, Platform from assets.models import Node, SystemUser, Asset, Platform
from assets.serializers import ProtocolsField from assets.serializers import ProtocolsField
from perms.serializers.asset.permission import ActionsField from perms.serializers.base import ActionsField
__all__ = [ __all__ = [
'NodeGrantedSerializer', 'NodeGrantedSerializer',

View File

@ -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]

View File

@ -3,7 +3,7 @@ import time
from django.db.models import Q from django.db.models import Q
from common.utils import get_logger from common.utils import get_logger
from perms.models import ApplicationPermission from perms.models import ApplicationPermission, Action
logger = get_logger(__file__) logger = get_logger(__file__)
@ -33,31 +33,38 @@ def get_user_all_app_perm_ids(user) -> set:
return app_perm_ids 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 = get_user_all_app_perm_ids(user)
app_perm_ids = ApplicationPermission.applications.through.objects.filter( app_perm_ids = ApplicationPermission.applications.through.objects.filter(
applicationpermission_id__in=app_perm_ids, applicationpermission_id__in=app_perm_ids,
application_id=application.id application_id=application.id
).values_list('applicationpermission_id', flat=True) ).values_list('applicationpermission_id', flat=True)
app_perm_ids = set(app_perm_ids) app_perm_ids = set(app_perm_ids)
app_perm_ids = ApplicationPermission.system_users.through.objects.filter( app_perm_ids = ApplicationPermission.system_users.through.objects.filter(
applicationpermission_id__in=app_perm_ids, applicationpermission_id__in=app_perm_ids,
systemuser_id=system_user.id systemuser_id=system_user.id
).values_list('applicationpermission_id', flat=True) ).values_list('applicationpermission_id', flat=True)
app_perm_ids = set(app_perm_ids) app_perm_ids = set(app_perm_ids)
app_perms = ApplicationPermission.objects.filter(
app_perm = ApplicationPermission.objects.filter(
id__in=app_perm_ids id__in=app_perm_ids
).order_by('-date_expired').first() ).order_by('-date_expired')
app_perm: ApplicationPermission if app_perms:
if app_perm: actions = set()
return True, app_perm.date_expired.timestamp() 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: 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): def get_application_system_user_ids(user, application):

View File

@ -11,7 +11,7 @@ from perms.utils.asset.user_permission import get_user_all_asset_perm_ids
logger = get_logger(__file__) 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(): if not system_user.protocol in asset.protocols_as_dict.keys():
return False, time.time() return False, time.time()
@ -50,10 +50,22 @@ def validate_permission(user, asset, system_user, action_name):
id__in=asset_perm_ids id__in=asset_perm_ids
).order_by('-date_expired') ).order_by('-date_expired')
for asset_perm in asset_perms: if asset_perms:
if action_name in Action.value_to_choices(asset_perm.actions): actions = set()
return True, asset_perm.date_expired.timestamp() actions_values = asset_perms.values_list('actions', flat=True)
return False, time.time() 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): def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset):

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -35,6 +35,16 @@ p {
<li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">Microsoft_Remote_Desktop_10.6.7_installer.pkg</a></li> <li><a href="/download/Microsoft_Remote_Desktop_10.6.7_installer.pkg">Microsoft_Remote_Desktop_10.6.7_installer.pkg</a></li>
</ul> </ul>
</div> </div>
{% if XPACK_ENABLED %}
<div class="group">
<h2>{% trans 'Windows Remote application publisher tools' %}</h2>
<p>{% trans 'Jmservisor is the program used to pull up remote applications in Windows Remote Application publisher' %}</p>
<ul>
<li><a href="/download/Jmservisor.msi">Jmservisor</a></li>
</ul>
</div>
{% endif %}
</div> </div>
<style> <style>
ul { ul {

View File

@ -9,7 +9,7 @@ from rest_framework.fields import DateTimeField
from rest_framework.response import Response from rest_framework.response import Response
from django.template import loader from django.template import loader
from terminal.models import CommandStorage from terminal.models import CommandStorage, Session
from terminal.filters import CommandFilter from terminal.filters import CommandFilter
from orgs.utils import current_org from orgs.utils import current_org
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
@ -146,15 +146,26 @@ class CommandViewSet(JMSBulkModelViewSet):
page = self.paginate_queryset(queryset) page = self.paginate_queryset(queryset)
if page is not None: if page is not None:
page = self.load_remote_addr(page)
serializer = self.get_serializer(page, many=True) serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)
# 适配像 ES 这种没有指定分页只返回少量数据的情况 # 适配像 ES 这种没有指定分页只返回少量数据的情况
queryset = queryset[:] queryset = queryset[:]
queryset = self.load_remote_addr(queryset)
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data) return Response(serializer.data)
def load_remote_addr(self, queryset):
commands = list(queryset)
session_ids = {command.session for command in commands}
sessions = Session.objects.filter(id__in=session_ids).values_list('id', 'remote_addr')
session_addr_map = {str(i): addr for i, addr in sessions}
for command in commands:
command.remote_addr = session_addr_map.get(command.session, '')
return commands
def get_queryset(self): def get_queryset(self):
command_storage_id = self.request.query_params.get('command_storage_id') command_storage_id = self.request.query_params.get('command_storage_id')
storage = CommandStorage.objects.get(id=command_storage_id) storage = CommandStorage.objects.get(id=command_storage_id)

View File

@ -6,7 +6,7 @@ import tarfile
from django.shortcuts import get_object_or_404, reverse from django.shortcuts import get_object_or_404, reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.encoding import escape_uri_path from django.utils.encoding import escape_uri_path
from django.http import FileResponse, HttpResponse from django.http import FileResponse
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from rest_framework import viewsets, views from rest_framework import viewsets, views
from rest_framework.response import Response from rest_framework.response import Response
@ -15,7 +15,7 @@ from rest_framework.decorators import action
from common.utils import model_to_json from common.utils import model_to_json
from .. import utils from .. import utils
from common.const.http import GET from common.const.http import GET
from common.utils import is_uuid, get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.mixins.api import AsyncApiMixin from common.mixins.api import AsyncApiMixin
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
from common.drf.filters import DatetimeRangeFilter from common.drf.filters import DatetimeRangeFilter
@ -24,13 +24,14 @@ from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from users.models import User from users.models import User
from ..utils import find_session_replay_local, download_session_replay from ..utils import find_session_replay_local, download_session_replay
from ..hands import SystemUser
from ..models import Session from ..models import Session
from .. import serializers from .. import serializers
from terminal.utils import is_session_approver
__all__ = [ __all__ = [
'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI' 'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI'
] ]
logger = get_logger(__name__) logger = get_logger(__name__)
@ -197,6 +198,9 @@ class SessionJoinValidateAPI(views.APIView):
msg = _('User does not exist: {}'.format(user_id)) msg = _('User does not exist: {}'.format(user_id))
return Response({'ok': False, 'msg': msg}, status=401) return Response({'ok': False, 'msg': msg}, status=401)
with tmp_to_org(session.org): with tmp_to_org(session.org):
if is_session_approver(session_id, user_id):
return Response({'ok': True, 'msg': ''}, status=200)
if not user.admin_or_audit_orgs: if not user.admin_or_audit_orgs:
msg = _('User does not have permission') msg = _('User does not have permission')
return Response({'ok': False, 'msg': msg}, status=401) return Response({'ok': False, 'msg': msg}, status=401)

View File

@ -3,14 +3,18 @@
import logging import logging
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.permissions import IsOrgAdminOrAppUser from common.permissions import IsOrgAdminOrAppUser
from ..models import Session, Task from ..models import Session, Task
from .. import serializers from .. import serializers
from terminal.utils import is_session_approver
from orgs.utils import tmp_to_root_org
__all__ = ['TaskViewSet', 'KillSessionAPI'] __all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI']
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)
@ -21,20 +25,45 @@ class TaskViewSet(BulkModelViewSet):
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
class KillSessionAPI(APIView): def kill_sessions(session_ids, user):
permission_classes = (IsOrgAdminOrAppUser,)
model = Task
def post(self, request, *args, **kwargs):
validated_session = [] validated_session = []
for session_id in request.data:
for session_id in session_ids:
session = get_object_or_none(Session, id=session_id) session = get_object_or_none(Session, id=session_id)
if session and not session.is_finished: if session and not session.is_finished:
validated_session.append(session_id) validated_session.append(session_id)
self.model.objects.create( Task.objects.create(
name="kill_session", args=session.id, terminal=session.terminal, name="kill_session", args=session.id, terminal=session.terminal,
kwargs={ kwargs={
'terminated_by': str(request.user) 'terminated_by': str(user)
} }
) )
return validated_session
class KillSessionAPI(APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def post(self, request, *args, **kwargs):
session_ids = request.data
user = request.user
validated_session = kill_sessions(session_ids, user)
return Response({"ok": validated_session}) return Response({"ok": validated_session})
class KillSessionForTicketAPI(APIView):
permission_classes = (IsAuthenticated, )
def post(self, request, *args, **kwargs):
session_ids = request.data
user_id = request.user.id
for session_id in session_ids:
if not is_session_approver(session_id, user_id):
return Response({}, status=status.HTTP_403_FORBIDDEN)
with tmp_to_root_org():
validated_session = kill_sessions(session_ids, request.user)
return Response({"ok": validated_session})

View File

@ -4,6 +4,7 @@ import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils.common import lazyproperty
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
@ -27,6 +28,15 @@ class AbstractSessionCommand(OrgModelMixin):
class Meta: class Meta:
abstract = True abstract = True
@lazyproperty
def remote_addr(self):
from terminal.models import Session
session = Session.objects.filter(id=self.session).first()
if session:
return session.remote_addr
else:
return ''
@classmethod @classmethod
def get_risk_level_str(cls, risk_level): def get_risk_level_str(cls, risk_level):
risk_mapper = dict(cls.RISK_LEVEL_CHOICES) risk_mapper = dict(cls.RISK_LEVEL_CHOICES)

View File

@ -19,6 +19,7 @@ class SessionCommandSerializer(serializers.Serializer):
risk_level_display = serializers.SerializerMethodField(label=_('Risk level display')) risk_level_display = serializers.SerializerMethodField(label=_('Risk level display'))
org_id = serializers.CharField(max_length=36, required=False, default='', allow_null=True, allow_blank=True) org_id = serializers.CharField(max_length=36, required=False, default='', allow_null=True, allow_blank=True)
timestamp = serializers.IntegerField(label=_('Timestamp')) timestamp = serializers.IntegerField(label=_('Timestamp'))
remote_addr = serializers.CharField(read_only=True, label=_('Remote Address'))
@staticmethod @staticmethod
def get_risk_level_display(obj): def get_risk_level_display(obj):

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.13 on 2021-12-29 08:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0041_auto_20211105_1605'),
]
operations = [
migrations.AlterField(
model_name='session',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql'), ('redis', 'redis'), ('oracle', 'oracle'), ('mariadb', 'mariadb'), ('sqlserver', 'sqlserver'), ('postgresql', 'postgresql'), ('k8s', 'kubernetes')], db_index=True, default='ssh', max_length=16),
),
]

View File

@ -29,6 +29,7 @@ class Session(OrgModelMixin):
VNC = 'vnc', 'vnc' VNC = 'vnc', 'vnc'
TELNET = 'telnet', 'telnet' TELNET = 'telnet', 'telnet'
MYSQL = 'mysql', 'mysql' MYSQL = 'mysql', 'mysql'
REDIS = 'redis', 'redis'
ORACLE = 'oracle', 'oracle' ORACLE = 'oracle', 'oracle'
MARIADB = 'mariadb', 'mariadb' MARIADB = 'mariadb', 'mariadb'
SQLSERVER = 'sqlserver', 'sqlserver' SQLSERVER = 'sqlserver', 'sqlserver'
@ -162,7 +163,8 @@ class Session(OrgModelMixin):
@property @property
def db_protocols(self): def db_protocols(self):
_PROTOCOL = self.PROTOCOL _PROTOCOL = self.PROTOCOL
return [_PROTOCOL.MYSQL, _PROTOCOL.MARIADB, _PROTOCOL.ORACLE, _PROTOCOL.POSTGRESQL, _PROTOCOL.SQLSERVER] return [_PROTOCOL.MYSQL, _PROTOCOL.MARIADB, _PROTOCOL.REDIS,
_PROTOCOL.ORACLE, _PROTOCOL.POSTGRESQL, _PROTOCOL.SQLSERVER]
@property @property
def can_terminate(self): def can_terminate(self):

View File

@ -30,6 +30,7 @@ urlpatterns = [
api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}), api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}),
name='session-replay'), name='session-replay'),
path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'), path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'),
path('tasks/kill-session-for-ticket/', api.KillSessionForTicketAPI.as_view(), name='kill-session-for-ticket'),
path('terminals/config/', api.TerminalConfig.as_view(), name='terminal-config'), path('terminals/config/', api.TerminalConfig.as_view(), name='terminal-config'),
path('commands/export/', api.CommandExportApi.as_view(), name="command-export"), path('commands/export/', api.CommandExportApi.as_view(), name="command-export"),
path('commands/insecure-command/', api.InsecureCommandAlertAPI.as_view(), name="command-alert"), path('commands/insecure-command/', api.InsecureCommandAlertAPI.as_view(), name="command-alert"),

View File

@ -11,6 +11,9 @@ import jms_storage
from common.utils import get_logger from common.utils import get_logger
from . import const from . import const
from .models import ReplayStorage from .models import ReplayStorage
from tickets.models import TicketSession, TicketStep, TicketAssignee
from tickets.const import ProcessStatus
logger = get_logger(__name__) logger = get_logger(__name__)
@ -247,3 +250,11 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
prometheus_metrics.append('\n') prometheus_metrics.append('\n')
prometheus_metrics_text = '\n'.join(prometheus_metrics) prometheus_metrics_text = '\n'.join(prometheus_metrics)
return prometheus_metrics_text return prometheus_metrics_text
def is_session_approver(session_id, user_id):
ticket = TicketSession.get_ticket_by_session_id(session_id)
if not ticket:
return False
ok = ticket.has_all_assignee(user_id)
return ok

View File

@ -3,3 +3,4 @@
from .ticket import * from .ticket import *
from .comment import * from .comment import *
from .common import * from .common import *
from .relation import *

View File

@ -0,0 +1,30 @@
from rest_framework.mixins import CreateModelMixin
from rest_framework import views
from rest_framework.response import Response
from rest_framework import status
from common.drf.api import JMSGenericViewSet
from common.permissions import IsOrgAdminOrAppUser
from tickets.models import TicketSession
from tickets.serializers import TicketSessionRelationSerializer
from terminal.serializers import SessionSerializer
from orgs.utils import tmp_to_root_org
class TicketSessionRelationViewSet(CreateModelMixin, JMSGenericViewSet):
queryset = TicketSession
serializer_class = TicketSessionRelationSerializer
permission_classes = (IsOrgAdminOrAppUser, )
class TicketSessionApi(views.APIView):
def get(self, request, *args, **kwargs):
with tmp_to_root_org():
ticketsession = TicketSession.objects.filter(ticket=self.kwargs['ticket_id']).first()
if not ticketsession:
return Response(status=status.HTTP_404_NOT_FOUND)
session = ticketsession.session
serializer = SessionSerializer(session)
return Response(serializer.data)

View File

@ -94,7 +94,6 @@ class TicketFlowViewSet(JMSBulkModelViewSet):
def perform_create_or_update(self, serializer): def perform_create_or_update(self, serializer):
instance = serializer.save() instance = serializer.save()
instance.save() instance.save()
instance.rules.model.change_assignees_display(instance.rules.all())
def perform_create(self, serializer): def perform_create(self, serializer):
self.perform_create_or_update(serializer) self.perform_create_or_update(serializer)

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.13 on 2021-12-15 09:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tickets', '0010_auto_20210812_1618'),
]
operations = [
migrations.RemoveField(
model_name='approvalrule',
name='assignees_display',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.13 on 2021-12-29 08:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('terminal', '0042_auto_20211229_1619'),
('tickets', '0011_remove_approvalrule_assignees_display'),
]
operations = [
migrations.CreateModel(
name='TicketSession',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('session', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_relation', to='terminal.session')),
('ticket', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='session_relation', to='tickets.ticket')),
],
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 3.1.13 on 2022-01-10 07:37
from django.db import migrations, models
from common.utils.timezone import as_current_tz
def fill_ticket_serial_number(apps, schema_editor):
Ticket = apps.get_model('tickets', 'Ticket')
tickets = Ticket.objects.all().order_by('date_created')
curr_day = '00000000'
curr_num = 1
print(f'\nFill ticket serial number ... ', end='')
for ticket in tickets:
# 跑这个脚本的时候,所有 ticket.serial_num == null
date_created = as_current_tz(ticket.date_created)
date_str = date_created.strftime('%Y%m%d')
if date_str != curr_day:
curr_day = date_str
curr_num = 1
ticket.serial_num = curr_day + '%04d' % curr_num
curr_num += 1
Ticket.objects.bulk_update(tickets, fields=('serial_num',))
print(len(tickets), end='')
class Migration(migrations.Migration):
dependencies = [
('tickets', '0012_ticketsession'),
]
operations = [
migrations.AddField(
model_name='ticket',
name='serial_num',
field=models.CharField(max_length=256, null=True, unique=True, verbose_name='Serial number'),
),
migrations.RunPython(fill_ticket_serial_number),
]

View File

@ -3,4 +3,4 @@
from .ticket import * from .ticket import *
from .comment import * from .comment import *
from .flow import * from .flow import *
from .relation import *

View File

@ -3,13 +3,11 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from users.models import User
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.db.encoder import ModelJSONFieldEncoder
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from orgs.models import Organization from orgs.utils import tmp_to_root_org, tmp_to_org, get_current_org_id
from orgs.utils import tmp_to_root_org, tmp_to_org
from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
from ..signals import post_or_update_change_ticket_flow_approval
__all__ = ['TicketFlow', 'ApprovalRule'] __all__ = ['TicketFlow', 'ApprovalRule']
@ -29,10 +27,6 @@ class ApprovalRule(CommonModelMixin):
'users.User', related_name='assigned_ticket_flow_approval_rule', 'users.User', related_name='assigned_ticket_flow_approval_rule',
verbose_name=_("Assignees") verbose_name=_("Assignees")
) )
assignees_display = models.JSONField(
encoder=ModelJSONFieldEncoder, default=list,
verbose_name=_('Assignees display')
)
class Meta: class Meta:
verbose_name = _('Ticket flow approval rule') verbose_name = _('Ticket flow approval rule')
@ -40,9 +34,19 @@ class ApprovalRule(CommonModelMixin):
def __str__(self): def __str__(self):
return '{}({})'.format(self.id, self.level) return '{}({})'.format(self.id, self.level)
@classmethod def get_assignees(self, org_id=None):
def change_assignees_display(cls, qs): assignees = []
post_or_update_change_ticket_flow_approval.send(sender=cls, qs=qs) org_id = org_id if org_id else get_current_org_id()
with tmp_to_org(org_id):
if self.strategy == TicketApprovalStrategy.super_admin:
assignees = User.get_super_admins()
elif self.strategy == TicketApprovalStrategy.org_admin:
assignees = User.get_org_admins()
elif self.strategy == TicketApprovalStrategy.super_org_admin:
assignees = User.get_super_and_org_admins()
elif self.strategy == TicketApprovalStrategy.custom_user:
assignees = self.assignees.all()
return assignees
class TicketFlow(CommonModelMixin, OrgModelMixin): class TicketFlow(CommonModelMixin, OrgModelMixin):

View File

@ -0,0 +1,14 @@
from django.db import models
from django.db.models import Model
class TicketSession(Model):
ticket = models.ForeignKey('tickets.Ticket', related_name='session_relation', on_delete=models.CASCADE, db_constraint=False)
session = models.ForeignKey('terminal.Session', related_name='ticket_relation', on_delete=models.CASCADE, db_constraint=False)
@classmethod
def get_ticket_by_session_id(cls, session_id):
relation = cls.objects.filter(session=session_id).first()
if relation:
return relation.ticket
return None

View File

@ -3,7 +3,11 @@
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from datetime import timedelta
from django.db.utils import IntegrityError
from common.exceptions import JMSException
from common.utils.timezone import as_current_tz
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.db.encoder import ModelJSONFieldEncoder from common.db.encoder import ModelJSONFieldEncoder
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
@ -13,7 +17,7 @@ from tickets.signals import post_change_ticket_action
from tickets.handler import get_ticket_handler from tickets.handler import get_ticket_handler
from tickets.errors import AlreadyClosed from tickets.errors import AlreadyClosed
__all__ = ['Ticket'] __all__ = ['Ticket', 'TicketStep', 'TicketAssignee']
class TicketStep(CommonModelMixin): class TicketStep(CommonModelMixin):
@ -73,6 +77,7 @@ class Ticket(CommonModelMixin, OrgModelMixin):
'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, null=True, 'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, null=True,
verbose_name=_("TicketFlow") verbose_name=_("TicketFlow")
) )
serial_num = models.CharField(max_length=256, unique=True, null=True, verbose_name=_('Serial number'))
class Meta: class Meta:
ordering = ('-date_created',) ordering = ('-date_created',)
@ -140,24 +145,29 @@ class Ticket(CommonModelMixin, OrgModelMixin):
self.status = TicketStatus.closed self.status = TicketStatus.closed
def create_related_node(self): def create_related_node(self):
org_id = self.flow.org_id
approval_rule = self.get_current_ticket_flow_approve() approval_rule = self.get_current_ticket_flow_approve()
ticket_step = TicketStep.objects.create(ticket=self, level=self.approval_step) ticket_step = TicketStep.objects.create(ticket=self, level=self.approval_step)
ticket_assignees = [] ticket_assignees = []
assignees = approval_rule.assignees.all() assignees = approval_rule.get_assignees(org_id=org_id)
for assignee in assignees: for assignee in assignees:
ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee)) ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
TicketAssignee.objects.bulk_create(ticket_assignees) TicketAssignee.objects.bulk_create(ticket_assignees)
def create_process_map(self): def create_process_map(self):
org_id = self.flow.org_id
approval_rules = self.flow.rules.order_by('level') approval_rules = self.flow.rules.order_by('level')
nodes = list() nodes = list()
for node in approval_rules: for node in approval_rules:
assignees = node.get_assignees(org_id=org_id)
assignee_ids = [assignee.id for assignee in assignees]
assignees_display = [str(assignee) for assignee in assignees]
nodes.append( nodes.append(
{ {
'approval_level': node.level, 'approval_level': node.level,
'state': ProcessStatus.notified, 'state': ProcessStatus.notified,
'assignees': [i for i in node.assignees.values_list('id', flat=True)], 'assignees': assignee_ids,
'assignees_display': node.assignees_display 'assignees_display': assignees_display
} }
) )
return nodes return nodes
@ -240,3 +250,49 @@ class Ticket(CommonModelMixin, OrgModelMixin):
def body(self): def body(self):
_body = self.handler.get_body() _body = self.handler.get_body()
return _body return _body
def get_serial_num_date(self):
date_created = as_current_tz(self.date_created)
date = date_created.strftime('%Y%m%d')
return date
def get_last_serail_num(self):
date_created = as_current_tz(self.date_created)
date_prefix = date_created.strftime('%Y%m%d')
ticket = Ticket.objects.select_for_update().filter(
serial_num__startswith=date_prefix
).order_by('-date_created').first()
if ticket:
# 202212010001
num_str = ticket.serial_num[8:]
num = int(num_str)
return num
return None
def get_next_serail_num(self):
num = self.get_last_serail_num()
if num is None:
num = 0
return '%04d' % (num + 1)
def construct_serial_num(self):
date_prefix = self.get_serial_num_date()
num_suffix = self.get_next_serail_num()
return date_prefix + num_suffix
def update_serial_num_if_need(self):
if self.serial_num:
return
try:
self.serial_num = self.construct_serial_num()
self.save(update_fields=('serial_num',))
except IntegrityError as e:
if e.args[0] == 1062:
# 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突
# 但概率小,这里只报错,用户重新提交即可
raise JMSException(detail=_('Please try again'), code='please_try_again')
raise e

View File

@ -2,3 +2,4 @@
# #
from .ticket import * from .ticket import *
from .comment import * from .comment import *
from .relation import *

Some files were not shown because too many files have changed in this diff Show More