mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Update the default RBAC solution (#593)
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.common.security.rbac import DependsRBAC
|
||||
from backend.database.db import CurrentSession
|
||||
from backend.plugin.casbin.schema.api import CreateApiParam, GetApiDetail, UpdateApiParam
|
||||
from backend.plugin.casbin.service.api_service import api_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/all', summary='获取所有接口', dependencies=[DependsJwtAuth])
|
||||
async def get_all_apis() -> ResponseSchemaModel[list[GetApiDetail]]:
|
||||
data = await api_service.get_all()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取接口详情', dependencies=[DependsJwtAuth])
|
||||
async def get_api(pk: Annotated[int, Path(description='API ID')]) -> ResponseSchemaModel[GetApiDetail]:
|
||||
api = await api_service.get(pk=pk)
|
||||
return response_base.success(data=api)
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有接口',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_apis(
|
||||
request: Request,
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='API 名称')] = None,
|
||||
method: Annotated[str | None, Query(description='请求方法')] = None,
|
||||
path: Annotated[str | None, Query(description='API 路径')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetApiDetail]]:
|
||||
api_select = await api_service.get_select(request=request, name=name, method=method, path=path)
|
||||
page_data = await paging_data(db, api_select)
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建接口',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:api:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_api(obj: CreateApiParam) -> ResponseModel:
|
||||
await api_service.create(obj=obj)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新接口',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:api:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_api(pk: Annotated[int, Path(description='API ID')], obj: UpdateApiParam) -> ResponseModel:
|
||||
count = await api_service.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'',
|
||||
summary='批量删除接口',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:api:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_api(pk: Annotated[list[int], Query(description='API ID 列表')]) -> ResponseModel:
|
||||
count = await api_service.delete(pk=pk)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.common.security.rbac import DependsRBAC
|
||||
from backend.database.db import CurrentSession
|
||||
from backend.plugin.casbin.schema.casbin_rule import (
|
||||
CreateGroupParam,
|
||||
CreatePolicyParam,
|
||||
DeleteAllPoliciesParam,
|
||||
DeleteGroupParam,
|
||||
DeletePolicyParam,
|
||||
GetPolicyDetail,
|
||||
UpdatePoliciesParam,
|
||||
UpdatePolicyParam,
|
||||
)
|
||||
from backend.plugin.casbin.service.casbin_service import casbin_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有权限策略',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_casbin(
|
||||
db: CurrentSession,
|
||||
ptype: Annotated[str | None, Query(description='策略类型:p / g')] = None,
|
||||
sub: Annotated[str | None, Query(description='用户 UUID / 角色 ID')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetPolicyDetail]]:
|
||||
casbin_select = await casbin_service.get_casbin_list(ptype=ptype, sub=sub)
|
||||
page_data = await paging_data(db, casbin_select)
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.get('/policies', summary='获取所有 P 权限策略', dependencies=[DependsJwtAuth])
|
||||
async def get_all_policies(
|
||||
role: Annotated[int | None, Query(description='角色 ID')] = None,
|
||||
) -> ResponseSchemaModel[list[list[str]]]:
|
||||
policies = await casbin_service.get_policy_list(role=role)
|
||||
return response_base.success(data=policies)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/policy',
|
||||
summary='添加 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_policy(p: CreatePolicyParam) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.create_policy(p=p)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/policies',
|
||||
summary='添加多组 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:group:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_policies(ps: list[CreatePolicyParam]) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.create_policies(ps=ps)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put(
|
||||
'/policy',
|
||||
summary='更新 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_policy(obj: UpdatePolicyParam) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.update_policy(obj=obj)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put(
|
||||
'/policies',
|
||||
summary='更新多组 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:group:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_policies(obj: UpdatePoliciesParam) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.update_policies(obj=obj)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/policy',
|
||||
summary='删除 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_policy(p: DeletePolicyParam) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.delete_policy(p=p)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/policies',
|
||||
summary='删除多组 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:group:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_policies(ps: list[DeletePolicyParam]) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.delete_policies(ps=ps)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/policies/all',
|
||||
summary='删除所有 P 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:p:empty')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_policies(sub: DeleteAllPoliciesParam) -> ResponseModel:
|
||||
count = await casbin_service.delete_all_policies(sub=sub)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.get('/groups', summary='获取所有 G 权限策略', dependencies=[DependsJwtAuth])
|
||||
async def get_all_groups() -> ResponseSchemaModel[list[list[str]]]:
|
||||
data = await casbin_service.get_group_list()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/group',
|
||||
summary='添加 G 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_group(g: CreateGroupParam) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.create_group(g=g)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/groups',
|
||||
summary='添加多组 G 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:group:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_groups(gs: list[CreateGroupParam]) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.create_groups(gs=gs)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/group',
|
||||
summary='删除 G 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_group(g: DeleteGroupParam) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.delete_group(g=g)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/groups',
|
||||
summary='删除多组 G 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:group:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_groups(gs: list[DeleteGroupParam]) -> ResponseSchemaModel[bool]:
|
||||
data = await casbin_service.delete_groups(gs=gs)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/groups/all',
|
||||
summary='删除所有 G 权限策略',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('casbin:g:empty')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_groups(uuid: Annotated[UUID, Query()]) -> ResponseModel:
|
||||
count = await casbin_service.delete_all_groups(uuid=uuid)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
from backend.core.conf import settings
|
||||
|
||||
|
||||
class CasbinSettings(BaseSettings):
|
||||
"""Casbin 配置"""
|
||||
|
||||
# RBAC
|
||||
RBAC_CASBIN_EXCLUDE: set[tuple[str, str]] = {
|
||||
('POST', f'{settings.FASTAPI_API_V1_PATH}/auth/logout'),
|
||||
('POST', f'{settings.FASTAPI_API_V1_PATH}/auth/token/new'),
|
||||
}
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_casbin_settings() -> CasbinSettings:
|
||||
"""获取 Casbin 配置"""
|
||||
return CasbinSettings()
|
||||
|
||||
|
||||
casbin_settings = get_casbin_settings()
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.common.security.permission import filter_data_permission
|
||||
from backend.plugin.casbin.model import Api
|
||||
from backend.plugin.casbin.schema.api import CreateApiParam, UpdateApiParam
|
||||
|
||||
|
||||
class CRUDApi(CRUDPlus[Api]):
|
||||
"""API 数据库操作类"""
|
||||
|
||||
async def get(self, db: AsyncSession, pk: int) -> Api | None:
|
||||
"""
|
||||
获取 API
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: API ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, pk)
|
||||
|
||||
async def get_list(self, request: Request, name: str = None, method: str = None, path: str = None) -> Select:
|
||||
"""
|
||||
获取 API 列表
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param name: API 名称
|
||||
:param method: 请求方法
|
||||
:param path: API 路径
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
if name is not None:
|
||||
filters.update(name__like=f'%{name}%')
|
||||
if method is not None:
|
||||
filters.update(method=method)
|
||||
if path is not None:
|
||||
filters.update(path__like=f'%{path}%')
|
||||
stmt = await self.select_order('created_time', 'desc', **filters)
|
||||
return stmt.where(filter_data_permission(request))
|
||||
|
||||
async def get_all(self, db: AsyncSession) -> Sequence[Api]:
|
||||
"""
|
||||
获取所有 API
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
return await self.select_models(db)
|
||||
|
||||
async def get_by_name(self, db: AsyncSession, name: str) -> Api | None:
|
||||
"""
|
||||
通过名称获取 API
|
||||
|
||||
:param db: 数据库会话
|
||||
:param name: API 名称
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, name=name)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateApiParam) -> None:
|
||||
"""
|
||||
创建 API
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 创建 API 参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def update(self, db: AsyncSession, pk: int, obj: UpdateApiParam) -> int:
|
||||
"""
|
||||
更新 API
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: API ID
|
||||
:param obj: 更新 API 参数
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, pk, obj)
|
||||
|
||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||
"""
|
||||
删除 API
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: API ID 列表
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
||||
|
||||
|
||||
api_dao: CRUDApi = CRUDApi(Api)
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.plugin.casbin.model import CasbinRule
|
||||
from backend.plugin.casbin.schema.casbin_rule import DeleteAllPoliciesParam
|
||||
|
||||
|
||||
class CRUDCasbin(CRUDPlus[CasbinRule]):
|
||||
"""Casbin 规则数据库操作类"""
|
||||
|
||||
async def get_list(self, ptype: str, sub: str) -> Select:
|
||||
"""
|
||||
获取策略列表
|
||||
|
||||
:param ptype: 策略类型
|
||||
:param sub: 用户 UUID / 角色 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_order('id', 'desc', ptype=ptype, v0__like=f'%{sub}%')
|
||||
|
||||
async def delete_policies_by_sub(self, db: AsyncSession, sub: DeleteAllPoliciesParam) -> int:
|
||||
"""
|
||||
删除角色所有 P 策略
|
||||
|
||||
:param db: 数据库会话
|
||||
:param sub: 删除所有 P 策略参数
|
||||
:return:
|
||||
"""
|
||||
filters = [sub.role]
|
||||
if sub.uuid:
|
||||
filters.append(sub.uuid)
|
||||
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, v0__mor={'eq': filters})
|
||||
|
||||
async def delete_groups_by_uuid(self, db: AsyncSession, uuid: UUID) -> int:
|
||||
"""
|
||||
删除用户所有 G 策略
|
||||
|
||||
:param db: 数据库会话
|
||||
:param uuid: 用户 UUID
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, v0=str(uuid))
|
||||
|
||||
|
||||
casbin_dao: CRUDCasbin = CRUDCasbin(CasbinRule)
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend.plugin.casbin.model.api import Api
|
||||
from backend.plugin.casbin.model.casbin_rule import CasbinRule
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.model import Base, id_key
|
||||
|
||||
|
||||
class Api(Base):
|
||||
"""API 表"""
|
||||
|
||||
__tablename__ = 'sys_api'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(50), unique=True, comment='API 名称')
|
||||
method: Mapped[str] = mapped_column(String(16), comment='请求方法')
|
||||
path: Mapped[str] = mapped_column(String(500), comment='API 路径')
|
||||
remark: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='备注')
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.model import MappedBase, id_key
|
||||
|
||||
|
||||
class CasbinRule(MappedBase):
|
||||
"""Casbin 规则表"""
|
||||
|
||||
__tablename__ = 'sys_casbin_rule'
|
||||
|
||||
id: Mapped[id_key]
|
||||
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p / g')
|
||||
v0: Mapped[str] = mapped_column(String(255), comment='用户 UUID / 角色 ID')
|
||||
v1: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='API 路径 / 角色名称')
|
||||
v2: Mapped[str | None] = mapped_column(String(255), comment='请求方法')
|
||||
v3: Mapped[str | None] = mapped_column(String(255), comment='预留字段')
|
||||
v4: Mapped[str | None] = mapped_column(String(255), comment='预留字段')
|
||||
v5: Mapped[str | None] = mapped_column(String(255), comment='预留字段')
|
||||
|
||||
def __str__(self) -> str:
|
||||
arr = [self.ptype]
|
||||
for v in (self.v0, self.v1, self.v2, self.v3, self.v4, self.v5):
|
||||
if v is None:
|
||||
break
|
||||
arr.append(v)
|
||||
return ', '.join(arr)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<CasbinRule {self.id}: "{str(self)}">'
|
||||
@@ -1,10 +0,0 @@
|
||||
[app]
|
||||
include = 'admin'
|
||||
|
||||
[api.api]
|
||||
prefix = '/apis'
|
||||
tags = '系统API'
|
||||
|
||||
[api.casbin]
|
||||
prefix = '/casbin'
|
||||
tags = '系统Casbin权限'
|
||||
@@ -1,2 +0,0 @@
|
||||
casbin>=1.38.0
|
||||
casbin_async_sqlalchemy_adapter>=1.7.0
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.enums import MethodType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class ApiSchemaBase(SchemaBase):
|
||||
"""API 基础模型"""
|
||||
|
||||
name: str = Field(description='API 名称')
|
||||
method: MethodType = Field(MethodType.GET, description='请求方法')
|
||||
path: str = Field(description='API 路径')
|
||||
remark: str | None = Field(None, description='备注')
|
||||
|
||||
|
||||
class CreateApiParam(ApiSchemaBase):
|
||||
"""创建 API 参数"""
|
||||
|
||||
|
||||
class UpdateApiParam(ApiSchemaBase):
|
||||
"""更新 API 参数"""
|
||||
|
||||
|
||||
class GetApiDetail(ApiSchemaBase):
|
||||
"""API 详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='API ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.enums import MethodType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class CreatePolicyParam(SchemaBase):
|
||||
"""创建 P 策略参数"""
|
||||
|
||||
sub: str = Field(description='用户 UUID / 角色 ID')
|
||||
path: str = Field(description='API 路径')
|
||||
method: MethodType = Field(MethodType.GET, description='请求方法')
|
||||
|
||||
|
||||
class UpdatePolicyParam(SchemaBase):
|
||||
"""更新 P 策略参数"""
|
||||
|
||||
old: CreatePolicyParam = Field(description='原策略')
|
||||
new: CreatePolicyParam = Field(description='新策略')
|
||||
|
||||
|
||||
class UpdatePoliciesParam(SchemaBase):
|
||||
"""批量更新策略参数"""
|
||||
|
||||
old: list[CreatePolicyParam] = Field(description='原策略列表')
|
||||
new: list[CreatePolicyParam] = Field(description='新策略列表')
|
||||
|
||||
|
||||
class DeletePolicyParam(CreatePolicyParam):
|
||||
"""删除策略参数"""
|
||||
|
||||
|
||||
class DeleteAllPoliciesParam(SchemaBase):
|
||||
"""删除所有策略参数"""
|
||||
|
||||
uuid: str | None = Field(None, description='用户 UUID')
|
||||
role: str = Field(description='角色')
|
||||
|
||||
|
||||
class CreateGroupParam(SchemaBase):
|
||||
"""创建 G 策略参数"""
|
||||
|
||||
uuid: str = Field(description='用户 UUID')
|
||||
role: str = Field(description='角色')
|
||||
|
||||
|
||||
class DeleteGroupParam(CreateGroupParam):
|
||||
"""删除 G 策略参数"""
|
||||
|
||||
|
||||
class GetPolicyDetail(SchemaBase):
|
||||
"""策略详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='规则 ID')
|
||||
ptype: str = Field(description='规则类型, p / g')
|
||||
v0: str = Field(description='用户 UUID / 角色 ID')
|
||||
v1: str = Field(description='API 路径 / 角色')
|
||||
v2: str | None = Field(None, description='请求方法')
|
||||
v3: str | None = Field(None, description='预留字段')
|
||||
v4: str | None = Field(None, description='预留字段')
|
||||
v5: str | None = Field(None, description='预留字段')
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import Select
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.database.db import async_db_session
|
||||
from backend.plugin.casbin.crud.crud_api import api_dao
|
||||
from backend.plugin.casbin.model import Api
|
||||
from backend.plugin.casbin.schema.api import CreateApiParam, UpdateApiParam
|
||||
|
||||
|
||||
class ApiService:
|
||||
"""API 服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get(*, pk: int) -> Api:
|
||||
"""
|
||||
获取 API
|
||||
|
||||
:param pk: API ID
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session() as db:
|
||||
api = await api_dao.get(db, pk)
|
||||
if not api:
|
||||
raise errors.NotFoundError(msg='接口不存在')
|
||||
return api
|
||||
|
||||
@staticmethod
|
||||
async def get_select(*, request: Request, name: str = None, method: str = None, path: str = None) -> Select:
|
||||
"""
|
||||
获取 API 查询对象
|
||||
|
||||
:param request: 请求对象
|
||||
:param name: API 名称
|
||||
:param method: 请求方法
|
||||
:param path: API 路径
|
||||
:return:
|
||||
"""
|
||||
return await api_dao.get_list(request=request, name=name, method=method, path=path)
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> Sequence[Api]:
|
||||
"""获取所有 API"""
|
||||
async with async_db_session() as db:
|
||||
apis = await api_dao.get_all(db)
|
||||
return apis
|
||||
|
||||
@staticmethod
|
||||
async def create(*, obj: CreateApiParam) -> None:
|
||||
"""
|
||||
创建 API
|
||||
|
||||
:param obj: 创建 API 参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
api = await api_dao.get_by_name(db, obj.name)
|
||||
if api:
|
||||
raise errors.ForbiddenError(msg='接口已存在')
|
||||
await api_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def update(*, pk: int, obj: UpdateApiParam) -> int:
|
||||
"""
|
||||
更新 API
|
||||
|
||||
:param pk: API ID
|
||||
:param obj: 更新 API 参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
api = await api_dao.get(db, pk)
|
||||
if not api:
|
||||
raise errors.NotFoundError(msg='接口不存在')
|
||||
count = await api_dao.update(db, pk, obj)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
"""
|
||||
删除 API
|
||||
|
||||
:param pk: API ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await api_dao.delete(db, pk)
|
||||
return count
|
||||
|
||||
|
||||
api_service: ApiService = ApiService()
|
||||
@@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import Select
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.database.db import async_db_session
|
||||
from backend.plugin.casbin.crud.crud_casbin import casbin_dao
|
||||
from backend.plugin.casbin.schema.casbin_rule import (
|
||||
CreateGroupParam,
|
||||
CreatePolicyParam,
|
||||
DeleteAllPoliciesParam,
|
||||
DeleteGroupParam,
|
||||
DeletePolicyParam,
|
||||
UpdatePoliciesParam,
|
||||
UpdatePolicyParam,
|
||||
)
|
||||
from backend.plugin.casbin.utils.rbac import casbin_enforcer
|
||||
|
||||
|
||||
class CasbinService:
|
||||
"""Casbin 权限服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_casbin_list(*, ptype: str, sub: str) -> Select:
|
||||
"""
|
||||
获取 Casbin 规则列表
|
||||
|
||||
:param ptype: 策略类型
|
||||
:param sub: 用户 UUID / 角色 ID
|
||||
:return:
|
||||
"""
|
||||
return await casbin_dao.get_list(ptype, sub)
|
||||
|
||||
@staticmethod
|
||||
async def get_policy_list(*, role: int | None) -> list:
|
||||
"""
|
||||
获取 P 策略列表
|
||||
|
||||
:param role: 角色ID
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
if role is not None:
|
||||
data = enforcer.get_filtered_named_policy('p', 0, str(role))
|
||||
else:
|
||||
data = enforcer.get_policy()
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def create_policy(*, p: CreatePolicyParam) -> bool:
|
||||
"""
|
||||
创建 P 策略
|
||||
|
||||
:param p: 策略参数
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.add_policy(p.sub, p.path, p.method)
|
||||
if not data:
|
||||
raise errors.ForbiddenError(msg='权限已存在')
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def create_policies(*, ps: list[CreatePolicyParam]) -> bool:
|
||||
"""
|
||||
批量创建 P 策略
|
||||
|
||||
:param ps: 策略参数列表
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.add_policies([list(p.model_dump().values()) for p in ps])
|
||||
if not data:
|
||||
raise errors.ForbiddenError(msg='权限已存在')
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def update_policy(*, obj: UpdatePolicyParam) -> bool:
|
||||
"""
|
||||
更新 P 策略
|
||||
|
||||
:param obj: 更新 P 策略参数
|
||||
:return:
|
||||
"""
|
||||
old_obj = obj.old
|
||||
new_obj = obj.new
|
||||
enforcer = await casbin_enforcer()
|
||||
_p = enforcer.has_policy(old_obj.sub, old_obj.path, old_obj.method)
|
||||
if not _p:
|
||||
raise errors.NotFoundError(msg='权限不存在')
|
||||
data = await enforcer.update_policy(
|
||||
[old_obj.sub, old_obj.path, old_obj.method],
|
||||
[new_obj.sub, new_obj.path, new_obj.method],
|
||||
)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def update_policies(*, obj: UpdatePoliciesParam) -> bool:
|
||||
"""
|
||||
批量更新 P 策略
|
||||
|
||||
:param obj: 更新 P 策略参数
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.update_policies(
|
||||
[list(o.model_dump().values()) for o in obj.old],
|
||||
[list(n.model_dump().values()) for n in obj.new],
|
||||
)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_policy(*, p: DeletePolicyParam) -> bool:
|
||||
"""
|
||||
删除 P 策略
|
||||
|
||||
:param p: 删除参数
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
_p = enforcer.has_policy(p.sub, p.path, p.method)
|
||||
if not _p:
|
||||
raise errors.NotFoundError(msg='权限不存在')
|
||||
data = await enforcer.remove_policy(p.sub, p.path, p.method)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_policies(*, ps: list[DeletePolicyParam]) -> bool:
|
||||
"""
|
||||
批量删除 P 策略
|
||||
|
||||
:param ps: 删除参数列表
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.remove_policies([list(p.model_dump().values()) for p in ps])
|
||||
if not data:
|
||||
raise errors.NotFoundError(msg='权限不存在')
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_all_policies(*, sub: DeleteAllPoliciesParam) -> int:
|
||||
"""
|
||||
删除所有 P 策略
|
||||
|
||||
:param sub: 删除参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await casbin_dao.delete_policies_by_sub(db, sub)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def get_group_list() -> list:
|
||||
"""获取 G 策略列表"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = enforcer.get_grouping_policy()
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def create_group(*, g: CreateGroupParam) -> bool:
|
||||
"""
|
||||
创建 G 策略
|
||||
|
||||
:param g: 创建 G 策略参数
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.add_grouping_policy(g.uuid, g.role)
|
||||
if not data:
|
||||
raise errors.ForbiddenError(msg='权限已存在')
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def create_groups(*, gs: list[CreateGroupParam]) -> bool:
|
||||
"""
|
||||
批量创建 G 策略
|
||||
|
||||
:param gs: 创建参数列表
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.add_grouping_policies([list(g.model_dump().values()) for g in gs])
|
||||
if not data:
|
||||
raise errors.ForbiddenError(msg='权限已存在')
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_group(*, g: DeleteGroupParam) -> bool:
|
||||
"""
|
||||
删除 G 策略
|
||||
|
||||
:param g: 删除参数
|
||||
:return:
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
_g = enforcer.has_grouping_policy(g.uuid, g.role)
|
||||
if not _g:
|
||||
raise errors.NotFoundError(msg='权限不存在')
|
||||
data = await enforcer.remove_grouping_policy(g.uuid, g.role)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_groups(*, gs: list[DeleteGroupParam]) -> bool:
|
||||
"""
|
||||
批量删除 G 策略
|
||||
|
||||
:param gs: 删除参数列表
|
||||
:return: 是否成功
|
||||
"""
|
||||
enforcer = await casbin_enforcer()
|
||||
data = await enforcer.remove_grouping_policies([list(g.model_dump().values()) for g in gs])
|
||||
if not data:
|
||||
raise errors.NotFoundError(msg='权限不存在')
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def delete_all_groups(*, uuid: UUID) -> int:
|
||||
"""
|
||||
删除所有 G 策略
|
||||
|
||||
:param uuid: 用户uuid
|
||||
:return: 删除数量
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await casbin_dao.delete_groups_by_uuid(db, uuid)
|
||||
return count
|
||||
|
||||
|
||||
casbin_service: CasbinService = CasbinService()
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import casbin
|
||||
import casbin_async_sqlalchemy_adapter
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from backend.common.exception.errors import AuthorizationError
|
||||
from backend.database.db import async_engine
|
||||
from backend.plugin.casbin.conf import casbin_settings
|
||||
from backend.plugin.casbin.model import CasbinRule
|
||||
|
||||
|
||||
async def casbin_enforcer() -> casbin.AsyncEnforcer:
|
||||
"""获取 casbin 执行器"""
|
||||
# 模型定义:https://casbin.org/zh/docs/category/model
|
||||
_CASBIN_RBAC_MODEL_CONF_TEXT = """
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
|
||||
"""
|
||||
adapter = casbin_async_sqlalchemy_adapter.Adapter(async_engine, db_class=CasbinRule)
|
||||
model = casbin.AsyncEnforcer.new_model(text=_CASBIN_RBAC_MODEL_CONF_TEXT)
|
||||
enforcer = casbin.AsyncEnforcer(model, adapter)
|
||||
await enforcer.load_policy()
|
||||
return enforcer
|
||||
|
||||
|
||||
async def casbin_verify(request: Request) -> None:
|
||||
"""
|
||||
Casbin 权限校验
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
method = request.method
|
||||
path = request.url.path
|
||||
|
||||
# casbin 鉴权白名单
|
||||
if (method, path) in casbin_settings.RBAC_CASBIN_EXCLUDE:
|
||||
return
|
||||
|
||||
# casbin 权限校验
|
||||
user_uuid = request.user.uuid
|
||||
enforcer = await casbin_enforcer()
|
||||
if not enforcer.enforce(user_uuid, path, method):
|
||||
raise AuthorizationError
|
||||
Reference in New Issue
Block a user