Compare commits

...

14 Commits
v3 ... v3.2

Author SHA1 Message Date
Bai
34c3147264 fix: 修复删除组织时组织根节点未被删除的问题 2023-05-16 16:37:08 +08:00
老广
1cf57c822d Merge pull request #10417 from jumpserver/pr@v3.2@fix_categorytree1
fix: 修复资产类型树循环显示的问题
2023-05-10 11:07:18 +08:00
Bai
edfac357b9 fix: 修复资产类型树循环显示的问题 2023-05-10 02:53:20 +00:00
feng
2e5dfd8bab fix: /prometheus/metrics/ api 500 2023-05-08 14:51:18 +08:00
Bai
53da79ae46 perf: 优化迁移后的 Redis 数据库平台从 Redis6+ 修改为 Redis6 2023-05-04 17:31:57 +08:00
Bai
e258772242 fix: 修复迁移应用时(组织下只有根节点,同步后的应用资产没有设置节点的问题) 2023-05-04 16:36:52 +08:00
Bai
0a5bf9cf03 fix: 修复迁移redis资产账号丢失的问题(系统用户用户名为空字符串) 2023-05-04 15:44:08 +08:00
Eric
ac7c865b67 fix: 修复旧 ssh 私钥,解析失败的问题 2023-04-27 17:49:29 +08:00
feng
ff6a8fa4d7 perf: 开源 acl去除 review 2023-04-21 18:41:26 +08:00
ibuler
802d6136d6 perf: 账号模版 protocols 过滤 2023-04-21 17:11:46 +08:00
ibuler
7e8bb9752c perf: 优化自定义类型的冲突 2023-04-21 15:20:50 +08:00
feng
3fb0197e99 perf: 创建资产 nodes 可为空 默认 default 2023-04-21 14:57:50 +08:00
ibuler
7127b2da93 perf: 去掉 debug msg 2023-04-21 11:32:21 +08:00
ibuler
48b3699591 perf: 优化支持 自定义 applet
perf: 优化平台
2023-04-21 11:31:49 +08:00
22 changed files with 93 additions and 69 deletions

View File

@@ -1,13 +1,13 @@
from django_filters import rest_framework as drf_filters from django_filters import rest_framework as drf_filters
from assets.const import Protocol
from accounts import serializers from accounts import serializers
from accounts.models import AccountTemplate from accounts.models import AccountTemplate
from orgs.mixins.api import OrgBulkModelViewSet from assets.const import Protocol
from rbac.permissions import RBACPermission from common.drf.filters import BaseFilterSet
from common.permissions import UserConfirmation, ConfirmType from common.permissions import UserConfirmation, ConfirmType
from common.views.mixins import RecordViewLogMixin from common.views.mixins import RecordViewLogMixin
from common.drf.filters import BaseFilterSet from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
class AccountTemplateFilterSet(BaseFilterSet): class AccountTemplateFilterSet(BaseFilterSet):
@@ -27,6 +27,8 @@ class AccountTemplateFilterSet(BaseFilterSet):
continue continue
_st = protocol_secret_type_map[p].get('secret_types', []) _st = protocol_secret_type_map[p].get('secret_types', [])
secret_types.update(_st) secret_types.update(_st)
if not secret_types:
secret_types = ['password']
queryset = queryset.filter(secret_type__in=secret_types) queryset = queryset.filter(secret_type__in=secret_types)
return queryset return queryset

View File

@@ -28,7 +28,6 @@ class ChangeSecretMixin(models.Model):
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy') default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
) )
accounts: list[str] # account usernames
get_all_assets: callable # get all assets get_all_assets: callable # get all assets
class Meta: class Meta:

View File

