Merge branch 'v3' of github.com:jumpserver/jumpserver into v3

pull/8873/head
Jiangjie.Bai 2022-09-13 14:23:35 +08:00
commit 4a21f86cd1
49 changed files with 320 additions and 454 deletions

View File

@ -1,309 +0,0 @@
# -*- coding: utf-8 -*-
#
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView, ListAPIView
from django.shortcuts import get_object_or_404
from django.db.models import Q
from common.utils import get_logger, get_object_or_none
from common.mixins.api import SuggestionMixin, RenderToJsonMixin
from users.models import User, UserGroup
from users.serializers import UserSerializer, UserGroupSerializer
from users.filters import UserFilter
from perms.models import AssetPermission
from perms.serializers import AssetPermissionSerializer
from perms.filters import AssetPermissionFilter
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from assets.api import FilterAssetByNodeMixin
from ..models import Asset, Node, Platform, Gateway
from .. import serializers
from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual,
test_system_users_connectivity_a_asset, push_system_users_a_asset
)
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetGatewayListApi', 'AssetPlatformViewSet',
'AssetTaskCreateApi', 'AssetsTaskCreateApi',
'AssetPermUserListApi', 'AssetPermUserPermissionsListApi',
'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi',
]
class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet):
"""
API endpoint that allows Asset to be viewed or edited.
"""
model = Asset
filterset_fields = {
'hostname': ['exact'],
'ip': ['exact'],
'system_users__id': ['exact'],
'platform__base': ['exact'],
'is_active': ['exact'],
'protocols': ['exact', 'icontains']
}
search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
ordering = ('hostname', )
serializer_classes = {
'default': serializers.AssetSerializer,
'suggestion': serializers.MiniAssetSerializer
}
rbac_perms = {
'match': 'assets.match_asset'
}
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
def set_assets_node(self, assets):
if not isinstance(assets, list):
assets = [assets]
node_id = self.request.query_params.get('node_id')
if not node_id:
return
node = get_object_or_none(Node, pk=node_id)
if not node:
return
node.assets.add(*assets)
def perform_create(self, serializer):
assets = serializer.save()
self.set_assets_node(assets)
class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
rbac_perms = {
'retrieve': 'assets.view_gateway'
}
def get_object(self):
asset_pk = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_pk)
return asset.platform
class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']
search_fields = ['name']
def check_object_permissions(self, request, obj):
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
self.permission_denied(
request, message={"detail": "Internal platform"}
)
return super().check_object_permissions(request, obj)
class AssetsTaskMixin:
def perform_assets_task(self, serializer):
data = serializer.validated_data
action = data['action']
assets = data.get('assets', [])
if action == "refresh":
task = update_assets_hardware_info_manual.delay(assets)
else:
# action == 'test':
task = test_assets_connectivity_manual.delay(assets)
return task
def perform_create(self, serializer):
task = self.perform_assets_task(serializer)
self.set_task_to_serializer_data(serializer, task)
def set_task_to_serializer_data(self, serializer, task):
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetTaskSerializer
def create(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
request.data['asset'] = pk
request.data['assets'] = [pk]
return super().create(request, *args, **kwargs)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo',
'push_system_user': 'assets.push_assetsystemuser',
'test': 'assets.test_assetconnectivity',
'test_system_user': 'assets.test_assetconnectivity'
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
def perform_asset_task(self, serializer):
data = serializer.validated_data
action = data['action']
if action not in ['push_system_user', 'test_system_user']:
return
asset = data['asset']
system_users = data.get('system_users')
if not system_users:
system_users = asset.get_all_system_users()
if action == 'push_system_user':
task = push_system_users_a_asset.delay(system_users, asset=asset)
elif action == 'test_system_user':
task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset)
else:
task = None
return task
def perform_create(self, serializer):
task = self.perform_asset_task(serializer)
if not task:
task = self.perform_assets_task(serializer)
self.set_task_to_serializer_data(serializer, task)
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetsTaskSerializer
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo',
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
class AssetGatewayListApi(generics.ListAPIView):
serializer_class = serializers.GatewayWithAuthSerializer
rbac_perms = {
'list': 'assets.view_gateway'
}
def get_queryset(self):
asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
if not asset.domain:
return Gateway.objects.none()
queryset = asset.domain.gateways.filter(protocol='ssh')
return queryset
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
rbac_perms = {
'GET': 'perms.view_assetpermission'
}
def get_object(self):
asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
return asset
def get_asset_related_perms(self):
asset = self.get_object()
nodes = asset.get_all_nodes(flat=True)
perms = AssetPermission.objects.filter(Q(assets=asset) | Q(nodes__in=nodes))
return perms
class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
filterset_class = UserFilter
search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
serializer_class = UserSerializer
rbac_perms = {
'GET': 'perms.view_assetpermission'
}
def get_queryset(self):
perms = self.get_asset_related_perms()
users = User.objects.filter(
Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms)
).distinct()
return users
class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
serializer_class = UserGroupSerializer
def get_queryset(self):
perms = self.get_asset_related_perms()
user_groups = UserGroup.objects.filter(assetpermissions__in=perms).distinct()
return user_groups
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
model = AssetPermission
serializer_class = AssetPermissionSerializer
filterset_class = AssetPermissionFilter
search_fields = ('name',)
rbac_perms = {
'list': 'perms.view_assetpermission'
}
def get_object(self):
asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
return asset
def filter_asset_related(self, queryset):
asset = self.get_object()
nodes = asset.get_all_nodes(flat=True)
perms = queryset.filter(Q(assets=asset) | Q(nodes__in=nodes))
return perms
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_asset_related(queryset)
return queryset
class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_user_related(queryset)
queryset = queryset.distinct()
return queryset
def filter_user_related(self, queryset):
user = self.get_perm_user()
user_groups = user.groups.all()
perms = queryset.filter(Q(users=user) | Q(user_groups__in=user_groups))
return perms
def get_perm_user(self):
user_id = self.kwargs.get('perm_user_id')
user = get_object_or_404(User, pk=user_id)
return user
class AssetPermUserGroupPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_user_group_related(queryset)
queryset = queryset.distinct()
return queryset
def filter_user_group_related(self, queryset):
user_group = self.get_perm_user_group()
perms = queryset.filter(user_groups=user_group)
return perms
def get_perm_user_group(self):
user_group_id = self.kwargs.get('perm_user_group_id')
user_group = get_object_or_404(UserGroup, pk=user_group_id)
return user_group

