jumpserver/apps/perms/utils/asset/user_permission.py

525 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from functools import reduce, wraps
from operator import or_, and_
from uuid import uuid4
import threading
import inspect
from django.conf import settings
from django.db.models import F, Q, Value, BooleanField
from django.utils.translation import ugettext_lazy as _
from common.http import is_true
from common.utils import get_logger
from common.const.distributed_lock_key import UPDATE_MAPPING_NODE_TASK_LOCK_KEY
from orgs.utils import tmp_to_root_org
from common.utils.timezone import dt_formater, now
from assets.models import Node, Asset, FavoriteAsset
from django.db.transaction import atomic
from orgs import lock
from perms.models import UserGrantedMappingNode, RebuildUserTreeTask, AssetPermission
from users.models import User
logger = get_logger(__name__)
ADD = 'add'
REMOVE = 'remove'
UNGROUPED_NODE_KEY = 'ungrouped'
UNGROUPED_NODE_VALUE = _('Ungrouped')
FAVORITE_NODE_KEY = 'favorite'
FAVORITE_NODE_VALUE = _('Favorite')
TMP_GRANTED_FIELD = '_granted'
TMP_ASSET_GRANTED_FIELD = '_asset_granted'
TMP_GRANTED_ASSETS_AMOUNT_FIELD = '_granted_assets_amount'
# 使用场景
# `Node.objects.annotate(**node_annotate_mapping_node)`
node_annotate_mapping_node = {
TMP_GRANTED_FIELD: F('mapping_nodes__granted'),
TMP_ASSET_GRANTED_FIELD: F('mapping_nodes__asset_granted'),
TMP_GRANTED_ASSETS_AMOUNT_FIELD: F('mapping_nodes__assets_amount')
}
# 使用场景
# `Node.objects.annotate(**node_annotate_set_granted)`
node_annotate_set_granted = {
TMP_GRANTED_FIELD: Value(True, output_field=BooleanField()),
}
def is_direct_granted_by_annotate(node):
return getattr(node, TMP_GRANTED_FIELD, False)
def is_asset_granted(node):
return getattr(node, TMP_ASSET_GRANTED_FIELD, False)
def get_granted_assets_amount(node):
return getattr(node, TMP_GRANTED_ASSETS_AMOUNT_FIELD, 0)
def set_granted(obj):
setattr(obj, TMP_GRANTED_FIELD, True)
def set_asset_granted(obj):
setattr(obj, TMP_ASSET_GRANTED_FIELD, True)
VALUE_TEMPLATE = '{stage}:{rand_str}:thread:{thread_name}:{thread_id}:{now}'
def _generate_value(stage=lock.DOING):
cur_thread = threading.current_thread()
return VALUE_TEMPLATE.format(
stage=stage,
thread_name=cur_thread.name,
thread_id=cur_thread.ident,
now=dt_formater(now()),
rand_str=uuid4()
)
def build_user_mapping_node_lock(func):
@wraps(func)
def wrapper(*args, **kwargs):
call_args = inspect.getcallargs(func, *args, **kwargs)
user = call_args.get('user')
if user is None or not isinstance(user, User):
raise ValueError('You function must have `user` argument')
key = UPDATE_MAPPING_NODE_TASK_LOCK_KEY.format(user_id=user.id)
doing_value = _generate_value()
commiting_value = _generate_value(stage=lock.COMMITING)
try:
locked = lock.acquire(key, doing_value, timeout=600)
if not locked:
logger.error(f'update_mapping_node_task_locked_failed for user: {user.id}')
raise lock.SomeoneIsDoingThis
with atomic(savepoint=False):
func(*args, **kwargs)
ok = lock.change_lock_state_to_commiting(key, doing_value, commiting_value)
if not ok:
logger.error(f'update_mapping_node_task_timeout for user: {user.id}')
raise lock.Timeout
finally:
lock.release(key, commiting_value, doing_value)
return wrapper
@build_user_mapping_node_lock
def rebuild_user_mapping_nodes_if_need_with_lock(user: User):
tasks = RebuildUserTreeTask.objects.filter(user=user)
if tasks:
tasks.delete()
rebuild_user_mapping_nodes(user)
@build_user_mapping_node_lock
def rebuild_user_mapping_nodes_with_lock(user: User):
rebuild_user_mapping_nodes(user)
def compute_tmp_mapping_node_from_perm(user: User, asset_perms_id=None):
node_only_fields = ('id', 'key', 'parent_key', 'assets_amount')
if asset_perms_id is None:
asset_perms_id = get_user_all_assetpermissions_id(user)
# 查询直接授权节点
nodes = Node.objects.filter(
granted_by_permissions__id__in=asset_perms_id
).distinct().only(*node_only_fields)
granted_key_set = {_node.key for _node in nodes}
def _has_ancestor_granted(node):
"""
判断一个节点是否有授权过的祖先节点
"""
ancestor_keys = set(node.get_ancestor_keys())
return ancestor_keys & granted_key_set
key2leaf_nodes_mapper = {}
# 给授权节点设置 _granted 标识,同时去重
for _node in nodes:
if _has_ancestor_granted(_node):
continue
if _node.key not in key2leaf_nodes_mapper:
set_granted(_node)
key2leaf_nodes_mapper[_node.key] = _node
# 查询授权资产关联的节点设置
def process_direct_granted_assets():
# 查询直接授权资产
asset_ids = Asset.objects.filter(
granted_by_permissions__id__in=asset_perms_id
).distinct().values_list('id', flat=True)
# 查询授权资产关联的节点设置
granted_asset_nodes = Node.objects.filter(
assets__id__in=asset_ids
).distinct().only(*node_only_fields)
# 给资产授权关联的节点设置 _asset_granted 标识,同时去重
for _node in granted_asset_nodes:
if _has_ancestor_granted(_node):
continue
if _node.key not in key2leaf_nodes_mapper:
key2leaf_nodes_mapper[_node.key] = _node
set_asset_granted(key2leaf_nodes_mapper[_node.key])
if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
process_direct_granted_assets()
leaf_nodes = key2leaf_nodes_mapper.values()
# 计算所有祖先节点
ancestor_keys = set()
for _node in leaf_nodes:
ancestor_keys.update(_node.get_ancestor_keys())
# 从祖先节点 key 中去掉同时也是叶子节点的 key
ancestor_keys -= key2leaf_nodes_mapper.keys()
# 查出祖先节点
ancestors = Node.objects.filter(key__in=ancestor_keys).only(*node_only_fields)
return [*leaf_nodes, *ancestors]
def create_mapping_nodes(user, nodes):
to_create = []
for node in nodes:
_granted = getattr(node, TMP_GRANTED_FIELD, False)
_asset_granted = getattr(node, TMP_ASSET_GRANTED_FIELD, False)
_granted_assets_amount = getattr(node, TMP_GRANTED_ASSETS_AMOUNT_FIELD, 0)
to_create.append(UserGrantedMappingNode(
user=user,
node=node,
key=node.key,
parent_key=node.parent_key,
granted=_granted,
asset_granted=_asset_granted,
assets_amount=_granted_assets_amount,
))
UserGrantedMappingNode.objects.bulk_create(to_create)
def set_node_granted_assets_amount(user, node, asset_perms_id=None):
"""
不依赖`UserGrantedMappingNode`直接查询授权计算资产数量
"""
_granted = getattr(node, TMP_GRANTED_FIELD, False)
if _granted:
assets_amount = node.assets_amount
else:
if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
assets_amount = count_direct_granted_node_assets(user, node.key, asset_perms_id)
else:
assets_amount = count_node_all_granted_assets(user, node.key, asset_perms_id)
setattr(node, TMP_GRANTED_ASSETS_AMOUNT_FIELD, assets_amount)
@tmp_to_root_org()
def rebuild_user_mapping_nodes(user):
logger.info(f'>>> {dt_formater(now())} start rebuild {user} mapping nodes')
# 先删除旧的授权树🌲
UserGrantedMappingNode.objects.filter(user=user).delete()
asset_perms_id = get_user_all_assetpermissions_id(user)
if not asset_perms_id:
# 没有授权直接返回
return
tmp_nodes = compute_tmp_mapping_node_from_perm(user, asset_perms_id=asset_perms_id)
for _node in tmp_nodes:
set_node_granted_assets_amount(user, _node, asset_perms_id)
create_mapping_nodes(user, tmp_nodes)
logger.info(f'>>> {dt_formater(now())} end rebuild {user} mapping nodes')
def rebuild_all_user_mapping_nodes():
from users.models import User
users = User.objects.all()
for user in users:
rebuild_user_mapping_nodes(user)
def get_user_granted_nodes_list_via_mapping_node(user):
"""
这里的 granted nodes, 是整棵树需要的node推算出来的也算
:param user:
:return:
"""
# 获取 `UserGrantedMappingNode` 中对应的 `Node`
nodes = Node.objects.filter(
mapping_nodes__user=user,
).annotate(
**node_annotate_mapping_node
).distinct()
key_to_node_mapper = {}
nodes_descendant_q = Q()
for node in nodes:
if not is_direct_granted_by_annotate(node):
# 未授权的节点资产数量设置为 `UserGrantedMappingNode` 中的数量
node.assets_amount = get_granted_assets_amount(node)
else:
# 直接授权的节点
# 增加查询后代节点的过滤条件
nodes_descendant_q |= Q(key__startswith=f'{node.key}:')
key_to_node_mapper[node.key] = node
if nodes_descendant_q:
descendant_nodes = Node.objects.filter(
nodes_descendant_q
).annotate(
**node_annotate_set_granted
)
for node in descendant_nodes:
key_to_node_mapper[node.key] = node
all_nodes = key_to_node_mapper.values()
return all_nodes
def get_user_granted_all_assets(
user, via_mapping_node=True,
include_direct_granted_assets=True, asset_perms_id=None):
if asset_perms_id is None:
asset_perms_id = get_user_all_assetpermissions_id(user)
if via_mapping_node:
granted_node_keys = UserGrantedMappingNode.objects.filter(
user=user, granted=True,
).values_list('key', flat=True).distinct()
else:
granted_node_keys = Node.objects.filter(
granted_by_permissions__id__in=asset_perms_id
).distinct().values_list('key', flat=True)
granted_node_keys = Node.clean_children_keys(granted_node_keys)
granted_node_q = Q()
for _key in granted_node_keys:
granted_node_q |= Q(nodes__key__startswith=f'{_key}:')
granted_node_q |= Q(nodes__key=_key)
if include_direct_granted_assets:
assets__id = get_user_direct_granted_assets(user, asset_perms_id).values_list('id', flat=True)
q = granted_node_q | Q(id__in=list(assets__id))
else:
q = granted_node_q
if q:
return Asset.org_objects.filter(q).distinct()
else:
return Asset.org_objects.none()
def get_node_all_granted_assets(user: User, key):
"""
此算法依据 `UserGrantedMappingNode` 的数据查询
1. 查询该节点下的直接授权节点
2. 查询该节点下授权资产关联的节点
"""
assets = Asset.objects.none()
# 查询该节点下的授权节点
granted_mapping_nodes = UserGrantedMappingNode.objects.filter(
user=user, granted=True,
).filter(
Q(key__startswith=f'{key}:') | Q(key=key)
)
# 根据授权节点构建资产查询条件
granted_nodes_qs = []
for _node in granted_mapping_nodes:
granted_nodes_qs.append(Q(nodes__key__startswith=f'{_node.key}:'))
granted_nodes_qs.append(Q(nodes__key=_node.key))
# 查询该节点下的资产授权节点
only_asset_granted_mapping_nodes = UserGrantedMappingNode.objects.filter(
user=user,
asset_granted=True,
granted=False,
).filter(Q(key__startswith=f'{key}:') | Q(key=key))
# 根据资产授权节点构建查询
only_asset_granted_nodes_qs = []
for _node in only_asset_granted_mapping_nodes:
only_asset_granted_nodes_qs.append(Q(nodes__id=_node.node_id))
q = []
if granted_nodes_qs:
q.append(reduce(or_, granted_nodes_qs))
if only_asset_granted_nodes_qs:
only_asset_granted_nodes_q = reduce(or_, only_asset_granted_nodes_qs)
asset_perms_id = get_user_all_assetpermissions_id(user)
only_asset_granted_nodes_q &= Q(granted_by_permissions__id__in=list(asset_perms_id))
q.append(only_asset_granted_nodes_q)
if q:
assets = Asset.objects.filter(reduce(or_, q)).distinct()
return assets
def get_direct_granted_node_ids(user: User, key, asset_perms_id=None):
if asset_perms_id is None:
asset_perms_id = get_user_all_assetpermissions_id(user)
# 先查出该节点下的直接授权节点
granted_nodes = Node.objects.filter(
Q(key__startswith=f'{key}:') | Q(key=key)
).filter(
granted_by_permissions__id__in=asset_perms_id
).distinct().only('id', 'key')
node_ids = set()
# 根据直接授权节点查询他们的子节点
q = Q()
for _node in granted_nodes:
q |= Q(key__startswith=f'{_node.key}:')
node_ids.add(_node.id)
if q:
descendant_ids = Node.objects.filter(q).values_list('id', flat=True).distinct()
node_ids.update(descendant_ids)
return node_ids
def get_node_all_granted_assets_from_perm(user: User, key, asset_perms_id=None):
"""
此算法依据 `AssetPermission` 的数据查询
1. 查询该节点下的直接授权节点
2. 查询该节点下授权资产关联的节点
"""
if asset_perms_id is None:
asset_perms_id = get_user_all_assetpermissions_id(user)
# 直接授权资产查询条件
q = (
Q(nodes__key__startswith=f'{key}:') | Q(nodes__key=key)
) & Q(granted_by_permissions__id__in=asset_perms_id)
node_ids = get_direct_granted_node_ids(user, key, asset_perms_id)
q |= Q(nodes__id__in=node_ids)
asset_qs = Asset.objects.filter(q).distinct()
return asset_qs
def get_direct_granted_node_assets_from_perm(user: User, key, asset_perms_id=None):
node_ids = get_direct_granted_node_ids(user, key, asset_perms_id)
asset_qs = Asset.objects.filter(nodes__id__in=node_ids).distinct()
return asset_qs
def count_node_all_granted_assets(user: User, key, asset_perms_id=None):
return get_node_all_granted_assets_from_perm(user, key, asset_perms_id).count()
def count_direct_granted_node_assets(user: User, key, asset_perms_id=None):
return get_direct_granted_node_assets_from_perm(user, key, asset_perms_id).count()
def get_indirect_granted_node_children(user, key=''):
"""
获取用户授权树中未授权节点的子节点
只匹配在 `UserGrantedMappingNode` 中存在的节点
"""
nodes = Node.objects.filter(
mapping_nodes__user=user,
parent_key=key
).annotate(
_granted_assets_amount=F('mapping_nodes__assets_amount'),
_granted=F('mapping_nodes__granted')
).distinct()
# 设置节点授权资产数量
for _node in nodes:
if not is_direct_granted_by_annotate(_node):
_node.assets_amount = get_granted_assets_amount(_node)
return nodes
def get_top_level_granted_nodes(user):
nodes = list(get_indirect_granted_node_children(user, key=''))
if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
ungrouped_node = get_ungrouped_node(user)
nodes.insert(0, ungrouped_node)
favorite_node = get_favorite_node(user)
nodes.insert(0, favorite_node)
return nodes
def get_user_all_assetpermissions_id(user: User):
asset_perms_id = AssetPermission.objects.valid().filter(
Q(users=user) | Q(user_groups__users=user)
).distinct().values_list('id', flat=True)
# !!! 这个很重要,必须转换成 list避免 Django 生成嵌套子查询
asset_perms_id = list(asset_perms_id)
return asset_perms_id
def get_user_direct_granted_assets(user, asset_perms_id=None):
if asset_perms_id is None:
asset_perms_id = get_user_all_assetpermissions_id(user)
assets = Asset.org_objects.filter(granted_by_permissions__id__in=asset_perms_id).distinct()
return assets
def count_user_direct_granted_assets(user, asset_perms_id=None):
count = get_user_direct_granted_assets(
user, asset_perms_id=asset_perms_id
).values_list('id').count()
return count
def get_ungrouped_node(user, asset_perms_id=None):
assets_amount = count_user_direct_granted_assets(user, asset_perms_id)
return Node(
id=UNGROUPED_NODE_KEY,
key=UNGROUPED_NODE_KEY,
value=UNGROUPED_NODE_VALUE,
assets_amount=assets_amount
)
def get_favorite_node(user, asset_perms_id=None):
assets_amount = FavoriteAsset.get_user_favorite_assets(
user, asset_perms_id=asset_perms_id
).values_list('id').count()
return Node(
id=FAVORITE_NODE_KEY,
key=FAVORITE_NODE_KEY,
value=FAVORITE_NODE_VALUE,
assets_amount=assets_amount
)
def rebuild_user_tree_if_need(request, user):
"""
升级授权树策略后,用户的数据可能还未初始化,为防止用户显示没有数据
先检查 MappingNode 如果没有数据,同步创建用户授权树
"""
if is_true(request.query_params.get('rebuild_tree')) or \
not UserGrantedMappingNode.objects.filter(user=user).exists():
try:
rebuild_user_mapping_nodes_with_lock(user)
except lock.SomeoneIsDoingThis:
# 您的数据正在初始化,请稍等
raise lock.SomeoneIsDoingThis(
detail=_('Please wait while your data is being initialized'),
code='rebuild_tree_conflict'
)