diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py index 2a9af8a90..09afc842b 100644 --- a/backend/app/deps/authorization_deps.py +++ b/backend/app/deps/authorization_deps.py @@ -92,6 +92,48 @@ async def __call__( ) +class FileAuthorization: + """We use class dependency so that we can provide the `permission` parameter to the dependency. + For more info see https://fastapi.tiangolo.com/advanced/advanced-dependencies/.""" + + def __init__(self, role: str): + self.role = role + + async def __call__( + self, + file_id: str, + db: MongoClient = Depends(get_db), + current_user: str = Depends(get_current_username), + ): + if (file := await db["files"].find_one({"_id": ObjectId(file_id)})) is not None: + file_out = FileOut.from_mongo(file) + if ( + authorization_q := await db["authorization"].find_one( + { + "$and": [ + {"dataset_id": ObjectId(file_out.dataset_id)}, + { + "$or": [ + {"creator": current_user}, + {"user_ids": current_user}, + ] + }, + ] + } + ) + ) is not None: + authorization = AuthorizationDB.from_mongo(authorization_q) + if access(authorization.role, self.role): + return True + + raise HTTPException( + status_code=403, + detail=f"User `{current_user} does not have `{self.role}` permission on file {file_id}", + ) + else: + raise HTTPException(status_code=404, detail=f"File {file_id} not found") + + def access(user_role: RoleType, role_required: RoleType) -> bool: """Enforce implied role hierarchy OWNER > EDITOR > UPLOADER > VIEWER""" if user_role == RoleType.OWNER: diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index aa3c10564..3cc540bb0 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -24,6 +24,7 @@ from pymongo import MongoClient from app import dependencies +from app.deps.authorization_deps import FileAuthorization from app.config import settings from app.search.connect import ( insert_record, @@ -211,6 +212,7 @@ async def update_file( es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), credentials: HTTPAuthorizationCredentials = Security(security), rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(FileAuthorization("uploader")), ): # Check all connection and abort if any one of them is not available if db is None or fs is None or es is None: @@ -309,6 +311,7 @@ async def download_file( version: Optional[int] = None, db: MongoClient = Depends(dependencies.get_db), fs: Minio = Depends(dependencies.get_fs), + allow: bool = Depends(FileAuthorization("viewer")), ): # If file exists in MongoDB, download from Minio if (file := await db["files"].find_one({"_id": ObjectId(file_id)})) is not None: @@ -353,6 +356,7 @@ async def delete_file( db: MongoClient = Depends(dependencies.get_db), fs: Minio = Depends(dependencies.get_fs), es: Elasticsearch = Depends(dependencies.get_elasticsearchclient), + allow: bool = Depends(FileAuthorization("editor")), ): if (file := await db["files"].find_one({"_id": ObjectId(file_id)})) is not None: await remove_file_entry(file_id, db, fs, es) @@ -365,14 +369,15 @@ async def delete_file( async def get_file_summary( file_id: str, db: MongoClient = Depends(dependencies.get_db), + allow: bool = Depends(FileAuthorization("viewer")), ): if (file := await db["files"].find_one({"_id": ObjectId(file_id)})) is not None: # TODO: Incrementing too often (3x per page view) # file["views"] += 1 # db["files"].replace_one({"_id": ObjectId(file_id)}, file) return FileOut.from_mongo(file) - - raise HTTPException(status_code=404, detail=f"File {file_id} not found") + else: + raise HTTPException(status_code=404, detail=f"File {file_id} not found") @router.get("/{file_id}/versions", response_model=List[FileVersion]) @@ -381,6 +386,7 @@ async def get_file_versions( db: MongoClient = Depends(dependencies.get_db), skip: int = 0, limit: int = 20, + allow: bool = Depends(FileAuthorization("viewer")), ): if (file := await db["files"].find_one({"_id": ObjectId(file_id)})) is not None: mongo_versions = [] @@ -410,6 +416,7 @@ async def get_file_extract( credentials: HTTPAuthorizationCredentials = Security(security), db: MongoClient = Depends(dependencies.get_db), rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(FileAuthorization("uploader")), ): if extractorName is None: raise HTTPException(status_code=400, detail=f"No extractorName specified") @@ -447,6 +454,7 @@ async def resubmit_file_extractions( user=Depends(get_current_user), db: MongoClient = Depends(dependencies.get_db), rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq), + allow: bool = Depends(FileAuthorization("editor")), ): """This route will check metadata. We get the extractors run from metadata from extractors. Then they are resubmitted. At present parameters are not stored. This will change once Jobs are