You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jumpserver/apps/perms/utils/user_perm_tree.py

414 lines
15 KiB

import time
from collections import defaultdict
from django.conf import settings
from django.core.cache import cache
from django.db import transaction
from assets.models import Asset
from assets.utils import NodeAssetsUtil
from common.db.models import output_as_string
from common.decorators import on_transaction_commit, merge_delay_run
from common.utils import get_logger
from common.utils.common import lazyproperty, timeit
from orgs.models import Organization
from orgs.utils import (
current_org,
tmp_to_org,
tmp_to_root_org
)
from perms.locks import UserGrantedTreeRebuildLock
from perms.models import (
AssetPermission,
UserAssetGrantedTreeNodeRelation,
PermNode
)
from users.models import User
from . import UserPermAssetUtil
from .permission import AssetPermissionUtil
logger = get_logger(__name__)
__all__ = [
'UserPermTreeRefreshUtil',
'UserPermTreeExpireUtil'
]
class _UserPermTreeCacheMixin:
""" 缓存数据 users: {org_id, org_id }, 记录用户授权树已经构建完成的组织集合 """
cache_key_template = 'perms.user.node_tree.built_orgs.user_id:{user_id}'
def get_cache_key(self, user_id):
return self.cache_key_template.format(user_id=user_id)
@lazyproperty
def client(self):
return cache.client.get_client(write=True)
class UserPermTreeRefreshUtil(_UserPermTreeCacheMixin):
""" 用户授权树刷新工具, 针对某一个用户授权树的刷新 """
def __init__(self, user):
self.user = user
@lazyproperty
def orgs(self):
return self.user.orgs.distinct()
@lazyproperty
def org_ids(self):
return [str(o.id) for o in self.orgs]
@lazyproperty
def cache_key_user(self):
return self.get_cache_key(self.user.id)
@lazyproperty
def cache_key_time(self):
key = 'perms.user.node_tree.built_time.{}'.format(self.user.id)
return key
@timeit
def refresh_if_need(self, force=False):
built_just_now = False if settings.ASSET_SIZE == 'small' else cache.get(self.cache_key_time)
if built_just_now:
logger.info('Refresh user perm tree just now, pass: {}'.format(built_just_now))
return
to_refresh_orgs = self.orgs if force else self._get_user_need_refresh_orgs()
if not to_refresh_orgs:
logger.info('Not have to refresh orgs')
return
logger.info("Delay refresh user orgs: {} {}".format(self.user, [o.name for o in to_refresh_orgs]))
sync = True if settings.ASSET_SIZE == 'small' else False
refresh_user_orgs_perm_tree.apply(sync=sync, user_orgs=((self.user, tuple(to_refresh_orgs)),))
with tmp_to_root_org():
refresh_user_favorite_assets.apply(sync=sync, users=(self.user,))
@timeit
def refresh_tree_manual(self):
"""
用来手动 debug
:return:
"""
built_just_now = cache.get(self.cache_key_time)
if built_just_now:
logger.info('Refresh just now, pass: {}'.format(built_just_now))
return
to_refresh_orgs = self._get_user_need_refresh_orgs()
if not to_refresh_orgs:
logger.info('Not have to refresh orgs for user: {}'.format(self.user))
return
self.perform_refresh_user_tree(to_refresh_orgs)
@timeit
def perform_refresh_user_tree(self, to_refresh_orgs):
# 再判断一次,毕竟构建树比较慢
built_just_now = cache.get(self.cache_key_time)
if built_just_now:
logger.info('Refresh user perm tree just now, pass: {}'.format(built_just_now))
return
self._clean_user_perm_tree_for_legacy_org()
if settings.ASSET_SIZE != 'small':
ttl = settings.PERM_TREE_REGEN_INTERVAL
cache.set(self.cache_key_time, int(time.time()), ttl)
lock = UserGrantedTreeRebuildLock(self.user.id)
got = lock.acquire(blocking=False)
if not got:
logger.info('User perm tree rebuild lock not acquired, pass')
return
try:
for org in to_refresh_orgs:
self._rebuild_user_perm_tree_for_org(org)
self._mark_user_orgs_refresh_finished(to_refresh_orgs)
finally:
lock.release()
def _rebuild_user_perm_tree_for_org(self, org):
with tmp_to_org(org):
start = time.time()
UserPermTreeBuildUtil(self.user).rebuild_user_perm_tree()
end = time.time()
logger.info(
'Refresh user perm tree: [{user}] org [{org}] {use_time:.2f}s'
''.format(user=self.user, org=org, use_time=end - start)
)
def _clean_user_perm_tree_for_legacy_org(self):
with tmp_to_root_org():
""" Clean user legacy org node relations """
user_relations = UserAssetGrantedTreeNodeRelation.objects.filter(user=self.user)
user_legacy_org_relations = user_relations.exclude(org_id__in=self.org_ids)
user_legacy_org_relations.delete()
def _get_user_need_refresh_orgs(self):
cached_org_ids = self.client.smembers(self.cache_key_user)
cached_org_ids = {oid.decode() for oid in cached_org_ids}
to_refresh_org_ids = set(self.org_ids) - cached_org_ids
to_refresh_orgs = list(Organization.objects.filter(id__in=to_refresh_org_ids))
logger.info(f'Need to refresh orgs: {to_refresh_orgs}')
return to_refresh_orgs
def _mark_user_orgs_refresh_finished(self, orgs):
org_ids = [str(org.id) for org in orgs]
self.client.sadd(self.cache_key_user, *org_ids)
class UserPermTreeExpireUtil(_UserPermTreeCacheMixin):
""" 用户授权树过期工具 """
@lazyproperty
def cache_key_all_user(self):
return self.get_cache_key('*')
def expire_perm_tree_for_nodes_assets(self, node_ids, asset_ids):
node_perm_ids = AssetPermissionUtil().get_permissions_for_nodes(node_ids, flat=True)
asset_perm_ids = AssetPermissionUtil().get_permissions_for_assets(asset_ids, flat=True)
perm_ids = set(node_perm_ids) | set(asset_perm_ids)
self.expire_perm_tree_for_perms(perm_ids)
@tmp_to_root_org()
def expire_perm_tree_for_perms(self, perm_ids):
org_perm_ids = AssetPermission.objects.filter(id__in=perm_ids).values_list('org_id', 'id')
org_perms_mapper = defaultdict(set)
for org_id, perm_id in org_perm_ids:
org_perms_mapper[org_id].add(perm_id)
for org_id, perms_id in org_perms_mapper.items():
user_ids = AssetPermission.get_all_users_for_perms(perm_ids, flat=True)
self.expire_perm_tree_for_users_orgs(user_ids, [org_id])
def expire_perm_tree_for_user_group(self, user_group):
group_ids = [user_group.id]
org_ids = [user_group.org_id]
self.expire_perm_tree_for_user_groups_orgs(group_ids, org_ids)
def expire_perm_tree_for_user_groups_orgs(self, group_ids, org_ids):
user_ids = User.groups.through.objects \
.filter(usergroup_id__in=group_ids) \
.values_list('user_id', flat=True).distinct()
self.expire_perm_tree_for_users_orgs(user_ids, org_ids)
@on_transaction_commit
def expire_perm_tree_for_users_orgs(self, user_ids, org_ids):
user_ids = list(user_ids)
org_ids = [str(oid) for oid in org_ids]
with self.client.pipeline() as p:
for uid in user_ids:
cache_key = self.get_cache_key(uid)
p.srem(cache_key, *org_ids)
p.execute()
users_display = ','.join([str(i) for i in user_ids[:3]])
if len(user_ids) > 3:
users_display += '...'
orgs_display = ','.join([str(i) for i in org_ids[:3]])
if len(org_ids) > 3:
orgs_display += '...'
logger.info('Expire perm tree for users: [{}], orgs: [{}]'.format(users_display, orgs_display))
def expire_perm_tree_for_all_user(self):
keys = self.client.keys(self.cache_key_all_user)
with self.client.pipeline() as p:
for k in keys:
p.delete(k)
p.execute()
logger.info('Expire all user perm tree')
@merge_delay_run(ttl=20)
def refresh_user_orgs_perm_tree(user_orgs=()):
for user, orgs in user_orgs:
util = UserPermTreeRefreshUtil(user)
util.perform_refresh_user_tree(orgs)
@merge_delay_run(ttl=20)
def refresh_user_favorite_assets(users=()):
for user in users:
util = UserPermAssetUtil(user)
util.refresh_favorite_assets()
util.refresh_type_nodes_tree_cache()
class UserPermTreeBuildUtil(object):
node_only_fields = ('id', 'key', 'parent_key', 'org_id')
def __init__(self, user):
self.user = user
self.user_perm_ids = AssetPermissionUtil().get_permissions_for_user(self.user, flat=True)
# {key: node}
self._perm_nodes_key_node_mapper = {}
def rebuild_user_perm_tree(self):
with transaction.atomic():
self.clean_user_perm_tree()
if not self.user_perm_ids:
logger.info('User({}) not have permissions'.format(self.user))
return
self.compute_perm_nodes()
self.compute_perm_nodes_asset_amount()
self.create_mapping_nodes()
def clean_user_perm_tree(self):
UserAssetGrantedTreeNodeRelation.objects.filter(user=self.user).delete()
def compute_perm_nodes(self):
self._compute_perm_nodes_for_direct()
self._compute_perm_nodes_for_direct_asset_if_need()
self._compute_perm_nodes_for_ancestor()
def compute_perm_nodes_asset_amount(self):
""" 这里计算的是一个组织的授权树 """
computed = self._only_compute_root_node_assets_amount_if_need()
if computed:
return
nodekey_assetid_mapper = defaultdict(set)
org_id = current_org.id
for key in self.perm_node_keys_for_granted:
asset_ids = PermNode.get_all_asset_ids_by_node_key(org_id, key)
nodekey_assetid_mapper[key].update(asset_ids)
for asset_id, node_id in self.direct_asset_id_node_id_pairs:
node_key = self.perm_nodes_id_key_mapper.get(node_id)
if not node_key:
continue
nodekey_assetid_mapper[node_key].add(asset_id)
util = NodeAssetsUtil(self.perm_nodes, nodekey_assetid_mapper)
util.generate()
for node in self.perm_nodes:
assets_amount = util.get_assets_amount(node.key)
node.assets_amount = assets_amount
def create_mapping_nodes(self):
to_create = []
for node in self.perm_nodes:
relation = UserAssetGrantedTreeNodeRelation(
user=self.user,
node=node,
node_key=node.key,
node_parent_key=node.parent_key,
node_from=node.node_from,
node_assets_amount=node.assets_amount,
org_id=node.org_id
)
to_create.append(relation)
UserAssetGrantedTreeNodeRelation.objects.bulk_create(to_create)
def _compute_perm_nodes_for_direct(self):
""" 直接授权的节点(叶子节点)"""
for node in self.direct_nodes:
if self.has_any_ancestor_direct_permed(node):
continue
node.node_from = node.NodeFrom.granted
self._perm_nodes_key_node_mapper[node.key] = node
def _compute_perm_nodes_for_direct_asset_if_need(self):
""" 直接授权的资产所在的节点(叶子节点)"""
if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
return
for node in self.direct_asset_nodes:
if self.has_any_ancestor_direct_permed(node):
continue
if node.key in self._perm_nodes_key_node_mapper:
continue
node.node_from = node.NodeFrom.asset
self._perm_nodes_key_node_mapper[node.key] = node
def _compute_perm_nodes_for_ancestor(self):
""" 直接授权节点 和 直接授权资产所在节点 的所有祖先节点 (构造完整树) """
ancestor_keys = set()
for node in self._perm_nodes_key_node_mapper.values():
ancestor_keys.update(node.get_ancestor_keys())
ancestor_keys -= set(self._perm_nodes_key_node_mapper.keys())
ancestors = PermNode.objects.filter(key__in=ancestor_keys).only(*self.node_only_fields)
for node in ancestors:
node.node_from = node.NodeFrom.child
self._perm_nodes_key_node_mapper[node.key] = node
@lazyproperty
def perm_node_keys_for_granted(self):
keys = [
key for key, node in self._perm_nodes_key_node_mapper.items()
if node.node_from == node.NodeFrom.granted
]
return keys
@lazyproperty
def perm_nodes_id_key_mapper(self):
mapper = {
node.id.hex: node.key
for key, node in self._perm_nodes_key_node_mapper.items()
}
return mapper
def _only_compute_root_node_assets_amount_if_need(self):
if len(self.perm_nodes) != 1:
return False
root_node = self.perm_nodes[0]
if not root_node.is_org_root():
return False
if root_node.node_from != root_node.NodeFrom.granted:
return False
root_node.granted_assets_amount = len(root_node.get_all_asset_ids())
return True
@lazyproperty
def perm_nodes(self):
""" 授权树的所有节点 """
return list(self._perm_nodes_key_node_mapper.values())
def has_any_ancestor_direct_permed(self, node):
""" 任何一个祖先节点被直接授权 """
return bool(set(node.get_ancestor_keys()) & set(self.direct_node_keys))
@lazyproperty
def direct_node_keys(self):
""" 直接授权的节点 keys """
return {n.key for n in self.direct_nodes}
@lazyproperty
def direct_nodes(self):
""" 直接授权的节点 """
node_ids = AssetPermission.nodes.through.objects \
.filter(assetpermission_id__in=self.user_perm_ids) \
.values_list('node_id', flat=True).distinct()
nodes = PermNode.objects.filter(id__in=node_ids).only(*self.node_only_fields)
return nodes
@lazyproperty
def direct_asset_nodes(self):
""" 获取直接授权的资产所在的节点 """
node_ids = [node_id for asset_id, node_id in self.direct_asset_id_node_id_pairs]
nodes = PermNode.objects.filter(id__in=node_ids).distinct().only(*self.node_only_fields)
return nodes
@lazyproperty
def direct_asset_id_node_id_pairs(self):
""" 直接授权的资产 id 和 节点 id """
asset_node_pairs = Asset.nodes.through.objects \
.filter(asset_id__in=self.direct_asset_ids) \
.annotate(
str_asset_id=output_as_string('asset_id'),
str_node_id=output_as_string('node_id')
).values_list('str_asset_id', 'str_node_id')
asset_node_pairs = list(asset_node_pairs)
return asset_node_pairs
@lazyproperty
def direct_asset_ids(self):
""" 直接授权的资产 ids """
asset_ids = AssetPermission.assets.through.objects \
.filter(assetpermission_id__in=self.user_perm_ids) \
.values_list('asset_id', flat=True) \
.distinct()
return asset_ids