diff --git a/api/api_v1/api.py b/api/api_v1/api.py index 6173cfe..6535a67 100644 --- a/api/api_v1/api.py +++ b/api/api_v1/api.py @@ -5,6 +5,7 @@ from .endpoints import folder from .endpoints import space from .endpoints import dashboard from .endpoints import report +from .endpoints import authority api_router = APIRouter() @@ -14,3 +15,5 @@ api_router.include_router(folder.router, tags=["文件夹接口"], prefix='/fold api_router.include_router(space.router, tags=["空间接口"], prefix='/space') api_router.include_router(dashboard.router, tags=["看板接口"], prefix='/dashboard') api_router.include_router(report.router, tags=["报表接口"], prefix='/report') + +api_router.include_router(authority.router, tags=["权限管理接口"], prefix='/authority') diff --git a/api/api_v1/endpoints/authority.py b/api/api_v1/endpoints/authority.py new file mode 100644 index 0000000..4b0449d --- /dev/null +++ b/api/api_v1/endpoints/authority.py @@ -0,0 +1,132 @@ +import pymongo +from fastapi import APIRouter, Depends, Request +from motor.motor_asyncio import AsyncIOMotorDatabase +import crud, schemas +from core.config import settings +from core.security import get_password_hash + +from db import get_database +from api import deps +from utils import casbin_enforcer + +router = APIRouter() + + +@router.get("/api_list") +async def api_list(request: Request, current_user: schemas.UserDB = Depends(deps.get_current_user)) -> schemas.Msg: + """api 列表""" + app = request.app + data = [] + for r in app.routes: + path = r.path + name = r.description if hasattr(r, 'description') else r.name + data.append({'api': path, 'name': name}) + return schemas.Msg(code=0, msg='ok', data=data) + + +@router.post("/add_role") +async def add_role(request: Request, data_in: schemas.CasbinRoleCreate, + db: AsyncIOMotorDatabase = Depends(get_database), + current_user: schemas.UserDB = Depends(deps.get_current_user) + ) -> schemas.Msg: + """创建角色""" + role = ( + 'g', + 'root', + data_in.role_name, + None + ) + await crud.authority.create(db, role) + + for item in data_in.role_api: + await crud.authority.create(db, ( + 'p', + data_in.role_name, + item, + '*' + )) + return schemas.Msg(code=0, msg='ok') + + +@router.post("/add_account") +async def add_account(request: Request, + data_in: schemas.AccountCreate, + db: AsyncIOMotorDatabase = Depends(get_database), + current_user: schemas.UserDB = Depends(deps.get_current_user) + ) -> schemas.Msg: + """创建账号 并设置角色""" + + account = schemas.UserCreate(name=data_in.name, password=settings.DEFAULT_PASSWORD) + try: + await crud.user.create(db, account) + except pymongo.errors.DuplicateKeyError: + return schemas.Msg(code=-1, msg='用户名已存在') + rule = ( + 'g', + data_in.name, + data_in.role_name, + None + ) + await crud.authority.create(db, rule) + + return schemas.Msg(code=0, msg='ok') + + +@router.get("/all_role") +async def all_role(request: Request, + db: AsyncIOMotorDatabase = Depends(get_database), + current_user: schemas.UserDB = Depends(deps.get_current_user) + ) -> schemas.Msg: + """获取所有角色 和 角色权限""" + routes = {} + for item in request.app.routes: + routes[item.path] = item.description if hasattr(item, 'description') else item.name + roles = casbin_enforcer.get_all_roles() + permissions = {} + for role in roles: + for _, path, _ in casbin_enforcer.get_permissions_for_user(role): + permissions.setdefault(role, []) + if path == '*': + permissions[role].clear() + + permissions[role] = [{ + 'path': k, + 'name': v + } for k, v in routes.items()] + break + + if path in routes: + permissions[role].append( + { + 'path': path, + 'name': routes[path] + } + ) + + return schemas.Msg(code=0, msg='ok', data={'roles': roles, 'permissions': permissions}) + + +@router.post("/set_role") +async def set_role(request: Request, + data_id: schemas.AccountSetRole, + db: AsyncIOMotorDatabase = Depends(get_database), + current_user: schemas.UserDB = Depends(deps.get_current_user) + ) -> schemas.Msg: + """设置账号角色""" + casbin_enforcer.delete_user(data_id.name) + casbin_enforcer.add_role_for_user(data_id.name, data_id.role_name) + crud.authority.update_upsert(db, {'prtype': 'g', 'v0': data_id.name}, v1=data_id.role_name) + + return schemas.Msg(code=0, msg='ok') + + + + +# @router.get("/delete_user") +# async def delete_user(request: Request, +# data_id: schemas.AccountDeleteUser, +# db: AsyncIOMotorDatabase = Depends(get_database), +# current_user: schemas.UserDB = Depends(deps.get_current_user) +# ) -> schemas.Msg: +# pass +# return schemas.Msg(code=0, msg='暂时没有') diff --git a/api/api_v1/endpoints/dashboard.py b/api/api_v1/endpoints/dashboard.py index f27c8da..ed02423 100644 --- a/api/api_v1/endpoints/dashboard.py +++ b/api/api_v1/endpoints/dashboard.py @@ -15,11 +15,11 @@ async def create( db: AsyncIOMotorDatabase = Depends(get_database), current_user: schemas.UserDB = Depends(deps.get_current_user) ) -> schemas.Msg: + """创建看板""" try: await crud.dashboard.create(db, data_in, user_id=current_user.id) except pymongo.errors.DuplicateKeyError: - return schemas.Msg(code=-1, msg='error', data='看板已存在') - # todo 建默认文件夹 + return schemas.Msg(code=-1, msg='看板已存在', data='看板已存在') return schemas.Msg(code=0, msg='ok', data='创建成功') @@ -30,7 +30,7 @@ async def delete( db: AsyncIOMotorDatabase = Depends(get_database), current_user: schemas.UserDB = Depends(deps.get_current_user) ) -> schemas.Msg: - # 删除Dashboard 自己创建的 + """删除看板""" del_dashboard = await crud.dashboard.delete(db, _id=data_in.id, user_id=current_user.id) if del_dashboard.deleted_count == 0: @@ -39,7 +39,7 @@ async def delete( @router.post("/move") -async def delete( +async def move( data_in: schemas.DashboardMove, db: AsyncIOMotorDatabase = Depends(get_database), current_user: schemas.UserDB = Depends(deps.get_current_user) @@ -58,12 +58,13 @@ async def add_report(data_in: schemas.AddReport, db: AsyncIOMotorDatabase = Depends(get_database), current_user: schemas.UserDB = Depends(deps.get_current_user) ): + """添加报表""" res = await crud.dashboard.update_one(db, id=data_in.id, **{'$push': {'reports': {'$each': data_in.report_ids}}}) return schemas.Msg(code=0, msg='ok', data='ok') @router.post("/del_report") -async def add_report(data_in: schemas.DelReport, +async def del_report(data_in: schemas.DelReport, db: AsyncIOMotorDatabase = Depends(get_database), current_user: schemas.UserDB = Depends(deps.get_current_user) ): @@ -74,7 +75,7 @@ async def add_report(data_in: schemas.DelReport, @router.get("/") -async def add_report(_id: str, +async def dashboards(_id: str, db: AsyncIOMotorDatabase = Depends(get_database), current_user: schemas.UserDB = Depends(deps.get_current_user) ): diff --git a/api/api_v1/endpoints/folder.py b/api/api_v1/endpoints/folder.py index 6cfe197..90d8f17 100644 --- a/api/api_v1/endpoints/folder.py +++ b/api/api_v1/endpoints/folder.py @@ -19,7 +19,7 @@ async def create( try: await crud.folder.create(db, data_in, user_id=current_user.id) except pymongo.errors.DuplicateKeyError: - return schemas.Msg(code=-1, msg='error', data='文件夹已存在') + return schemas.Msg(code=-1, msg='文件夹已存在', data='文件夹已存在') # todo 建默认文件夹 return schemas.Msg(code=0, msg='ok', data='创建成功') diff --git a/api/api_v1/endpoints/project.py b/api/api_v1/endpoints/project.py index ffded54..677143f 100644 --- a/api/api_v1/endpoints/project.py +++ b/api/api_v1/endpoints/project.py @@ -65,7 +65,8 @@ async def read_kanban( # 我的空间 where = { 'project_id': data_in.id, - '$or': [{'rw_members': current_user.id}, {'r_members': current_user.id}] + 'members._id': current_user.id + # '$or': [{'rw_members': current_user.id}, {'r_members': current_user.id}] } spaces = await crud.space.find_many(db, **where) # 空间 文件夹 看板 @@ -75,7 +76,7 @@ async def read_kanban( 'children': [], '_id': item['_id'] }) - res['spaces'][-1]['authority'] = 'rw' if current_user.id in item['rw_members'] else 'r' + res['spaces'][-1]['authority'] = 'rw' if current_user.id in item['members'] else 'r' for f in await crud.folder.find_many(db, pid=item['_id']): res['spaces'][-1]['children'].append({ diff --git a/api/api_v1/endpoints/space.py b/api/api_v1/endpoints/space.py index 983b498..351fcbe 100644 --- a/api/api_v1/endpoints/space.py +++ b/api/api_v1/endpoints/space.py @@ -17,10 +17,16 @@ async def create( ) -> schemas.Msg: """创建空间""" try: - await crud.space.create(db, data_in, user_id=current_user.id) + if data_in.is_all_member: + data_in.members.clear() + users = await crud.user.find_many(db) + for user in users: + if user['_id'] == current_user.id: + continue + data_in.members.append(schemas.space.Member(**user, authority=data_in.authority)) + await crud.space.create(db, data_in, user=current_user) except pymongo.errors.DuplicateKeyError: return schemas.Msg(code=-1, msg='空间已存在', data='空间已存在') - # todo 建默认文件夹 return schemas.Msg(code=0, msg='创建成功', data='创建成功') diff --git a/core/config.py b/core/config.py index 1228d2d..c9d4204 100644 --- a/core/config.py +++ b/core/config.py @@ -23,6 +23,8 @@ class Settings(BaseSettings): FIRST_SUPERUSER_PASSWORD: str = '123456' FIRST_NAME: str = 'root' + DEFAULT_PASSWORD = '123456' + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 SECRET_KEY: str = 'ZaFX6EypK6PtuhGv11q4DLRvAb0csiLx4dbKUwLwCe8' diff --git a/crud/__init__.py b/crud/__init__.py index 443e6d8..b12ce8a 100644 --- a/crud/__init__.py +++ b/crud/__init__.py @@ -4,3 +4,4 @@ from .crud_folder import folder from .crud_space import space from .crud_dashboard import dashboard from .crud_report import report +from .crud_authority import authority diff --git a/crud/base.py b/crud/base.py index 54fd77e..eda7ccd 100644 --- a/crud/base.py +++ b/crud/base.py @@ -25,3 +25,9 @@ class CRUDBase: async def update_one(self, db, id, **kwargs): return await db[self.coll_name].update_one({'_id': id}, kwargs) + + async def update_upsert(self, db, where: dict, **kwargs): + return await db[self.coll_name].update_one(where, {'$set': kwargs}, upsert=True) + + async def distinct(self, db, key, filter=None): + return await db[self.coll_name].distinct(key, filter) diff --git a/crud/crud_authority.py b/crud/crud_authority.py new file mode 100644 index 0000000..0741644 --- /dev/null +++ b/crud/crud_authority.py @@ -0,0 +1,31 @@ +import pymongo +from motor.motor_asyncio import AsyncIOMotorDatabase + +from core.config import settings +from crud.base import CRUDBase +from schemas import * +from utils import * + +__all__ = 'authority', + + +class CRUDAuthority(CRUDBase): + + async def create(self, db: AsyncIOMotorDatabase, *args): + casbin_model.add_policy(args[0], args[0], args[1:]) + data = {'ptype': args[0], + 'v0': args[1], + 'v1': args[2], + 'v2': args[3], + } + await self.update_upsert(db, data, **data) + + async def create_index(self, db: AsyncIOMotorDatabase): + await db[self.coll_name].create_index( + [('ptype', pymongo.DESCENDING), ('v0', pymongo.DESCENDING), ('v1', pymongo.DESCENDING), + ('v2', pymongo.DESCENDING)], + unique=True) + + + +authority = CRUDAuthority(settings.CASBIN_COLL) diff --git a/crud/crud_space.py b/crud/crud_space.py index 44b834e..2537bc2 100644 --- a/crud/crud_space.py +++ b/crud/crud_space.py @@ -9,12 +9,11 @@ __all__ = 'space', class CRUDSpace(CRUDBase): - async def create(self, db: AsyncIOMotorDatabase, obj_in: SpaceCreate, user_id: str): + async def create(self, db: AsyncIOMotorDatabase, obj_in: SpaceCreate, user: UserDB): + obj_in.members.append({'_id': user.id, 'name': user.name, 'authority': 'rw'}) db_obj = SpaceDB( - **obj_in.dict(), user_id=user_id, - rw_members=[user_id], + **obj_in.dict(by_alias=True), user_id=user.id, _id=uuid.uuid1().hex - ) await db[self.coll_name].insert_one(db_obj.dict(by_alias=True)) diff --git a/crud/crud_user.py b/crud/crud_user.py index 2fee1df..aab0c5b 100644 --- a/crud/crud_user.py +++ b/crud/crud_user.py @@ -34,7 +34,7 @@ class CRUDUser(CRUDBase): return user_obj async def create_index(self, db: AsyncIOMotorDatabase): - await db[self.coll_name].create_index('username', unique=True) + await db[self.coll_name].create_index('name', unique=True) user = CRUDUser('user') diff --git a/db/init_db.py b/db/init_db.py index 6c4a594..038cd83 100644 --- a/db/init_db.py +++ b/db/init_db.py @@ -38,10 +38,24 @@ async def space_index(): async def dashboard_index(): await crud.dashboard.create_index(db) + async def report_index(): await crud.report.create_index(db) +async def authority_init(): + await crud.authority.create_index(db) + await crud.authority.create(db, 'p', 'admin', '*', '*') + await crud.authority.create(db, 'g', 'root', 'admin', None) + await crud.authority.create(db, 'p', '*', '/docs', '*') + await crud.authority.create(db, 'p', '*', '/openapi.json', '*') + + await crud.authority.create(db, 'p', '*', '/api/v1/user/login', '*') + + await crud.authority.create(db, 'p', '*', '/api/v1/project/', '*') + await crud.authority.create(db, 'p', '*', '/api/v1/project/kanban', '*') + + async def main(): await create_superuser() await project_index() @@ -49,6 +63,7 @@ async def main(): await space_index() await dashboard_index() await report_index() + await authority_init() loop = asyncio.get_event_loop() diff --git a/db/init_menu.py b/db/init_menu.py new file mode 100644 index 0000000..43aaaa8 --- /dev/null +++ b/db/init_menu.py @@ -0,0 +1,13 @@ +data = [ + {'title': '用户接口', 'powerarr': [ + {'title': '获取所有用户', 'path': '/api/v1/all_user'}, + ] + }, + {'title': '项目接口', 'powerarr': [ + {'title': '创建项目', 'path': '/api/v1/project/create'}, + ] + }, + {'title': '文件夹接口', 'powerarr': [ + {'title': '创建项目', 'path': '/api/v1/project/create'}, + ]}, +] diff --git a/main.py b/main.py index 0afb1a4..9b0537c 100644 --- a/main.py +++ b/main.py @@ -5,13 +5,15 @@ import uvicorn from fastapi import FastAPI import casbin +from api.deps import get_current_user2 from core.config import settings from starlette.middleware.cors import CORSMiddleware -from starlette.authentication import AuthenticationBackend, AuthenticationError, SimpleUser, AuthCredentials +from starlette.authentication import AuthenticationBackend, AuthenticationError, AuthCredentials, BaseUser, SimpleUser from starlette.middleware.authentication import AuthenticationMiddleware +from fastapi_authz import CasbinMiddleware - -from db import connect_to_mongo, close_mongo_connection +from db import connect_to_mongo, close_mongo_connection, get_database +from utils import * app = FastAPI(title=settings.PROJECT_NAME) @@ -24,31 +26,50 @@ if settings.BACKEND_CORS_ORIGINS: allow_headers=["*"], ) app.add_event_handler("startup", connect_to_mongo) + app.add_event_handler("shutdown", close_mongo_connection) +class CurrentUser(BaseUser): + def __init__(self, username: str, user_id: str) -> None: + self.username = username + self.id = user_id + + @property + def is_authenticated(self) -> bool: + return True + + @property + def display_name(self) -> str: + return self.username + + @property + def identity(self) -> str: + return '' + + class BasicAuth(AuthenticationBackend): async def authenticate(self, request): if "Authorization" not in request.headers: return None auth = request.headers["Authorization"] + if len(auth) < 20: + return None try: - scheme, credentials = auth.split() - decoded = base64.b64decode(credentials).decode("ascii") + user = get_current_user2(auth.split(' ')[1]) except (ValueError, UnicodeDecodeError, binascii.Error): raise AuthenticationError("Invalid basic auth credentials") - username, _, password = decoded.partition(":") - return AuthCredentials(["authenticated"]), SimpleUser(username) + return AuthCredentials(["authenticated"]), CurrentUser(user.name, user.id) -# enforcer = casbin.Enforcer('rbac_model.conf', 'rbac_policy.csv') -# app.add_middleware(CasbinMiddleware, enforcer=enforcer) -# app.add_middleware(AuthenticationMiddleware, backend=BasicAuth()) + +app.add_middleware(CasbinMiddleware, enforcer=casbin_enforcer) +app.add_middleware(AuthenticationMiddleware, backend=BasicAuth()) from api.api_v1.api import api_router - app.include_router(api_router, prefix=settings.API_V1_STR) + if __name__ == '__main__': - uvicorn.run(app='main:app', host="0.0.0.0", port=8889, reload=True, debug=True) + uvicorn.run(app='main2:app', host="0.0.0.0", port=8889, reload=True, debug=True) diff --git a/main2.py b/main2.py index aa9ff3c..db2b1f9 100644 --- a/main2.py +++ b/main2.py @@ -13,7 +13,7 @@ from starlette.middleware.authentication import AuthenticationMiddleware from fastapi_authz import CasbinMiddleware from db import connect_to_mongo, close_mongo_connection, get_database -from utils import Adapter +from utils import * app = FastAPI(title=settings.PROJECT_NAME) @@ -64,17 +64,12 @@ class BasicAuth(AuthenticationBackend): return AuthCredentials(["authenticated"]), CurrentUser(user.name, user.id) - -enforcer = casbin.Enforcer('rbac_model.conf', Adapter(settings.DATABASE_URI,settings.MDB_DB)) -app.add_middleware(CasbinMiddleware, enforcer=enforcer) +app.add_middleware(CasbinMiddleware, enforcer=casbin_enforcer) app.add_middleware(AuthenticationMiddleware, backend=BasicAuth()) - - - from api.api_v1.api import api_router - app.include_router(api_router, prefix=settings.API_V1_STR) + if __name__ == '__main__': uvicorn.run(app='main2:app', host="0.0.0.0", port=8899, reload=True, debug=True) diff --git a/rbac_policy.py b/rbac_policy.py index 6d0a186..d079dcc 100644 --- a/rbac_policy.py +++ b/rbac_policy.py @@ -3,19 +3,19 @@ import casbin from core.config import settings from pymongo import MongoClient -from utils import Adapter +from utils import * client = MongoClient(settings.DATABASE_URI) db = client[settings.MDB_DB] collection = db[settings.CASBIN_COLL] -adapter = Adapter(settings.DATABASE_URI, settings.MDB_DB) -enforcer = casbin.Enforcer('rbac_model.conf', adapter) -model = enforcer.get_model() -model.add_policy('g', 'g', ['root', 'superAdmin', ]) -model.add_policy('g', 'g', ['legu', 'admin']) -adapter.save_policy(model) -res = enforcer.enforce('alice', 'data1', 'read') + +# casbin_model.add_policy('g', 'g', ['root', 'superAdmin', ]) +# casbin_model.add_policy('g', 'g', ['legu', 'admin']) +# casbin_enforcer.add_role_for_user('user', 'role') +res = casbin_enforcer.delete_user('user') print(res) +casbin_adapter.save_policy(casbin_model) + diff --git a/schemas/__init__.py b/schemas/__init__.py index 28e1487..e4dcc3f 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -4,4 +4,5 @@ from .project import * from .folder import * from .space import * from .dashboard import * -from .report import * \ No newline at end of file +from .report import * +from .authotity import * \ No newline at end of file diff --git a/schemas/authotity.py b/schemas/authotity.py new file mode 100644 index 0000000..86d3856 --- /dev/null +++ b/schemas/authotity.py @@ -0,0 +1,35 @@ +from enum import Enum +from typing import List + +from pydantic import BaseModel + + +class Ptype(str, Enum): + p = 'p' + g = 'g' + + +class CasbinRoleCreate(BaseModel): + role_name: str + role_api: List + + +class CasbinDB(BaseModel): + ptype: Ptype + v0: str + v1: str + v2: str + + +class AccountCreate(BaseModel): + name: str + role_name: str + + +class AccountDeleteUser(BaseModel): + name: str + + +class AccountSetRole(BaseModel): + name: str + role_name: str diff --git a/schemas/space.py b/schemas/space.py index 6ce26f1..f5f332f 100644 --- a/schemas/space.py +++ b/schemas/space.py @@ -12,10 +12,23 @@ class SpaceBase(BaseModel): name: str = None +class Authority(str, Enum): + rw = 'rw' + r = 'r' + + +class Member(DBBase): + name: str + authority: Authority + + # 解析请求json 创建项目 class SpaceCreate(SpaceBase): name: str project_id: str + members: List[Member] = [] + is_all_member: bool = False + authority: Authority = 'r' class SpaceDelete(DBBase): @@ -28,6 +41,5 @@ class SpaceDB(DBBase): name: str user_id: str project_id: str - rw_members: List[str] = [] - r_members: List[str] = [] + members: List[Member] = [] create_date: datetime = datetime.now() diff --git a/schemas/user.py b/schemas/user.py index d910822..0bff6b0 100644 --- a/schemas/user.py +++ b/schemas/user.py @@ -21,8 +21,8 @@ class UserLogin(BaseModel): class UserCreate(UserBase): - email: EmailStr password: str + name: str # **************************************************************************** @@ -30,13 +30,10 @@ class UserCreate(UserBase): class UserDB(DBBase): - email: EmailStr - is_superuser: bool + email: EmailStr = None + is_superuser: bool = False name: str class UserDBRW(UserDB): hashed_password: str - - - diff --git a/utils/__init__.py b/utils/__init__.py index a7dde9b..38fc832 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1 +1 @@ -from .adapter import Adapter +from .adapter import * diff --git a/utils/adapter.py b/utils/adapter.py index 5efbcb6..e14cb3d 100644 --- a/utils/adapter.py +++ b/utils/adapter.py @@ -2,6 +2,10 @@ import casbin from casbin import persist from pymongo import MongoClient +from core.config import settings + +__all__ = 'casbin_adapter', 'casbin_enforcer', 'casbin_model' + class CasbinRule: ''' @@ -82,7 +86,7 @@ class Adapter(persist.Adapter): line.v4 = rule[4] if len(rule) > 5: line.v5 = rule[5] - self._collection.update_one(line.dict(), {'$set':line.dict()}, upsert=True) + self._collection.update_one(line.dict(), {'$set': line.dict()}, upsert=True) def save_policy(self, model): ''' @@ -110,3 +114,8 @@ class Adapter(persist.Adapter): delete policy rules for matching filters from mongodb """ pass + + +casbin_adapter = Adapter(settings.DATABASE_URI, settings.MDB_DB) +casbin_enforcer = casbin.Enforcer('rbac_model.conf', casbin_adapter) +casbin_model = casbin_enforcer.get_model() diff --git a/utils/async_adapter.py b/utils/async_adapter.py index a4c7d04..a3ace43 100644 --- a/utils/async_adapter.py +++ b/utils/async_adapter.py @@ -1,3 +1,4 @@ + from casbin import persist