diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index ae1311971..fc1885720 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time - from rest_framework import generics from rest_framework.serializers import ValidationError from rest_framework.views import APIView diff --git a/apps/assets/apps.py b/apps/assets/apps.py index 59e31dd97..e210a1708 100644 --- a/apps/assets/apps.py +++ b/apps/assets/apps.py @@ -7,5 +7,7 @@ class AssetsConfig(AppConfig): name = 'assets' def ready(self): - from . import signals_handler super().ready() + from . import signals_handler + from .models import Node + Node.initial_some_nodes() diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 889d1de1f..2594d2763 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext from django.core.cache import cache from orgs.mixins.models import OrgModelMixin, OrgManager -from orgs.utils import set_current_org, get_current_org +from orgs.utils import set_current_org, get_current_org, tmp_to_root_org, tmp_to_org from orgs.models import Organization @@ -25,37 +25,42 @@ class NodeQuerySet(models.QuerySet): class TreeMixin: tree_created_time = None - tree_updated_time_cache_key = 'NODE_TREE_CREATED_AT' - tree_update_time_cache_time = 3600 + tree_updated_time_cache_key = 'NODE_TREE_UPDATED_AT' + tree_cache_time = 3600 + tree_assets_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT' + tree_assets_created_time = None _tree_service = None @classmethod def tree(cls): - # Todo: 有待优化, 因为每次刷新都会导致其他节点的tree失效 完成 - # TOdo: 游离的资产,在树上显示的数量不对 - # Todo: ungroup node - # Todo: api key页面有bug 完成 from ..utils import TreeService tree_updated_time = cache.get(cls.tree_updated_time_cache_key, 0) if not cls.tree_created_time or \ tree_updated_time > cls.tree_created_time: - print("New tree") tree = TreeService.new() cls.tree_created_time = time.time() + cls.tree_assets_created_time = time.time() cls._tree_service = tree return tree + node_assets_updated_time = cache.get(cls.tree_assets_cache_key, 0) + if not cls.tree_assets_created_time or \ + node_assets_updated_time > cls.tree_assets_created_time: + cls._tree_service.init_assets_async() return cls._tree_service @classmethod - def expire_cache_tree(cls): + def refresh_tree(cls): key = cls.tree_updated_time_cache_key - ttl = cls.tree_update_time_cache_time + ttl = cls.tree_cache_time value = time.time() cache.set(key, value, ttl) @classmethod - def refresh_tree(cls): - cls.expire_cache_tree() + def refresh_node_assets(cls): + key = cls.tree_assets_cache_key + ttl = cls.tree_cache_time + value = time.time() + cache.set(key, value, ttl) @property def _tree(self): @@ -183,6 +188,31 @@ class FamilyMixin: key_list.pop() return keys + def get_next_child_key(self): + mark = self.child_mark + self.child_mark += 1 + self.save() + return "{}:{}".format(self.key, mark) + + def get_next_child_preset_name(self): + name = ugettext("New node") + values = [ + child.value[child.value.rfind(' '):] + for child in self.get_children() + if child.value.startswith(name) + ] + values = [int(value) for value in values if value.strip().isdigit()] + count = max(values) + 1 if values else 1 + return '{} {}'.format(name, count) + + def create_child(self, value, _id=None): + with transaction.atomic(): + child_key = self.get_next_child_key() + child = self.__class__.objects.create( + id=_id, key=child_key, value=value + ) + return child + class FullValueMixin: _full_value = None @@ -246,7 +276,85 @@ class NodeAssetsMixin: return Asset.objects.filter(nodes__key__regex=pattern) -class Node(OrgModelMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin): +class SomeNodesMixin: + key = '' + default_key = '1' + default_value = 'Default' + ungrouped_key = '-10' + ungrouped_value = _('ungrouped') + empty_key = '-11' + empty_value = _("empty") + + def is_default_node(self): + return self.key == self.default_key + + def is_org_root(self): + if self.key.isdigit(): + return True + else: + return False + + @classmethod + def create_org_root_node(cls): + # 如果使用current_org 在set_current_org时会死循环 + ori_org = get_current_org() + with transaction.atomic(): + if not ori_org.is_real(): + return cls.default_node() + set_current_org(Organization.root()) + org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$') + org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) + if not org_nodes_roots_keys: + org_nodes_roots_keys = ['1'] + key = max([int(k) for k in org_nodes_roots_keys]) + key = str(key + 1) if key != 0 else '2' + set_current_org(ori_org) + root = cls.objects.create(key=key, value=ori_org.name) + return root + + @classmethod + def org_root(cls): + root = cls.objects.filter(key__regex=r'^[0-9]+$') + if root: + return root[0] + else: + return cls.create_org_root_node() + + @classmethod + def ungrouped_node(cls): + with tmp_to_org(Organization.system()): + defaults = {'value': cls.ungrouped_key} + obj, created = cls.objects.get_or_create( + defaults=defaults, key=cls.ungrouped_key + ) + return obj + + @classmethod + def empty_node(cls): + with tmp_to_org(Organization.system()): + defaults = {'value': cls.empty_value} + obj, created = cls.objects.get_or_create( + defaults=defaults, key=cls.empty_key + ) + return obj + + @classmethod + def default_node(cls): + with tmp_to_org(Organization.default()): + defaults = {'value': cls.default_value} + obj, created = cls.objects.get_or_create( + defaults=defaults, key=cls.default_key, + ) + return obj + + @classmethod + def initial_some_nodes(cls): + cls.default_node() + cls.empty_node() + cls.ungrouped_node() + + +class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' value = models.CharField(max_length=128, verbose_name=_("Value")) @@ -290,75 +398,13 @@ class Node(OrgModelMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixi def level(self): return len(self.key.split(':')) - def get_next_child_key(self): - mark = self.child_mark - self.child_mark += 1 - self.save() - return "{}:{}".format(self.key, mark) - - def get_next_child_preset_name(self): - name = ugettext("New node") - values = [ - child.value[child.value.rfind(' '):] - for child in self.get_children() - if child.value.startswith(name) - ] - values = [int(value) for value in values if value.strip().isdigit()] - count = max(values) + 1 if values else 1 - return '{} {}'.format(name, count) - - def create_child(self, value, _id=None): - with transaction.atomic(): - child_key = self.get_next_child_key() - child = self.__class__.objects.create( - id=_id, key=child_key, value=value - ) - return child - @classmethod def refresh_nodes(cls): cls.refresh_tree() - def is_default_node(self): - return self.key == '1' - - def is_org_root(self): - if self.key.isdigit(): - return True - else: - return False - @classmethod - def create_org_root_node(cls): - # 如果使用current_org 在set_current_org时会死循环 - ori_org = get_current_org() - with transaction.atomic(): - if not ori_org.is_real(): - return cls.default_node() - set_current_org(Organization.root()) - org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$') - org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) - if not org_nodes_roots_keys: - org_nodes_roots_keys = ['1'] - key = max([int(k) for k in org_nodes_roots_keys]) - key = str(key + 1) if key != 0 else '2' - set_current_org(ori_org) - root = cls.objects.create(key=key, value=ori_org.name) - return root - - @classmethod - def org_root(cls): - root = cls.objects.filter(key__regex=r'^[0-9]+$') - if root: - return root[0] - else: - return cls.create_org_root_node() - - @classmethod - def default_node(cls): - defaults = {'value': 'Default'} - obj, created = cls.objects.get_or_create(defaults=defaults, key='1') - return obj + def refresh_assets(cls): + cls.refresh_node_assets() def as_tree_node(self): from common.tree import TreeNode diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index 7be8b9d47..e22857d54 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -14,7 +14,9 @@ __all__ = [ class NodeSerializer(BulkOrgResourceModelSerializer): name = serializers.ReadOnlyField(source='value') - value = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("value")) + value = serializers.CharField( + required=False, allow_blank=True, allow_null=True, label=_("value") + ) class Meta: model = Node diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 266fe44bf..03b14f79b 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -2,8 +2,9 @@ # from collections import defaultdict from django.db.models.signals import ( - post_save, m2m_changed, pre_delete, pre_save, pre_init, post_init + post_save, m2m_changed, post_delete ) +from django.db.models.aggregates import Count from django.dispatch import receiver from common.utils import get_logger @@ -29,29 +30,36 @@ def test_asset_conn_on_created(asset): test_asset_connectivity_util.delay([asset]) -@receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") +@receiver(post_save, sender=Asset) @on_transaction_commit def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): """ 当资产创建时,更新硬件信息,更新可连接性 + 确保资产必须属于一个节点 """ if created: - logger.info("Asset `{}` create signal received".format(instance)) + logger.info("Asset create signal recv: {}".format(instance)) # 获取资产硬件信息 update_asset_hardware_info_on_created(instance) test_asset_conn_on_created(instance) + # 确保资产存在一个节点 + has_node = instance.nodes.all().exists() + if not has_node: + instance.nodes.add(Node.org_root()) -@receiver(pre_delete, sender=Asset, dispatch_uid="my_unique_identifier") + +@receiver(post_delete, sender=Asset) def on_asset_delete(sender, instance=None, **kwargs): """ 当资产删除时,刷新节点,节点中存在节点和资产的关系 """ - Node.refresh_nodes() + logger.debug("Asset delete signal recv: {}".format(instance)) + Node.refresh_assets() -@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") +@receiver(post_save, sender=SystemUser, dispatch_uid="jms") def on_system_user_update(sender, instance=None, created=True, **kwargs): """ 当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上, @@ -60,61 +68,126 @@ def on_system_user_update(sender, instance=None, created=True, **kwargs): 关联到上面 """ if instance and not created: - logger.info("System user `{}` update signal received".format(instance)) + logger.info("System user update signal recv: {}".format(instance)) assets = instance.assets.all().valid() push_system_user_to_assets.delay(instance, assets) -@receiver(m2m_changed, sender=SystemUser.assets.through, dispatch_uid="my_unique_identifier") -def on_system_user_assets_change(sender, instance=None, **kwargs): +@receiver(m2m_changed, sender=SystemUser.assets.through) +def on_system_user_assets_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs): """ 当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中 """ - if instance and kwargs["action"] == "post_add": - assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - push_system_user_to_assets.delay(instance, assets) + if action != "post_add": + return + logger.debug("System user assets change signal recv: {}".format(instance)) + queryset = model.objects.filter(pk__in=pk_set) + if model == Asset: + system_users = [instance] + assets = queryset + else: + system_users = queryset + assets = [instance] + for system_user in system_users: + push_system_user_to_assets.delay(system_user, assets) -@receiver(m2m_changed, sender=SystemUser.nodes.through, dispatch_uid="my_unique_identifier") -def on_system_user_nodes_change(sender, instance=None, **kwargs): +@receiver(m2m_changed, sender=SystemUser.nodes.through) +def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs): """ - 当系统用户和节点关系发生变化时,应该将节点关联到新的系统用户上 + 当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上 """ - if instance and kwargs["action"] == "post_add": - logger.info("System user `{}` nodes update signal received".format(instance)) - nodes_keys = kwargs['model'].objects.filter( - pk__in=kwargs['pk_set'] - ).values_list('key', flat=True) - assets = Node.get_nodes_all_assets(nodes_keys) - instance.assets.add(*tuple(assets)) + if action != "post_add": + return + logger.info("System user `{}` nodes update signal recv".format(instance)) + + queryset = model.objects.filter(pk__in=pk_set) + if model == Node: + nodes_keys = queryset.values_list('key', flat=True) + system_users = [instance] + else: + nodes_keys = [instance.key] + system_users = queryset + assets = Node.get_nodes_all_assets(nodes_keys) + for system_user in system_users: + system_user.assets.add(*tuple(assets)) -@receiver(m2m_changed, sender=Asset.nodes.through, dispatch_uid="my_unique_identifier") -def on_asset_nodes_changed(sender, instance=None, **kwargs): +@receiver(m2m_changed, sender=Asset.nodes.through) +def on_asset_nodes_change(sender, instance=None, action='', **kwargs): + """ + 资产节点发生变化时,刷新节点 + """ + if action.startswith('post'): + logger.debug("Asset nodes change signal recv: {}".format(instance)) + Node.refresh_assets() + + +@receiver(m2m_changed, sender=Asset.nodes.through) +def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None, **kwargs): """ 当资产的节点发生变化时,或者 当节点的资产关系发生变化时, 节点下新增的资产,添加到节点关联的系统用户中 - 并刷新节点 """ - if isinstance(instance, Asset): - logger.debug("Asset nodes change signal received: {}".format(instance)) - # 节点资产发生变化时,将资产关联到节点关联的系统用户 - if kwargs['action'] == 'post_add': - nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users_assets = defaultdict(set) - system_users = SystemUser.objects.filter(nodes__in=nodes) - for system_user in system_users: - system_users_assets[system_user].add(instance) - for system_user, assets in system_users_assets.items(): - system_user.assets.add(*tuple(assets)) - if isinstance(instance, Node): - logger.debug("Node assets change signal received: {}".format(instance)) - - Node.refresh_nodes() + if action != "post_add": + return + logger.debug("Assets node add signal recv: {}".format(action)) + queryset = model.objects.filter(pk__in=pk_set) + if model == Node: + nodes = queryset + assets = [instance] + else: + nodes = [instance] + assets = queryset + # 节点资产发生变化时,将资产关联到节点关联的系统用户, 只关注新增的 + system_users_assets = defaultdict(set) + system_users = SystemUser.objects.filter(nodes__in=nodes) + for system_user in system_users: + system_users_assets[system_user].update(set(assets)) + for system_user, _assets in system_users_assets.items(): + system_user.assets.add(*tuple(_assets)) -@receiver(post_save, sender=Node) -def on_node_update_or_created(sender, instance=None, created=False, **kwargs): +@receiver(m2m_changed, sender=Asset.nodes.through) +def on_asset_nodes_remove(sender, instance=None, action='', model=None, + pk_set=None, **kwargs): + + """ + 监控资产删除节点关系, 或节点删除资产,避免产生游离资产 + """ + if action not in ["post_remove", "pre_clear", "post_clear"]: + return + if action == "pre_clear": + if model == Node: + instance._nodes = list(instance.nodes.all()) + else: + instance._assets = list(instance.assets.all()) + return + logger.debug("Assets node remove signal recv: {}".format(action)) + if action == "post_remove": + queryset = model.objects.filter(pk__in=pk_set) + else: + if model == Node: + queryset = instance._nodes + else: + queryset = instance._assets + if model == Node: + assets = [instance] + else: + assets = queryset + if isinstance(assets, list): + assets_not_has_node = [] + for asset in assets: + if asset.nodes.all().count() == 0: + assets_not_has_node.append(asset.id) + else: + assets_not_has_node = assets.annotate(nodes_count=Count('nodes'))\ + .filter(nodes_count=0).values_list('id', flat=True) + Node.org_root().assets.add(*tuple(assets_not_has_node)) + + +@receiver([post_save, post_delete], sender=Node) +def on_node_update_or_created(sender, **kwargs): # 刷新节点 Node.refresh_nodes() diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index cd64e5b85..f6c48f22a 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -37,6 +37,7 @@
+ {% trans 'Loading' %} ...
diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 9e994eba8..58e0d86e5 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -124,7 +124,6 @@
-{% include 'assets/_node_tree.html' %} {% include 'assets/_asset_update_modal.html' %} {% include 'assets/_asset_import_modal.html' %} {% include 'assets/_asset_list_modal.html' %} diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 473f6c46b..222933f94 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -59,6 +59,8 @@ class TreeService(Tree): tag_sep = ' / ' cache_key = '_NODE_FULL_TREE' cache_time = 3600 + has_empty_node = False + has_ungrouped_node = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -119,9 +121,9 @@ class TreeService(Tree): return [self.get_node(i, deep=deep) for i in ancestor_ids] def get_node_full_tag(self, nid): - ancestors = self.ancestors(nid) + ancestors = self.ancestors(nid, with_self=True) ancestors.reverse() - return self.tag_sep.join(n.tag for n in ancestors) + return self.tag_sep.join([n.tag for n in ancestors]) def get_family(self, nid, deep=False): ancestors = self.ancestors(nid, with_self=False, deep=deep) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 803c62b83..27df6314f 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -290,10 +290,10 @@ LOGGING = { 'handlers': ['syslog'], 'level': 'INFO' }, - 'django.db': { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' - } + # 'django.db': { + # 'handlers': ['console', 'file'], + # 'level': 'DEBUG' + # } } } diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 3f54e14a7..672b975dc 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -64,8 +64,11 @@ class OrgModelMixin(models.Model): sep = '@' def save(self, *args, **kwargs): - if current_org is not None and current_org.is_real(): - self.org_id = current_org.id + org = get_current_org() + if org is not None and (org.is_real() or org.is_system()): + self.org_id = org.id + elif org is not None and org.is_default(): + self.org_id = '' return super().save(*args, **kwargs) @property diff --git a/apps/orgs/models.py b/apps/orgs/models.py index e46c2b55d..03b46e9bc 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -21,6 +21,8 @@ class Organization(models.Model): ROOT_NAME = 'ROOT' DEFAULT_ID = 'DEFAULT' DEFAULT_NAME = 'DEFAULT' + SYSTEM_ID = '00000000-0000-0000-0000-000000000002' + SYSTEM_NAME = 'SYSTEM' _user_admin_orgs = None class Meta: @@ -55,6 +57,8 @@ class Organization(models.Model): return cls.default() elif id_or_name in [cls.ROOT_ID, cls.ROOT_NAME]: return cls.root() + elif id_or_name in [cls.SYSTEM_ID, cls.SYSTEM_NAME]: + return cls.system() try: if is_uuid(id_or_name): @@ -89,7 +93,7 @@ class Organization(models.Model): return False def is_real(self): - return self.id not in (self.DEFAULT_NAME, self.ROOT_ID) + return self.id not in (self.DEFAULT_NAME, self.ROOT_ID, self.SYSTEM_ID) @classmethod def get_user_admin_orgs(cls, user): @@ -111,17 +115,18 @@ class Organization(models.Model): def root(cls): return cls(id=cls.ROOT_ID, name=cls.ROOT_NAME) + @classmethod + def system(cls): + return cls(id=cls.SYSTEM_ID, name=cls.SYSTEM_NAME) + def is_root(self): - if self.id is self.ROOT_ID: - return True - else: - return False + return self.id is self.ROOT_ID def is_default(self): - if self.id is self.DEFAULT_ID: - return True - else: - return False + return self.id is self.DEFAULT_ID + + def is_system(self): + return self.id is self.SYSTEM_ID def change_to(self): from .utils import set_current_org diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 8f380b169..90dde34d4 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from werkzeug.local import LocalProxy +from contextlib import contextmanager from common.local import thread_local from .models import Organization @@ -52,4 +53,22 @@ def get_current_org_id_for_serializer(): return org_id +@contextmanager +def tmp_to_root_org(): + ori_org = get_current_org() + set_to_root_org() + yield + if ori_org is not None: + set_current_org(ori_org) + + +@contextmanager +def tmp_to_org(org): + ori_org = get_current_org() + set_current_org(org) + yield + if ori_org is not None: + set_current_org(ori_org) + + current_org = LocalProxy(get_current_org) diff --git a/apps/perms/api/asset_permission.py b/apps/perms/api/asset_permission.py index 0c9a43ae5..36bf00d9d 100644 --- a/apps/perms/api/asset_permission.py +++ b/apps/perms/api/asset_permission.py @@ -40,24 +40,14 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): return self.serializer_class def filter_valid(self, queryset): - valid = self.request.query_params.get('is_valid', None) - if valid is None: + valid_query = self.request.query_params.get('is_valid', None) + if valid_query is None: return queryset - if valid in ['0', 'N', 'false', 'False']: - valid = False + invalid = valid_query in ['0', 'N', 'false', 'False'] + if invalid: + queryset = queryset.invalid() else: - valid = True - now = timezone.now() - if valid: - queryset = queryset.filter(is_active=True).filter( - date_start__lt=now, date_expired__gt=now, - ) - else: - queryset = queryset.filter( - Q(is_active=False) | - Q(date_start__gt=now) | - Q(date_expired__lt=now) - ) + queryset = queryset.valid() return queryset def filter_system_user(self, queryset): diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py index bb29ce6a1..f965f8ec4 100644 --- a/apps/perms/api/user_permission.py +++ b/apps/perms/api/user_permission.py @@ -15,10 +15,6 @@ from orgs.utils import set_to_root_org from ..utils import ( ParserNode, AssetPermissionUtilV2 ) -from .mixin import ( - UserPermissionCacheMixin, GrantAssetsMixin, NodesWithUngroupMixin -) -from .. import const from ..hands import User, Asset, Node, SystemUser, NodeSerializer from .. import serializers from ..models import Action @@ -66,8 +62,8 @@ class UserGrantedAssetsApi(UserPermissionMixin, ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.AssetGrantedSerializer only_fields = serializers.AssetGrantedSerializer.Meta.only_fields - filter_fields = ['hostname', 'ip'] - search_fields = filter_fields + filter_fields = ['hostname', 'ip', 'id', 'comment'] + search_fields = ['hostname', 'ip', 'comment'] def filter_by_nodes(self, queryset): node_id = self.request.query_params.get("node") @@ -109,12 +105,21 @@ class UserGrantedNodesApi(UserPermissionMixin, ListAPIView): 查询用户授权的所有节点的API """ permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.GrantedNodeSerializer + serializer_class = serializers.NodeGrantedSerializer only_fields = NodeSerializer.Meta.only_fields + util = None + + def get(self, request, *args, **kwargs): + self.util = AssetPermissionUtilV2(self.obj) + return super().get(request, *args, **kwargs) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["tree"] = self.util.user_tree + return context def get_queryset(self): - util = AssetPermissionUtilV2(self.obj) - node_keys = util.get_nodes() + node_keys = self.util.get_nodes() queryset = Node.objects.filter(key__in=node_keys) return queryset @@ -131,7 +136,6 @@ class UserGrantedNodeChildrenApi(UserGrantedNodesApi): system_user_id = self.request.query_params.get("system_user") self.util = AssetPermissionUtilV2(self.obj) if system_user_id: - system_user = get_object_or_404(SystemUser, id=system_user_id) self.util.filter_permissions(system_users=system_user_id) self.tree = self.util.get_user_tree() diff --git a/apps/perms/const.py b/apps/perms/const.py index 4ccab5e38..476fc9d3f 100644 --- a/apps/perms/const.py +++ b/apps/perms/const.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import ugettext_lazy as _ UNGROUPED_NODE_ID = "00000000-0000-0000-0000-000000000002" +UNGROUPED_NODE_KEY = '-2' +UNGROUPED_NODE_VALUE = _("Ungrouped") EMPTY_NODE_ID = "00000000-0000-0000-0000-000000000003" -EMPTY_NODE_KEY = "1:-2" +EMPTY_NODE_KEY = "-3" +EMPTY_NODE_VALUE = _("Empty") diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 7215e01bb..fff6c56f3 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -5,7 +5,7 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from common.utils import date_expired_default, set_or_append_attr_bulk +from common.utils import date_expired_default from orgs.mixins.models import OrgModelMixin from assets.models import Asset, SystemUser, Node diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index 02e580c8a..b36d009b3 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -4,6 +4,7 @@ import uuid from django.utils.translation import ugettext_lazy as _ from django.db import models +from django.db.models import Q from django.utils import timezone from orgs.mixins.models import OrgModelMixin @@ -24,6 +25,18 @@ class BasePermissionQuerySet(models.QuerySet): return self.active().filter(date_start__lt=timezone.now()) \ .filter(date_expired__gt=timezone.now()) + def inactive(self): + return self.filter(is_active=False) + + def invalid(self): + now = timezone.now + q = ( + Q(is_active=False) | + Q(date_start__gt=now) | + Q(date_expired__lt=now) + ) + return self.filter(q) + class BasePermissionManager(OrgManager): def valid(self): diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 9f7ea4ecf..149b618eb 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -9,8 +9,8 @@ from assets.serializers import ProtocolsField from .asset_permission import ActionsField __all__ = [ - 'GrantedNodeSerializer', - 'NodeGrantedSerializer', 'AssetGrantedSerializer', + 'NodeGrantedSerializer', + 'AssetGrantedSerializer', 'ActionsSerializer', 'AssetSystemUserSerializer', ] @@ -43,36 +43,29 @@ class AssetGrantedSerializer(serializers.ModelSerializer): "id", "hostname", "ip", "protocols", "os", 'domain', "platform", "comment", "org_id", ] - fields = only_fields + fields = only_fields + ['org_name'] read_only_fields = fields class NodeGrantedSerializer(serializers.ModelSerializer): - """ - 授权资产组 - """ - # assets_granted = AssetGrantedSerializer(many=True, read_only=True) - # assets_amount = serializers.ReadOnlyField() - name = serializers.ReadOnlyField(source='value') + assets_amount = serializers.SerializerMethodField() - # assets_only_fields = AssetGrantedSerializer.Meta.only_fields - # system_users_only_fields = AssetGrantedSerializer.system_users_only_fields - - class Meta: - model = Node - only_fields = ['id', 'key', 'value', "org_id"] - fields = only_fields + ['name'] - read_only_fields = fields - - -class GrantedNodeSerializer(serializers.ModelSerializer): class Meta: model = Node fields = [ - 'id', 'name', 'key', 'value', + 'id', 'name', 'key', 'value', 'org_id', "assets_amount" ] read_only_fields = fields + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tree = self.context.get("tree") + + def get_assets_amount(self, obj): + if not self.tree: + return 0 + return self.tree.assets_amount(obj.key) + class ActionsSerializer(serializers.Serializer): actions = ActionsField(read_only=True) diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index faadaf1a8..3736e3a84 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -15,25 +15,23 @@ logger = get_logger(__file__) @on_transaction_commit def on_permission_created(sender, instance=None, created=False, **kwargs): pass - # AssetPermissionUtil.expire_all_cache() @receiver(post_save, sender=AssetPermission) def on_permission_update(sender, **kwargs): pass - # AssetPermissionUtil.expire_all_cache() @receiver(post_delete, sender=AssetPermission) def on_permission_delete(sender, **kwargs): pass - # AssetPermissionUtil.expire_all_cache() @receiver(m2m_changed, sender=AssetPermission.nodes.through) -def on_permission_nodes_changed(sender, instance=None, **kwargs): - # AssetPermissionUtil.expire_all_cache() - if isinstance(instance, AssetPermission) and kwargs['action'] == 'post_add': +def on_permission_nodes_changed(sender, instance=None, action='', **kwargs): + if action != 'post_add': + return + if isinstance(instance, AssetPermission): logger.debug("Asset permission nodes change signal received") nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) system_users = instance.system_users.all() @@ -42,9 +40,10 @@ def on_permission_nodes_changed(sender, instance=None, **kwargs): @receiver(m2m_changed, sender=AssetPermission.assets.through) -def on_permission_assets_changed(sender, instance=None, **kwargs): - # AssetPermissionUtil.expire_all_cache() - if isinstance(instance, AssetPermission) and kwargs['action'] == 'post_add': +def on_permission_assets_changed(sender, instance=None, action='', **kwargs): + if action != 'post_add': + return + if isinstance(instance, AssetPermission): logger.debug("Asset permission assets change signal received") assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) system_users = instance.system_users.all() @@ -53,13 +52,15 @@ def on_permission_assets_changed(sender, instance=None, **kwargs): @receiver(m2m_changed, sender=AssetPermission.system_users.through) -def on_permission_system_users_changed(sender, instance=None, **kwargs): - # AssetPermissionUtil.expire_all_cache() - if isinstance(instance, AssetPermission) and kwargs['action'] == 'post_add': - logger.debug("Asset permission system_users change signal received") +def on_permission_system_users_changed(sender, instance=None, action='', **kwargs): + if action != 'post_add': + return + if isinstance(instance, AssetPermission): system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + logger.debug("Asset permission system_users change signal received") assets = instance.assets.all() nodes = instance.nodes.all() for system_user in system_users: system_user.nodes.add(*tuple(nodes)) system_user.assets.add(*tuple(assets)) + diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 8fa37beb5..3f7874753 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -4,6 +4,7 @@ from collections import defaultdict from functools import reduce from django.db.models import Q +from django.conf import settings from orgs.utils import set_to_root_org from common.utils import get_logger, timeit @@ -11,6 +12,7 @@ from common.tree import TreeNode from assets.utils import TreeService from ..models import AssetPermission from ..hands import Node, Asset, SystemUser +from .. import const logger = get_logger(__file__) @@ -129,6 +131,9 @@ class AssetPermissionUtilV2: @timeit def add_direct_nodes_to_user_tree(self, user_tree): + """ + 将授权规则的节点放到用户树上, 从full tree中粘贴子树 + """ nodes_direct_keys = self.permissions \ .exclude(nodes__isnull=True) \ .values_list('nodes__key', flat=True) \ @@ -153,6 +158,10 @@ class AssetPermissionUtilV2: @timeit def add_single_assets_node_to_user_tree(self, user_tree): + """ + 将单独授权的资产放到树上,如果设置了单独资产到 未分组中,则放到未分组中 + 如果没有,则查询资产属于的资产组,放到树上 + """ # 添加单独授权资产的节点 nodes_single_assets = defaultdict(set) queryset = self.permissions.exclude(assets__isnull=True) \ @@ -161,13 +170,26 @@ class AssetPermissionUtilV2: for item in queryset: nodes_single_assets[item[1]].add(item[0]) - # Todo: 游离资产 nodes_single_assets.pop(None, None) for key in tuple(nodes_single_assets.keys()): if user_tree.contains(key): nodes_single_assets.pop(key) + # 如果要设置到ungroup中 + if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: + node_key = Node.ungrouped_key + node_value = Node.ungrouped_value + user_tree.create_node( + identifier=node_key, tag=node_value, + parent=user_tree.root, + ) + assets = set() + for _assets in nodes_single_assets.values(): + assets.update(set(_assets)) + user_tree.set_assets(node_key, assets) + return + # 获取单独授权资产,并没有在授权的节点上 for key, assets in nodes_single_assets.items(): node = self.full_tree.get_node(key, deep=True) @@ -180,11 +202,17 @@ class AssetPermissionUtilV2: @timeit def parse_user_tree_to_full_tree(self, user_tree): + """ + 经过前面两个动作,用户授权的节点已放到树上,但是树不是完整的, + 这里要讲树构造成一个完整的书 + """ # 开始修正user_tree,保证父节点都在树上 root_children = user_tree.children('') for child in root_children: if child.identifier.isdigit(): continue + if child.identifier.startswith('-'): + continue ancestors = self.full_tree.ancestors( child.identifier, with_self=False, deep=True ) @@ -194,6 +222,19 @@ class AssetPermissionUtilV2: user_tree.safe_add_ancestors(ancestors) user_tree.move_node(child.identifier, parent_id) + @staticmethod + def add_empty_node_if_need(user_tree): + """ + 添加空节点,如果根节点没有子节点的话 + """ + if not user_tree.children(user_tree.root): + node_key = Node.empty_key + node_value = Node.empty_value + user_tree.create_node( + identifier=node_key, tag=node_value, + parent=user_tree.root, + ) + @timeit def get_user_tree(self): if self._user_tree: @@ -207,6 +248,7 @@ class AssetPermissionUtilV2: self.add_direct_nodes_to_user_tree(user_tree) self.add_single_assets_node_to_user_tree(user_tree) self.parse_user_tree_to_full_tree(user_tree) + self.add_empty_node_if_need(user_tree) self._user_tree = user_tree return user_tree diff --git a/apps/perms/utils/remote_app_permission.py b/apps/perms/utils/remote_app_permission.py index a612b9ffb..ae60766ae 100644 --- a/apps/perms/utils/remote_app_permission.py +++ b/apps/perms/utils/remote_app_permission.py @@ -74,6 +74,14 @@ def construct_remote_apps_tree_root(): def parse_remote_app_to_tree_node(parent, remote_app): + system_user = remote_app.system_user + user = { + 'id': system_user.id, + 'name': system_user.name, + 'username': system_user.username, + 'protocol': system_user.protocol, + 'login_mode': system_user.login_mode, + } tree_node = { 'id': remote_app.id, 'name': remote_app.name, @@ -82,6 +90,6 @@ def parse_remote_app_to_tree_node(parent, remote_app): 'open': False, 'isParent': False, 'iconSkin': 'file', - 'meta': {'type': 'remote_app'} + 'meta': {'type': 'remote_app', 'user': user} } return TreeNode(**tree_node) diff --git a/apps/users/templates/users/_granted_assets.html b/apps/users/templates/users/_granted_assets.html index 25240ee03..7904af4fb 100644 --- a/apps/users/templates/users/_granted_assets.html +++ b/apps/users/templates/users/_granted_assets.html @@ -4,6 +4,7 @@
+ {% trans 'Loading' %} ...