Compare commits

..

3 Commits

Author SHA1 Message Date
Eric
c8b049349c perf: 修改 Jmservisor 的下载地址名称 2021-12-24 11:35:24 +08:00
Michael Bai
2ff92c5141 fix: 下载页面添加Jmservisor拉起脚本 2021-12-17 15:53:44 +08:00
Michael Bai
3260f0eba8 fix: 修改SAML2.0 Logo 2021-12-17 15:25:00 +08:00
161 changed files with 1114 additions and 3134 deletions

View File

@@ -20,8 +20,6 @@ jobs:
run: |
TAG=$(basename ${GITHUB_REF})
VERSION=${TAG/v/}
wget https://raw.githubusercontent.com/jumpserver/installer/v${VERSION}/quick_start.sh
sed -i "s@Version=.*@Version=v${VERSION}@g" quick_start.sh
echo "::set-output name=TAG::$TAG"
echo "::set-output name=VERSION::$VERSION"
- name: Create Release
@@ -33,16 +31,6 @@ jobs:
config-name: release-config.yml
version: ${{ steps.get_version.outputs.TAG }}
tag: ${{ steps.get_version.outputs.TAG }}
- name: Upload Quick Start Script
id: upload-release-quick-start-shell
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./quick_start.sh
asset_name: quick_start.sh
asset_content_type: application/text
build-and-release:
needs: create-realese
@@ -55,4 +43,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-realese.outputs.upload_url }}
upload_url: ${{ needs.create-realese.outputs.upload_url }}

View File

@@ -1,23 +0,0 @@
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-slim as stage-build
FROM python:3.8.6-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
@@ -9,7 +9,7 @@ ADD . .
RUN cd utils && bash -ixeu build.sh
# 构建运行时环境
FROM python:3.8-slim
FROM python:3.8.6-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple

View File

@@ -124,7 +124,7 @@ JumpServer是一款安全产品请参考 [基本安全建议](https://docs.ju
### License & Copyright
Copyright (c) 2014-2022 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2021 飞致云 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

View File

@@ -85,7 +85,7 @@ If you find a security problem, please contact us directly
- 400-052-0755
### License & Copyright
Copyright (c) 2014-2022 FIT2CLOUD Tech, Inc., All rights reserved.
Copyright (c) 2014-2021 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

View File

@@ -7,14 +7,3 @@ JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755
# Security Policy
JumpServer is a security product, The installation and development should follow our security tips.
## Reporting a Vulnerability
All security bugs should be reported to the contact as below:
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755

View File

@@ -60,8 +60,7 @@ class LoginAssetCheckAPI(CreateAPIView):
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
'ticket_detail_url': ticket_detail_url,
'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees],
'ticket_id': str(ticket.id)
'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees]
}
return data

View File

@@ -44,7 +44,11 @@ class ApplicationAccountViewSet(JMSBulkModelViewSet):
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = Account.get_queryset()
queryset = Account.objects.all() \
.annotate(type=F('app__type')) \
.annotate(app_display=F('app__name')) \
.annotate(systemuser_display=F('systemuser__name')) \
.annotate(category=F('app__category'))
return queryset

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
# 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,24 +0,0 @@
# Generated by Django 3.1.13 on 2022-01-18 06:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('applications', '0015_auto_20220112_2035'),
]
operations = [
migrations.AlterField(
model_name='account',
name='app',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Application'),
),
migrations.AlterField(
model_name='historicalaccount',
name='app',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Application'),
),
]

View File

@@ -1,6 +1,5 @@
from django.db import models
from simple_history.models import HistoricalRecords
from django.db.models import F
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty
@@ -8,12 +7,8 @@ from assets.models.base import BaseUser
class Account(BaseUser):
app = models.ForeignKey(
'applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Application')
)
systemuser = models.ForeignKey(
'assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user")
)
app = models.ForeignKey('applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Database'))
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
version = models.IntegerField(default=1, verbose_name=_('Version'))
history = HistoricalRecords()
@@ -65,10 +60,6 @@ class Account(BaseUser):
def type(self):
return self.app.type
@lazyproperty
def attrs(self):
return self.app.attrs
@lazyproperty
def app_display(self):
return self.systemuser.name
@@ -93,14 +84,5 @@ class Account(BaseUser):
app = '*'
return '{}@{}'.format(username, app)
@classmethod
def get_queryset(cls):
queryset = cls.objects.all() \
.annotate(type=F('app__type')) \
.annotate(app_display=F('app__name')) \
.annotate(systemuser_display=F('systemuser__name')) \
.annotate(category=F('app__category'))
return queryset
def __str__(self):
return self.smart_name

View File