View File

@ -26,10 +26,11 @@ __all__ = [
class AssetFilterSet(BaseFilterSet):
type = django_filters.CharFilter(field_name='platform__type', lookup_expr='exact')
category = django_filters.CharFilter(field_name='platform__category', lookup_expr='exact')
hostname = django_filters.CharFilter(field_name='name', lookup_expr='exact')
class Meta:
model = Asset
fields = ['name', 'ip', 'is_active', 'type', 'category']
fields = ['name', 'ip', 'is_active', 'type', 'category', 'hostname']
class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
@ -39,7 +40,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
model = Asset
filterset_class = AssetFilterSet
search_fields = ("name", "ip")
ordering_fields = ("name", "ip", "port")
ordering_fields = ("name", "ip")
ordering = ('name',)
serializer_classes = (
('default', serializers.AssetSerializer),
@ -58,21 +59,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
NodeFilterBackend
]
def set_assets_node(self, assets):
if not isinstance(assets, list):
assets = [assets]
node_id = self.request.query_params.get('node_id')
if not node_id:
return
node = get_object_or_none(Node, pk=node_id)
if not node:
return
node.assets.add(*assets)
def perform_create(self, serializer):
assets = serializer.save()
self.set_assets_node(assets)
@action(methods=['GET'], detail=True, url_path='platform')
def platform(self, *args, **kwargs):
asset = self.get_object()

View File

View File

@ -2,11 +2,11 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from common.drf.api import JMSModelViewSet
from common.drf.serializers import GroupedChoiceSerailizer
from common.drf.serializers import GroupedChoiceSerializer
from assets.models import Platform
from assets.serializers import PlatformSerializer, PlatformOpsMethodSerializer
from assets.const import AllTypes, Category
from assets.playbooks.platform import filter_platform_methods
from assets.const import AllTypes
from assets.playbooks import filter_platform_methods
__all__ = ['AssetPlatformViewSet']
@ -16,7 +16,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
queryset = Platform.objects.all()
serializer_classes = {
'default': PlatformSerializer,
'categories': GroupedChoiceSerailizer
'categories': GroupedChoiceSerializer
}
filterset_fields = ['name', 'category', 'type']
search_fields = ['name']

View File

@ -13,4 +13,4 @@ class AssetsConfig(AppConfig):
def ready(self):
super().ready()
# from . import signal_handlers
from . import signal_handlers

View File

@ -92,7 +92,7 @@ class HostTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
def platform_constraints(cls):
return {
cls.LINUX: {
'_protocols': ['ssh', 'sftp', 'rdp', 'vnc', 'telnet']
'_protocols': ['ssh', 'rdp', 'vnc', 'telnet']
},
cls.WINDOWS: {
'_protocols': ['ssh', 'rdp', 'vnc'],
@ -131,7 +131,7 @@ class DatabaseTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
class WebTypes(PlatformMixin, ChoicesMixin, models.TextChoices):
General = 'general', 'General'
WEBSITE = 'website', _('General Website')
class CloudTypes(PlatformMixin, ChoicesMixin, models.TextChoices):

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.14 on 2022-09-08 11:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20220901_1542'),
]
operations = [
migrations.AddField(
model_name='platform',
name='domain_enabled',
field=models.BooleanField(default=True, verbose_name='Domain enalbed'),
),
migrations.AlterField(
model_name='account',
name='asset',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='assets.asset', verbose_name='Asset'),
),
migrations.AlterField(
model_name='asset',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 3.2.14 on 2022-09-09 11:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0111_auto_20220908_1958'),
]
operations = [
migrations.AddField(
model_name='web',
name='autofill',
field=models.CharField(default='basic', max_length=16),
),
migrations.AddField(
model_name='web',
name='password_selector',
field=models.CharField(blank=True, default='', max_length=128),
),
migrations.AddField(
model_name='web',
name='submit_selector',
field=models.CharField(blank=True, default='', max_length=128),
),
migrations.AddField(
model_name='web',
name='username_selector',
field=models.CharField(blank=True, default='', max_length=128),
),
migrations.AlterField(
model_name='platform',
name='domain_enabled',
field=models.BooleanField(default=True, verbose_name='Domain enabled'),
),
]

View File

@ -10,7 +10,7 @@ __all__ = ['Account', 'AccountTemplate']
class Account(BaseAccount):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
asset = models.ForeignKey('assets.Asset', related_name='accounts', on_delete=models.CASCADE, verbose_name=_('Asset'))
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = HistoricalRecords()

View File

@ -6,3 +6,7 @@ from .common import Asset
class Web(Asset):
url = models.CharField(max_length=1024, verbose_name=_("url"))
autofill = models.CharField(max_length=16, default='basic')
username_selector = models.CharField(max_length=128, blank=True, default='')
password_selector = models.CharField(max_length=128, blank=True, default='')
submit_selector = models.CharField(max_length=128, blank=True, default='')

View File

@ -31,6 +31,7 @@ class Platform(models.Model):
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
# 资产有关的
charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
protocols = models.ManyToManyField(PlatformProtocol, blank=True, verbose_name=_("Protocols"))
gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))

View File

@ -0,0 +1,65 @@
import os
import yaml
from functools import partial
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def check_platform_method(manifest, manifest_path):
required_keys = ['category', 'method', 'name', 'id', 'type']
less_key = set(required_keys) - set(manifest.keys())
if less_key:
raise ValueError("Manifest missing keys: {}, {}".format(less_key, manifest_path))
if not isinstance(manifest['type'], list):
raise ValueError("Manifest type must be a list: {}".format(manifest_path))
return True
def check_platform_methods(methods):
ids = [m['id'] for m in methods]
for i, _id in enumerate(ids):
if _id in ids[i+1:]:
raise ValueError("Duplicate id: {}".format(_id))
def get_platform_methods():
methods = []
for root, dirs, files in os.walk(BASE_DIR, topdown=False):
for name in files:
path = os.path.join(root, name)
if not path.endswith('manifest.yml'):
continue
with open(path, 'r') as f:
manifest = yaml.safe_load(f)
check_platform_method(manifest, path)
manifest['dir'] = os.path.dirname(path)
methods.append(manifest)
check_platform_methods(methods)
return methods
def filter_key(manifest, attr, value):
manifest_value = manifest.get(attr, '')
if isinstance(manifest_value, str):
manifest_value = [manifest_value]
return value in manifest_value or 'all' in manifest_value
def filter_platform_methods(category, tp, method):
methods = platform_ops_methods
if category:
methods = filter(partial(filter_key, attr='category', value=category), methods)
if tp:
methods = filter(partial(filter_key, attr='type', value=tp), methods)
if method:
methods = filter(lambda x: x['method'] == method, methods)
return methods
platform_ops_methods = get_platform_methods()
if __name__ == '__main__':
print(get_platform_methods())

View File

@ -1,4 +1,3 @@
{% for account in accounts %}
- hosts: {{ account.asset.name }}
vars:
account:
@ -7,4 +6,3 @@
public_key: {{ account.public_key }}
roles:
- change_password
{% endfor %}

View File

@ -1,69 +0,0 @@
import os
import yaml
from functools import partial
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def check_platform_method(manifest, manifest_path):
required_keys = ['category', 'method', 'name', 'id', 'type']
less_key = set(required_keys) - set(manifest.keys())
if less_key:
raise ValueError("Manifest missing keys: {}, {}".format(less_key, manifest_path))
if not isinstance(manifest['type'], list):
raise ValueError("Manifest type must be a list: {}".format(manifest_path))
return True
def check_platform_methods(methods):
ids = [m['id'] for m in methods]
for i, _id in enumerate(ids):
if _id in ids[i+1:]:
raise ValueError("Duplicate id: {}".format(_id))
def get_platform_methods():
methods = []
for root, dirs, files in os.walk(BASE_DIR, topdown=False):
for name in dirs:
path = os.path.join(root, name)
rel_path = path.replace(BASE_DIR, '.')
if len(rel_path.split('/')) != 3:
continue
manifest_path = os.path.join(path, 'manifest.yml')
if not os.path.exists(manifest_path):
continue
with open(manifest_path, 'r') as f:
manifest = yaml.safe_load(f)
check_platform_method(manifest, manifest_path)
methods.append(manifest)
check_platform_methods(methods)
return methods
def filter_key(manifest, attr, value):
manifest_value = manifest.get(attr, '')
if isinstance(manifest_value, str):
manifest_value = [manifest_value]
return value in manifest_value or 'all' in manifest_value
def filter_platform_methods(category, tp, method):
methods = platform_ops_methods
if category:
methods = filter(partial(filter_key, attr='category', value=category), methods)
if tp:
methods = filter(partial(filter_key, attr='type', value=tp), methods)
if method:
methods = filter(lambda x: x['method'] == method, methods)
return methods
platform_ops_methods = get_platform_methods()
if __name__ == '__main__':
print(get_platform_methods())

View File

@ -1,9 +0,0 @@
id: change_password_example
name: Change password example
category: host
method: change_password
vars:
account:
username: test
password: teset123
public_key: test

View File

@ -1,6 +0,0 @@
#!/usr/bin/env python
#
"""
Will run with the args:
$0 $asset_json $account_json
"""

View File

@ -19,6 +19,8 @@ class AccountFieldsSerializerMixin(serializers.ModelSerializer):
extra_kwargs = {
'private_key': {'write_only': True},
'public_key': {'write_only': True},
'token': {'write_only': True},
'password': {'write_only': True},
}
def validate_name(self, value):

View File

@ -8,7 +8,7 @@ from django.db.models import F
from common.drf.serializers import JMSWritableNestedModelSerializer
from common.drf.fields import LabeledChoiceField, ObjectRelatedField
from ..account import AccountSerializer
from ...models import Asset, Node, Platform, Protocol, Label, Domain
from ...models import Asset, Node, Platform, Protocol, Label, Domain, Account
from ...const import Category, AllTypes
__all__ = [
@ -49,6 +49,17 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
}
class AssetAccountSerializer(AccountSerializer):
add_org_fields = False
class Meta(AccountSerializer.Meta):
fields_mini = [
'id', 'name', 'username', 'privileged', 'version'
]
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase', 'token']
fields = fields_mini + fields_write_only
class AssetSerializer(JMSWritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices, read_only=True, label=_('Type'))
@ -56,22 +67,14 @@ class AssetSerializer(JMSWritableNestedModelSerializer):
platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform'))
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
labels = AssetLabelSerializer(many=True, required=False, label=_('Labels'))
accounts = AccountSerializer(many=True, required=False, label=_('Accounts'))
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
"""
资产的数据结构
"""
class Meta:
model = Asset
fields_mini = [
'id', 'name', 'ip',
]
fields_small = fields_mini + [
'is_active', 'comment',
]
fields_fk = [
'domain', 'platform', 'platform',
]
fields_mini = ['id', 'name', 'ip']
fields_small = fields_mini + ['is_active', 'comment']
fields_fk = ['domain', 'platform', 'platform']
fields_m2m = [
'nodes', 'labels', 'accounts', 'protocols', 'nodes_display',
]
@ -83,16 +86,8 @@ class AssetSerializer(JMSWritableNestedModelSerializer):
extra_kwargs = {
'name': {'label': _("Name")},
'ip': {'label': _('IP/Host')},
'protocol': {'write_only': True},
'port': {'write_only': True},
'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
}
def __init__(self, *args, **kwargs):
data = kwargs.get('data', {})
self.accounts_data = data.pop('accounts', [])
super().__init__(*args, **kwargs)
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
@ -115,6 +110,17 @@ class AssetSerializer(JMSWritableNestedModelSerializer):
nodes_to_set.append(node)
instance.nodes.set(nodes_to_set)
def validate_nodes(self, nodes):
print("Nodes: ", nodes)
if nodes:
return nodes
request = self.context.get('request')
if not request:
return []
node_id = request.query_params.get('node_id')
if not node_id:
return []
@atomic
def create(self, validated_data):
nodes_display = validated_data.pop('nodes_display', '')

View File

@ -8,4 +8,7 @@ __all__ = ['WebSerializer']
class WebSerializer(AssetSerializer):
class Meta(AssetSerializer.Meta):
model = Web
fields = AssetSerializer.Meta.fields + ['url']
fields = AssetSerializer.Meta.fields + [
'url', 'autofill', 'username_selector',
'password_selector', 'submit_selector'
]

View File

@ -17,13 +17,13 @@ class ProtocolSettingSerializer(serializers.Serializer):
('tls', 'TLS'),
('nla', 'NLA'),
]
# Common
required = serializers.BooleanField(required=True, initial=False, label=_("Required"))
# RDP
console = serializers.BooleanField(required=False)
security = serializers.ChoiceField(choices=SECURITY_CHOICES, default='any', required=False)
security = serializers.ChoiceField(choices=SECURITY_CHOICES, default='any')
# SFTP
sftp_home = serializers.CharField(default='/tmp', required=False)
sftp_enabled = serializers.BooleanField(default=True, label=_("SFTP enabled"))
sftp_home = serializers.CharField(default='/tmp', label=_("SFTP home"))
class PlatformProtocolsSerializer(serializers.ModelSerializer):
@ -38,7 +38,6 @@ class PlatformSerializer(JMSWritableNestedModelSerializer):
type = LabeledChoiceField(choices=AllTypes.choices, label=_("Type"))
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
protocols = PlatformProtocolsSerializer(label=_('Protocols'), many=True, required=False)
type_constraints = serializers.ReadOnlyField(required=False, read_only=True)
su_method = LabeledChoiceField(
choices=[('sudo', 'sudo su -'), ('su', 'su - ')],
label='切换方式', required=False, default='sudo'
@ -48,17 +47,17 @@ class PlatformSerializer(JMSWritableNestedModelSerializer):
model = Platform
fields_mini = ['id', 'name', 'internal']
fields_small = fields_mini + [
'category', 'type',
'category', 'type', 'charset',
]
fields = fields_small + [
'protocols_enabled', 'protocols',
'protocols_enabled', 'protocols', 'domain_enabled',
'gather_facts_enabled', 'gather_facts_method',
'su_enabled', 'su_method',
'gather_accounts_enabled', 'gather_accounts_method',
'create_account_enabled', 'create_account_method',
'verify_account_enabled', 'verify_account_method',
'change_password_enabled', 'change_password_method',
'type_constraints', 'comment', 'charset',
'comment',
]
extra_kwargs = {
'su_enabled': {'label': '启用切换账号'},

View File

@ -9,6 +9,8 @@ logger = get_logger(__name__)
@receiver(pre_save, sender=Account)
def on_account_pre_create(sender, instance, **kwargs):
# Todo: 是否只有更改密码的时候才有版本增加, bitwarden 只有再改密码的时候,
# 才会有版本,代表的是 password_version
# 升级版本号
instance.version += 1
# 即使在 root 组织也不怕

View File

@ -10,7 +10,7 @@ router = BulkRouter()
router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'hosts', api.HostViewSet, 'host')
router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'webs', api.WebViewSet, 'web')
router.register(r'web', api.WebViewSet, 'web')
router.register(r'clouds', api.CloudViewSet, 'cloud')
router.register(r'networks', api.NetworkViewSet, 'network')
router.register(r'accounts', api.AccountViewSet, 'account')