@@ -3,6 +3,7 @@ from rest_framework import serializers
from acls.models.base import ActionChoices from acls.models.base import ActionChoices
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from jumpserver.utils import has_valid_xpack_license
from orgs.models import Organization from orgs.models import Organization
from users.models import User from users.models import User
@@ -51,7 +52,26 @@ class ACLAccountsSerializer(serializers.Serializer):
) )
class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): class ActionAclSerializer(serializers.Serializer):
action = LabeledChoiceField(
choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action")
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_action_choices()
def set_action_choices(self):
action = self.fields.get("action")
if not action:
return
choices = action.choices
if not has_valid_xpack_license():
choices.pop(ActionChoices.review, None)
action._choices = choices
class BaseUserAssetAccountACLSerializerMixin(ActionAclSerializer, serializers.Serializer):
users = ACLUsersSerializer(label=_('User')) users = ACLUsersSerializer(label=_('User'))
assets = ACLAssestsSerializer(label=_('Asset')) assets = ACLAssestsSerializer(label=_('Asset'))
accounts = ACLAccountsSerializer(label=_('Account')) accounts = ACLAccountsSerializer(label=_('Account'))
@@ -77,9 +97,6 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer):
reviewers_amount = serializers.IntegerField( reviewers_amount = serializers.IntegerField(
read_only=True, source="reviewers.count", label=_('Reviewers amount') read_only=True, source="reviewers.count", label=_('Reviewers amount')
) )
action = LabeledChoiceField(
choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action")
)
class Meta: class Meta:
fields_mini = ["id", "name"] fields_mini = ["id", "name"]

View File

@@ -2,12 +2,11 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from common.serializers import BulkModelSerializer, MethodSerializer from common.serializers import BulkModelSerializer, MethodSerializer
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.serializers.fields import ObjectRelatedField
from jumpserver.utils import has_valid_xpack_license
from users.models import User from users.models import User
from .base import ActionAclSerializer
from .rules import RuleSerializer from .rules import RuleSerializer
from ..models import LoginACL from ..models import LoginACL
from ..models.base import ActionChoices
__all__ = [ __all__ = [
"LoginACLSerializer", "LoginACLSerializer",
@@ -18,12 +17,11 @@ common_help_text = _(
) )
class LoginACLSerializer(BulkModelSerializer): class LoginACLSerializer(ActionAclSerializer, BulkModelSerializer):
user = ObjectRelatedField(queryset=User.objects, label=_("User")) user = ObjectRelatedField(queryset=User.objects, label=_("User"))
reviewers = ObjectRelatedField( reviewers = ObjectRelatedField(
queryset=User.objects, label=_("Reviewers"), many=True, required=False queryset=User.objects, label=_("Reviewers"), many=True, required=False
) )
action = LabeledChoiceField(choices=ActionChoices.choices, label=_('Action'))
reviewers_amount = serializers.IntegerField( reviewers_amount = serializers.IntegerField(
read_only=True, source="reviewers.count", label=_("Reviewers amount") read_only=True, source="reviewers.count", label=_("Reviewers amount")
) )
@@ -45,18 +43,5 @@ class LoginACLSerializer(BulkModelSerializer):
"is_active": {"default": True}, "is_active": {"default": True},
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_action_choices()
def set_action_choices(self):
action = self.fields.get("action")
if not action:
return
choices = action.choices
if not has_valid_xpack_license():
choices.pop(LoginACL.ActionChoices.review, None)
action._choices = choices
def get_rules_serializer(self): def get_rules_serializer(self):
return RuleSerializer() return RuleSerializer()

View File

@@ -3,6 +3,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from common.api import JMSGenericViewSet from common.api import JMSGenericViewSet
from common.permissions import IsValidUser
from assets.serializers import CategorySerializer, TypeSerializer from assets.serializers import CategorySerializer, TypeSerializer
from assets.const import AllTypes from assets.const import AllTypes
@@ -14,7 +15,7 @@ class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
'default': CategorySerializer, 'default': CategorySerializer,
'types': TypeSerializer 'types': TypeSerializer
} }
permission_classes = () permission_classes = (IsValidUser,)
def get_queryset(self): def get_queryset(self):
return AllTypes.categories() return AllTypes.categories()

View File