@@ -1,5 +1,4 @@
from collections import defaultdict
from urllib.parse import urlencode, parse_qsl
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -8,8 +7,6 @@ from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from common.tree import TreeNode
from assets.models import Asset, SystemUser
from ..utils import KubernetesTree
from .. import const
@@ -19,13 +16,6 @@ class ApplicationTreeNodeMixin:
type: str
category: str
@staticmethod
def create_tree_id(pid, type, v):
i = dict(parse_qsl(pid))
i[type] = v
tree_id = urlencode(i)
return tree_id
@classmethod
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
show_empty=True, show_count=True):
@@ -75,13 +65,13 @@ class ApplicationTreeNodeMixin:
return node
@classmethod
def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True):
def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True):
nodes = []
categories = const.AppType.category_types_mapper().keys()
for category in categories:
i = cls.create_tree_id(pid, 'category', category.value)
i = root_node.id + '_' + category.value
node = cls.create_choice_node(
category, i, pid=pid, tp='category',
category, i, pid=root_node.id, tp='category',
counts=counts, opened=False, show_empty=show_empty,
show_count=show_count
)
@@ -91,20 +81,17 @@ class ApplicationTreeNodeMixin:
return nodes
@classmethod
def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True):
def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True):
nodes = []
temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper()
types = const.AppType.type_category_mapper().keys()
for tp in types:
for tp in const.AppType.type_category_mapper().keys():
category = type_category_mapper.get(tp)
pid = cls.create_tree_id(pid, 'category', category.value)
i = cls.create_tree_id(pid, 'type', tp.value)
pid = root_node.id + '_' + category.value
i = root_node.id + '_' + tp.value
node = cls.create_choice_node(
tp, i, pid, tp='type', counts=counts, opened=False,
show_empty=show_empty, show_count=show_count
)
pid = temp_pid
if not node:
continue
nodes.append(node)
@@ -121,27 +108,9 @@ class ApplicationTreeNodeMixin:
counts[category] += 1
return counts
@classmethod
def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True):
counts = cls.get_tree_node_counts(queryset)
tree_nodes = []
# 类别的节点
tree_nodes += cls.create_category_tree_nodes(
pid, counts, show_empty=show_empty,
show_count=show_count
)
# 类型的节点
tree_nodes += cls.create_types_tree_nodes(
pid, counts, show_empty=show_empty,
show_count=show_count
)
return tree_nodes
@classmethod
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
counts = cls.get_tree_node_counts(queryset)
tree_nodes = []
# 根节点有可能是组织名称
@@ -149,36 +118,31 @@ class ApplicationTreeNodeMixin:
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
tree_nodes.append(root_node)
tree_nodes += cls.create_category_type_tree_nodes(
queryset, root_node.id, show_empty=show_empty, show_count=show_count
# 类别的节点
tree_nodes += cls.create_category_tree_nodes(
root_node, counts, show_empty=show_empty,
show_count=show_count
)
# 类型的节点
tree_nodes += cls.create_types_tree_nodes(
root_node, counts, show_empty=show_empty,
show_count=show_count
)
# 应用的节点
for app in queryset:
node = app.as_tree_node(root_node.id)
tree_nodes.append(node)
pid = root_node.id + '_' + app.type
tree_nodes.append(app.as_tree_node(pid))
return tree_nodes
def create_app_tree_pid(self, root_id):
pid = self.create_tree_id(root_id, 'category', self.category)
pid = self.create_tree_id(pid, 'type', self.type)
return pid
def as_tree_node(self, pid, 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):
def as_tree_node(self, pid):
icon_skin_category_mapper = {
'remote_app': 'chrome',
'db': 'database',
'cloud': 'cloud'
}
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
pid = self.create_app_tree_pid(pid)
node = TreeNode(**{
'id': str(self.id),
'name': self.name,

View File

@@ -1,5 +1,6 @@
# coding: utf-8
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
@@ -8,8 +9,7 @@ from assets.serializers.base import AuthSerializerMixin
from common.drf.serializers import MethodSerializer
from .attrs import (
category_serializer_classes_mapping,
type_serializer_classes_mapping,
type_secret_serializer_classes_mapping
type_serializer_classes_mapping
)
from .. import models
from .. import const
@@ -23,28 +23,17 @@ __all__ = [
class AppSerializerMixin(serializers.Serializer):
attrs = MethodSerializer()
@property
def app(self):
if isinstance(self.instance, models.Application):
instance = self.instance
else:
instance = None
return instance
def get_attrs_serializer(self):
default_serializer = serializers.Serializer(read_only=True)
instance = self.app
if instance:
_type = instance.type
_category = instance.category
if isinstance(self.instance, models.Application):
_type = self.instance.type
_category = self.instance.category
else:
_type = self.context['request'].query_params.get('type')
_category = self.context['request'].query_params.get('category')
if _type:
if isinstance(self, AppAccountSecretSerializer):
serializer_class = type_secret_serializer_classes_mapping.get(_type)
else:
serializer_class = type_serializer_classes_mapping.get(_type)
serializer_class = type_serializer_classes_mapping.get(_type)
elif _category:
serializer_class = category_serializer_classes_mapping.get(_category)
else:
@@ -95,13 +84,11 @@ class MiniAppSerializer(serializers.ModelSerializer):
fields = AppSerializer.Meta.fields_mini
class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResourceModelSerializer):
class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
category_display = serializers.SerializerMethodField(label=_('Category display'))
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
type_display = serializers.SerializerMethodField(label=_('Type display'))
date_created = serializers.DateTimeField(label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True)
date_updated = serializers.DateTimeField(label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True)
category_mapper = dict(const.AppCategory.choices)
type_mapper = dict(const.AppType.choices)
@@ -109,11 +96,10 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
class Meta:
model = models.Account
fields_mini = ['id', 'username', 'version']
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase']
fields_other = ['date_created', 'date_updated']
fields_write_only = ['password', 'private_key']
fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
fields = fields_mini + fields_fk + fields_write_only + fields_other + [
'type', 'type_display', 'category', 'category_display', 'attrs'
fields = fields_mini + fields_fk + fields_write_only + [
'type', 'type_display', 'category', 'category_display',
]
extra_kwargs = {
'username': {'default': '', 'required': False},
@@ -126,14 +112,6 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
'ignore_conflicts': True
}
@property
def app(self):
if isinstance(self.instance, models.Account):
instance = self.instance.app
else:
instance = None
return instance
def get_category_display(self, obj):
return self.category_mapper.get(obj.category)
@@ -153,11 +131,6 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
class AppAccountSecretSerializer(AppAccountSerializer):
class Meta(AppAccountSerializer.Meta):
fields_backup = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},

View File

@@ -1,6 +1,7 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
__all__ = ['CloudSerializer']

View File

@@ -10,6 +10,7 @@ from assets.models import Asset
logger = get_logger(__file__)
__all__ = ['RemoteAppSerializer']

View File

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

View File

@@ -3,7 +3,8 @@ from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
__all__ = ['ChromeSerializer']
class ChromeSerializer(RemoteAppSerializer):
@@ -16,16 +17,10 @@ class ChromeSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
)
chrome_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Chrome username'), allow_null=True,
max_length=128, allow_blank=True, required=False, label=_('Username'), allow_null=True,
)
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'),
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True
)
class ChromeSecretSerializer(ChromeSerializer):
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'),
allow_null=True
)

View File

@@ -1,9 +1,11 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
__all__ = ['CustomSerializer']
class CustomSerializer(RemoteAppSerializer):
@@ -16,17 +18,10 @@ class CustomSerializer(RemoteAppSerializer):
allow_null=True,
)
custom_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
max_length=128, allow_blank=True, required=False, label=_('Username'),
allow_null=True,
)
custom_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Custom password'),
allow_null=True,
)
class CustomSecretSerializer(RemoteAppSerializer):
custom_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Custom password'),
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True,
)

View File

@@ -1,5 +1,6 @@
from ..application_category import CloudSerializer
__all__ = ['K8SSerializer']

View File

@@ -1,5 +1,6 @@
from .mysql import MySQLSerializer
__all__ = ['MariaDBSerializer']

View File

@@ -3,9 +3,13 @@ from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['MySQLSerializer']
class MySQLSerializer(DBSerializer):
port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True)

View File

@@ -3,7 +3,8 @@ from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
__all__ = ['MySQLWorkbenchSerializer']
class MySQLWorkbenchSerializer(RemoteAppSerializer):
@@ -26,17 +27,10 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
allow_null=True,
)
mysql_workbench_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
max_length=128, allow_blank=True, required=False, label=_('Username'),
allow_null=True,
)
mysql_workbench_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Mysql workbench password'),
allow_null=True,
)
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
mysql_workbench_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Mysql workbench password'),
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True,
)

View File

@@ -3,8 +3,10 @@ from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['OracleSerializer']
class OracleSerializer(DBSerializer):
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)

View File

@@ -3,8 +3,10 @@ from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['PostgreSerializer']
class PostgreSerializer(DBSerializer):
port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True)

View File

@@ -1,11 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['RedisSerializer']
class RedisSerializer(DBSerializer):
port = serializers.IntegerField(default=6379, label=_('Port'), allow_null=True)

View File

@@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['SQLServerSerializer']

View File

@@ -3,7 +3,8 @@ from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
__all__ = ['VMwareClientSerializer']
class VMwareClientSerializer(RemoteAppSerializer):
@@ -22,17 +23,10 @@ class VMwareClientSerializer(RemoteAppSerializer):
allow_null=True
)
vmware_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
max_length=128, allow_blank=True, required=False, label=_('Username'),
allow_null=True
)
vmware_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Vmware password'),
allow_null=True
)
class VMwareClientSecretSerializer(RemoteAppSerializer):
vmware_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Vmware password'),
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True
)

View File

