From d23446016dadad78245ae66535f68fc9b509d418 Mon Sep 17 00:00:00 2001 From: "Jiangjie.Bai" Date: Thu, 15 Sep 2022 10:46:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B7=BB=E5=8A=A0=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=9B=B8=E5=85=B3=E7=9A=84=E8=8E=B7=E5=8F=96=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E8=B4=A6=E5=8F=B7API=E3=80=81Model=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/asset/common.py | 24 +++- apps/perms/api/user_permission/common.py | 38 +++++++ apps/perms/api/user_permission/nodes.py | 6 +- apps/perms/models/asset_permission.py | 131 +++++++++++++++++++--- apps/perms/serializers/user_permission.py | 18 ++- apps/perms/urls/asset_permission.py | 19 +--- apps/users/models/user.py | 15 +++ generateV3Data.py | 38 +++++++ 8 files changed, 250 insertions(+), 39 deletions(-) create mode 100644 generateV3Data.py diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index a9396e17c..fe8027cbd 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -5,8 +5,10 @@ import logging import uuid from functools import reduce +from collections import Iterable from django.db import models +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty @@ -57,13 +59,17 @@ class NodesRelationMixin: return nodes def get_all_nodes(self, flat=False): - nodes = [] + from ..node import Node + node_keys = set() for node in self.get_nodes(): - _nodes = node.get_ancestors(with_self=True) - nodes.append(_nodes) + ancestor_keys = node.get_ancestor_keys(with_self=True) + node_keys.update(ancestor_keys) + nodes = Node.objects.filter(key__in=node_keys).distinct() if flat: - nodes = list(reduce(lambda x, y: set(x) | set(y), nodes)) - return nodes + node_ids = set(nodes.values_list('id', flat=True)) + return node_ids + else: + return nodes class Asset(AbsConnectivity, NodesRelationMixin, JMSOrgBaseModel): @@ -161,6 +167,14 @@ class Asset(AbsConnectivity, NodesRelationMixin, JMSOrgBaseModel): tree_node = TreeNode(**data) return tree_node + def filter_accounts(self, account_names=None): + if account_names is None: + return self.accounts.all() + assert isinstance(account_names, Iterable), '`account_names` must be an iterable object' + queries = Q(name__in=account_names) | Q(username__in=account_names) + accounts = self.accounts.filter(queries) + return accounts + class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset") diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py index f4c615921..87b78fe8c 100644 --- a/apps/perms/api/user_permission/common.py +++ b/apps/perms/api/user_permission/common.py @@ -3,6 +3,7 @@ import uuid import time +from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from rest_framework.views import APIView, Response @@ -20,6 +21,7 @@ from common.utils import get_logger, lazyproperty from perms.hands import User, Asset from perms import serializers +from perms.models import AssetPermission logger = get_logger(__name__) @@ -28,6 +30,8 @@ __all__ = [ 'ValidateUserAssetPermissionApi', 'GetUserAssetPermissionActionsApi', 'MyGrantedAssetSystemUsersApi', + 'UserGrantedAssetAccounts', + 'MyGrantedAssetAccounts', ] @@ -138,3 +142,37 @@ class MyGrantedAssetSystemUsersApi(UserGrantedAssetSystemUsersForAdminApi): def user(self): return self.request.user + +class UserGrantedAssetAccounts(ListAPIView): + serializer_class = serializers.AccountsGrantedSerializer + rbac_perms = { + 'list': 'perms.view_userassets' + } + + @lazyproperty + def user(self): + user_id = self.kwargs.get('pk') + return User.objects.get(id=user_id) + + @lazyproperty + def asset(self): + asset_id = self.kwargs.get('asset_id') + kwargs = {'id': asset_id, 'is_active': True} + asset = get_object_or_404(Asset, **kwargs) + return asset + + def get_queryset(self): + # 获取用户-资产的授权规则 + assetperms = AssetPermission.filter_permissions(self.user, self.asset) + account_names = AssetPermission.get_account_names(assetperms) + accounts = self.asset.filter_accounts(account_names) + # 构造默认包含的账号,如: @INPUT @USER + return accounts + + +class MyGrantedAssetAccounts(UserGrantedAssetAccounts): + permission_classes = (IsValidUser,) + + @lazyproperty + def user(self): + return self.request.user diff --git a/apps/perms/api/user_permission/nodes.py b/apps/perms/api/user_permission/nodes.py index 7dcbd85e2..74f0b6728 100644 --- a/apps/perms/api/user_permission/nodes.py +++ b/apps/perms/api/user_permission/nodes.py @@ -19,7 +19,7 @@ from perms.utils.user_permission import UserGrantedNodesQueryUtils logger = get_logger(__name__) __all__ = [ - 'UserGrantedNodesForAdminApi', + 'UserGrantedNodesApi', 'MyGrantedNodesApi', 'MyGrantedNodesAsTreeApi', 'UserGrantedNodeChildrenForAdminApi', @@ -118,11 +118,11 @@ class MyGrantedNodeChildrenAsTreeApi(AssetRoleUserMixin, UserGrantedNodeChildren return permissions -class UserGrantedNodesForAdminApi(AssetRoleAdminMixin, UserGrantedNodesMixin, BaseGrantedNodeApi): +class UserGrantedNodesApi(AssetRoleAdminMixin, UserGrantedNodesMixin, BaseGrantedNodeApi): pass -class MyGrantedNodesApi(AssetRoleUserMixin, UserGrantedNodesMixin, BaseGrantedNodeApi): +class MyGrantedNodesApi(AssetRoleUserMixin, UserGrantedNodesApi): pass diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 04b90c0ca..61f5a4fd2 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -1,17 +1,17 @@ import uuid import logging +from functools import reduce from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.db import models from django.db.models import F, Q, TextChoices -from assets.models import Asset, Node, FamilyMixin +from assets.models import Asset, Node, FamilyMixin, Account from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgManager from common.utils import lazyproperty, date_expired_default from common.db.models import BaseCreateUpdateModel, BitOperationChoice, UnionQuerySet - __all__ = [ 'AssetPermission', 'PermNode', 'UserAssetGrantedTreeNodeRelation', @@ -85,20 +85,27 @@ class AssetPermissionManager(OrgManager): class AssetPermission(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) - users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss') - user_groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss') - assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) - nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) + users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), + related_name='%(class)ss') + user_groups = models.ManyToManyField('users.UserGroup', blank=True, + verbose_name=_("User group"), related_name='%(class)ss') + assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', + blank=True, verbose_name=_("Asset")) + nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, + verbose_name=_("Nodes")) # 只保存 @ALL (@INPUT @USER 默认包含,将来在全局设置中进行控制) # 特殊的账号描述 # ['@ALL',] # 指定账号授权 # ['web', 'root',] accounts = models.JSONField(default=list, verbose_name=_("Accounts")) - actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_("Actions")) + actions = models.IntegerField(choices=Action.DB_CHOICES, default=Action.ALL, + verbose_name=_("Actions")) is_active = models.BooleanField(default=True, verbose_name=_('Active')) - date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) - date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired')) + date_start = models.DateTimeField(default=timezone.now, db_index=True, + verbose_name=_("Date start")) + date_expired = models.DateTimeField(default=date_expired_default, db_index=True, + verbose_name=_('Date expired')) created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket')) @@ -106,6 +113,11 @@ class AssetPermission(OrgModelMixin): objects = AssetPermissionManager.from_queryset(AssetPermissionQuerySet)() + class SpecialAccount(models.TextChoices): + ALL = '@ALL', 'All' + INPUT = '@INPUT', 'Input' + USER = '@USER', 'User' + class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset permission") @@ -174,14 +186,17 @@ class AssetPermission(OrgModelMixin): models.Prefetch('assets', queryset=Asset.objects.all().only('id')), ).order_by() - def get_all_assets(self): + def get_all_assets(self, flat=False): from assets.models import Node nodes_keys = self.nodes.all().values_list('key', flat=True) asset_ids = set(self.assets.all().values_list('id', flat=True)) nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys) asset_ids.update(nodes_asset_ids) - assets = Asset.objects.filter(id__in=asset_ids) - return assets + if flat: + return asset_ids + else: + assets = Asset.objects.filter(id__in=asset_ids) + return assets def users_display(self): names = [user.username for user in self.users.all()] @@ -199,6 +214,94 @@ class AssetPermission(OrgModelMixin): names = [node.full_value for node in self.nodes.all()] return names + def get_asset_accounts(self): + asset_ids = self.get_all_assets(flat=True) + queries = Q(asset_id__in=asset_ids) \ + & (Q(username__in=self.accounts) | Q(name__in=self.accounts)) + accounts = Account.objects.filter(queries) + return accounts + + @classmethod + def get_account_names(cls, perms): + account_names = set() + for perm in perms: + perm: cls + if not isinstance(perm.accounts, list): + continue + account_names.update(perm.accounts) + return account_names + + @classmethod + def filter_permissions(cls, user=None, asset=None, account=None): + """ 获取同时包含 用户-资产-账号 的授权规则 """ + assetperm_ids = [] + if user: + user_assetperm_ids = cls.filter_permissions_by_user(user, flat=True) + assetperm_ids.append(user_assetperm_ids) + if asset: + asset_assetperm_ids = cls.filter_permissions_by_asset(asset, flat=True) + assetperm_ids.append(asset_assetperm_ids) + if account: + account_assetperm_ids = cls.filter_permissions_by_account(account, flat=True) + assetperm_ids.append(account_assetperm_ids) + # & 是同时满足,比如有用户,但是用户的规则是空,那么返回也应该是空 + assetperm_ids = list(reduce(lambda x, y: set(x) & set(y), assetperm_ids)) + assetperms = cls.objects.filter(id__in=assetperm_ids).valid().order_by('-date_expired') + return assetperms + + @classmethod + def filter_permissions_by_user(cls, user, with_group=True, flat=False): + assetperm_ids = set() + user_assetperm_ids = AssetPermission.users.through.objects \ + .filter(user_id=user.id) \ + .values_list('assetpermission_id', flat=True) \ + .distinct() + assetperm_ids.update(user_assetperm_ids) + + if with_group: + usergroup_ids = user.get_groups(flat=True) + usergroups_assetperm_id = AssetPermission.user_groups.through.objects \ + .filter(usergroup_id__in=usergroup_ids) \ + .values_list('assetpermission_id', flat=True) \ + .distinct() + assetperm_ids.update(usergroups_assetperm_id) + + if flat: + return assetperm_ids + else: + assetperms = cls.objects.filter(id__in=assetperm_ids).valid() + return assetperms + + @classmethod + def filter_permissions_by_asset(cls, asset, with_node=True, flat=False): + assetperm_ids = set() + asset_assetperm_ids = AssetPermission.assets.through.objects \ + .filter(asset_id=asset.id) \ + .values_list('assetpermission_id', flat=True) + assetperm_ids.update(asset_assetperm_ids) + + if with_node: + node_ids = asset.get_all_nodes(flat=True) + node_assetperm_ids = AssetPermission.nodes.through.objects \ + .filter(node_id__in=node_ids) \ + .values_list('assetpermission_id', flat=True) + assetperm_ids.update(node_assetperm_ids) + + if flat: + return assetperm_ids + else: + assetperms = cls.objects.filter(id__in=assetperm_ids).valid() + return assetperms + + @classmethod + def filter_permissions_by_account(cls, account, flat=False): + assetperms = cls.objects.filter(accounts__contains=account).valid() + if flat: + assetperm_ids = assetperms.values_list('id', flat=True) + return assetperm_ids + else: + return assetperms + class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, BaseCreateUpdateModel): class NodeFrom(TextChoices): @@ -210,7 +313,8 @@ class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, BaseCreateUpd node = models.ForeignKey('assets.Node', default=None, on_delete=models.CASCADE, db_constraint=False, null=False, related_name='granted_node_rels') node_key = models.CharField(max_length=64, verbose_name=_("Key"), db_index=True) - node_parent_key = models.CharField(max_length=64, default='', verbose_name=_('Parent key'), db_index=True) + node_parent_key = models.CharField(max_length=64, default='', verbose_name=_('Parent key'), + db_index=True) node_from = models.CharField(choices=NodeFrom.choices, max_length=16, db_index=True) node_assets_amount = models.IntegerField(default=0) @@ -297,4 +401,3 @@ class PermedAsset(Asset): ('view_userassets', _('Can view user assets')), ('view_usergroupassets', _('Can view usergroup assets')), ] - diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index f7f541e7a..c1d24ba05 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -4,20 +4,19 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from assets.models import Node, Asset, Platform +from assets.models import Node, Asset, Platform, Account from perms.serializers.permission import ActionsField __all__ = [ 'NodeGrantedSerializer', 'AssetGrantedSerializer', 'ActionsSerializer', + 'AccountsGrantedSerializer' ] class AssetGrantedSerializer(serializers.ModelSerializer): - """ - 被授权资产的数据结构 - """ + """ 被授权资产的数据结构 """ platform = serializers.SlugRelatedField( slug_field='name', queryset=Platform.objects.all(), label=_("Platform") ) @@ -44,3 +43,14 @@ class NodeGrantedSerializer(serializers.ModelSerializer): class ActionsSerializer(serializers.Serializer): actions = ActionsField(read_only=True) + +class AccountsGrantedSerializer(serializers.ModelSerializer): + """ 授权的账号序列类 """ + + # Todo: 添加前端登录逻辑中需要的一些字段,比如:是否需要手动输入密码 + # need_manual = serializers.BooleanField(label=_('Need manual input')) + + class Meta: + model = Account + fields = ['id', 'name', 'username'] + read_only_fields = fields diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index 7099aa137..28af1a94d 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -5,7 +5,6 @@ from rest_framework_bulk.routes import BulkRouter from .. import api -# v3 Done router = BulkRouter() router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, 'asset-permissions-users-relation') @@ -14,42 +13,31 @@ router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRe router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation') user_permission_urlpatterns = [ - # 统一说明: - # ``: `User.pk` - # 直接授权:在 `AssetPermission` 中关联的对象 - - # --------------------------------------------------------- # 以 serializer 格式返回 path('/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'), path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'), - # Tree Node 的数据格式返回 path('/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'), path('assets/tree/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), path('ungroup/assets/tree/', api.MyUngroupAssetsAsTreeApi.as_view(), name='my-ungroup-assets-as-tree'), - # ^--------------------------------------------------------^ # 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点 # 以 serializer 格式返回 - path('/nodes/', api.UserGrantedNodesForAdminApi.as_view(), name='user-nodes'), + path('/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'), - # 以 Tree Node 的数据格式返回 path('/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), - # ^--------------------------------------------------------^ # 一层一层的获取用户授权的节点, # 以 Serializer 的数据格式返回 path('/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'), path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'), - # 以 Tree Node 的数据格式返回 path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(), name='user-nodes-children-as-tree'), # 部分调用位置 # - 普通用户 -> 我的资产 -> 展开节点 时调用 path('nodes/children/tree/', api.MyGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), - # ^--------------------------------------------------------^ # 此接口会返回整棵树 # 普通用户 -> 命令执行 -> 左侧树 @@ -75,6 +63,11 @@ user_permission_urlpatterns = [ # Asset System users path('/assets//system-users/', api.UserGrantedAssetSystemUsersForAdminApi.as_view(), name='user-asset-system-users'), path('assets//system-users/', api.MyGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), + + # Todo: 增加 + # 获取所有和资产相关联的账号列表 + path('/assets//accounts/', api.UserGrantedAssetAccounts.as_view(), name='user-asset-accounts'), + path('assets//accounts/', api.MyGrantedAssetAccounts.as_view(), name='my-asset-accounts') ] user_group_permission_urlpatterns = [ diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 78d5eb540..659a894a9 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -918,6 +918,21 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): return True return False + def get_groups(self, flat=False): + from users.models import UserGroup + usergroup_ids = self.groups.through.objects\ + .filter(user_id=self.id)\ + .distinct()\ + .values_list('usergroup_id', flat=True) + usergroups = UserGroup.objects.filter(id__in=usergroup_ids) + if flat: + usergroup_ids = usergroups.values_list('id', flat=True) + return usergroup_ids + else: + return usergroups + + + class UserPasswordHistory(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) diff --git a/generateV3Data.py b/generateV3Data.py new file mode 100644 index 000000000..673037e99 --- /dev/null +++ b/generateV3Data.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# + +# >>> Django 环境配置 +import django +import os +import sys + +if os.path.exists('../apps'): + sys.path.insert(0, '../apps') +elif os.path.exists('./apps'): + sys.path.insert(0, './apps') + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +APPS_DIR = os.path.join(BASE_DIR, 'apps') +sys.path.insert(0, APPS_DIR) + +os.environ.setdefault('PYTHONOPTIMIZE', '1') +if os.getuid() == 0: + os.environ.setdefault('C_FORCE_ROOT', '1') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings") +django.setup() + +# <<< + + +class Generator(object): + + def generate(self): + pass + + def generate_assets(self): + pass + + +if __name__ == '__main__': + pass