@@ -57,11 +57,23 @@ class SerializeToTreeNodeMixin:
] ]
return data return data
@lazyproperty
def support_types(self):
from assets.const import AllTypes
return AllTypes.get_types_values(exclude_custom=True)
def get_icon(self, asset):
if asset.type in self.support_types:
return asset.type
else:
return 'file'
@timeit @timeit
def serialize_assets(self, assets, node_key=None): def serialize_assets(self, assets, node_key=None):
sftp_enabled_platform = PlatformProtocol.objects \ sftp_enabled_platform = PlatformProtocol.objects \
.filter(name='ssh', setting__sftp_enabled=True) \ .filter(name='ssh', setting__sftp_enabled=True) \
.values_list('platform', flat=True).distinct() .values_list('platform', flat=True) \
.distinct()
if node_key is None: if node_key is None:
get_pid = lambda asset: getattr(asset, 'parent_key', '') get_pid = lambda asset: getattr(asset, 'parent_key', '')
else: else:
@@ -75,7 +87,7 @@ class SerializeToTreeNodeMixin:
'pId': get_pid(asset), 'pId': get_pid(asset),
'isParent': False, 'isParent': False,
'open': False, 'open': False,
'iconSkin': asset.type, 'iconSkin': self.get_icon(asset),
'chkDisabled': not asset.is_active, 'chkDisabled': not asset.is_active,
'meta': { 'meta': {
'type': 'asset', 'type': 'asset',

View File

@@ -163,8 +163,10 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
# 资源数量统计可选项 (asset, account) # 资源数量统计可选项 (asset, account)
count_resource = self.request.query_params.get('count_resource', 'asset') count_resource = self.request.query_params.get('count_resource', 'asset')
if include_asset and self.request.query_params.get('key'): if not self.request.query_params.get('key'):
nodes = AllTypes.to_tree_nodes(include_asset, count_resource=count_resource)
elif include_asset:
nodes = self.get_assets() nodes = self.get_assets()
else: else:
nodes = AllTypes.to_tree_nodes(include_asset, count_resource=count_resource) nodes = []
return Response(data=nodes) return Response(data=nodes)

View File

@@ -40,7 +40,6 @@ def get_platform_automation_methods(path):
continue continue
with open(path, 'r') as f: with open(path, 'r') as f:
print("path: ", path)
manifest = yaml_load_with_i18n(f) manifest = yaml_load_with_i18n(f)
check_platform_method(manifest, path) check_platform_method(manifest, path)
manifest['dir'] = os.path.dirname(path) manifest['dir'] = os.path.dirname(path)

View File

@@ -8,7 +8,7 @@ class CustomTypes(BaseType):
platforms = list(cls.get_custom_platforms()) platforms = list(cls.get_custom_platforms())
except Exception: except Exception:
return [] return []
types = [p.type for p in platforms] types = set([p.type for p in platforms])
return [(t, t) for t in types] return [(t, t) for t in types]
@classmethod @classmethod
@@ -48,11 +48,7 @@ class CustomTypes(BaseType):
@classmethod @classmethod
def internal_platforms(cls): def internal_platforms(cls):
return { return {}
# cls.PUBLIC: [],
# cls.PRIVATE: [{'name': 'Vmware-vSphere'}],
# cls.K8S: [{'name': 'Kubernetes'}],
}
@classmethod @classmethod
def get_custom_platforms(cls): def get_custom_platforms(cls):

View File

@@ -151,15 +151,18 @@ class AllTypes(ChoicesMixin):
) )
@classmethod @classmethod
def get_types(cls): def get_types(cls, exclude_custom=False):
choices = [] choices = []
for i in dict(cls.category_types()).values():
choices.extend(i.get_types()) for name, tp in dict(cls.category_types()).items():
if name == Category.CUSTOM and exclude_custom:
continue
choices.extend(tp.get_types())
return choices return choices
@classmethod @classmethod
def get_types_values(cls): def get_types_values(cls, exclude_custom=False):
choices = cls.get_types() choices = cls.get_types(exclude_custom=exclude_custom)
return [c.value for c in choices] return [c.value for c in choices]
@staticmethod @staticmethod

View File

