将接口级权限改为业务语言
1. 更新权限配置格式 - 从 "METHOD:/path" 改为 "module:operation" - 例如: "subjects:create", "visits:update", "risk_issues:delete" - 更易理解和管理 2. 新增权限操作列表 API - GET /api/v1/api-permissions/operations - 返回所有权限操作及其描述 3. 更新前端权限管理界面 - 显示业务语言的权限名称 - 按模块和操作类型筛选 - 改进用户体验 4. 修复导入错误 - 更新 MODULE_TO_ENDPOINTS 为 OPERATION_TO_ENDPOINTS - 禁用 API 端点注册表初始化 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,27 @@ from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, PROJECT_PERMISSIO
|
|||||||
router = APIRouter(prefix="/api-permissions", tags=["api-permissions"])
|
router = APIRouter(prefix="/api-permissions", tags=["api-permissions"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/operations",
|
||||||
|
summary="获取系统中所有权限操作",
|
||||||
|
description="返回系统中所有权限操作及其描述",
|
||||||
|
)
|
||||||
|
async def list_api_operations() -> dict[str, list[dict]]:
|
||||||
|
"""获取所有权限操作"""
|
||||||
|
operations_list = [
|
||||||
|
{
|
||||||
|
"operation_key": key,
|
||||||
|
"module": config["module"],
|
||||||
|
"action": config["action"],
|
||||||
|
"description": config["description"],
|
||||||
|
"default_roles": config["default_roles"],
|
||||||
|
}
|
||||||
|
for key, config in API_ENDPOINT_PERMISSIONS.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {"operations": operations_list}
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/endpoints",
|
"/endpoints",
|
||||||
summary="获取系统中所有已注册的API端点",
|
summary="获取系统中所有已注册的API端点",
|
||||||
@@ -48,6 +69,27 @@ async def list_api_endpoints(
|
|||||||
return {"endpoints": endpoints_list}
|
return {"endpoints": endpoints_list}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/operations",
|
||||||
|
summary="获取系统中所有权限操作",
|
||||||
|
description="返回系统中所有权限操作及其描述",
|
||||||
|
)
|
||||||
|
async def list_api_operations() -> dict[str, list[dict]]:
|
||||||
|
"""获取所有权限操作"""
|
||||||
|
operations_list = [
|
||||||
|
{
|
||||||
|
"operation_key": key,
|
||||||
|
"module": config["module"],
|
||||||
|
"action": config["action"],
|
||||||
|
"description": config["description"],
|
||||||
|
"default_roles": config["default_roles"],
|
||||||
|
}
|
||||||
|
for key, config in API_ENDPOINT_PERMISSIONS.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {"operations": operations_list}
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"",
|
"",
|
||||||
summary="获取项目的接口级权限矩阵",
|
summary="获取项目的接口级权限矩阵",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ api_router.include_router(sites.router, prefix="/studies/{study_id}/sites", tags
|
|||||||
api_router.include_router(members.router, prefix="/studies/{study_id}/members", tags=["study-members"])
|
api_router.include_router(members.router, prefix="/studies/{study_id}/members", tags=["study-members"])
|
||||||
api_router.include_router(project_permissions.router, prefix="/studies/{study_id}/permissions", tags=["project-permissions"])
|
api_router.include_router(project_permissions.router, prefix="/studies/{study_id}/permissions", tags=["project-permissions"])
|
||||||
api_router.include_router(api_permissions.router, prefix="/studies/{study_id}", tags=["api-permissions"])
|
api_router.include_router(api_permissions.router, prefix="/studies/{study_id}", tags=["api-permissions"])
|
||||||
|
api_router.include_router(api_permissions.router, tags=["api-permissions"])
|
||||||
api_router.include_router(attachments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/attachments", tags=["attachments"])
|
api_router.include_router(attachments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/attachments", tags=["attachments"])
|
||||||
api_router.include_router(attachments.global_router, prefix="/attachments", tags=["attachments"])
|
api_router.include_router(attachments.global_router, prefix="/attachments", tags=["attachments"])
|
||||||
api_router.include_router(audit_logs.router, prefix="/studies/{study_id}/audit-logs", tags=["audit-logs"])
|
api_router.include_router(audit_logs.router, prefix="/studies/{study_id}/audit-logs", tags=["audit-logs"])
|
||||||
|
|||||||
+213
-236
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.models.study_role_permission import StudyRolePermission
|
from app.models.study_role_permission import StudyRolePermission
|
||||||
from app.models.api_endpoint_permission import ApiEndpointPermission
|
from app.models.api_endpoint_permission import ApiEndpointPermission
|
||||||
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, MODULE_TO_ENDPOINTS
|
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_TO_ENDPOINTS
|
||||||
from app.core.permission_cache import get_permission_cache
|
from app.core.permission_cache import get_permission_cache
|
||||||
|
|
||||||
PROJECT_PERMISSION_ROLES = ("ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA")
|
PROJECT_PERMISSION_ROLES = ("ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA")
|
||||||
|
|||||||
+2
-2
@@ -39,8 +39,8 @@ async def lifespan(_: FastAPI):
|
|||||||
async with SessionLocal() as session:
|
async with SessionLocal() as session:
|
||||||
await ensure_admin_exists(session)
|
await ensure_admin_exists(session)
|
||||||
# Initialize API endpoint registry
|
# Initialize API endpoint registry
|
||||||
from app.core.decorators import initialize_api_endpoint_registry
|
# from app.core.decorators import initialize_api_endpoint_registry
|
||||||
await initialize_api_endpoint_registry(session)
|
# await initialize_api_endpoint_registry(session)
|
||||||
yield
|
yield
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
await scheduler_task
|
await scheduler_task
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export const fetchApiEndpointPermissions = (studyId: string) =>
|
|||||||
export const updateApiEndpointPermissions = (studyId: string, payload: ApiEndpointPermissionsUpdate) =>
|
export const updateApiEndpointPermissions = (studyId: string, payload: ApiEndpointPermissionsUpdate) =>
|
||||||
apiPut<ApiEndpointPermissionsResponse>(`/api/v1/studies/${studyId}/api-permissions`, payload);
|
apiPut<ApiEndpointPermissionsResponse>(`/api/v1/studies/${studyId}/api-permissions`, payload);
|
||||||
|
|
||||||
|
export const fetchApiOperations = () =>
|
||||||
|
apiGet<{ operations: Array<{ operation_key: string; module: string; action: string; description: string; default_roles: string[] }> }>(`/api/v1/api-permissions/operations`);
|
||||||
|
|
||||||
// 权限系统监控API(新增)
|
// 权限系统监控API(新增)
|
||||||
export const fetchPermissionMetrics = () =>
|
export const fetchPermissionMetrics = () =>
|
||||||
apiGet<PermissionMetricsResponse>(`/api/v1/permission-monitoring/metrics`);
|
apiGet<PermissionMetricsResponse>(`/api/v1/permission-monitoring/metrics`);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="api-permissions-toolbar">
|
<div class="api-permissions-toolbar">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
placeholder="搜索端点..."
|
placeholder="搜索权限..."
|
||||||
clearable
|
clearable
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
>
|
>
|
||||||
@@ -23,29 +23,29 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<el-select v-model="filterMethod" placeholder="筛选方法" clearable style="width: 120px">
|
<el-select v-model="filterAction" placeholder="筛选操作" clearable style="width: 120px">
|
||||||
<el-option label="全部方法" value="" />
|
<el-option label="全部操作" value="" />
|
||||||
<el-option label="GET" value="GET" />
|
<el-option label="读取" value="read" />
|
||||||
<el-option label="POST" value="POST" />
|
<el-option label="写入" value="write" />
|
||||||
<el-option label="PATCH" value="PATCH" />
|
|
||||||
<el-option label="DELETE" value="DELETE" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 权限矩阵表格 -->
|
<!-- 权限矩阵表格 -->
|
||||||
<div class="api-permissions-table-wrapper">
|
<div class="api-permissions-table-wrapper">
|
||||||
<el-table :data="filteredEndpoints" border stripe>
|
<el-table :data="filteredOperations" border stripe>
|
||||||
<el-table-column prop="endpoint_key" label="端点" width="250">
|
<el-table-column prop="operation_key" label="权限操作" width="200">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="endpoint-cell">
|
<div class="operation-cell">
|
||||||
<el-tag :type="getMethodType(row.method)" size="small">
|
<el-tag :type="getActionType(row.action)" size="small">
|
||||||
{{ row.method }}
|
{{ row.action === 'read' ? '读取' : '写入' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="endpoint-path">{{ row.path }}</span>
|
<span class="operation-name">{{ row.operation_key }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="description" label="描述" width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
<el-table-column prop="module" label="模块" width="100" />
|
<el-table-column prop="module" label="模块" width="100" />
|
||||||
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
:model-value="isEndpointAllowed(row.endpoint_key, role)"
|
:model-value="isOperationAllowed(row.operation_key, role)"
|
||||||
@change="(val) => onPermissionChange(row.endpoint_key, role, val as boolean)"
|
@change="(val) => onPermissionChange(row.operation_key, role, val as boolean)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -68,15 +68,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { Search } from "@element-plus/icons-vue";
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
import type { ApiEndpointPermissionsResponse } from "@/types/api";
|
import type { ApiEndpointPermissionsResponse } from "@/types/api";
|
||||||
|
import { fetchApiOperations } from "@/api/projectPermissions";
|
||||||
|
|
||||||
interface Endpoint {
|
interface Operation {
|
||||||
endpoint_key: string;
|
operation_key: string;
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
module: string;
|
module: string;
|
||||||
|
action: string;
|
||||||
|
description: string;
|
||||||
|
default_roles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -93,7 +96,8 @@ const emit = defineEmits<Emits>();
|
|||||||
|
|
||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
const filterModule = ref("");
|
const filterModule = ref("");
|
||||||
const filterMethod = ref("");
|
const filterAction = ref("");
|
||||||
|
const operations = ref<Operation[]>([]);
|
||||||
|
|
||||||
// 从矩阵中提取角色列表
|
// 从矩阵中提取角色列表
|
||||||
const roles = computed(() => {
|
const roles = computed(() => {
|
||||||
@@ -101,80 +105,56 @@ const roles = computed(() => {
|
|||||||
return Object.keys(props.matrix).sort();
|
return Object.keys(props.matrix).sort();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 从矩阵中提取端点列表
|
// 加载权限操作列表
|
||||||
const endpoints = computed(() => {
|
const loadOperations = async () => {
|
||||||
if (!props.matrix) return [];
|
try {
|
||||||
|
const { data } = await fetchApiOperations();
|
||||||
const endpointSet = new Set<string>();
|
operations.value = data.operations;
|
||||||
Object.values(props.matrix).forEach((rolePerms) => {
|
} catch (error) {
|
||||||
Object.keys(rolePerms).forEach((endpoint) => {
|
ElMessage.error("加载权限操作失败");
|
||||||
endpointSet.add(endpoint);
|
console.error(error);
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(endpointSet)
|
|
||||||
.map((endpoint_key) => {
|
|
||||||
const [method, ...pathParts] = endpoint_key.split(":");
|
|
||||||
const path = pathParts.join(":");
|
|
||||||
const module = extractModule(path);
|
|
||||||
|
|
||||||
return {
|
|
||||||
endpoint_key,
|
|
||||||
method,
|
|
||||||
path,
|
|
||||||
module,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.endpoint_key.localeCompare(b.endpoint_key));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 提取模块名称
|
|
||||||
const extractModule = (path: string): string => {
|
|
||||||
const match = path.match(/\/(\w+)/);
|
|
||||||
return match ? match[1] : "unknown";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取唯一的模块列表
|
// 获取唯一的模块列表
|
||||||
const uniqueModules = computed(() => {
|
const uniqueModules = computed(() => {
|
||||||
return [...new Set(endpoints.value.map((e) => e.module))].sort();
|
return [...new Set(operations.value.map((o) => o.module))].sort();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 过滤后的端点列表
|
// 过滤后的操作列表
|
||||||
const filteredEndpoints = computed(() => {
|
const filteredOperations = computed(() => {
|
||||||
return endpoints.value.filter((endpoint) => {
|
return operations.value.filter((operation) => {
|
||||||
const matchSearch =
|
const matchSearch =
|
||||||
!searchText.value ||
|
!searchText.value ||
|
||||||
endpoint.endpoint_key.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
operation.operation_key.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
||||||
endpoint.path.toLowerCase().includes(searchText.value.toLowerCase());
|
operation.description.toLowerCase().includes(searchText.value.toLowerCase());
|
||||||
|
|
||||||
const matchModule = !filterModule.value || endpoint.module === filterModule.value;
|
const matchModule = !filterModule.value || operation.module === filterModule.value;
|
||||||
const matchMethod = !filterMethod.value || endpoint.method === filterMethod.value;
|
const matchAction = !filterAction.value || operation.action === filterAction.value;
|
||||||
|
|
||||||
return matchSearch && matchModule && matchMethod;
|
return matchSearch && matchModule && matchAction;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查端点是否允许
|
// 检查操作是否允许
|
||||||
const isEndpointAllowed = (endpoint_key: string, role: string): boolean => {
|
const isOperationAllowed = (operation_key: string, role: string): boolean => {
|
||||||
if (!props.matrix || !props.matrix[role]) return false;
|
if (!props.matrix || !props.matrix[role]) return false;
|
||||||
const perm = props.matrix[role][endpoint_key];
|
const perm = props.matrix[role][operation_key];
|
||||||
if (!perm) return false;
|
if (!perm) return false;
|
||||||
return typeof perm === "boolean" ? perm : perm.allowed;
|
return typeof perm === "boolean" ? perm : perm.allowed;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取方法的标签类型
|
// 获取操作的标签类型
|
||||||
const getMethodType = (method: string): string => {
|
const getActionType = (action: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
GET: "info",
|
read: "info",
|
||||||
POST: "success",
|
write: "success",
|
||||||
PATCH: "warning",
|
|
||||||
DELETE: "danger",
|
|
||||||
};
|
};
|
||||||
return typeMap[method] || "info";
|
return typeMap[action] || "info";
|
||||||
};
|
};
|
||||||
|
|
||||||
// 权限变更处理
|
// 权限变更处理
|
||||||
const onPermissionChange = (endpoint_key: string, role: string, allowed: boolean) => {
|
const onPermissionChange = (operation_key: string, role: string, allowed: boolean) => {
|
||||||
if (!props.matrix) return;
|
if (!props.matrix) return;
|
||||||
|
|
||||||
const updatedMatrix: ApiEndpointPermissionsResponse = JSON.parse(JSON.stringify(props.matrix));
|
const updatedMatrix: ApiEndpointPermissionsResponse = JSON.parse(JSON.stringify(props.matrix));
|
||||||
@@ -183,10 +163,14 @@ const onPermissionChange = (endpoint_key: string, role: string, allowed: boolean
|
|||||||
updatedMatrix[role] = {};
|
updatedMatrix[role] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedMatrix[role][endpoint_key] = allowed;
|
updatedMatrix[role][operation_key] = allowed;
|
||||||
|
|
||||||
emit("update", updatedMatrix);
|
emit("update", updatedMatrix);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOperations();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
Reference in New Issue
Block a user