mirror of https://github.com/jumpserver/jumpserver
				
				
				
			perf: ad as asset
							parent
							
								
									5e25361ee8
								
							
						
					
					
						commit
						3f452daee8
					
				| 
						 | 
				
			
			@ -131,9 +131,26 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
 | 
			
		|||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def alias(self):
 | 
			
		||||
        """
 | 
			
		||||
        别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
 | 
			
		||||
        """
 | 
			
		||||
        if self.username.startswith('@'):
 | 
			
		||||
            return self.username
 | 
			
		||||
        return self.name
 | 
			
		||||
        return self.id
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def ad_domain(self):
 | 
			
		||||
        if self.username.startswith('@'):
 | 
			
		||||
            return None
 | 
			
		||||
        if self.platform.category == 'ad':
 | 
			
		||||
            return self.asset.ad.domain_name
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def full_username(self):
 | 
			
		||||
        if self.ad_domain:
 | 
			
		||||
            return '{}@{}'.format(self.username, self.ad_domain)
 | 
			
		||||
        return self.username
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def has_secret(self):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -241,7 +241,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
 | 
			
		|||
            'date_change_secret', 'change_secret_status'
 | 
			
		||||
        ]
 | 
			
		||||
        fields = BaseAccountSerializer.Meta.fields + [
 | 
			
		||||
            'su_from', 'asset', 'version',
 | 
			
		||||
            'su_from', 'asset', 'version', "ad_domain",
 | 
			
		||||
            'source', 'source_id', 'secret_reset',
 | 
			
		||||
        ] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields
 | 
			
		||||
        read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,3 +7,4 @@ from .gpt import *
 | 
			
		|||
from .host import *
 | 
			
		||||
from .permission import *
 | 
			
		||||
from .web import *
 | 
			
		||||
from .ad import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
from assets.models import AD, Asset
 | 
			
		||||
from assets.serializers import ADSerializer
 | 
			
		||||
 | 
			
		||||
from .asset import AssetViewSet
 | 
			
		||||
 | 
			
		||||
__all__ = ['ADViewSet']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ADViewSet(AssetViewSet):
 | 
			
		||||
    model = AD
 | 
			
		||||
    perm_model = Asset
 | 
			
		||||
 | 
			
		||||
    def get_serializer_classes(self):
 | 
			
		||||
        serializer_classes = super().get_serializer_classes()
 | 
			
		||||
        serializer_classes['default'] = ADSerializer
 | 
			
		||||
        return serializer_classes
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ from rest_framework.decorators import action
 | 
			
		|||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.status import HTTP_200_OK
 | 
			
		||||
 | 
			
		||||
from accounts.serializers import AccountSerializer
 | 
			
		||||
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
 | 
			
		||||
from assets import serializers
 | 
			
		||||
from assets.exceptions import NotSupportedTemporarilyError
 | 
			
		||||