@@ -1,15 +1,15 @@
import copy
from rest_framework import serializers
from applications import const
from . import application_category, application_type
__all__ = [
'category_serializer_classes_mapping',
'type_serializer_classes_mapping',
'get_serializer_class_by_application_type',
'type_secret_serializer_classes_mapping'
]
# define `attrs` field `category serializers mapping`
# ---------------------------------------------------
@@ -25,37 +25,19 @@ category_serializer_classes_mapping = {
type_serializer_classes_mapping = {
# db
const.AppType.mysql.value: application_type.MySQLSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mariadb.value: application_type.MariaDBSerializer,
const.AppType.oracle.value: application_type.OracleSerializer,
const.AppType.pgsql.value: application_type.PostgreSerializer,
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
# cloud
const.AppType.k8s.value: application_type.K8SSerializer
}
remote_app_serializer_classes_mapping = {
# remote-app
const.AppType.chrome.value: application_type.ChromeSerializer,
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
const.AppType.vmware_client.value: application_type.VMwareClientSerializer,
const.AppType.custom.value: application_type.CustomSerializer
const.AppType.custom.value: application_type.CustomSerializer,
# cloud
const.AppType.k8s.value: application_type.K8SSerializer
}
type_serializer_classes_mapping.update(remote_app_serializer_classes_mapping)
remote_app_secret_serializer_classes_mapping = {
# remote-app
const.AppType.chrome.value: application_type.ChromeSecretSerializer,
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSecretSerializer,
const.AppType.vmware_client.value: application_type.VMwareClientSecretSerializer,
const.AppType.custom.value: application_type.CustomSecretSerializer
}
type_secret_serializer_classes_mapping = copy.deepcopy(type_serializer_classes_mapping)
type_secret_serializer_classes_mapping.update(remote_app_secret_serializer_classes_mapping)
def get_serializer_class_by_application_type(_application_type):
return type_serializer_classes_mapping.get(_application_type)

View File

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

View File

@@ -1,186 +0,0 @@
# -*- coding: utf-8 -*-
from urllib3.exceptions import MaxRetryError
from urllib.parse import urlencode
from kubernetes.client import api_client
from kubernetes.client.api import core_v1_api
from kubernetes import client
from kubernetes.client.exceptions import ApiException
from rest_framework.generics import get_object_or_404
from common.utils import get_logger
from common.tree import TreeNode
from assets.models import SystemUser
from .. import const
logger = get_logger(__file__)
class KubernetesClient:
def __init__(self, url, token):
self.url = url
self.token = token
def get_api(self):
configuration = client.Configuration()
configuration.host = self.url
configuration.verify_ssl = False
configuration.api_key = {"authorization": "Bearer " + self.token}
c = api_client.ApiClient(configuration=configuration)
api = core_v1_api.CoreV1Api(c)
return api
def get_namespace_list(self):
api = self.get_api()
namespace_list = []
for ns in api.list_namespace().items:
namespace_list.append(ns.metadata.name)
return namespace_list
def get_services(self):
api = self.get_api()
ret = api.list_service_for_all_namespaces(watch=False)
for i in ret.items:
print("%s \t%s \t%s \t%s \t%s \n" % (
i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports))
def get_pod_info(self, namespace, pod):
api = self.get_api()
resp = api.read_namespaced_pod(namespace=namespace, name=pod)
return resp
def get_pod_logs(self, namespace, pod):
api = self.get_api()
log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200)
return log_content
def get_pods(self):
api = self.get_api()
try:
ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3))
except MaxRetryError:
logger.warning('Kubernetes connection timed out')
return
except ApiException as e:
if e.status == 401:
logger.warning('Kubernetes User not authenticated')
else:
logger.warning(e)
return
data = {}
for i in ret.items:
namespace = i.metadata.namespace
pod_info = {
'pod_name': i.metadata.name,
'containers': [j.name for j in i.spec.containers]
}
if namespace in data:
data[namespace].append(pod_info)
else:
data[namespace] = [pod_info, ]
return data
@staticmethod
def get_kubernetes_data(app_id, system_user_id):
from ..models import Application
app = get_object_or_404(Application, id=app_id)
system_user = get_object_or_404(SystemUser, id=system_user_id)
k8s = KubernetesClient(app.attrs['cluster'], system_user.token)
return k8s.get_pods()
class KubernetesTree:
def __init__(self, tree_id):
self.tree_id = tree_id
def as_tree_node(self, app):
pid = app.create_app_tree_pid(self.tree_id)
app_id = str(app.id)
parent_info = {'app_id': app_id}
node = self.create_tree_node(
app_id, pid, app.name, 'k8s', parent_info
)
return node
def as_system_user_tree_node(self, system_user, parent_info):
from ..models import ApplicationTreeNodeMixin
system_user_id = str(system_user.id)
username = system_user.username
username = username if username else '*'
name = f'{system_user.name}({username})'
pid = urlencode({'app_id': self.tree_id})
i = ApplicationTreeNodeMixin.create_tree_id(pid, 'system_user_id', system_user_id)
parent_info.update({'system_user_id': system_user_id})
node = self.create_tree_node(
i, pid, name, 'system_user', parent_info, icon='user-tie'
)
return node
def as_namespace_pod_tree_node(self, name, meta, type, counts=0, is_container=False):
from ..models import ApplicationTreeNodeMixin
i = ApplicationTreeNodeMixin.create_tree_id(self.tree_id, type, name)
meta.update({type: name})
name = name if is_container else f'{name}({counts})'
node = self.create_tree_node(
i, self.tree_id, name, type, meta, icon='cloud', is_container=is_container
)
return node
@staticmethod
def create_tree_node(id_, pid, name, identity, parent_info, icon='', is_container=False):
node = TreeNode(**{
'id': id_,
'name': name,
'title': name,
'pId': pid,
'isParent': not is_container,
'open': False,
'iconSkin': icon,
'parentInfo': urlencode(parent_info),
'meta': {
'type': 'application',
'data': {
'category': const.AppCategory.cloud,
'type': const.AppType.k8s,
'identity': identity
}
}
})
return node
def async_tree_node(self, parent_info):
pod_name = parent_info.get('pod')
app_id = parent_info.get('app_id')
namespace = parent_info.get('namespace')
system_user_id = parent_info.get('system_user_id')
tree_nodes = []
data = KubernetesClient.get_kubernetes_data(app_id, system_user_id)
if not data:
return tree_nodes
if pod_name:
for container in next(
filter(
lambda x: x['pod_name'] == pod_name, data[namespace]
)
)['containers']:
container_node = self.as_namespace_pod_tree_node(
container, parent_info, 'container', is_container=True
)
tree_nodes.append(container_node)
elif namespace:
for pod in data[namespace]:
pod_nodes = self.as_namespace_pod_tree_node(
pod['pod_name'], parent_info, 'pod', len(pod['containers'])
)
tree_nodes.append(pod_nodes)
elif system_user_id:
for namespace, pods in data.items():
namespace_node = self.as_namespace_pod_tree_node(
namespace, parent_info, 'namespace', len(pods)
)
tree_nodes.append(namespace_node)
return tree_nodes

View File

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

View File

@@ -64,7 +64,9 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = AuthBook.get_queryset()
queryset = super().get_queryset() \
.annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname'))
return queryset
@action(methods=['post'], detail=True, url_path='verify')

View File

@@ -1,55 +0,0 @@
# -*- 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',)
filterset_fields = ('trigger', 'plan_id')
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 common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins.api import OrgModelViewSet
from orgs.mixins import generics
from orgs.utils import current_org
from ..hands import IsOrgAdmin
@@ -42,13 +42,19 @@ __all__ = [
]
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
class NodeViewSet(SuggestionMixin, OrgModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
search_fields = ('value', )
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
# 仅支持根节点指直接创建子节点下的节点需要通过children接口创建
def perform_create(self, serializer):
child_key = Node.org_root().get_next_child_key()
serializer.validated_data["key"] = child_key
serializer.save()
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
def check_assets_amount_task(self, request):
task = check_node_assets_amount_task.delay(current_org.id)

View File

@@ -196,21 +196,41 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
return CommandFilterRuleSerializer
def get_queryset(self):
user_groups = []
user_id = self.request.query_params.get('user_id')
user = get_object_or_none(User, pk=user_id)
if user:
user_groups.extend(list(user.groups.all()))
user_group_id = self.request.query_params.get('user_group_id')
user_group = get_object_or_none(UserGroup, pk=user_group_id)
if user_group:
user_groups.append(user_group)
system_user_id = self.kwargs.get('pk', None)
system_user = get_object_or_none(SystemUser, pk=system_user_id)
if not system_user:
system_user_id = self.request.query_params.get('system_user_id')
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset_id = self.request.query_params.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
application_id = self.request.query_params.get('application_id')
rules = CommandFilterRule.get_queryset(
user_id=user_id,
user_group_id=user_group_id,
system_user_id=system_user_id,
asset_id=asset_id,
application_id=application_id
)
application = get_object_or_none(Application, pk=application_id)
q = Q()
if user:
q |= Q(users=user)
if user_groups:
q |= Q(user_groups__in=set(user_groups))
if system_user:
q |= Q(system_users=system_user)
if asset:
q |= Q(assets=asset)
if application:
q |= Q(applications=application)
if q:
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
rule_ids = cmd_filters.values_list('rules', flat=True)
rules = CommandFilterRule.objects.filter(id__in=rule_ids)
else:
rules = CommandFilterRule.objects.none()
return rules

View File

@@ -1,62 +0,0 @@
# 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,4 +12,3 @@ from .utils import *
from .authbook import *
from .gathered_user import *
from .favorite_asset import *
from .backup import *

View File

@@ -2,7 +2,6 @@
#
from django.db import models
from django.db.models import F
from django.utils.translation import ugettext_lazy as _
from simple_history.models import HistoricalRecords
@@ -117,15 +116,6 @@ class AuthBook(BaseUser, AbsConnectivity):
self.asset.save()
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
@classmethod
def get_queryset(cls):
queryset = cls.objects.all() \
.annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname')) \
.annotate(platform=F('asset__platform__name')) \
.annotate(protocols=F('asset__protocols'))
return queryset
def __str__(self):
return self.smart_name

View File

@@ -1,145 +0,0 @@
#!/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

@@ -4,19 +4,15 @@ import uuid
import re
from django.db import models
from django.db.models import Q
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils.translation import ugettext_lazy as _
from users.models import User, UserGroup
from applications.models import Application
from ..models import SystemUser, Asset
from common.utils import lazyproperty, get_logger, get_object_or_none
from common.utils import lazyproperty, get_logger
from orgs.mixins.models import OrgModelMixin
logger = get_logger(__file__)
__all__ = [
'CommandFilter', 'CommandFilterRule'
]
@@ -76,14 +72,10 @@ class CommandFilterRule(OrgModelMixin):
confirm = 2, _('Reconfirm')
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
filter = models.ForeignKey(
'CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules'
)
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
priority = models.IntegerField(
default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]
)
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)])
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
# 动作: 附加字段
@@ -180,34 +172,3 @@ class CommandFilterRule(OrgModelMixin):
ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(applicant=session.user_obj)
return ticket
@classmethod
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, asset_id=None, application_id=None):
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
user_groups.extend(list(user.groups.all()))
user_group = get_object_or_none(UserGroup, pk=user_group_id)
if user_group:
user_groups.append(user_group)
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset = get_object_or_none(Asset, pk=asset_id)
application = get_object_or_none(Application, pk=application_id)
q = Q()
if user:
q |= Q(users=user)
if user_groups:
q |= Q(user_groups__in=set(user_groups))
if system_user:
q |= Q(system_users=system_user)
if asset:
q |= Q(assets=asset)
if application:
q |= Q(applications=application)
if q:
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
rule_ids = cmd_filters.values_list('rules', flat=True)
rules = cls.objects.filter(id__in=rule_ids)
else:
rules = cls.objects.none()
return rules

View File

@@ -5,7 +5,6 @@
import logging
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache
@@ -30,7 +29,6 @@ class ProtocolMixin:
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
oracle = 'oracle', 'Oracle'
mariadb = 'mariadb', 'MariaDB'
postgresql = 'postgresql', 'PostgreSQL'
@@ -46,8 +44,7 @@ class ProtocolMixin:
Protocol.rdp
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
Protocol.mysql, Protocol.redis, Protocol.oracle,
Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver
Protocol.mysql, Protocol.oracle, Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
Protocol.k8s
@@ -150,37 +147,15 @@ class AuthMixin:
def load_asset_special_auth(self, asset, username=''):
"""
AuthBook 的数据状态
| asset | systemuser | username |
1 | * | * | x |
2 | * | x | * |
当前 AuthBook 只有以上两种状态systemuser 与 username 不会并存。
正常的资产与系统用户关联产生的是第1种状态改密则产生第2种状态。改密之后
只有 username 而没有 systemuser 。
Freq: 关联同一资产的多个系统用户指定同一用户名时,修改用户密码会影响所有系统用户
这里有一个不对称的行为,同名系统用户密码覆盖
当有相同 username 的多个系统用户时,有改密动作之后,所有的同名系统用户都使用最后
一次改动,但如果没有发生过改密,同名系统用户使用的密码还是各自的。
"""
if username == '':
username = self.username
authbook = AuthBook.objects.filter(
asset=asset, username=username, systemuser__isnull=True
).order_by('-date_created').first()
if not authbook:
authbook = AuthBook.objects.filter(
asset=asset, systemuser=self
).order_by('-date_created').first()
if not authbook:
authbooks = list(AuthBook.objects.filter(asset=asset, systemuser=self))
if len(authbooks) == 0:
return None
elif len(authbooks) == 1:
authbook = authbooks[0]
else:
authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True)
authbook = authbooks[0]
authbook.load_auth()
self.password = authbook.password
self.private_key = authbook.private_key

View File

@@ -1,25 +0,0 @@
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,4 +11,3 @@ from .cmd_filter import *
from .gathered_user import *
from .favorite_asset import *
from .account import *
from .backup import *

View File

@@ -6,25 +6,16 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin
from .utils import validate_password_contains_left_double_curly_bracket
from common.utils.encode import ssh_pubkey_gen
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
ip = serializers.ReadOnlyField(label=_("IP"))
hostname = serializers.ReadOnlyField(label=_("Hostname"))
platform = serializers.ReadOnlyField(label=_("Platform"))
protocols = serializers.SerializerMethodField(label=_("Protocols"))
date_created = serializers.DateTimeField(
label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True
)
date_updated = serializers.DateTimeField(
label=_('Date updated'), format="%Y/%m/%d %H:%M:%S", read_only=True
)
class Meta:
model = AuthBook
fields_mini = ['id', 'username', 'ip', 'hostname', 'platform', 'protocols', 'version']
fields_write_only = ['password', 'private_key', "public_key", 'passphrase']
fields_mini = ['id', 'username', 'ip', 'hostname', 'version']
fields_write_only = ['password', 'private_key', "public_key"]
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
fields_small = fields_mini + fields_write_only + fields_other
fields_fk = ['asset', 'systemuser', 'systemuser_display']
@@ -41,24 +32,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
}
ref_name = 'AssetAccountSerializer'
def _validate_gen_key(self, attrs):
private_key = attrs.get('private_key')
if not private_key:
return attrs
password = attrs.get('passphrase')
username = attrs.get('username')
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs['public_key'] = public_key
return attrs
def validate(self, attrs):
attrs = self._validate_gen_key(attrs)
return attrs
def get_protocols(self, v):
return v.protocols.replace(' ', ', ')
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
@@ -72,10 +45,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class AccountSecretSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},

View File

@@ -5,6 +5,8 @@ from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from users.models import User, UserGroup
from perms.models import AssetPermission
from ..models import Asset, Node, Platform, SystemUser
__all__ = [

View File

@@ -1,52 +0,0 @@
# -*- 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,12 +1,10 @@
# -*- coding: utf-8 -*-
#
from io import StringIO
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
from assets.models import Type
from common.utils import ssh_pubkey_gen, validate_ssh_private_key
class AuthSerializer(serializers.ModelSerializer):
@@ -30,28 +28,17 @@ class AuthSerializer(serializers.ModelSerializer):
return self.instance
class AuthSerializerMixin(serializers.ModelSerializer):
passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Key password')
)
class AuthSerializerMixin:
def validate_password(self, password):
return password
def validate_private_key(self, private_key):
if not private_key:
return
passphrase = self.initial_data.get('passphrase')
passphrase = passphrase if passphrase else None
valid = validate_ssh_private_key(private_key, password=passphrase)
password = self.initial_data.get("password")
valid = validate_ssh_private_key(private_key, password)
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
private_key = ssh_private_key_gen(private_key, password=passphrase)
string_io = StringIO()
private_key.write_private_key(string_io)
private_key = string_io.getvalue()
raise serializers.ValidationError(_("private key invalid"))
return private_key
def validate_public_key(self, public_key):
@@ -63,7 +50,6 @@ class AuthSerializerMixin(serializers.ModelSerializer):
value = validated_data.get(field)
if not value:
validated_data.pop(field, None)
validated_data.pop('passphrase', None)
def create(self, validated_data):
self.clean_auth_fields(validated_data)
@@ -72,24 +58,3 @@ class AuthSerializerMixin(serializers.ModelSerializer):
def update(self, instance, validated_data):
self.clean_auth_fields(validated_data)
return super().update(instance, validated_data)
class TypesField(serializers.MultipleChoiceField):
def __init__(self, *args, **kwargs):
kwargs['choices'] = Type.CHOICES
super().__init__(*args, **kwargs)
def to_representation(self, value):
return Type.value_to_choices(value)
def to_internal_value(self, data):
if data is None:
return data
return Type.choices_to_value(data)
class ActionsDisplayField(TypesField):
def to_representation(self, value):
values = super().to_representation(value)
choices = dict(Type.CHOICES)
return [choices.get(i) for i in values]

View File

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

View File

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node
__all__ = [
'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer", "NodeTaskSerializer",
@@ -16,9 +17,6 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
value = serializers.CharField(
required=False, allow_blank=True, allow_null=True, label=_("value")
)
full_value = serializers.CharField(
required=False, allow_blank=True, allow_null=True, label=_("Full value")
)
class Meta:
model = Node
@@ -42,19 +40,6 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
)
return data
def create(self, validated_data):
full_value = validated_data.get('full_value')
# 直接多层级创建
if full_value:
node = Node.create_node_by_full_value(full_value)
# 根据 value 在 root 下创建
else:
key = Node.org_root().get_next_child_key()
validated_data['key'] = key
node = Node.objects.create(**validated_data)
return node
class NodeAssetsSerializer(BulkOrgResourceModelSerializer):
assets = serializers.PrimaryKeyRelatedField(

View File

@@ -4,7 +4,7 @@ from django.db.models import Count
from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen
from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re
from common.validators import alphanumeric_re, alphanumeric_cn_re
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import SystemUser, Asset
from .utils import validate_password_contains_left_double_curly_bracket
@@ -33,7 +33,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = SystemUser
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key', 'passphrase']
fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [
'token', 'ssh_key_fingerprint',
'type', 'type_display', 'protocol', 'is_asset_protocol',
@@ -51,7 +51,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'trim_whitespace': False,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'cmd_filters': {"required": False, 'label': _('Command filter')},
'cmd_filters': {"required": False},
'public_key': {"write_only": True},
'private_key': {"write_only": True},
'token': {"write_only": True},
@@ -107,12 +107,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def validate_username(self, username):
protocol = self.get_initial_value("protocol")
if username:
regx = alphanumeric_re
if protocol == SystemUser.Protocol.telnet:
regx = alphanumeric_cn_re
elif protocol == SystemUser.Protocol.rdp:
regx = alphanumeric_win_re
else:
regx = alphanumeric_re
if not regx.match(username):
raise serializers.ValidationError(_('Special char not allowed'))
return username
@@ -122,8 +119,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return ''
login_mode = self.get_initial_value("login_mode")
if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc \
and protocol != SystemUser.Protocol.redis:
if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc:
msg = _('* Automatic login mode must fill in the username.')
raise serializers.ValidationError(msg)
return username
@@ -145,9 +141,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def validate_password(self, password):
super().validate_password(password)
auto_gen_key = self.get_initial_value('auto_generate_key', False)
private_key = self.get_initial_value('private_key')
login_mode = self.get_initial_value('login_mode')
auto_gen_key = self.get_initial_value("auto_generate_key", False)
private_key = self.get_initial_value("private_key")
login_mode = self.get_initial_value("login_mode")
if not self.instance and not auto_gen_key and not password and \
not private_key and login_mode == SystemUser.LOGIN_AUTO:
@@ -191,9 +187,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return attrs
def _validate_gen_key(self, attrs):
username = attrs.get('username', 'manual')
auto_gen_key = attrs.pop('auto_generate_key', False)
protocol = attrs.get('protocol')
username = attrs.get("username", "manual")
auto_gen_key = attrs.pop("auto_generate_key", False)
protocol = attrs.get("protocol")
if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
return attrs
@@ -201,17 +197,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
# 自动生成
if auto_gen_key and not self.instance:
password = SystemUser.gen_password()
attrs['password'] = password
attrs["password"] = password
if protocol == SystemUser.Protocol.ssh:
private_key, public_key = SystemUser.gen_key(username)
attrs['private_key'] = private_key
attrs['public_key'] = public_key
attrs["private_key"] = private_key
attrs["public_key"] = public_key
# 如果设置了private key没有设置public key则生成
elif attrs.get('private_key'):
private_key = attrs['private_key']
password = attrs.get('password')
elif attrs.get("private_key", None):
private_key = attrs["private_key"]
password = attrs.get("password")
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs['public_key'] = public_key
attrs["public_key"] = public_key
return attrs
def _validate_login_mode(self, attrs):
@@ -236,7 +232,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset \
queryset = queryset\
.annotate(assets_amount=Count("assets")) \
.prefetch_related('nodes', 'cmd_filters')
return queryset

View File

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

View File

@@ -1,223 +0,0 @@
import os
import time
import pandas as pd
from collections import defaultdict, OrderedDict
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from assets.models import AuthBook
from assets.serializers import AccountSecretSerializer
from assets.notifications import AccountBackupExecutionTaskMsg
from applications.models import Account
from applications.const import AppType
from applications.serializers import AppAccountSecretSerializer
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 BaseAccountHandler:
@classmethod
def unpack_data(cls, serializer_data, data=None):
if data is None:
data = {}
for k, v in serializer_data.items():
if isinstance(v, OrderedDict):
cls.unpack_data(v, data)
else:
data[k] = v
return data
@classmethod
def get_header_fields(cls, serializer: serializers.Serializer):
try:
backup_fields = getattr(serializer, 'Meta').fields_backup
except AttributeError:
backup_fields = serializer.fields.keys()
header_fields = {}
for field in backup_fields:
v = serializer.fields[field]
if isinstance(v, serializers.Serializer):
_fields = cls.get_header_fields(v)
header_fields.update(_fields)
else:
header_fields[field] = v.label
return header_fields
@classmethod
def create_row(cls, account, serializer_cls):
serializer = serializer_cls(account)
data = cls.unpack_data(serializer.data)
header_fields = cls.get_header_fields(serializer)
row_dict = {}
for field, header_name in header_fields.items():
row_dict[header_name] = data[field]
return row_dict
class AssetAccountHandler(BaseAccountHandler):
@staticmethod
def get_filename(plan_name):
filename = os.path.join(
PATH, f'{plan_name}-{_("Asset")}-{local_now_display()}-{time.time()}.xlsx'
)
return filename
@classmethod
def create_df(cls):
df_dict = defaultdict(list)
sheet_name = AuthBook._meta.verbose_name
accounts = AuthBook.get_queryset()
for account in accounts:
account.load_auth()
row = cls.create_row(account, AccountSecretSerializer)
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集{}条资产账号\033[0m'.format(accounts.count()))
return df_dict
class AppAccountHandler(BaseAccountHandler):
@staticmethod
def get_filename(plan_name):
filename = os.path.join(
PATH, f'{plan_name}-{_("Application")}-{local_now_display()}-{time.time()}.xlsx'
)
return filename
@classmethod
def create_df(cls):
df_dict = defaultdict(list)
accounts = Account.get_queryset()
for account in accounts:
account.load_auth()
app_type = account.type
sheet_name = AppType.get_label(app_type)
row = cls.create_row(account, AppAccountSecretSerializer)
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
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()
files = []
for account_type in self.execution.types:
handler = handler_map.get(account_type)
if not handler:
continue
df_dict = handler.create_df()
if not df_dict:
continue
filename = handler.get_filename(self.plan_name)
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)
files.append(filename)
timedelta = round((time.time() - time_start), 2)
logger.info('步骤完成: 用时 {}s'.format(timedelta))
return files
def send_backup_mail(self, files):
recipients = self.execution.plan_snapshot.get('recipients')
if not recipients:
return
if not files:
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))

View File

@@ -1,48 +0,0 @@
# -*- 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

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

View File

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

View File

@@ -1,7 +1,7 @@
# ~*~ coding: utf-8 ~*~
from celery import shared_task
from django.utils.translation import ugettext as _, gettext_noop
from django.utils.translation import ugettext as _
from common.utils import get_logger
from orgs.utils import org_aware_func
@@ -104,6 +104,6 @@ def test_accounts_connectivity_manual(accounts):
:param accounts: <AuthBook>对象
"""
for account in accounts:
task_name = gettext_noop("Test account connectivity: ") + str(account)
task_name = _("Test account connectivity: {}").format(account)
test_account_connectivity_util(account, task_name)
print(".\n")

View File

@@ -2,7 +2,7 @@
from itertools import groupby
from collections import defaultdict
from celery import shared_task
from django.utils.translation import gettext_noop
from django.utils.translation import ugettext as _
from common.utils import get_logger
from orgs.utils import org_aware_func
@@ -46,7 +46,7 @@ def test_asset_connectivity_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = gettext_noop("Test assets connectivity. ")
task_name = _("Test assets connectivity")
hosts = clean_ansible_task_hosts(assets)
if not hosts:
@@ -88,7 +88,7 @@ def test_asset_connectivity_util(assets, task_name=None):
@shared_task(queue="ansible")
def test_asset_connectivity_manual(asset):
task_name = gettext_noop("Test assets connectivity: ") + str(asset)
task_name = _("Test assets connectivity: {}").format(asset)
summary = test_asset_connectivity_util([asset], task_name=task_name)
if summary.get('dark'):
@@ -99,7 +99,7 @@ def test_asset_connectivity_manual(asset):
@shared_task(queue="ansible")
def test_assets_connectivity_manual(assets):
task_name = gettext_noop("Test assets connectivity: ") + str([asset.hostname for asset in assets])
task_name = _("Test assets connectivity: {}").format([asset.hostname for asset in assets])
summary = test_asset_connectivity_util(assets, task_name=task_name)
if summary.get('dark'):
@@ -110,7 +110,8 @@ def test_assets_connectivity_manual(assets):
@shared_task(queue="ansible")
def test_node_assets_connectivity_manual(node):
task_name = gettext_noop("Test if the assets under the node are connectable: ") + node.name
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
assets = node.get_all_assets()
result = test_asset_connectivity_util(assets, task_name=task_name)
return result

View File

@@ -1,20 +0,0 @@
# -*- 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

@@ -4,7 +4,7 @@ import json
import re
from celery import shared_task
from django.utils.translation import ugettext as _, gettext_noop
from django.utils.translation import ugettext as _
from common.utils import (
capacity_convert, sum_capacity, get_logger
@@ -94,7 +94,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
"""
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = gettext_noop("Update some assets hardware info. ")
task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hosts = clean_ansible_task_hosts(assets)
if not hosts:
@@ -111,13 +111,13 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task(queue="ansible")
def update_asset_hardware_info_manual(asset):
task_name = gettext_noop("Update asset hardware info: ") + str(asset.hostname)
task_name = _("Update asset hardware info: {}").format(asset.hostname)
update_assets_hardware_info_util([asset], task_name=task_name)
@shared_task(queue="ansible")
def update_assets_hardware_info_manual(assets):
task_name = gettext_noop("Update assets hardware info: ") + str([asset.hostname for asset in assets])
task_name = _("Update assets hardware info: {}").format([asset.hostname for asset in assets])
update_assets_hardware_info_util(assets, task_name=task_name)
@@ -134,7 +134,7 @@ def update_assets_hardware_info_period():
@shared_task(queue="ansible")
def update_node_assets_hardware_info_manual(node):
task_name = gettext_noop("Update node asset hardware information: ") + str(node.name)
task_name = _("Update node asset hardware information: {}").format(node.name)
assets = node.get_all_assets()
result = update_assets_hardware_info_util(assets, task_name=task_name)
return result

View File

@@ -4,7 +4,7 @@ import re
from collections import defaultdict
from celery import shared_task
from django.utils.translation import gettext_noop
from django.utils.translation import ugettext as _
from django.utils import timezone
from orgs.utils import tmp_to_org, org_aware_func
@@ -108,7 +108,7 @@ def add_asset_users(assets, results):
def gather_asset_users(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = gettext_noop("Gather assets users")
task_name = _("Gather assets users")
assets = clean_ansible_task_hosts(assets)
if not assets:
return

View File

@@ -3,11 +3,11 @@
from itertools import groupby
from celery import shared_task
from common.db.utils import get_object_if_need, get_objects
from django.utils.translation import ugettext as _, gettext_noop
from django.db.models import Empty
from django.utils.translation import ugettext as _
from django.db.models import Empty, Q
from common.utils import encrypt_password, get_logger
from assets.models import SystemUser, Asset
from assets.models import SystemUser, Asset, AuthBook
from orgs.utils import org_aware_func, tmp_to_root_org
from . import const
from .utils import clean_ansible_task_hosts, group_asset_by_platform
@@ -178,7 +178,6 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
"""
获取推送系统用户的 ansible 命令,跟资产无关
:param system_user:
:param platform:
:param username: 当动态时,近推送某个
@@ -210,10 +209,18 @@ def push_system_user_util(system_user, assets, task_name, username=None):
if not assets:
return {}
# 资产按平台分类
assets_sorted = sorted(assets, key=group_asset_by_platform)
platform_hosts = groupby(assets_sorted, key=group_asset_by_platform)
def run_task(_tasks, _hosts):
if not _tasks:
return
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True,
)
task.run()
if system_user.username_same_with_user:
if username is None:
# 动态系统用户,但是没有指定 username
@@ -225,15 +232,6 @@ def push_system_user_util(system_user, assets, task_name, username=None):
assert username is None, 'Only Dynamic user can assign `username`'
usernames = [system_user.username]
def run_task(_tasks, _hosts):
if not _tasks:
return
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True,
)
task.run()
for platform, _assets in platform_hosts:
_assets = list(_assets)
if not _assets:
@@ -241,11 +239,36 @@ def push_system_user_util(system_user, assets, task_name, username=None):
print(_("Start push system user for platform: [{}]").format(platform))
print(_("Hosts count: {}").format(len(_assets)))
for u in usernames:
for a in _assets:
system_user.load_asset_special_auth(a, u)
tasks = get_push_system_user_tasks(system_user, platform, username=u)
run_task(tasks, [a])
id_asset_map = {_asset.id: _asset for _asset in _assets}
asset_ids = id_asset_map.keys()
no_special_auth = []
special_auth_set = set()
auth_books = AuthBook.objects.filter(asset_id__in=asset_ids).filter(
Q(username__in=usernames) | Q(systemuser__username__in=usernames)
).prefetch_related('systemuser')
for auth_book in auth_books:
auth_book.load_auth()
special_auth_set.add((auth_book.username, auth_book.asset_id))
for _username in usernames:
no_special_assets = []
for asset_id in asset_ids:
if (_username, asset_id) not in special_auth_set:
no_special_assets.append(id_asset_map[asset_id])
if no_special_assets:
no_special_auth.append((_username, no_special_assets))
for _username, no_special_assets in no_special_auth:
tasks = get_push_system_user_tasks(system_user, platform, username=_username)
run_task(tasks, no_special_assets)
for auth_book in auth_books:
system_user._merge_auth(auth_book)
tasks = get_push_system_user_tasks(system_user, platform, username=auth_book.username)
asset = id_asset_map[auth_book.asset_id]
run_task(tasks, [asset])
@shared_task(queue="ansible")
@@ -256,7 +279,7 @@ def push_system_user_to_assets_manual(system_user, username=None):
"""
system_user = get_object_if_need(SystemUser, system_user)
assets = system_user.get_related_assets()
task_name = gettext_noop("Push system users to assets: ") + system_user.name
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name=task_name, username=username)
@@ -268,7 +291,7 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
"""
# if username is None:
# username = system_user.username
task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format(
task_name = _("Push system users to asset: {}({}) => {}").format(
system_user.name, username, asset
)
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
@@ -289,7 +312,7 @@ def push_system_user_to_assets(system_user_id, asset_ids, username=None):
"""
system_user = SystemUser.objects.get(id=system_user_id)
assets = get_objects(Asset, asset_ids)
task_name = gettext_noop("Push system users to assets: ") + system_user.name
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name, username=username)

View File