@@ -20,12 +20,13 @@ def get_prop_name_id(apps, app, category):
def migrate_database_to_asset(apps, *args): def migrate_database_to_asset(apps, *args):
node_model = apps.get_model('assets', 'Node')
app_model = apps.get_model('applications', 'Application') app_model = apps.get_model('applications', 'Application')
db_model = apps.get_model('assets', 'Database') db_model = apps.get_model('assets', 'Database')
platform_model = apps.get_model('assets', 'Platform') platform_model = apps.get_model('assets', 'Platform')
applications = app_model.objects.filter(category='db') applications = app_model.objects.filter(category='db')
platforms = platform_model.objects.all().filter(internal=True) platforms = platform_model.objects.all().filter(internal=True).exclude(name='Redis6+')
platforms_map = {p.type: p for p in platforms} platforms_map = {p.type: p for p in platforms}
print() print()
@@ -84,11 +85,18 @@ def create_app_nodes(apps, org_id):
node_keys = node_model.objects.filter(org_id=org_id) \ node_keys = node_model.objects.filter(org_id=org_id) \
.filter(key__regex=child_pattern) \ .filter(key__regex=child_pattern) \
.values_list('key', flat=True) .values_list('key', flat=True)
if not node_keys: if node_keys:
return
node_key_split = [key.split(':') for key in node_keys] node_key_split = [key.split(':') for key in node_keys]
next_value = max([int(k[1]) for k in node_key_split]) + 1 next_value = max([int(k[1]) for k in node_key_split]) + 1
parent_key = node_key_split[0][0] parent_key = node_key_split[0][0]
else:
root_node = node_model.objects.filter(org_id=org_id)\
.filter(parent_key='', key__regex=r'^[0-9]+$').exclude(key__startswith='-').first()
if not root_node:
return
parent_key = root_node.key
next_value = 0
next_key = '{}:{}'.format(parent_key, next_value) next_key = '{}:{}'.format(parent_key, next_value)
name = 'Apps' name = 'Apps'
parent = node_model.objects.get(key=parent_key) parent = node_model.objects.get(key=parent_key)

View File

@@ -161,11 +161,12 @@ def migrate_db_accounts(apps, schema_editor):
name = f'{username}(token)' name = f'{username}(token)'
else: else:
secret_type = attr secret_type = attr
name = username name = username or f'{username}(password)'
auth_infos.append((name, secret_type, secret)) auth_infos.append((name, secret_type, secret))
if not auth_infos: if not auth_infos:
auth_infos.append((username, 'password', '')) name = username or f'{username}(password)'
auth_infos.append((name, 'password', ''))
for name, secret_type, secret in auth_infos: for name, secret_type, secret in auth_infos:
values['name'] = name values['name'] = name

View File

@@ -145,6 +145,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
'name': {'label': _("Name")}, 'name': {'label': _("Name")},
'address': {'label': _('Address')}, 'address': {'label': _('Address')},
'nodes_display': {'label': _('Node path')}, 'nodes_display': {'label': _('Node path')},
'nodes': {'allow_empty': True},
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from assets.const.web import FillType from assets.const.web import FillType
from common.serializers import WritableNestedModelSerializer from common.serializers import WritableNestedModelSerializer, type_field_map
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty from common.utils import lazyproperty
from ..const import Category, AllTypes from ..const import Category, AllTypes
@@ -88,14 +88,7 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
class PlatformCustomField(serializers.Serializer): class PlatformCustomField(serializers.Serializer):
TYPE_CHOICES = [ TYPE_CHOICES = [(t, t) for t, c in type_field_map.items()]
("str", "str"),
("text", "text"),
("int", "int"),
("bool", "bool"),
("choice", "choice"),
("list", "list"),
]
name = serializers.CharField(label=_("Name"), max_length=128) name = serializers.CharField(label=_("Name"), max_length=128)
label = serializers.CharField(label=_("Label"), max_length=128) label = serializers.CharField(label=_("Label"), max_length=128)
type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str') type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str')

View File

