feat: Folder authorization can optionally include sub-resources

This commit is contained in:
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.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 rest_framework import serializers
@ -208,7 +209,7 @@ class UserResourcePermissionSerializer(serializers.Serializer):
auth_target_type=auth_target_type,
permission_list=[ResourcePermission.VIEW,
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],
workspace_id=workspace_id,
user_id=user_id,
@ -326,6 +327,12 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
auth_target_type = serializers.CharField(required=True, label=_('resource'))
users_permission = ResourceUserPermissionEditRequest(required=False, many=True, label=_('users_permission'))
RESOURCE_MODEL_MAP = {
'APPLICATION': Application,
'KNOWLEDGE': Knowledge,
'TOOL': Tool
}
def get_queryset(self, instance):
user_query_set = QuerySet(model=get_dynamics_model({
@ -392,7 +399,28 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
))
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:
self.is_valid(raise_exception=True)
ResourceUserPermissionEditRequest(data=instance, many=True).is_valid(
@ -404,27 +432,36 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
users_permission = instance
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(
workspace_id=workspace_id,
target=target,
target__in=managed_resource_ids,
auth_target_type=auth_target_type,
user_id__in=users_id
).delete()
save_list = []
for item in users_permission:
permission = item['permission']
auth_type, permission_list = permission_map[permission]
save_list.append(WorkspaceUserResourcePermission(
target=target,
save_list = [
WorkspaceUserResourcePermission(
target=resource_id,
auth_target_type=auth_target_type,
workspace_id=workspace_id,
auth_type=auth_type,
auth_type=permission_map[item['permission']][0],
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:
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):
return result.success(ResourceUserPermissionSerializer(
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):
authentication_classes = [TokenAuth]

View File

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

View File

@ -119,6 +119,28 @@
</template>
</el-table-column>
</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
@ -128,13 +150,26 @@
@close="closeDialog"
>
<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">
<p class="color-text-primary lighter">{{ item.label }}</p>
<el-text class="color-secondary lighter">{{ item.desc }}</el-text>
</el-radio>
</template>
</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>
<div class="dialog-footer mt-24">
<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 { MsgSuccess, MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
import permissionMap from '@/permission'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
const route = useRoute()
import useStore from '@/stores'
const { user } = useStore()
const props = defineProps<{
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(() => {
return {
rootFolder: getPermissionOptions(true, true),
@ -207,6 +295,7 @@ watch(drawerVisible, (bool) => {
const loading = ref(false)
const targetId = ref('')
const folderData = ref<any>(null)
const permissionData = ref<any[]>([])
const searchType = ref('nick_name')
const searchForm = ref<any>({
@ -241,32 +330,59 @@ const handleSelectionChange = (val: any[]) => {
}
const dialogVisible = ref(false)
const singleSelectDialogVisible = ref(false)
const pendingPermissionChange = ref<{ val: any; row: any; } | null>(null)
const radioPermission = ref('')
const authAllChildren = ref(false)
function openMulConfigureDialog() {
if (multipleSelection.value.length === 0) {
return
}
dialogVisible.value = true
}
const batchAuthAllChildren = ref(false)
function submitDialog() {
if (multipleSelection.value.length === 0 || !radioPermission.value) {
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) => ({
user_id: item.id,
permission: radioPermission.value,
include_children: batchAuthAllChildren.value,
...(folderIds.length > 0 && { folder_ids: folderIds })
}))
submitPermissions(obj)
closeDialog()
}
function closeSingleSelectDialog() {
singleSelectDialogVisible.value = false
authAllChildren.value = false
pendingPermissionChange.value = null
getPermissionList()
}
function closeDialog() {
dialogVisible.value = false
radioPermission.value = ''
batchAuthAllChildren.value = false
multipleSelection.value = []
multipleTableRef.value?.clearSelection()
}
function permissionsHandle(val: any, row: any) {
if (props.isFolder) {
singleSelectDialogVisible.value = true
pendingPermissionChange.value = {val, row}
return
}
const obj = [
{
user_id: row.id,
@ -276,7 +392,7 @@ function permissionsHandle(val: any, row: any) {
submitPermissions(obj)
}
function submitPermissions(obj: any) {
function submitPermissions( obj: any) {
const workspaceId = user.getWorkspaceId() || 'default'
loadSharedApi({ type: 'resourceAuthorization', systemType: apiType.value })
.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
folderData.value = folder_data
drawerVisible.value = true
getPermissionList()
}

View File

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

View File

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

View File

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