@@ -3,7 +3,7 @@ from itertools import groupby
from collections import defaultdict
from celery import shared_task
from django.utils.translation import ugettext as _, gettext_noop
from django.utils.translation import ugettext as _
from assets.models import Asset
from common.utils import get_logger
@@ -115,7 +115,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
@shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_manual(system_user, asset_ids=None):
task_name = gettext_noop("Test system user connectivity: ") + str(system_user)
task_name = _("Test system user connectivity: {}").format(system_user)
if asset_ids:
assets = Asset.objects.filter(id__in=asset_ids)
else:
@@ -126,7 +126,7 @@ def test_system_user_connectivity_manual(system_user, asset_ids=None):
@shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_a_asset(system_user, asset):
task_name = gettext_noop("Test system user connectivity: ") + "{} => {}".format(
task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset
)
test_system_user_connectivity_util(system_user, [asset], task_name)
@@ -145,7 +145,7 @@ def test_system_user_connectivity_period():
return
queryset_map = SystemUser.objects.all_group_by_org()
for org, system_user in queryset_map.items():
task_name = gettext_noop("Test system user connectivity period: ") + str(system_user)
task_name = _("Test system user connectivity period: {}").format(system_user)
with tmp_to_org(org):
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name)

View File

@@ -0,0 +1 @@

View File

@@ -26,8 +26,6 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation')
router.register(r'backup', api.AccountBackupPlanViewSet, 'backup')
router.register(r'backup-execution', api.AccountBackupPlanExecutionViewSet, 'backup-execution')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')

View File

@@ -2,9 +2,8 @@ import uuid
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext, ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, Organization
from orgs.utils import current_org
@@ -64,10 +63,6 @@ class OperateLog(OrgModelMixin):
def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
@lazyproperty
def resource_type_display(self):
return gettext(self.resource_type)
def save(self, *args, **kwargs):
if current_org.is_root() and not self.org_id:
self.org_id = Organization.ROOT_ID
@@ -118,10 +113,6 @@ class UserLoginLog(models.Model):
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
backend = models.CharField(max_length=32, default='', verbose_name=_('Authentication backend'))
@property
def backend_display(self):
return gettext(self.backend)
@classmethod
def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None):
login_logs = cls.objects.all()

View File

@@ -35,33 +35,27 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
fields_mini = ['id']
fields_small = fields_mini + [
'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
'mfa', 'mfa_display', 'reason', 'reason_display', 'backend', 'backend_display',
'mfa', 'mfa_display', 'reason', 'reason_display', 'backend',
'status', 'status_display',
'datetime',
]
fields = fields_small
extra_kwargs = {
"user_agent": {'label': _('User agent')},
"reason_display": {'label': _('Reason display')},
'backend_display': {'label': _('Authentication backend')}
"reason_display": {'label': _('Reason display')}
}
class OperateLogSerializer(serializers.ModelSerializer):
action_display = serializers.CharField(source='get_action_display', label=_('Action'))
class Meta:
model = models.OperateLog
fields_mini = ['id']
fields_small = fields_mini + [
'user', 'action', 'action_display',
'resource_type', 'resource_type_display', 'resource',
'remote_addr', 'datetime', 'org_id'
'user', 'action', 'resource_type', 'resource', 'remote_addr',
'datetime',
'org_id'
]
fields = fields_small
extra_kwargs = {
'resource_type_display': {'label': _('Resource Type')}
}
class PasswordChangeLogSerializer(serializers.ModelSerializer):

View File

@@ -10,7 +10,6 @@ from django.utils import timezone
from django.utils.functional import LazyObject
from django.contrib.auth import BACKEND_SESSION_KEY
from django.utils.translation import ugettext_lazy as _
from django.utils import translation
from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request
@@ -84,8 +83,7 @@ def create_operate_log(action, sender, resource):
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
with translation.override('en'):
resource_type = sender._meta.verbose_name
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
data = {
@@ -292,16 +290,13 @@ def generate_data(username, request, login_type=None):
if login_type is None:
login_type = 'W'
with translation.override('en'):
backend = str(get_login_backend(request))
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent[0:254],
'datetime': timezone.now(),
'backend': backend,
'backend': get_login_backend(request)
}
return data

View File

@@ -28,7 +28,7 @@ from common.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
from perms.models.base import Action
from perms.models.asset_permission import Action
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
@@ -231,7 +231,7 @@ class SecretDetailMixin:
@staticmethod
def _get_application_secret_detail(application):
from perms.models.base import Action
from perms.models import Action
gateway = None
if not application.category_remote_app:
@@ -298,6 +298,9 @@ class SecretDetailMixin:
data['type'] = 'application'
data.update(app_detail)
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
@@ -388,10 +391,10 @@ class UserConnectionTokenViewSet(
asset = get_object_or_404(Asset, id=value.get('asset'))
if not asset.is_active:
raise serializers.ValidationError("Asset disabled")
has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user)
has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect')
else:
app = get_object_or_404(Application, id=value.get('application'))
has_perm, actions, expired_at = app_validate_permission(user, app, system_user)
has_perm, expired_at = app_validate_permission(user, app, system_user)
if not has_perm:
raise serializers.ValidationError('Permission expired or invalid')

View File