| 
						 | 
				
			
			@ -109,18 +110,19 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
 | 
			
		|||
        ("platform", serializers.PlatformSerializer),
 | 
			
		||||
        ("suggestion", serializers.MiniAssetSerializer),
 | 
			
		||||
        ("gateways", serializers.GatewaySerializer),
 | 
			
		||||
        ("accounts", AccountSerializer),
 | 
			
		||||
    )
 | 
			
		||||
    rbac_perms = (
 | 
			
		||||
        ("match", "assets.match_asset"),
 | 
			
		||||
        ("platform", "assets.view_platform"),
 | 
			
		||||
        ("gateways", "assets.view_gateway"),
 | 
			
		||||
        ("accounts", "assets.view_account"),
 | 
			
		||||
        ("spec_info", "assets.view_asset"),
 | 
			
		||||
        ("gathered_info", "assets.view_asset"),
 | 
			
		||||
        ("sync_platform_protocols", "assets.change_asset"),
 | 
			
		||||
    )
 | 
			
		||||
    extra_filter_backends = [
 | 
			
		||||
        IpInFilterBackend,
 | 
			
		||||
        NodeFilterBackend, AttrRulesFilterBackend
 | 
			
		||||
        IpInFilterBackend, NodeFilterBackend, AttrRulesFilterBackend
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def perform_destroy(self, instance):
 | 
			
		||||
| 
						 | 
				
			
			@ -156,6 +158,12 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
 | 
			
		|||
            gateways = asset.domain.gateways
 | 
			
		||||
        return self.get_paginated_response_from_queryset(gateways)
 | 
			
		||||
 | 
			
		||||
    @action(methods=["GET"], detail=True, url_path="accounts")
 | 
			
		||||
    def accounts(self, *args, **kwargs):
 | 
			
		||||
        asset = super().get_object()
 | 
			
		||||
        queryset = asset.all_accounts.all()
 | 
			
		||||
        return self.get_paginated_response_from_queryset(queryset)
 | 
			
		||||
 | 
			
		||||
    @action(methods=['post'], detail=False, url_path='sync-platform-protocols')
 | 
			
		||||
    def sync_platform_protocols(self, request, *args, **kwargs):
 | 
			
		||||
        platform_id = request.data.get('platform_id')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .base import BaseType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ADTypes(BaseType):
 | 
			
		||||
    AD = 'ad', _('Active Directory')
 | 
			
		||||
    WINDOWS_AD = 'windows_ad', _('Windows Active Directory')
 | 
			
		||||
    LDAP = 'ldap', _('LDAP')
 | 
			
		||||
    AZURE_AD = 'azure_ad', _('Azure Active Directory')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _get_base_constrains(cls) -> dict:
 | 
			
		||||
        return {
 | 
			
		||||
            '*': {
 | 
			
		||||
                'charset_enabled': False,
 | 
			
		||||
                'domain_enabled': True,
 | 
			
		||||
                'ad_enabled': False,
 | 
			
		||||
                'su_enabled': True,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _get_automation_constrains(cls) -> dict:
 | 
			
		||||
        constrains = {
 | 
			
		||||
            '*': {
 | 
			
		||||
                'ansible_enabled': True,
 | 
			
		||||
                'ping_enabled': True,
 | 
			
		||||
                'gather_facts_enabled': False,
 | 
			
		||||
                'verify_account_enabled': True,
 | 
			
		||||
                'change_secret_enabled': True,
 | 
			
		||||
                'push_account_enabled': True,
 | 
			
		||||
                'gather_accounts_enabled': True,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return constrains
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _get_protocol_constrains(cls) -> dict:
 | 
			
		||||
        return {
 | 
			
		||||
            cls.WINDOWS_AD: {
 | 
			
		||||
                'choices': ['rdp', 'ssh', 'vnc', 'winrm']
 | 
			
		||||
            },
 | 
			
		||||
            cls.LDAP: {
 | 
			
		||||
                'choices': ['ssh', 'ldap']
 | 
			
		||||
            },
 | 
			
		||||
            cls.AZURE_AD: {
 | 
			
		||||
                'choices': ['ldap']
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def internal_platforms(cls):
 | 
			
		||||
        return {
 | 
			
		||||
            cls.AD: [
 | 
			
		||||
                {'name': 'Active Directory'}
 | 
			
		||||
            ],
 | 
			
		||||
            cls.WINDOWS_AD: [
 | 
			
		||||
                {'name': 'Windows Active Directory'}
 | 
			
		||||
            ],
 | 
			
		||||
            cls.LDAP: [
 | 
			
		||||
                {'name': 'LDAP'}
 | 
			
		||||
            ],
 | 
			
		||||
            cls.AZURE_AD: [
 | 
			
		||||
                {'name': 'Azure Active Directory'}
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_community_types(cls):
 | 
			
		||||
        return [
 | 
			
		||||
            cls.LDAP,
 | 
			
		||||
        ]
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ class Category(ChoicesMixin, models.TextChoices):
 | 
			
		|||
    DATABASE = 'database', _("Database")
 | 
			
		||||
    CLOUD = 'cloud', _("Cloud service")
 | 
			
		||||
    WEB = 'web', _("Web")
 | 
			
		||||
    AD = 'ad', _("Active Directory")
 | 
			
		||||
    CUSTOM = 'custom', _("Custom type")
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ class DeviceTypes(BaseType):
 | 
			
		|||
            '*': {
 | 
			
		||||
                'charset_enabled': False,
 | 
			
		||||
                'domain_enabled': True,
 | 
			
		||||
                'ad_enabled': False,
 | 
			
		||||
                'su_enabled': True,
 | 
			
		||||
                'su_methods': ['enable', 'super', 'super_level']
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ class HostTypes(BaseType):
 | 
			
		|||
                'charset': 'utf-8',  # default
 | 
			
		||||
                'domain_enabled': True,
 | 
			
		||||
                'su_enabled': True,
 | 
			
		||||
                'ad_enabled': True,
 | 
			
		||||
                'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
 | 
			
		||||
            },
 | 
			
		||||
            cls.WINDOWS: {
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +57,6 @@ class HostTypes(BaseType):
 | 
			
		|||
                'change_secret_enabled': True,
 | 
			
		||||
                'push_account_enabled': True,
 | 
			
		||||
                'remove_account_enabled': True,
 | 
			
		||||
 | 
			
		||||
            },
 | 
			
		||||
            cls.WINDOWS: {
 | 
			
		||||
                'ansible_config': {
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +69,6 @@ class HostTypes(BaseType):
 | 
			
		|||
                'ping_enabled': False,
 | 
			
		||||
                'gather_facts_enabled': False,
 | 
			
		||||
                'gather_accounts_enabled': False,
 | 
			
		||||
                'verify_account_enabled': False,
 | 
			
		||||
                'change_secret_enabled': False,
 | 
			
		||||
                'push_account_enabled': False
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -126,5 +125,5 @@ class HostTypes(BaseType):
 | 
			
		|||
    @classmethod
 | 
			
		||||
    def get_community_types(cls) -> list:
 | 
			
		||||
        return [
 | 
			
		||||
            cls.LINUX, cls.UNIX, cls.WINDOWS, cls.OTHER_HOST
 | 
			
		||||
            cls.LINUX, cls.WINDOWS, cls.UNIX, cls.OTHER_HOST
 | 
			
		||||
        ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,13 +16,15 @@ from .device import DeviceTypes
 | 
			
		|||
from .gpt import GPTTypes
 | 
			
		||||
from .host import HostTypes
 | 
			
		||||
from .web import WebTypes
 | 
			
		||||
from .ad import ADTypes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllTypes(ChoicesMixin):
 | 
			
		||||
    choices: list
 | 
			
		||||
    includes = [
 | 
			
		||||
        HostTypes, DeviceTypes, DatabaseTypes,
 | 
			
		||||
        CloudTypes, WebTypes, CustomTypes, GPTTypes
 | 
			
		||||
        CloudTypes, WebTypes, CustomTypes,
 | 
			
		||||
        ADTypes, GPTTypes
 | 
			
		||||
    ]
 | 
			
		||||
    _category_constrains = {}
 | 
			
		||||
    _automation_methods = None
 | 
			
		||||
| 
						 | 
				
			
			@ -173,6 +175,7 @@ class AllTypes(ChoicesMixin):
 | 
			
		|||
            (Category.DATABASE, DatabaseTypes),
 | 
			
		||||
            (Category.WEB, WebTypes),
 | 
			
		||||
            (Category.CLOUD, CloudTypes),
 | 
			
		||||
            (Category.AD, ADTypes),
 | 
			
		||||
            (Category.CUSTOM, CustomTypes)
 | 
			
		||||
        ]
 | 
			
		||||
        return types
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,166 @@
 | 
			
		|||
# Generated by Django 4.1.13 on 2025-03-31 02:49
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
import django
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
from assets.const.types import AllTypes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_ad_host_type(apps, schema_editor):
 | 
			
		||||
    data = """
 | 
			
		||||
     [
 | 
			
		||||
       {
 | 
			
		||||
          "created_by": "system",
 | 
			
		||||
          "updated_by": "system",
 | 
			
		||||
          "comment": "",
 | 
			
		||||
          "name": "Windows AD",
 | 
			
		||||
          "category": "ad",
 | 
			
		||||
          "type": "windows_ad",
 | 
			
		||||
          "meta": {},
 | 
			
		||||
          "internal": true,
 | 
			
		||||
          "domain_enabled": true,
 | 
			
		||||
          "su_enabled": false,
 | 
			
		||||
          "su_method": null,
 | 
			
		||||
          "custom_fields": [],
 | 
			
		||||
          "automation": {
 | 
			
		||||
              "ansible_enabled": true,
 | 
			
		||||
              "ansible_config": {
 | 
			
		||||
                  "ansible_shell_type": "cmd",
 | 
			
		||||
                  "ansible_connection": "ssh"
 | 
			
		||||
              },
 | 
			
		||||
              "ping_enabled": true,
 | 
			
		||||
              "ping_method": "ping_by_rdp",
 | 
			
		||||
              "ping_params": {},
 | 
			
		||||
              "gather_facts_enabled": true,
 | 
			
		||||
              "gather_facts_method": "gather_facts_windows",
 | 
			
		||||
              "gather_facts_params": {},
 | 
			
		||||
              "change_secret_enabled": true,
 | 
			
		||||
              "change_secret_method": "change_secret_ad_windows",
 | 
			
		||||
              "change_secret_params": {},
 | 
			
		||||
              "push_account_enabled": true,
 | 
			
		||||
              "push_account_method": "push_account_ad_windows",
 | 
			
		||||
              "push_account_params": {},
 | 
			
		||||
              "verify_account_enabled": true,
 | 
			
		||||
              "verify_account_method": "verify_account_by_rdp",
 | 
			
		||||
              "verify_account_params": {},
 | 
			
		||||
              "gather_accounts_enabled": true,
 | 
			
		||||
              "gather_accounts_method": "gather_accounts_ad_windows",
 | 
			
		||||
              "gather_accounts_params": {},
 | 
			
		||||
              "remove_account_enabled": true,
 | 
			
		||||
              "remove_account_method": "remove_account_ad_windows",
 | 
			
		||||
              "remove_account_params": {}
 | 
			
		||||
          },
 | 
			
		||||
          "protocols": [
 | 
			
		||||
              {
 | 
			
		||||
                  "name": "rdp",
 | 
			
		||||
                  "port": 3389,
 | 
			
		||||
                  "primary": true,
 | 
			
		||||
                  "required": false,
 | 
			
		||||
                  "default": false,
 | 
			
		||||
                  "public": true,
 | 
			
		||||
                  "setting": {
 | 
			
		||||
                      "console": false,
 | 
			
		||||
                      "security": "any"
 | 
			
		||||
                  }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                  "name": "ssh",
 | 
			
		||||
                  "port": 22,
 | 
			
		||||
                  "primary": false,
 | 
			
		||||
                  "required": false,
 | 
			
		||||
                  "default": false,
 | 
			
		||||
                  "public": true,
 | 
			
		||||
                  "setting": {
 | 
			
		||||
                      "sftp_enabled": true,
 | 
			
		||||
                      "sftp_home": "/tmp"
 | 
			
		||||
                  }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                  "name": "vnc",
 | 
			
		||||
                  "port": 5900,
 | 
			
		||||
                  "primary": false,
 | 
			
		||||
                  "required": false,
 | 
			
		||||
                  "default": false,
 | 
			
		||||
                  "public": true,
 | 
			
		||||
                  "setting": {}
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                  "name": "winrm",
 | 
			
		||||
                  "port": 5985,
 | 
			
		||||
                  "primary": false,
 | 
			
		||||
                  "required": false,
 | 
			
		||||
                  "default": false,
 | 
			
		||||
                  "public": false,
 | 
			
		||||
                  "setting": {
 | 
			
		||||
                      "use_ssl": false
 | 
			
		||||
                  }
 | 
			
		||||
              }
 | 
			
		||||
          ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
    """
 | 
			
		||||
    platform_model = apps.get_model('assets', 'Platform')
 | 
			
		||||
    automation_cls = apps.get_model('assets', 'PlatformAutomation')
 | 
			
		||||
    platform_datas = json.loads(data)
 | 
			
		||||
 | 
			
		||||
    for platform_data in platform_datas:
 | 
			
		||||
        AllTypes.create_or_update_by_platform_data(platform_data, platform_cls=platform_model,
 | 
			
		||||
                                                   automation_cls=automation_cls)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("assets", "0015_automationexecution_type"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(add_ad_host_type),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="AD",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "asset_ptr",
 | 
			
		||||
                    models.OneToOneField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        parent_link=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        to="assets.asset",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "domain_name",
 | 
			
		||||
                    models.CharField(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        default="",
 | 
			
		||||
                        max_length=128,
 | 
			
		||||
                        verbose_name="Domain name",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Active Directory",
 | 
			
		||||
            },
 | 
			
		||||
            bases=("assets.asset",),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="platform",
 | 
			
		||||
            name="ad",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                related_name="ad_platforms",
 | 
			
		||||
                to="assets.ad",
 | 
			
		||||
                verbose_name="Active Directory",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="platform",
 | 
			
		||||
            name="ad_enabled",
 | 
			
		||||
            field=models.BooleanField(default=False, verbose_name="AD enabled"),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
from .ad import *
 | 
			
		||||
from .cloud import *
 | 
			
		||||
from .common import *
 | 
			
		||||
from .custom import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .common import Asset
 | 
			
		||||
 | 
			
		||||
__all__ = ['AD']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AD(Asset):
 | 
			
		||||
    domain_name = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Domain name"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Active Directory")
 | 
			
		||||
| 
						 | 
				
			
			@ -245,6 +245,20 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
 | 
			
		|||
        auto_config.update(model_to_dict(automation))
 | 
			
		||||
        return auto_config
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def all_accounts(self):
 | 
			
		||||
        if not self.joined_ad_id:
 | 
			
		||||
            queryset = self.accounts.all()
 | 
			
		||||
        else:
 | 
			
		||||
            queryset = self.accounts.model.objects.filter(asset__in=[self.id, self.joined_ad_id])
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def all_valid_accounts(self):
 | 
			
		||||
        queryset = (self.all_accounts.filter(is_active=True)
 | 
			
		||||
                    .prefetch_related('asset', 'asset__platform', 'asset__platform__ad'))
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def accounts_amount(self):
 | 
			
		||||
        return self.accounts.count()
 | 
			
		||||
| 
						 | 
				
			
			@ -259,6 +273,19 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
 | 
			
		|||
        protocol = self.protocols.all().filter(name=protocol).first()
 | 
			
		||||
        return protocol.port if protocol else 0
 | 
			
		||||
 | 
			
		||||
    def is_ad(self):
 | 
			
		||||
        return self.category == const.Category.AD
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def joined_ad_id(self):
 | 
			
		||||
        return self.platform.ad_id
 | 
			
		||||
 | 
			
		||||
    def is_joined_ad(self):
 | 
			
		||||
        if self.joined_ad_id:
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_valid(self):
 | 
			
		||||
        warning = ''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -102,6 +102,11 @@ class Platform(LabeledMixin, JMSBaseModel):
 | 
			
		|||
        max_length=8, verbose_name=_("Charset")
 | 
			
		||||
    )
 | 
			
		||||
    domain_enabled = models.BooleanField(default=True, verbose_name=_("Gateway enabled"))
 | 
			
		||||
    ad_enabled = models.BooleanField(default=False, verbose_name=_("AD enabled"))
 | 
			
		||||
    ad = models.ForeignKey(
 | 
			
		||||
        'assets.AD', on_delete=models.SET_NULL, null=True, blank=True,
 | 
			
		||||
        verbose_name=_("Active Directory"), related_name='ad_platforms'
 | 
			
		||||
    )
 | 
			
		||||
    # 账号有关的
 | 
			
		||||
    su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
 | 
			
		||||
    su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +120,11 @@ class Platform(LabeledMixin, JMSBaseModel):
 | 
			
		|||
    def assets_amount(self):
 | 
			
		||||
        return self.assets.count()
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if not self.ad_enabled:
 | 
			
		||||
            self.ad = None
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def default(cls):
 | 
			
		||||
        linux, created = cls.objects.get_or_create(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,3 +7,4 @@ from .device import *
 | 
			
		|||
from .gpt import *
 | 
			
		||||
from .host import *
 | 
			
		||||
from .web import *
 | 
			
		||||
from .ad import *
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from assets.models import AD
 | 
			
		||||
from .common import AssetSerializer
 | 
			
		||||
 | 
			
		||||
__all__ = ['ADSerializer']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ADSerializer(AssetSerializer):
 | 
			
		||||
    class Meta(AssetSerializer.Meta):
 | 
			
		||||
        model = AD
 | 
			
		||||
        fields = AssetSerializer.Meta.fields + [
 | 
			
		||||
            'domain_name',
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            **AssetSerializer.Meta.extra_kwargs,
 | 
			
		||||
            'domain_name': {
 | 
			
		||||
                'help_text': _(
 | 
			
		||||
                    'The domain name of the Active Directory'
 | 
			
		||||
                ),
 | 
			
		||||
                'label': _('Domain name')}
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +147,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
 | 
			
		|||
    protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
 | 
			
		||||
    accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
 | 
			
		||||
    nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
 | 
			
		||||
    platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'), attrs=('id', 'name', 'type'))
 | 
			
		||||
    platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'),
 | 
			
		||||
                                  attrs=('id', 'name', 'type', 'ad_id'))
 | 
			
		||||
    accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount'))
 | 
			
		||||
    _accounts = None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,12 +4,11 @@ from django.db.models import Count, Q
 | 
			
		|||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from assets.models.gateway import Gateway
 | 
			
		||||
from common.serializers import ResourceLabelsMixin
 | 
			
		||||
from common.serializers.fields import ObjectRelatedField
 | 
			
		||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
from .gateway import GatewayWithAccountSecretSerializer
 | 
			
		||||
from ..models import Domain
 | 
			
		||||
from ..models import Domain, Gateway
 | 
			
		||||
 | 
			
		||||
__all__ = ['DomainSerializer', 'DomainWithGatewaySerializer', 'DomainListSerializer']
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -194,7 +194,7 @@ class PlatformSerializer(ResourceLabelsMixin, CommonSerializerMixin, WritableNes
 | 
			
		|||
        ]
 | 
			
		||||
        fields_m2m = ['assets', 'assets_amount']
 | 
			
		||||
        fields = fields_small + fields_m2m + [
 | 
			
		||||
            "protocols", "domain_enabled", "su_enabled", "su_method",
 | 
			
		||||
            "protocols", "domain_enabled", "su_enabled", "su_method", "ad_enabled", "ad",
 | 
			
		||||
            "automation", "comment", "custom_fields", "labels"
 | 
			
		||||
        ] + read_only_fields
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ router.register(r'databases', api.DatabaseViewSet, 'database')
 | 
			
		|||
router.register(r'webs', api.WebViewSet, 'web')
 | 
			
		||||
router.register(r'clouds', api.CloudViewSet, 'cloud')
 | 
			
		||||
router.register(r'gpts', api.GPTViewSet, 'gpt')
 | 
			
		||||
router.register(r'directories', api.ADViewSet, 'ad')
 | 
			
		||||
router.register(r'customs', api.CustomViewSet, 'custom')
 | 
			
		||||
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
 | 
			
		||||
router.register(r'nodes', api.NodeViewSet, 'node')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,7 +95,9 @@ class QuerySetMixin:
 | 
			
		|||
    get_queryset: Callable
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = super().get_queryset()
 | 
			
		||||
        return super().get_queryset()
 | 
			
		||||
 | 
			
		||||
    def filter_queryset(self, queryset):
 | 
			
		||||
        if not hasattr(self, 'action'):
 | 
			
		||||
            return queryset
 | 
			
		||||
        if self.action == 'metadata':
 | 
			
		||||
| 
						 | 
				
			
			@ -105,8 +107,9 @@ class QuerySetMixin:
 | 
			
		|||
 | 
			
		||||
    def setup_eager_loading(self, queryset, is_paginated=False):
 | 
			
		||||
        is_export_request = self.request.query_params.get('format') in ['csv', 'xlsx']
 | 
			
		||||
        no_request_page = self.request.query_params.get('limit') is None
 | 
			
		||||
        # 不分页不走一般这个,是因为会消耗多余的 sql 查询, 不如分页的时候查询一次
 | 
			
		||||
        if not is_export_request and not is_paginated:
 | 
			
		||||
        if not is_export_request and not is_paginated and not no_request_page:
 | 
			
		||||
            return queryset
 | 
			
		||||
 | 
			
		||||
        serializer_class = self.get_serializer_class()
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +132,7 @@ class QuerySetMixin:
 | 
			
		|||
        serializer_class = self.get_serializer_class()
 | 
			
		||||
        if page and serializer_class:
 | 
			
		||||
            ids = [str(obj.id) for obj in page]
 | 
			
		||||
            page = self.get_queryset().filter(id__in=ids)
 | 
			
		||||
            page = model.objects.filter(id__in=ids)
 | 
			
		||||
            page = self.setup_eager_loading(page, is_paginated=True)
 | 
			
		||||
            page_mapper = {str(obj.id): obj for obj in page}
 | 
			
		||||
            page = [page_mapper.get(_id) for _id in ids if _id in page_mapper]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,6 +59,7 @@ class NodePermedSerializer(serializers.ModelSerializer):
 | 
			
		|||
 | 
			
		||||
class AccountsPermedSerializer(serializers.ModelSerializer):
 | 
			
		||||
    actions = ActionChoicesField(read_only=True)
 | 
			
		||||
    username = serializers.CharField(source='full_username', read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Account
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -88,7 +88,7 @@ class PermAssetDetailUtil:
 | 
			
		|||
        if not all_action_bit:
 | 
			
		||||
            return alias_action_bit_mapper, alias_date_expired_mapper
 | 
			
		||||
 | 
			
		||||
        asset_account_usernames = asset.accounts.all().active().values_list('username', flat=True)
 | 
			
		||||
        asset_account_usernames = asset.all_valid_accounts.values_list('username', flat=True)
 | 
			
		||||
        for username in asset_account_usernames:
 | 
			
		||||
            alias_action_bit_mapper[username] |= all_action_bit
 | 
			
		||||
            alias_date_expired_mapper[username].extend(
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +100,7 @@ class PermAssetDetailUtil:
 | 
			
		|||
    def map_alias_to_accounts(cls, alias_action_bit_mapper, alias_date_expired_mapper, asset, user):
 | 
			
		||||
        username_accounts_mapper = defaultdict(list)
 | 
			
		||||
        cleaned_accounts_expired = defaultdict(list)
 | 
			
		||||
        asset_accounts = asset.accounts.all().active()
 | 
			
		||||
        asset_accounts = asset.all_valid_accounts
 | 
			
		||||
 | 
			
		||||
        # 用户名 -> 账号
 | 
			
		||||
        for account in asset_accounts:
 | 
			
		||||
| 
						 | 
				
			
			@ -135,11 +135,18 @@ class PermAssetDetailUtil:
 | 
			
		|||
            alias_action_bit_mapper, alias_date_expired_mapper, asset, user
 | 
			
		||||
        )
 | 
			
		||||
        accounts = []
 | 
			
		||||
        virtual_accounts = []
 | 
			
		||||
        for account, action_bit in cleaned_accounts_action_bit.items():
 | 
			
		||||
            account.actions = action_bit
 | 
			
		||||
            account.date_expired = max(cleaned_accounts_expired[account])
 | 
			
		||||
            accounts.append(account)
 | 
			
		||||
        return accounts
 | 
			
		||||
 | 
			
		||||
            if account.username.startswith('@'):
 | 
			
		||||
                virtual_accounts.append(account)
 | 
			
		||||
            else:
 | 
			
		||||
                accounts.append(account)
 | 
			
		||||
        accounts.sort(key=lambda x: x.username)
 | 
			
		||||
        virtual_accounts.sort(key=lambda x: x.username)
 | 
			
		||||
        return accounts + virtual_accounts
 | 
			
		||||
 | 
			
		||||
    def check_perm_protocols(self, protocols):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue