feat: Folder authorization can optionally include sub-resources

v3.2
zhangzhanwei 2025-10-21 10:16:18 +08:00 committed by zhanweizhang7
parent cbac86a15d
commit 349402b2d8
7 changed files with 182 additions and 19 deletions

View File

@ -11,7 +11,8 @@ import os
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import QuerySet, Q from django.db.models import QuerySet, Q, TextField
from django.db.models.functions import Cast
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -208,7 +209,7 @@ class UserResourcePermissionSerializer(serializers.Serializer):
auth_target_type=auth_target_type, auth_target_type=auth_target_type,
permission_list=[ResourcePermission.VIEW, permission_list=[ResourcePermission.VIEW,
ResourcePermission.MANAGE] if ( ResourcePermission.MANAGE] if (
auth_type == ResourceAuthType.RESOURCE_PERMISSION_GROUP or is_folder) else [ auth_type == ResourceAuthType.RESOURCE_PERMISSION_GROUP or is_folder) else [
ResourcePermissionRole.ROLE], ResourcePermissionRole.ROLE],
workspace_id=workspace_id, workspace_id=workspace_id,
user_id=user_id, user_id=user_id,
@ -326,6 +327,12 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
auth_target_type = serializers.CharField(required=True, label=_('resource')) auth_target_type = serializers.CharField(required=True, label=_('resource'))
users_permission = ResourceUserPermissionEditRequest(required=False, many=True, label=_('users_permission')) users_permission = ResourceUserPermissionEditRequest(required=False, many=True, label=_('users_permission'))
RESOURCE_MODEL_MAP = {
'APPLICATION': Application,
'KNOWLEDGE': Knowledge,
'TOOL': Tool
}
def get_queryset(self, instance): def get_queryset(self, instance):
user_query_set = QuerySet(model=get_dynamics_model({ user_query_set = QuerySet(model=get_dynamics_model({
@ -392,7 +399,28 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
)) ))
return resource_user_permission_page_list return resource_user_permission_page_list
def edit(self, instance, with_valid=True): def get_has_manage_permission_resource_under_folders(self, current_user_id, folder_ids):
workspace_id = self.data.get("workspace_id")
auth_target_type = self.data.get("auth_target_type")
workspace_manage = is_workspace_manage(current_user_id, workspace_id)
resource_model = self.RESOURCE_MODEL_MAP[auth_target_type]
if workspace_manage:
current_user_managed_resources_ids = QuerySet(resource_model).filter(workspace_id=workspace_id, folder__in=folder_ids).annotate(
id_str=Cast('id', TextField())
).values_list("id", flat=True)
else:
current_user_managed_resources_ids = QuerySet(WorkspaceUserResourcePermission).filter(
workspace_id=workspace_id, user_id=current_user_id, auth_target_type=auth_target_type,
target__in=QuerySet(resource_model).filter(workspace_id=workspace_id, folder__in=folder_ids).annotate(
id_str=Cast('id', TextField())
).values_list("id", flat=True),
permission_list__contains=['MANAGE']).values_list('target', flat=True)
return current_user_managed_resources_ids
def edit(self, instance, with_valid=True, current_user_id=None):
if with_valid: if with_valid:
self.is_valid(raise_exception=True) self.is_valid(raise_exception=True)
ResourceUserPermissionEditRequest(data=instance, many=True).is_valid( ResourceUserPermissionEditRequest(data=instance, many=True).is_valid(
@ -404,27 +432,36 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
users_permission = instance users_permission = instance
users_id = [item["user_id"] for item in users_permission] users_id = [item["user_id"] for item in users_permission]
include_children = users_permission[0].get('include_children')
folder_ids = users_permission[0].get('folder_ids')
# 删除已存在的对应的用户在该资源下的权限 # 删除已存在的对应的用户在该资源下的权限
if include_children:
managed_resource_ids = list(
self.get_has_manage_permission_resource_under_folders(current_user_id, folder_ids)) + folder_ids
else:
managed_resource_ids = [target]
QuerySet(WorkspaceUserResourcePermission).filter( QuerySet(WorkspaceUserResourcePermission).filter(
workspace_id=workspace_id, workspace_id=workspace_id,
target=target, target__in=managed_resource_ids,
auth_target_type=auth_target_type, auth_target_type=auth_target_type,
user_id__in=users_id user_id__in=users_id
).delete() ).delete()
save_list = [] save_list = [
for item in users_permission: WorkspaceUserResourcePermission(
permission = item['permission'] target=resource_id,
auth_type, permission_list = permission_map[permission]
save_list.append(WorkspaceUserResourcePermission(
target=target,
auth_target_type=auth_target_type, auth_target_type=auth_target_type,
workspace_id=workspace_id, workspace_id=workspace_id,
auth_type=auth_type, auth_type=permission_map[item['permission']][0],
user_id=item["user_id"], user_id=item["user_id"],
permission_list=permission_list permission_list=permission_map[item['permission']][1]
)) )
for resource_id in managed_resource_ids
for item in users_permission
]
if save_list: if save_list:
QuerySet(WorkspaceUserResourcePermission).bulk_create(save_list) QuerySet(WorkspaceUserResourcePermission).bulk_create(save_list)

View File

@ -164,7 +164,7 @@ class WorkspaceResourceUserPermissionView(APIView):
def put(self, request: Request, workspace_id: str, target: str, resource: str): def put(self, request: Request, workspace_id: str, target: str, resource: str):
return result.success(ResourceUserPermissionSerializer( return result.success(ResourceUserPermissionSerializer(
data={'workspace_id': workspace_id, "target": target, 'auth_target_type': resource, }) data={'workspace_id': workspace_id, "target": target, 'auth_target_type': resource, })
.edit(instance=request.data)) .edit(instance=request.data, current_user_id=request.user.id))
class Page(APIView): class Page(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]

View File

@ -273,7 +273,7 @@ const currentNode = ref<Tree | null>(null)
const ResourceAuthorizationDrawerRef = ref() const ResourceAuthorizationDrawerRef = ref()
function openAuthorization(data: any) { function openAuthorization(data: any) {
currentNode.value = data currentNode.value = data
ResourceAuthorizationDrawerRef.value.open(data.id) ResourceAuthorizationDrawerRef.value.open(data.id, data)
} }
function refreshFolder() { function refreshFolder() {

View File

@ -119,6 +119,28 @@
</template> </template>
</el-table-column> </el-table-column>
</app-table> </app-table>
<!-- 单个资源授权提示框 -->
<el-dialog
v-model="singleSelectDialogVisible"
:title="$t('views.system.resourceAuthorization.setting.configure')"
destroy-on-close
@close="closeSingleSelectDialog"
>
<el-radio-group v-model="authAllChildren" class="radio-block">
<el-radio :value="false">
<p class="color-text-primary lighter">{{ $t('views.system.resourceAuthorization.setting.currentOnly') }}</p>
</el-radio>
<el-radio :value="true">
<p class="color-text-primary lighter">{{ $t('views.system.resourceAuthorization.setting.includeAll') }}</p>
</el-radio>
</el-radio-group>
<template #footer>
<div class="dialog-footer mt-24">
<el-button @click="closeSingleSelectDialog">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="confirmSinglePermission">{{ $t('common.confirm') }}</el-button>
</div>
</template>
</el-dialog>
<!-- 批量配置 弹出层 --> <!-- 批量配置 弹出层 -->
<el-dialog <el-dialog
@ -128,13 +150,26 @@
@close="closeDialog" @close="closeDialog"
> >
<el-radio-group v-model="radioPermission" class="radio-block"> <el-radio-group v-model="radioPermission" class="radio-block">
<template v-for="(item, index) in permissionOptions" :key="index"> <template v-for="(item, index) in getFolderPermissionOptions()" :key="index">
<el-radio :value="item.value" class="mr-16"> <el-radio :value="item.value" class="mr-16">
<p class="color-text-primary lighter">{{ item.label }}</p> <p class="color-text-primary lighter">{{ item.label }}</p>
<el-text class="color-secondary lighter">{{ item.desc }}</el-text> <el-text class="color-secondary lighter">{{ item.desc }}</el-text>
</el-radio> </el-radio>
</template> </template>
</el-radio-group> </el-radio-group>
<!-- 如果是文件夹显示子资源选项 -->
<div v-if="isFolder" class="mt-16">
<el-divider />
<div class="color-text-primary mb-8">{{ $t('views.system.resourceAuthorization.setting.effectiveResource') }}</div>
<el-radio-group v-model="batchAuthAllChildren" class="radio-block">
<el-radio :value="false">
<p class="color-text-primary lighter">{{ $t('views.system.resourceAuthorization.setting.currentOnly') }}</p>
</el-radio>
<el-radio :value="true">
<p class="color-text-primary lighter">{{ $t('views.system.resourceAuthorization.setting.includeAll') }}</p>
</el-radio>
</el-radio-group>
</div>
<template #footer> <template #footer>
<div class="dialog-footer mt-24"> <div class="dialog-footer mt-24">
<el-button @click="closeDialog"> {{ $t('common.cancel') }}</el-button> <el-button @click="closeDialog"> {{ $t('common.cancel') }}</el-button>
@ -151,9 +186,11 @@ import { getPermissionOptions } from '@/views/system/resource-authorization/cons
import AuthorizationApi from '@/api/system/resource-authorization' import AuthorizationApi from '@/api/system/resource-authorization'
import { MsgSuccess, MsgConfirm } from '@/utils/message' import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { t } from '@/locales' import { t } from '@/locales'
import permissionMap from '@/permission'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api' import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
const route = useRoute() const route = useRoute()
import useStore from '@/stores' import useStore from '@/stores'
const { user } = useStore() const { user } = useStore()
const props = defineProps<{ const props = defineProps<{
type: string type: string
@ -169,6 +206,57 @@ const apiType = computed(() => {
} }
}) })
const folderType = computed(() => {
if (route.path.includes('application')) {
return 'application'
}
else if (route.path.includes('knowledge')) {
return 'knowledge'
}
else if (route.path.includes('tool')) {
return 'tool'
}
else {return 'application'}
})
const permissionPrecise = computed(() => {
return permissionMap[folderType.value!]['workspace']
})
// id
function getAllFolderIds(data: any) {
if (!data) return []
return [data.id,...(data.children?.flatMap((child: any) => getAllFolderIds(child)) || [])]
}
// ManageID
function filterHasPermissionFolderIds(folderIds: string[]) {
return folderIds.filter(id => permissionPrecise.value.folderManage(id))
}
function confirmSinglePermission() {
if (!pendingPermissionChange.value) return
const { val, row } = pendingPermissionChange.value
let folderIds: string[] = []
if (authAllChildren.value && folderData.value) {
const allFolderIds = getAllFolderIds(folderData.value)
folderIds = filterHasPermissionFolderIds(allFolderIds)
}
const obj = [
{
user_id: row.id,
permission: val,
include_children: authAllChildren.value,
...(folderIds.length > 0 && {folder_ids: folderIds})
},
]
submitPermissions(obj)
singleSelectDialogVisible.value = false
authAllChildren.value = false
pendingPermissionChange.value = null
getPermissionList()
}
const permissionOptionMap = computed(() => { const permissionOptionMap = computed(() => {
return { return {
rootFolder: getPermissionOptions(true, true), rootFolder: getPermissionOptions(true, true),
@ -207,6 +295,7 @@ watch(drawerVisible, (bool) => {
const loading = ref(false) const loading = ref(false)
const targetId = ref('') const targetId = ref('')
const folderData = ref<any>(null)
const permissionData = ref<any[]>([]) const permissionData = ref<any[]>([])
const searchType = ref('nick_name') const searchType = ref('nick_name')
const searchForm = ref<any>({ const searchForm = ref<any>({
@ -241,32 +330,59 @@ const handleSelectionChange = (val: any[]) => {
} }
const dialogVisible = ref(false) const dialogVisible = ref(false)
const singleSelectDialogVisible = ref(false)
const pendingPermissionChange = ref<{ val: any; row: any; } | null>(null)
const radioPermission = ref('') const radioPermission = ref('')
const authAllChildren = ref(false)
function openMulConfigureDialog() { function openMulConfigureDialog() {
if (multipleSelection.value.length === 0) { if (multipleSelection.value.length === 0) {
return return
} }
dialogVisible.value = true dialogVisible.value = true
} }
const batchAuthAllChildren = ref(false)
function submitDialog() { function submitDialog() {
if (multipleSelection.value.length === 0 || !radioPermission.value) { if (multipleSelection.value.length === 0 || !radioPermission.value) {
return return
} }
let folderIds: string[] = []
if (props.isFolder && batchAuthAllChildren.value && folderData.value) {
const allFolderIds = getAllFolderIds(folderData.value)
folderIds = filterHasPermissionFolderIds(allFolderIds)
}
const obj = multipleSelection.value.map((item) => ({ const obj = multipleSelection.value.map((item) => ({
user_id: item.id, user_id: item.id,
permission: radioPermission.value, permission: radioPermission.value,
include_children: batchAuthAllChildren.value,
...(folderIds.length > 0 && { folder_ids: folderIds })
})) }))
submitPermissions(obj) submitPermissions(obj)
closeDialog() closeDialog()
} }
function closeSingleSelectDialog() {
singleSelectDialogVisible.value = false
authAllChildren.value = false
pendingPermissionChange.value = null
getPermissionList()
}
function closeDialog() { function closeDialog() {
dialogVisible.value = false dialogVisible.value = false
radioPermission.value = '' radioPermission.value = ''
batchAuthAllChildren.value = false
multipleSelection.value = [] multipleSelection.value = []
multipleTableRef.value?.clearSelection() multipleTableRef.value?.clearSelection()
} }
function permissionsHandle(val: any, row: any) { function permissionsHandle(val: any, row: any) {
if (props.isFolder) {
singleSelectDialogVisible.value = true
pendingPermissionChange.value = {val, row}
return
}
const obj = [ const obj = [
{ {
user_id: row.id, user_id: row.id,
@ -276,7 +392,7 @@ function permissionsHandle(val: any, row: any) {
submitPermissions(obj) submitPermissions(obj)
} }
function submitPermissions(obj: any) { function submitPermissions( obj: any) {
const workspaceId = user.getWorkspaceId() || 'default' const workspaceId = user.getWorkspaceId() || 'default'
loadSharedApi({ type: 'resourceAuthorization', systemType: apiType.value }) loadSharedApi({ type: 'resourceAuthorization', systemType: apiType.value })
.putResourceAuthorization(workspaceId, targetId.value, props.type, obj, loading) .putResourceAuthorization(workspaceId, targetId.value, props.type, obj, loading)
@ -311,8 +427,9 @@ const getPermissionList = () => {
}) })
} }
const open = (id: string) => { const open = (id: string, folder_data?: any) => {
targetId.value = id targetId.value = id
folderData.value = folder_data
drawerVisible.value = true drawerVisible.value = true
getPermissionList() getPermissionList()
} }

View File

@ -123,6 +123,9 @@ export default {
roleDesc: 'Authorize users based on their roles to access this resource', roleDesc: 'Authorize users based on their roles to access this resource',
notAuthorized: 'Not Authorized', notAuthorized: 'Not Authorized',
configure: 'Configure Permission', configure: 'Configure Permission',
currentOnly: 'Current resource only',
includeAll: 'Include all sub-resources',
effectiveResource: 'Effective Resource',
}, },
}, },
resource_management: { resource_management: {

View File

@ -124,6 +124,9 @@ export default {
roleDesc: '根据用户角色中的权限授权用户对该资源的操作权限', roleDesc: '根据用户角色中的权限授权用户对该资源的操作权限',
notAuthorized: '不授权', notAuthorized: '不授权',
configure: '配置权限', configure: '配置权限',
currentOnly: '仅当前资源',
includeAll: '包含所有子资源',
effectiveResource: '生效资源',
}, },
}, },
resource_management: { resource_management: {

View File

@ -123,6 +123,9 @@ export default {
roleDesc: '根據用戶角色中的權限授權用戶對該資源的操作權限', roleDesc: '根據用戶角色中的權限授權用戶對該資源的操作權限',
notAuthorized: '不授權', notAuthorized: '不授權',
configure: '配置權限', configure: '配置權限',
currentOnly: '僅當前資源',
includeAll: '包含所有子資源',
effectiveResource: '生效資源',
}, },
}, },
resource_management: { resource_management: {