@@ -12,7 +12,6 @@ from rest_framework.response import Response
from common.permissions import IsValidUser, NeedMFAVerify
from common.utils import get_logger
from common.exceptions import UnexpectError
from users.models.user import User
from ..serializers import OtpVerifySerializer
from .. import serializers
@@ -36,45 +35,30 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
"""
permission_classes = (AllowAny,)
serializer_class = serializers.MFASelectTypeSerializer
username = ''
ip = ''
def get_user_from_db(self, username):
try:
user = get_object_or_404(User, username=username)
return user
except Exception as e:
self.incr_mfa_failed_time(username, self.ip)
raise e
def get_user_from_db(self, username):
"""避免暴力测试用户名"""
ip = self.get_request_ip()
self.check_mfa_is_block(username, ip)
try:
user = get_object_or_404(User, username=username)
return user
except Exception as e:
self.incr_mfa_failed_time(username, ip)
raise e
def perform_create(self, serializer):
username = serializer.validated_data.get('username', '')
mfa_type = serializer.validated_data['type']
if not username:
user = self.get_user_from_session()
else:
user = self.get_user_from_db(username)
user = get_object_or_404(User, username=username)
mfa_backend = user.get_active_mfa_backend_by_type(mfa_type)
if not mfa_backend or not mfa_backend.challenge_required:
error = _('Current user not support mfa type: {}').format(mfa_type)
raise ValidationError({'error': error})
raise ValidationError('MFA type not support: {} {}'.format(mfa_type, mfa_backend))
mfa_backend.send_challenge()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
mfa_backend.send_challenge()
self.perform_create(serializer)
return Response(serializer.data, status=201)
except Exception as e:
raise UnexpectError(str(e))
logger.exception(e)
return Response({'error': str(e)}, status=400)
class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):

View File

@@ -76,10 +76,11 @@ class PrepareRequestMixin:
@staticmethod
def get_attribute_consuming_service():
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
name_prefix = settings.SITE_URL
if attr_mapping and isinstance(attr_mapping, dict):
attr_list = [
{
"name": sp_key,
"name": '{}/{}'.format(name_prefix, sp_key),
"friendlyName": idp_key, "isRequired": True
}
for idp_key, sp_key in attr_mapping.items()
@@ -167,10 +168,12 @@ class PrepareRequestMixin:
def get_attributes(self, saml_instance):
user_attrs = {}
real_key_index = len(settings.SITE_URL) + 1
attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
for attr, value in attrs.items():
attr = attr[real_key_index:]
if attr not in valid_attrs:
continue
user_attrs[attr] = self.value_to_str(value)

View File

@@ -1,21 +0,0 @@
# 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

@@ -335,11 +335,6 @@ class MFAMixin:
mfa_backends = User.get_user_mfa_backends(user)
return {'mfa_backends': mfa_backends}
@staticmethod
def incr_mfa_failed_time(username, ip):
util = MFABlockUtils(username, ip)
util.incr_failed_count()
class AuthPostCheckMixin:
@classmethod
@@ -455,10 +450,7 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
)
if not user:
self.raise_credential_error(errors.reason_password_failed)
self.request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
if user.is_expired:
elif user.is_expired:
self.raise_credential_error(errors.reason_user_expired)
elif not user.is_active:
self.raise_credential_error(errors.reason_user_inactive)

View File

@@ -44,4 +44,4 @@ class SSOToken(models.JMSBaseModel):
"""
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
expired = models.BooleanField(default=False, verbose_name=_('Expired'))
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name=_('User'), db_constraint=False)
user = models.ForeignKey('users.User', on_delete=models.PROTECT, 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.template.loader import render_to_string
from notifications.notifications import UserMessage
from common.utils import get_logger
from common.utils.timezone import local_now_display
logger = get_logger(__file__)
@@ -15,7 +15,8 @@ class DifferentCityLoginMessage(UserMessage):
super().__init__(user)
def get_html_msg(self) -> dict:
now = local_now_display()
now_local = timezone.localtime(timezone.now())
now = now_local.strftime("%Y-%m-%d %H:%M:%S")
subject = _('Different city login reminder')
context = dict(
subject=subject,
@@ -38,36 +39,3 @@ class DifferentCityLoginMessage(UserMessage):
ip = '8.8.8.8'
city = '洛杉矶'
return cls(user, ip, city)
class OAuthBindMessage(UserMessage):
def __init__(self, user, ip, oauth_name, oauth_id):
super().__init__(user)
self.ip = ip
self.oauth_name = oauth_name
self.oauth_id = oauth_id
def get_html_msg(self) -> dict:
now = local_now_display()
subject = self.oauth_name + ' ' + _('binding reminder')
context = dict(
subject=subject,
name=self.user.name,
username=self.user.username,
ip=self.ip,
time=now,
oauth_name=self.oauth_name,
oauth_id=self.oauth_id
)
message = render_to_string('authentication/_msg_oauth_bind.html', context)
return {
'subject': subject,
'message': message
}
@classmethod
def gen_test_msg(cls):
from users.models import User
user = User.objects.first()
ip = '8.8.8.8'
return cls(user, ip, _('WeCom'), '000000')

View File

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

View File

@@ -1,18 +0,0 @@
{% 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

@@ -109,7 +109,7 @@
}
.select-con {
width: 35%;
width: 30%;
}
.mfa-div {

View File

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

View File

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

View File

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

View File

@@ -66,47 +66,6 @@ class ChoiceSet(metaclass=ChoiceSetType):
choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明
class BitOperationChoice:
NONE = 0
NAME_MAP: dict
DB_CHOICES: tuple
NAME_MAP_REVERSE: dict
@classmethod
def value_to_choices(cls, value):
if isinstance(value, list):
return value
value = int(value)
choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i]
return choices
@classmethod
def value_to_choices_display(cls, value):
choices = cls.value_to_choices(value)
return [str(dict(cls.choices())[i]) for i in choices]
@classmethod
def choices_to_value(cls, value):
if not isinstance(value, list):
return cls.NONE
db_value = [
cls.NAME_MAP_REVERSE[v] for v in value
if v in cls.NAME_MAP_REVERSE.keys()
]
if not db_value:
return cls.NONE
def to_choices(x, y):
return x | y
result = reduce(to_choices, db_value)
return result
@classmethod
def choices(cls):
return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES]
class JMSBaseModel(Model):
created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))

View File

@@ -44,7 +44,6 @@ def get_objects(model, pks):
return objs
# 复制 django.db.close_old_connections, 因为它没有导出ide 提示有问题
def close_old_connections():
for conn in connections.all():
conn.close_if_unusable_or_obsolete()

View File

@@ -45,9 +45,3 @@ class MFAVerifyRequired(JMSException):
status_code = status.HTTP_400_BAD_REQUEST
default_code = 'mfa_verify_required'
default_detail = _('This action require verify your MFA')
class UnexpectError(JMSException):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_code = 'unexpect_error'
default_detail = _('Unexpect error occur')

View File

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

View File

@@ -23,7 +23,6 @@ class RedisPubSub:
def __init__(self, ch, db=10):
self.ch = ch
self.redis = get_redis_client(db)
self.subscriber = None
def subscribe(self):
ps = self.redis.pubsub()
@@ -42,9 +41,7 @@ class RedisPubSub:
:param handle: lambda item: do_something
:return:
"""
self.close_handle_msg()
sub = self.subscribe()
self.subscriber = sub
msgs = sub.listen()
try:
@@ -68,7 +65,3 @@ class RedisPubSub:
except Exception as e:
logger.error("Redis observer close error: ", e)
def close_handle_msg(self):
if self.subscriber:
self.subscriber.close()
self.subscriber = None

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
from django.core.cache import cache
from django.shortcuts import reverse, redirect
from django.utils.translation import gettext_noop
from django.shortcuts import reverse
from django.shortcuts import redirect
from .random import random_string

View File

@@ -1,14 +0,0 @@
from django.utils.translation import gettext
def translate_value(value):
if not value:
return value
sps = ['. ', ': ']
spb = {str(sp in value): sp for sp in sps}
sp = spb.get('True')
if not sp:
return value
tpl, data = value.split(sp, 1)
return gettext(tpl + sp) + data

View File

@@ -17,8 +17,6 @@ alphanumeric_re = re.compile(r'^[0-9a-zA-Z_@\-\.]*$')
alphanumeric_cn_re = re.compile(r'^[0-9a-zA-Z_@\-\.\u4E00-\u9FA5]*$')
alphanumeric_win_re = re.compile(r'^[0-9a-zA-Z_@#%&~\^\$\-\.\u4E00-\u9FA5]*$')
class ProjectUniqueValidator(UniqueTogetherValidator):
def __call__(self, attrs, serializer):

View File

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

View File

@@ -2,7 +2,6 @@
#
from functools import partial
from werkzeug.local import LocalProxy
from datetime import datetime
from django.conf import settings
from common.local import thread_local
@@ -27,18 +26,4 @@ def has_valid_xpack_license():
return License.has_valid_license()
def get_xpack_license_info() -> dict:
if has_valid_xpack_license():
from xpack.plugins.license.models import License
info = License.get_license_detail()
corporation = info.get('corporation', '')
else:
current_year = datetime.now().year
corporation = f'Copyright - FIT2CLOUD 飞致云 © 2014-{current_year}'
info = {
'corporation': corporation
}
return info
current_request = LocalProxy(partial(_find, 'current_request'))

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07244b630278a5574b97c46218ae453de71d01a1ea6682b88baa741a99cf8c22
size 97436
oid sha256:151597996418474bb0ec965084ef04c8e1f93b6546b6829a1e0174e9e1f2e43a
size 95238

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ from channels.generic.websocket import JsonWebsocketConsumer
from common.utils import get_logger
from common.db.utils import safe_db_connection
from .site_msg import SiteMessageUtil
from .signals_handler import NewSiteMsgSubPub
from .signals_handler import new_site_msg_chan
logger = get_logger(__name__)
@@ -13,10 +13,6 @@ logger = get_logger(__name__)
class SiteMsgWebsocket(JsonWebsocketConsumer):
refresh_every_seconds = 10
def __init__(self, *args, **kwargs):
super(SiteMsgWebsocket, self).__init__(*args, **kwargs)
self.subscriber = None
def connect(self):
user = self.scope["user"]
if user.is_authenticated:
@@ -27,10 +23,6 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
else:
self.close()
def disconnect(self, code):
if self.subscriber:
self.subscriber.close_handle_msg()
def receive(self, text_data=None, bytes_data=None, **kwargs):
data = json.loads(text_data)
refresh_every_seconds = data.get('refresh_every_seconds')
@@ -64,6 +56,4 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
if user_id in users:
ws.send_unread_msg_count()
subscriber = NewSiteMsgSubPub()
self.subscriber = subscriber
subscriber.keep_handle_msg(handle_new_site_msg_recv)
new_site_msg_chan.keep_handle_msg(handle_new_site_msg_recv)

View File

@@ -13,6 +13,5 @@ class OpsConfig(AppConfig):
from orgs.utils import set_current_org
set_current_org(Organization.root())
from .celery import signal_handler
from . import signals_handler
from . import notifications
super().ready()

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