@@ -7,6 +7,7 @@ example_info = [
type_field_map = { type_field_map = {
"str": serializers.CharField, "str": serializers.CharField,
"password": serializers.CharField,
"int": serializers.IntegerField, "int": serializers.IntegerField,
"bool": serializers.BooleanField, "bool": serializers.BooleanField,
"text": serializers.CharField, "text": serializers.CharField,
@@ -27,6 +28,8 @@ def set_default_if_need(data, i):
def set_default_by_type(tp, data, field_info): def set_default_by_type(tp, data, field_info):
if tp == 'str': if tp == 'str':
data['max_length'] = 4096 data['max_length'] = 4096
elif tp == 'password':
data['write_only'] = True
elif tp == 'choice': elif tp == 'choice':
choices = field_info.pop('choices', []) choices = field_info.pop('choices', [])
if isinstance(choices, str): if isinstance(choices, str):

View File

@@ -175,6 +175,8 @@ def _parse_ssh_private_key(text, password=None):
dsa.DSAPrivateKey, dsa.DSAPrivateKey,
ed25519.Ed25519PrivateKey, ed25519.Ed25519PrivateKey,
""" """
if not bool(password):
password = None
if isinstance(text, str): if isinstance(text, str):
try: try:
text = text.encode("utf-8") text = text.encode("utf-8")

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2dd0610d610c2660f35d50dc2871ac08cc09080d2503e1080a57d97c47fea471 oid sha256:591b458d6f8ea8d125bd584ca57768cd5aa5a7103b42e345eaadac744a73d475
size 114418 size 114412

View File

@@ -1060,7 +1060,7 @@ msgstr "Web"
#: assets/const/category.py:15 #: assets/const/category.py:15
msgid "Custom type" msgid "Custom type"
msgstr "自定义类型" msgstr "自定义"
#: assets/const/cloud.py:7 #: assets/const/cloud.py:7
msgid "Public cloud" msgid "Public cloud"

View File

@@ -80,7 +80,7 @@ def on_org_create_or_update(sender, instance, **kwargs):
@receiver(pre_delete, sender=Organization) @receiver(pre_delete, sender=Organization)
def on_org_delete(sender, instance, **kwargs): def delete_org_root_node_on_org_delete(sender, instance, **kwargs):
expire_orgs_mapping_for_memory(instance.id) expire_orgs_mapping_for_memory(instance.id)
# 删除该组织下所有 节点 # 删除该组织下所有 节点
@@ -91,7 +91,7 @@ def on_org_delete(sender, instance, **kwargs):
@receiver(post_delete, sender=Organization) @receiver(post_delete, sender=Organization)
def on_org_delete(sender, instance, **kwargs): def expire_user_orgs_on_org_delete(sender, instance, **kwargs):
expire_user_orgs() expire_user_orgs()

View File

@@ -123,7 +123,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
for i in range(0, len(account_ids), slice_count): for i in range(0, len(account_ids), slice_count):
push_accounts_to_assets_task.delay(account_ids[i:i + slice_count]) push_accounts_to_assets_task.delay(account_ids[i:i + slice_count])
def validate_accounts(self, usernames: list[str]): def validate_accounts(self, usernames):
template_ids = [] template_ids = []
account_usernames = [] account_usernames = []
for username in usernames: for username in usernames:

View File

@@ -98,7 +98,7 @@ class Applet(JMSBaseModel):
return return
try: try:
with open(os.path.join(d, 'platform.yml')) as f: with open(os.path.join(d, 'platform.yml')) as f:
data = yaml.safe_load(f) data = yaml_load_with_i18n(f)
except Exception as e: except Exception as e:
raise ValidationError({'error': _('Load platform.yml failed: {}').format(e)}) raise ValidationError({'error': _('Load platform.yml failed: {}').format(e)})

View File

@@ -140,7 +140,7 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
for component in self.components: for component in self.components:
if not component.is_alive: if not component.is_alive:
continue continue
component_stat = component.latest_stat component_stat = component.last_stat
if not component_stat: if not component_stat:
continue continue
metric_text = state_metric_text % ( metric_text = state_metric_text % (