View File

@ -14,6 +14,7 @@ __all__ = [
'MethodSerializer', 'EmptySerializer', 'BulkModelSerializer',
'AdaptedBulkListSerializer', 'CeleryTaskSerializer',
'SecretReadableMixin', 'JMSWritableNestedModelSerializer',
'GroupedChoiceSerializer',
]
@ -90,7 +91,7 @@ class ChoiceSerializer(serializers.Serializer):
value = serializers.CharField(label=_("Value"))
class GroupedChoiceSerailizer(ChoiceSerializer):
class GroupedChoiceSerializer(ChoiceSerializer):
children = ChoiceSerializer(many=True, label=_("Children"))

View File

@ -0,0 +1,123 @@
# Generated by Django 3.2.14 on 2022-09-08 11:58
import common.db.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0111_auto_20220908_1958'),
('ops', '0022_auto_20220817_1346'),
]
operations = [
migrations.CreateModel(
name='AutomationStrategy',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('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')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('accounts', models.JSONField(default=list, verbose_name='Accounts')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('assets', models.ManyToManyField(blank=True, related_name='automation_strategy', to='assets.Asset', verbose_name='Assets')),
('nodes', models.ManyToManyField(blank=True, related_name='automation_strategy', to='assets.Node', verbose_name='Nodes')),
],
options={
'verbose_name': 'Automation plan',
'unique_together': {('org_id', 'name')},
},
),
migrations.CreateModel(
name='AutomationStrategyExecution',
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_created', models.DateTimeField(auto_now_add=True)),
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, verbose_name='Automation snapshot')),
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')),
('strategy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', to='ops.automationstrategy', verbose_name='Automation strategy')),
],
options={
'verbose_name': 'Automation strategy execution',
},
),
migrations.CreateModel(
name='CollectStrategy',
fields=[
('automationstrategy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='ops.automationstrategy')),
],
options={
'verbose_name': 'Collect strategy',
},
bases=('ops.automationstrategy',),
),
migrations.CreateModel(
name='PushStrategy',
fields=[
('automationstrategy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='ops.automationstrategy')),
],
options={
'verbose_name': 'Push strategy',
},
bases=('ops.automationstrategy',),
),
migrations.CreateModel(
name='VerifyStrategy',
fields=[
('automationstrategy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='ops.automationstrategy')),
],
options={
'verbose_name': 'Verify strategy',
},
bases=('ops.automationstrategy',),
),
migrations.CreateModel(
name='AutomationStrategyTask',
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)),
('is_success', models.BooleanField(default=False, verbose_name='Is success')),
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.account', verbose_name='Account')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.asset', verbose_name='Asset')),
('execution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='task', to='ops.automationstrategyexecution', verbose_name='Automation strategy execution')),
],
options={
'verbose_name': 'Automation strategy task',
},
),
migrations.CreateModel(
name='ChangeAuthStrategy',
fields=[
('automationstrategy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='ops.automationstrategy')),
('is_password', models.BooleanField(default=True)),
('password_strategy', models.CharField(blank=True, choices=[('custom', 'Custom password'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], max_length=128, null=True, verbose_name='Password strategy')),
('password_rules', common.db.fields.JsonDictCharField(blank=True, max_length=2048, null=True, verbose_name='Password rules')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('is_ssh_key', models.BooleanField(default=False)),
('ssh_key_strategy', models.CharField(blank=True, choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], max_length=128, null=True, verbose_name='SSH Key strategy')),
('private_key', common.db.fields.EncryptTextField(blank=True, max_length=4096, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, max_length=4096, null=True, verbose_name='SSH public key')),
('recipients', models.ManyToManyField(blank=True, related_name='recipients_change_auth_strategy', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
],
options={
'verbose_name': 'Change auth strategy',
},
bases=('ops.automationstrategy',),
),
]

View File

@ -24,6 +24,7 @@ class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer):
"""
org_id = serializers.ReadOnlyField(default=get_current_org_id_for_serializer, label=_("Organization"))
org_name = serializers.ReadOnlyField(label=_("Org name"))
add_org_fields = True
def get_validators(self):
_validators = super().get_validators()
@ -38,7 +39,8 @@ class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer):
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(["org_id", "org_name"])
if self.add_org_fields:
fields.extend(["org_id", "org_name"])
return fields