From 0077fd98aaa8a0c2e4b5ec0dee0c8ba8b8334770 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 2 Sep 2023 17:54:24 +0700 Subject: [PATCH] Add required privileges to all endpoints --- gns3server/api/routes/controller/__init__.py | 12 +- gns3server/api/routes/controller/acl.py | 145 ++++++++-- .../api/routes/controller/appliances.py | 40 ++- gns3server/api/routes/controller/computes.py | 53 +++- .../routes/controller/dependencies/rbac.py | 6 +- gns3server/api/routes/controller/drawings.py | 57 +++- gns3server/api/routes/controller/groups.py | 64 ++++- gns3server/api/routes/controller/images.py | 44 ++- gns3server/api/routes/controller/links.py | 92 ++++++- gns3server/api/routes/controller/nodes.py | 191 +++++++++++-- gns3server/api/routes/controller/projects.py | 79 ++++-- gns3server/api/routes/controller/roles.py | 63 ++++- gns3server/api/routes/controller/snapshots.py | 48 +++- gns3server/api/routes/controller/symbols.py | 29 +- gns3server/api/routes/controller/templates.py | 56 +++- gns3server/api/routes/controller/users.py | 51 +++- gns3server/db/models/acl.py | 4 +- gns3server/db/models/privileges.py | 43 ++- gns3server/db/repositories/rbac.py | 25 +- gns3server/schemas/controller/rbac.py | 2 +- tests/api/routes/controller/test_acl.py | 114 ++++---- tests/api/routes/controller/test_roles.py | 6 +- tests/api/routes/controller/test_users.py | 2 +- tests/controller/test_rbac.py | 256 +++++++++++------- 24 files changed, 1125 insertions(+), 357 deletions(-) diff --git a/gns3server/api/routes/controller/__init__.py b/gns3server/api/routes/controller/__init__.py index 1b3f2d4f..8a012b95 100644 --- a/gns3server/api/routes/controller/__init__.py +++ b/gns3server/api/routes/controller/__init__.py @@ -43,35 +43,30 @@ router.include_router(users.router, prefix="/users", tags=["Users"]) router.include_router( groups.router, - dependencies=[Depends(get_current_active_user)], prefix="/groups", tags=["Users groups"] ) router.include_router( roles.router, - dependencies=[Depends(get_current_active_user)], prefix="/roles", tags=["Roles"] ) router.include_router( acl.router, - dependencies=[Depends(get_current_active_user)], prefix="/acl", tags=["ACL"] ) router.include_router( images.router, - dependencies=[Depends(get_current_active_user)], prefix="/images", tags=["Images"] ) router.include_router( templates.router, - dependencies=[Depends(get_current_active_user)], prefix="/templates", tags=["Templates"] ) @@ -83,21 +78,18 @@ router.include_router( router.include_router( nodes.router, - dependencies=[Depends(get_current_active_user)], prefix="/projects/{project_id}/nodes", tags=["Nodes"] ) router.include_router( links.router, - dependencies=[Depends(get_current_active_user)], prefix="/projects/{project_id}/links", tags=["Links"] ) router.include_router( drawings.router, - dependencies=[Depends(get_current_active_user)], prefix="/projects/{project_id}/drawings", tags=["Drawings"]) @@ -108,7 +100,6 @@ router.include_router( router.include_router( snapshots.router, - dependencies=[Depends(get_current_active_user)], prefix="/projects/{project_id}/snapshots", tags=["Snapshots"]) @@ -126,15 +117,14 @@ router.include_router( router.include_router( appliances.router, - dependencies=[Depends(get_current_active_user)], prefix="/appliances", tags=["Appliances"] ) router.include_router( gns3vm.router, - deprecated=True, dependencies=[Depends(get_current_active_user)], + deprecated=True, prefix="/gns3vm", tags=["GNS3 VM"] ) diff --git a/gns3server/api/routes/controller/acl.py b/gns3server/api/routes/controller/acl.py index 2bc4e1db..763bb194 100644 --- a/gns3server/api/routes/controller/acl.py +++ b/gns3server/api/routes/controller/acl.py @@ -30,13 +30,16 @@ from typing import List from gns3server import schemas from gns3server.controller.controller_error import ( ControllerBadRequestError, - ControllerNotFoundError, - ControllerForbiddenError, + ControllerNotFoundError ) +from gns3server.controller import Controller +from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.rbac import RbacRepository +from gns3server.db.repositories.images import ImagesRepository +from gns3server.db.repositories.templates import TemplatesRepository from .dependencies.database import get_repository -from .dependencies.authentication import get_current_active_user +from .dependencies.rbac import has_privilege import logging @@ -45,26 +48,121 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("", response_model=List[schemas.ACE]) +@router.get( + "/endpoints", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("ACE.Audit"))] +) +async def endpoints( + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) +) -> List[dict]: + """ + List all endpoints to be used in ACL entries. + """ + + controller = Controller.instance() + endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}] + + def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None: + if endpoint not in endpoints: + endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type}) + + # projects + add_to_endpoints("/projects", "All projects", "project") + projects = [p for p in controller.projects.values()] + for project in projects: + add_to_endpoints(f"/projects/{project.id}", f'Project "{project.name}"', "project") + + # nodes + add_to_endpoints(f"/projects/{project.id}/nodes", f'All nodes in project "{project.name}"', "node") + for node in project.nodes.values(): + add_to_endpoints( + f"/projects/{project.id}/nodes/{node['node_id']}", + f'Node "{node["name"]}" in project "{project.name}"', + endpoint_type="node" + ) + + # links + add_to_endpoints(f"/projects/{project.id}/links", f'All links in project "{project.name}"', "link") + for link in project.links.values(): + node_id_1 = link["nodes"][0]["node_id"] + node_id_2 = link["nodes"][1]["node_id"] + node_name_1 = project.nodes[node_id_1]["name"] + node_name_2 = project.nodes[node_id_2]["name"] + add_to_endpoints( + f"/projects/{project.id}/links/{link['link_id']}", + f'Link from "{node_name_1}" to "{node_name_2}" in project "{project.name}"', + endpoint_type="link" + ) + + # users + add_to_endpoints("/users", "All users", "user") + users = await users_repo.get_users() + for user in users: + add_to_endpoints(f"/users/{user.user_id}", f'User "{user.username}"', "user") + + # groups + add_to_endpoints("/groups", "All groups", "group") + groups = await users_repo.get_user_groups() + for group in groups: + add_to_endpoints(f"/groups/{group.user_group_id}", f'Group "{group.name}"', "group") + + # roles + add_to_endpoints("/roles", "All roles", "role") + roles = await rbac_repo.get_roles() + for role in roles: + add_to_endpoints(f"/roles/{role.role_id}", f'Role "{role.name}"', "role") + + # images + add_to_endpoints("/images", "All images", "image") + images = await images_repo.get_images() + for image in images: + add_to_endpoints(f"/images/{image.filename}", f'Image "{image.filename}"', "image") + + # templates + add_to_endpoints("/templates", "All templates", "template") + templates = await templates_repo.get_templates() + for template in templates: + add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template") + + return endpoints + + +@router.get( + "", + response_model=List[schemas.ACE], + dependencies=[Depends(has_privilege("ACE.Audit"))] +) async def get_aces( rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> List[schemas.ACE]: """ Get all ACL entries. + + Required privilege: ACE.Audit """ return await rbac_repo.get_aces() -@router.post("", response_model=schemas.ACE, status_code=status.HTTP_201_CREATED) +@router.post( + "", + response_model=schemas.ACE, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("ACE.Allocate"))] +) async def create_ace( request: Request, ace_create: schemas.ACECreate, - current_user: schemas.User = Depends(get_current_active_user), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> schemas.ACE: """ Create a new ACL entry. + + Required privilege: ACE.Allocate """ for route in request.app.routes: @@ -84,13 +182,19 @@ async def create_ace( raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint") -@router.get("/{ace_id}", response_model=schemas.ACE) +@router.get( + "/{ace_id}", + response_model=schemas.ACE, + dependencies=[Depends(has_privilege("ACE.Audit"))] +) async def get_ace( ace_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> schemas.ACE: """ Get an ACL entry. + + Required privilege: ACE.Audit """ ace = await rbac_repo.get_ace(ace_id) @@ -99,7 +203,11 @@ async def get_ace( return ace -@router.put("/{ace_id}", response_model=schemas.ACE) +@router.put( + "/{ace_id}", + response_model=schemas.ACE, + dependencies=[Depends(has_privilege("ACE.Modify"))] +) async def update_ace( ace_id: UUID, ace_update: schemas.ACEUpdate, @@ -107,6 +215,8 @@ async def update_ace( ) -> schemas.ACE: """ Update an ACL entry. + + Required privilege: ACE.Modify """ ace = await rbac_repo.get_ace(ace_id) @@ -116,13 +226,19 @@ async def update_ace( return await rbac_repo.update_ace(ace_id, ace_update) -@router.delete("/{ace_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{ace_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("ACE.Allocate"))] +) async def delete_ace( ace_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete an ACL entry. + + Required privilege: ACE.Allocate """ ace = await rbac_repo.get_ace(ace_id) @@ -132,14 +248,3 @@ async def delete_ace( success = await rbac_repo.delete_ace(ace_id) if not success: raise ControllerNotFoundError(f"ACL entry '{ace_id}' could not be deleted") - - -# @router.post("/prune", status_code=status.HTTP_204_NO_CONTENT) -# async def prune_permissions( -# rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -# ) -> None: -# """ -# Prune orphaned permissions. -# """ -# -# await rbac_repo.prune_permissions() diff --git a/gns3server/api/routes/controller/appliances.py b/gns3server/api/routes/controller/appliances.py index 918cca5e..93db5b20 100644 --- a/gns3server/api/routes/controller/appliances.py +++ b/gns3server/api/routes/controller/appliances.py @@ -20,7 +20,7 @@ API routes for appliances. import logging -from fastapi import APIRouter, Depends, Response, status +from fastapi import APIRouter, Depends, status from typing import Optional, List from uuid import UUID @@ -38,19 +38,28 @@ from gns3server.db.repositories.rbac import RbacRepository from .dependencies.authentication import get_current_active_user from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege + log = logging.getLogger(__name__) router = APIRouter() -@router.get("", response_model=List[schemas.Appliance], response_model_exclude_unset=True) +@router.get( + "", + response_model=List[schemas.Appliance], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Appliance.Audit"))] +) async def get_appliances( update: Optional[bool] = False, symbol_theme: Optional[str] = None ) -> List[schemas.Appliance]: """ Return all appliances known by the controller. + + Required privilege: Appliance.Audit """ controller = Controller.instance() @@ -60,10 +69,17 @@ async def get_appliances( return [c.asdict() for c in controller.appliance_manager.appliances.values()] -@router.get("/{appliance_id}", response_model=schemas.Appliance, response_model_exclude_unset=True) +@router.get( + "/{appliance_id}", + response_model=schemas.Appliance, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Appliance.Audit"))] +) def get_appliance(appliance_id: UUID) -> schemas.Appliance: """ Get an appliance file. + + Required privilege: Appliance.Audit """ controller = Controller.instance() @@ -73,10 +89,16 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance: return appliance.asdict() -@router.post("/{appliance_id}/version", status_code=status.HTTP_201_CREATED) +@router.post( + "/{appliance_id}/version", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Appliance.Allocate"))] +) def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> dict: """ - Add a version to an appliance + Add a version to an appliance. + + Required privilege: Appliance.Allocate """ controller = Controller.instance() @@ -98,7 +120,11 @@ def add_appliance_version(appliance_id: UUID, appliance_version: schemas.Applian return appliance.asdict() -@router.post("/{appliance_id}/install", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{appliance_id}/install", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Appliance.Allocate"))] +) async def install_appliance( appliance_id: UUID, version: Optional[str] = None, @@ -109,6 +135,8 @@ async def install_appliance( ) -> None: """ Install an appliance. + + Required privilege: Appliance.Allocate """ controller = Controller.instance() diff --git a/gns3server/api/routes/controller/computes.py b/gns3server/api/routes/controller/computes.py index 2fc9e55e..e9619d0b 100644 --- a/gns3server/api/routes/controller/computes.py +++ b/gns3server/api/routes/controller/computes.py @@ -24,10 +24,12 @@ from uuid import UUID from gns3server.controller import Controller from gns3server.db.repositories.computes import ComputesRepository +from gns3server.db.repositories.rbac import RbacRepository from gns3server.services.computes import ComputesService from gns3server import schemas from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}} @@ -43,6 +45,7 @@ router = APIRouter(responses=responses) 409: {"model": schemas.ErrorMessage, "description": "Could not create compute"}, 401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"}, }, + dependencies=[Depends(has_privilege("Compute.Allocate"))] ) async def create_compute( compute_create: schemas.ComputeCreate, @@ -51,15 +54,23 @@ async def create_compute( ) -> schemas.Compute: """ Create a new compute on the controller. + + Required privilege: Compute.Allocate """ return await ComputesService(computes_repo).create_compute(compute_create, connect) -@router.post("/{compute_id}/connect", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{compute_id}/connect", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Compute.Audit"))] +) async def connect_compute(compute_id: Union[str, UUID]) -> None: """ Connect to compute on the controller. + + Required privilege: Compute.Audit """ compute = Controller.instance().get_compute(str(compute_id)) @@ -67,29 +78,48 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None: await compute.connect(report_failed_connection=True) -@router.get("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True) +@router.get( + "/{compute_id}", + response_model=schemas.Compute, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Compute.Audit"))] +) async def get_compute( compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)) ) -> schemas.Compute: """ Return a compute from the controller. + + Required privilege: Compute.Audit """ return await ComputesService(computes_repo).get_compute(compute_id) -@router.get("", response_model=List[schemas.Compute], response_model_exclude_unset=True) +@router.get( + "", + response_model=List[schemas.Compute], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Compute.Audit"))] +) async def get_computes( computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), ) -> List[schemas.Compute]: """ Return all computes known by the controller. + + Required privilege: Compute.Audit """ return await ComputesService(computes_repo).get_computes() -@router.put("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True) +@router.put( + "/{compute_id}", + response_model=schemas.Compute, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Compute.Modify"))] +) async def update_compute( compute_id: Union[str, UUID], compute_update: schemas.ComputeUpdate, @@ -97,20 +127,31 @@ async def update_compute( ) -> schemas.Compute: """ Update a compute on the controller. + + Required privilege: Compute.Modify """ return await ComputesService(computes_repo).update_compute(compute_id, compute_update) -@router.delete("/{compute_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{compute_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Compute.Allocate"))] +) async def delete_compute( - compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)) + compute_id: Union[str, UUID], + computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a compute from the controller. + + Required privilege: Compute.Allocate """ await ComputesService(computes_repo).delete_compute(compute_id) + await rbac_repo.delete_all_ace_starting_with_path(f"/computes/{compute_id}") @router.get("/{compute_id}/docker/images", response_model=List[schemas.ComputeDockerImage]) diff --git a/gns3server/api/routes/controller/dependencies/rbac.py b/gns3server/api/routes/controller/dependencies/rbac.py index fd6e2dda..e6795348 100644 --- a/gns3server/api/routes/controller/dependencies/rbac.py +++ b/gns3server/api/routes/controller/dependencies/rbac.py @@ -37,10 +37,10 @@ def has_privilege( ): if not current_user.is_superadmin: path = re.sub(r"^/v[0-9]", "", request.url.path) # remove the prefix (e.g. "/v3") from URL path - print(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'") + log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'") if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name): raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)") - return current_user + return current_user return get_user_and_check_privilege @@ -57,7 +57,7 @@ def has_privilege_on_websocket( log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'") if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name): raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)") - return current_user + return current_user return get_user_and_check_privilege # class PrivilegeChecker: diff --git a/gns3server/api/routes/controller/drawings.py b/gns3server/api/routes/controller/drawings.py index 18b0f80a..22ca1b95 100644 --- a/gns3server/api/routes/controller/drawings.py +++ b/gns3server/api/routes/controller/drawings.py @@ -18,33 +18,51 @@ API routes for drawings. """ -from fastapi import APIRouter, Response, status +from fastapi import APIRouter, Depends, status from fastapi.encoders import jsonable_encoder from typing import List from uuid import UUID from gns3server.controller import Controller +from gns3server.db.repositories.rbac import RbacRepository from gns3server import schemas +from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege + responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}} router = APIRouter(responses=responses) -@router.get("", response_model=List[schemas.Drawing], response_model_exclude_unset=True) +@router.get( + "", + response_model=List[schemas.Drawing], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Drawing.Audit"))] +) async def get_drawings(project_id: UUID) -> List[schemas.Drawing]: """ Return the list of all drawings for a given project. + + Required privilege: Drawing.Audit """ project = await Controller.instance().get_loaded_project(str(project_id)) return [v.asdict() for v in project.drawings.values()] -@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Drawing) +@router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=schemas.Drawing, + dependencies=[Depends(has_privilege("Drawing.Allocate"))] +) async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing: """ Create a new drawing. + + Required privilege: Drawing.Allocate """ project = await Controller.instance().get_loaded_project(str(project_id)) @@ -52,10 +70,17 @@ async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> sch return drawing.asdict() -@router.get("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True) +@router.get( + "/{drawing_id}", + response_model=schemas.Drawing, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Drawing.Audit"))] +) async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing: """ Return a drawing. + + Required privilege: Drawing.Audit """ project = await Controller.instance().get_loaded_project(str(project_id)) @@ -63,10 +88,17 @@ async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing: return drawing.asdict() -@router.put("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True) +@router.put( + "/{drawing_id}", + response_model=schemas.Drawing, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Drawing.Modify"))] +) async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing: """ Update a drawing. + + Required privilege: Drawing.Modify """ project = await Controller.instance().get_loaded_project(str(project_id)) @@ -75,11 +107,22 @@ async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schem return drawing.asdict() -@router.delete("/{drawing_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_drawing(project_id: UUID, drawing_id: UUID) -> None: +@router.delete( + "/{drawing_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Drawing.Allocate"))] +) +async def delete_drawing( + project_id: UUID, + drawing_id: UUID, + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) +) -> None: """ Delete a drawing. + + Required privilege: Drawing.Allocate """ project = await Controller.instance().get_loaded_project(str(project_id)) await project.delete_drawing(str(drawing_id)) + await rbac_repo.delete_all_ace_starting_with_path(f"/drawings/{drawing_id}") diff --git a/gns3server/api/routes/controller/groups.py b/gns3server/api/routes/controller/groups.py index 9665ab3a..d4652353 100644 --- a/gns3server/api/routes/controller/groups.py +++ b/gns3server/api/routes/controller/groups.py @@ -19,7 +19,7 @@ API routes for user groups. """ -from fastapi import APIRouter, Depends, Response, status +from fastapi import APIRouter, Depends, status from uuid import UUID from typing import List @@ -33,6 +33,8 @@ from gns3server.controller.controller_error import ( from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.rbac import RbacRepository + +from .dependencies.rbac import has_privilege from .dependencies.database import get_repository import logging @@ -42,12 +44,18 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("", response_model=List[schemas.UserGroup]) +@router.get( + "", + response_model=List[schemas.UserGroup], + dependencies=[Depends(has_privilege("Group.Audit"))] +) async def get_user_groups( users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> List[schemas.UserGroup]: """ Get all user groups. + + Required privilege: Group.Audit """ return await users_repo.get_user_groups() @@ -56,7 +64,8 @@ async def get_user_groups( @router.post( "", response_model=schemas.UserGroup, - status_code=status.HTTP_201_CREATED + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Group.Allocate"))] ) async def create_user_group( user_group_create: schemas.UserGroupCreate, @@ -64,6 +73,8 @@ async def create_user_group( ) -> schemas.UserGroup: """ Create a new user group. + + Required privilege: Group.Allocate """ if await users_repo.get_user_group_by_name(user_group_create.name): @@ -72,13 +83,19 @@ async def create_user_group( return await users_repo.create_user_group(user_group_create) -@router.get("/{user_group_id}", response_model=schemas.UserGroup) +@router.get( + "/{user_group_id}", + response_model=schemas.UserGroup, + dependencies=[Depends(has_privilege("Group.Audit"))] +) async def get_user_group( user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.UserGroup: """ Get a user group. + + Required privilege: Group.Audit """ user_group = await users_repo.get_user_group(user_group_id) @@ -87,7 +104,11 @@ async def get_user_group( return user_group -@router.put("/{user_group_id}", response_model=schemas.UserGroup) +@router.put( + "/{user_group_id}", + response_model=schemas.UserGroup, + dependencies=[Depends(has_privilege("Group.Modify"))] +) async def update_user_group( user_group_id: UUID, user_group_update: schemas.UserGroupUpdate, @@ -95,6 +116,8 @@ async def update_user_group( ) -> schemas.UserGroup: """ Update a user group. + + Required privilege: Group.Modify """ user_group = await users_repo.get_user_group(user_group_id) if not user_group: @@ -108,14 +131,18 @@ async def update_user_group( @router.delete( "/{user_group_id}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Group.Allocate"))] ) async def delete_user_group( - user_group_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + user_group_id: UUID, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> None: """ - Delete a user group + Delete a user group. + + Required privilege: Group.Allocate """ user_group = await users_repo.get_user_group(user_group_id) @@ -128,15 +155,22 @@ async def delete_user_group( success = await users_repo.delete_user_group(user_group_id) if not success: raise ControllerError(f"User group '{user_group_id}' could not be deleted") + await rbac_repo.delete_all_ace_starting_with_path(f"/groups/{user_group_id}") -@router.get("/{user_group_id}/members", response_model=List[schemas.User]) +@router.get( + "/{user_group_id}/members", + response_model=List[schemas.User], + dependencies=[Depends(has_privilege("Group.Audit"))] +) async def get_user_group_members( user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> List[schemas.User]: """ Get all user group members. + + Required privilege: Group.Audit """ return await users_repo.get_user_group_members(user_group_id) @@ -144,7 +178,8 @@ async def get_user_group_members( @router.put( "/{user_group_id}/members/{user_id}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Group.Modify"))] ) async def add_member_to_group( user_group_id: UUID, @@ -153,6 +188,8 @@ async def add_member_to_group( ) -> None: """ Add member to a user group. + + Required privilege: Group.Modify """ user = await users_repo.get_user(user_id) @@ -166,7 +203,8 @@ async def add_member_to_group( @router.delete( "/{user_group_id}/members/{user_id}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Group.Modify"))] ) async def remove_member_from_group( user_group_id: UUID, @@ -175,6 +213,8 @@ async def remove_member_from_group( ) -> None: """ Remove member from a user group. + + Required privilege: Group.Modify """ user = await users_repo.get_user(user_id) diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index a37566fa..f4780db0 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -22,7 +22,7 @@ import os import logging import urllib.parse -from fastapi import APIRouter, Request, Response, Depends, status +from fastapi import APIRouter, Request, Depends, status from starlette.requests import ClientDisconnect from sqlalchemy.orm.exc import MultipleResultsFound from typing import List, Optional @@ -43,25 +43,37 @@ from gns3server.controller.controller_error import ( from .dependencies.authentication import get_current_active_user from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege log = logging.getLogger(__name__) router = APIRouter() -@router.get("", response_model=List[schemas.Image]) +@router.get( + "", + response_model=List[schemas.Image], + dependencies=[Depends(has_privilege("Image.Audit"))] +) async def get_images( images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), image_type: Optional[schemas.ImageType] = None ) -> List[schemas.Image]: """ Return all images. + + Required privilege: Image.Audit """ return await images_repo.get_images(image_type) -@router.post("/upload/{image_path:path}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED) +@router.post( + "/upload/{image_path:path}", + response_model=schemas.Image, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Image.Allocate"))] +) async def upload_image( image_path: str, request: Request, @@ -76,6 +88,8 @@ async def upload_image( Example: curl -X POST http://host:port/v3/images/upload/my_image_name.qcow2 \ -H 'Authorization: Bearer ' --data-binary @"/path/to/image.qcow2" + + Required privilege: Image.Allocate """ image_path = urllib.parse.unquote(image_path) @@ -110,13 +124,19 @@ async def upload_image( return image -@router.get("/{image_path:path}", response_model=schemas.Image) +@router.get( + "/{image_path:path}", + response_model=schemas.Image, + dependencies=[Depends(has_privilege("Image.Audit"))] +) async def get_image( image_path: str, images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> schemas.Image: """ Return an image. + + Required privilege: Image.Audit """ image_path = urllib.parse.unquote(image_path) @@ -126,13 +146,19 @@ async def get_image( return image -@router.delete("/{image_path:path}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{image_path:path}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Image.Allocate"))] +) async def delete_image( image_path: str, images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> None: """ Delete an image. + + Required privilege: Image.Allocate """ image_path = urllib.parse.unquote(image_path) @@ -161,12 +187,18 @@ async def delete_image( raise ControllerError(f"Image '{image_path}' could not be deleted") -@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/prune", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Image.Allocate"))] +) async def prune_images( images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> None: """ Prune images not attached to any template. + + Required privilege: Image.Allocate """ await images_repo.prune_images() diff --git a/gns3server/api/routes/controller/links.py b/gns3server/api/routes/controller/links.py index c228751f..28743b75 100644 --- a/gns3server/api/routes/controller/links.py +++ b/gns3server/api/routes/controller/links.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 GNS3 Technologies Inc. +# Copyright (C) 2023 GNS3 Technologies Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,7 +21,7 @@ API routes for links. import multidict import aiohttp -from fastapi import APIRouter, Depends, Request, Response, status +from fastapi import APIRouter, Depends, Request, status from fastapi.responses import StreamingResponse from fastapi.encoders import jsonable_encoder from typing import List @@ -29,10 +29,14 @@ from uuid import UUID from gns3server.controller import Controller from gns3server.controller.controller_error import ControllerError +from gns3server.db.repositories.rbac import RbacRepository from gns3server.controller.link import Link from gns3server.utils.http_client import HTTPClient from gns3server import schemas +from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege + import logging log = logging.getLogger(__name__) @@ -52,10 +56,17 @@ async def dep_link(project_id: UUID, link_id: UUID) -> Link: return link -@router.get("", response_model=List[schemas.Link], response_model_exclude_unset=True) +@router.get( + "", + response_model=List[schemas.Link], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Link.Audit"))] +) async def get_links(project_id: UUID) -> List[schemas.Link]: """ Return all links for a given project. + + Required privilege: Link.Audit """ project = await Controller.instance().get_loaded_project(str(project_id)) @@ -70,10 +81,13 @@ async def get_links(project_id: UUID) -> List[schemas.Link]: 404: {"model": schemas.ErrorMessage, "description": "Could not find project"}, 409: {"model": schemas.ErrorMessage, "description": "Could not create link"}, }, + dependencies=[Depends(has_privilege("Link.Allocate"))] ) async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link: """ Create a new link. + + Required privilege: Link.Allocate """ project = await Controller.instance().get_loaded_project(str(project_id)) @@ -99,28 +113,47 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema return link.asdict() -@router.get("/{link_id}/available_filters") +@router.get( + "/{link_id}/available_filters", + dependencies=[Depends(has_privilege("Link.Audit"))] +) async def get_filters(link: Link = Depends(dep_link)) -> List[dict]: """ Return all filters available for a given link. + + Required privilege: Link.Audit """ return link.available_filters() -@router.get("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True) +@router.get( + "/{link_id}", + response_model=schemas.Link, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Link.Audit"))] +) async def get_link(link: Link = Depends(dep_link)) -> schemas.Link: """ Return a link. + + Required privilege: Link.Audit """ return link.asdict() -@router.put("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True) +@router.put( + "/{link_id}", + response_model=schemas.Link, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Link.Modify"))] +) async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link: """ Update a link. + + Required privilege: Link.Modify """ link_data = jsonable_encoder(link_data, exclude_unset=True) @@ -135,30 +168,54 @@ async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_li return link.asdict() -@router.delete("/{link_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_link(project_id: UUID, link: Link = Depends(dep_link)) -> None: +@router.delete( + "/{link_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Link.Allocate"))] +) +async def delete_link( + project_id: UUID, + link: Link = Depends(dep_link), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) +) -> None: """ Delete a link. + + Required privilege: Link.Allocate """ project = await Controller.instance().get_loaded_project(str(project_id)) await project.delete_link(link.id) + await rbac_repo.delete_all_ace_starting_with_path(f"/links/{link.id}") -@router.post("/{link_id}/reset", response_model=schemas.Link) +@router.post( + "/{link_id}/reset", + response_model=schemas.Link, + dependencies=[Depends(has_privilege("Link.Modify"))] +) async def reset_link(link: Link = Depends(dep_link)) -> schemas.Link: """ Reset a link. + + Required privilege: Link.Modify """ await link.reset() return link.asdict() -@router.post("/{link_id}/capture/start", status_code=status.HTTP_201_CREATED, response_model=schemas.Link) +@router.post( + "/{link_id}/capture/start", + status_code=status.HTTP_201_CREATED, + response_model=schemas.Link, + dependencies=[Depends(has_privilege("Link.Capture"))] +) async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> schemas.Link: """ Start packet capture on the link. + + Required privilege: Link.Capture """ await link.start_capture( @@ -168,19 +225,30 @@ async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> s return link.asdict() -@router.post("/{link_id}/capture/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{link_id}/capture/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Link.Capture"))] +) async def stop_capture(link: Link = Depends(dep_link)) -> None: """ Stop packet capture on the link. + + Required privilege: Link.Capture """ await link.stop_capture() -@router.get("/{link_id}/capture/stream") +@router.get( + "/{link_id}/capture/stream", + dependencies=[Depends(has_privilege("Link.Capture"))] +) async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> StreamingResponse: """ Stream the PCAP capture file from compute. + + Required privilege: Link.Capture """ if not link.capturing: diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index 20ce2479..cc93c0c1 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -34,8 +34,12 @@ from gns3server.controller.project import Project from gns3server.utils import force_unix_path from gns3server.utils.http_client import HTTPClient from gns3server.controller.controller_error import ControllerForbiddenError, ControllerBadRequestError +from gns3server.db.repositories.rbac import RbacRepository from gns3server import schemas +from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege, has_privilege_on_websocket + import logging log = logging.getLogger(__name__) @@ -108,10 +112,13 @@ async def dep_node(node_id: UUID, project: Project = Depends(dep_project)) -> No 404: {"model": schemas.ErrorMessage, "description": "Could not find project"}, 409: {"model": schemas.ErrorMessage, "description": "Could not create node"}, }, + dependencies=[Depends(has_privilege("Node.Allocate"))] ) async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node: """ Create a new node. + + Required privilege: Node.Allocate """ controller = Controller.instance() @@ -121,65 +128,89 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends( return node.asdict() -@router.get("", response_model=List[schemas.Node], response_model_exclude_unset=True) -async def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]: +@router.get( + "", + response_model=List[schemas.Node], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Node.Audit"))] +) +def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]: """ Return all nodes belonging to a given project. + + Required privilege: Node.Audit """ return [v.asdict() for v in project.nodes.values()] -@router.post("/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post("/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node."))]) async def start_all_nodes(project: Project = Depends(dep_project)) -> None: """ Start all nodes belonging to a given project. + + Required privilege: Node.PowerMgmt """ await project.start_all() -@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]) async def stop_all_nodes(project: Project = Depends(dep_project)) -> None: """ Stop all nodes belonging to a given project. + + Required privilege: Node.PowerMgmt """ await project.stop_all() -@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]) async def suspend_all_nodes(project: Project = Depends(dep_project)) -> None: """ Suspend all nodes belonging to a given project. + + Required privilege: Node.PowerMgmt """ await project.suspend_all() -@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]) async def reload_all_nodes(project: Project = Depends(dep_project)) -> None: """ Reload all nodes belonging to a given project. + + Required privilege: Node.PowerMgmt """ await project.stop_all() await project.start_all() -@router.get("/{node_id}", response_model=schemas.Node) +@router.get("/{node_id}", response_model=schemas.Node, dependencies=[Depends(has_privilege("Node.Audit"))]) def get_node(node: Node = Depends(dep_node)) -> schemas.Node: """ Return a node from a given project. + + Required privilege: Node.Audit """ return node.asdict() -@router.put("/{node_id}", response_model=schemas.Node, response_model_exclude_unset=True) +@router.put( + "/{node_id}", + response_model=schemas.Node, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Node.Modify"))] +) async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node: """ Update a node. + + Required privilege: Node.Modify """ node_data = jsonable_encoder(node_data, exclude_unset=True) @@ -197,85 +228,142 @@ async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_no "/{node_id}", status_code=status.HTTP_204_NO_CONTENT, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}}, + dependencies=[Depends(has_privilege("Node.Allocate"))] ) -async def delete_node(node_id: UUID, project: Project = Depends(dep_project)) -> None: +async def delete_node( + node_id: UUID, project: Project = Depends(dep_project), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), +) -> None: """ Delete a node from a project. + + Required privilege: Node.Allocate """ await project.delete_node(str(node_id)) + await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}/nodes/{node_id}") -@router.post("/{node_id}/duplicate", response_model=schemas.Node, status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/duplicate", + response_model=schemas.Node, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Node.Allocate"))] +) async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)) -> schemas.Node: """ Duplicate a node. + + Required privilege: Node.Allocate """ new_node = await node.project.duplicate_node(node, duplicate_data.x, duplicate_data.y, duplicate_data.z) return new_node.asdict() -@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/start", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.PowerMgmt"))] +) async def start_node(start_data: dict, node: Node = Depends(dep_node)) -> None: """ Start a node. + + Required privilege: Node.PowerMgmt """ await node.start(data=start_data) -@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/stop", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.PowerMgmt"))] +) async def stop_node(node: Node = Depends(dep_node)) -> None: """ Stop a node. + + Required privilege: Node.PowerMgmt """ await node.stop() -@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/suspend", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.PowerMgmt"))] +) async def suspend_node(node: Node = Depends(dep_node)) -> None: """ Suspend a node. + + Required privilege: Node.PowerMgmt """ await node.suspend() -@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/reload", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.PowerMgmt"))] +) async def reload_node(node: Node = Depends(dep_node)) -> None: """ Reload a node. + + Required privilege: Node.PowerMgmt """ await node.reload() -@router.post("/{node_id}/isolate", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/isolate", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Link.Modify"))] +) async def isolate_node(node: Node = Depends(dep_node)) -> None: """ Isolate a node (suspend all attached links). + + Required privilege: Link.Modify """ for link in node.links: await link.update_suspend(True) -@router.post("/{node_id}/unisolate", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/unisolate", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Link.Modify"))] +) async def unisolate_node(node: Node = Depends(dep_node)) -> None: """ Un-isolate a node (resume all attached suspended links). + + Required privilege: Link.Modify """ for link in node.links: await link.update_suspend(False) -@router.get("/{node_id}/links", response_model=List[schemas.Link], response_model_exclude_unset=True) +@router.get( + "/{node_id}/links", + response_model=List[schemas.Link], + response_model_exclude_unset=True, + dependencies = [Depends(has_privilege("Link.Audit"))] +) async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]: """ Return all the links connected to a node. + + Required privilege: Link.Audit """ links = [] @@ -284,10 +372,12 @@ async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]: return links -@router.get("/{node_id}/dynamips/auto_idlepc") +@router.get("/{node_id}/dynamips/auto_idlepc", dependencies=[Depends(has_privilege("Node.Audit"))]) async def auto_idlepc(node: Node = Depends(dep_node)) -> dict: """ Compute an Idle-PC value for a Dynamips node + + Required privilege: Node.Audit """ if node.node_type != "dynamips": @@ -295,10 +385,12 @@ async def auto_idlepc(node: Node = Depends(dep_node)) -> dict: return await node.dynamips_auto_idlepc() -@router.get("/{node_id}/dynamips/idlepc_proposals") +@router.get("/{node_id}/dynamips/idlepc_proposals", dependencies=[Depends(has_privilege("Node.Audit"))]) async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]: """ Compute a list of potential idle-pc values for a Dynamips node + + Required privilege: Node.Audit """ if node.node_type != "dynamips": @@ -306,7 +398,11 @@ async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]: return await node.dynamips_idlepc_proposals() -@router.post("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/qemu/disk_image/{disk_name}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.Allocate"))] +) async def create_disk_image( disk_name: str, disk_data: schemas.QemuDiskImageCreate, @@ -314,6 +410,8 @@ async def create_disk_image( ) -> None: """ Create a Qemu disk image. + + Required privilege: Node.Allocate """ if node.node_type != "qemu": @@ -321,7 +419,11 @@ async def create_disk_image( await node.post(f"/disk_image/{disk_name}", data=disk_data.model_dump(exclude_unset=True)) -@router.put("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT) +@router.put( + "/{node_id}/qemu/disk_image/{disk_name}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.Allocate"))] +) async def update_disk_image( disk_name: str, disk_data: schemas.QemuDiskImageUpdate, @@ -329,6 +431,8 @@ async def update_disk_image( ) -> None: """ Update a Qemu disk image. + + Required privilege: Node.Allocate """ if node.node_type != "qemu": @@ -336,13 +440,19 @@ async def update_disk_image( await node.put(f"/disk_image/{disk_name}", data=disk_data.model_dump(exclude_unset=True)) -@router.delete("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{node_id}/qemu/disk_image/{disk_name}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.Allocate"))] +) async def delete_disk_image( disk_name: str, node: Node = Depends(dep_node) ) -> None: """ Delete a Qemu disk image. + + Required privilege: Node.Allocate """ if node.node_type != "qemu": @@ -350,10 +460,12 @@ async def delete_disk_image( await node.delete(f"/disk_image/{disk_name}") -@router.get("/{node_id}/files/{file_path:path}") +@router.get("/{node_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Node.Audit"))]) async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response: """ - Return a file in the node directory + Return a file from the node directory. + + Required privilege: Node.Audit """ path = force_unix_path(file_path) @@ -369,10 +481,16 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response: return Response(res.body, media_type="application/octet-stream", status_code=res.status) -@router.post("/{node_id}/files/{file_path:path}", status_code=status.HTTP_201_CREATED) +@router.post( + "/{node_id}/files/{file_path:path}", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Node.Modify"))] +) async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)): """ Write a file in the node directory. + + Required privilege: Node.Modify """ path = force_unix_path(file_path) @@ -389,10 +507,12 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n # FIXME: response with correct status code (from compute) -@router.websocket("/{node_id}/console/ws") +@router.websocket("/{node_id}/console/ws", dependencies=[Depends(has_privilege_on_websocket("Node.Console"))]) async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None: """ WebSocket console. + + Required privilege: Node.Console """ compute = node.compute @@ -447,16 +567,31 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No log.error(f"Client error received when forwarding to compute console WebSocket: {e}") -@router.post("/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.Console"))] +) async def reset_console_all_nodes(project: Project = Depends(dep_project)) -> None: """ Reset console for all nodes belonging to the project. + + Required privilege: Node.Console """ await project.reset_console_all() -@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT) +@router.post( + "/{node_id}/console/reset", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Node.Console"))] +) async def console_reset(node: Node = Depends(dep_node)) -> None: + """ + Reset a console for a given node. + + Required privilege: Node.Console + """ await node.post("/console/reset") diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index ca441d85..d228fcf7 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -45,11 +45,10 @@ from gns3server.controller.import_project import import_project as import_contro from gns3server.controller.export_project import export_project as export_controller_project from gns3server.utils.asyncio import aiozipstream from gns3server.utils.path import is_safe_path -from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.templates import TemplatesRepository +from gns3server.db.repositories.rbac import RbacRepository from gns3server.services.templates import TemplatesService -from .dependencies.authentication import get_current_active_user from .dependencies.rbac import has_privilege, has_privilege_on_websocket from .dependencies.database import get_repository @@ -67,31 +66,21 @@ def dep_project(project_id: UUID) -> Project: return project -CHUNK_SIZE = 1024 * 8 # 8KB - - -@router.get("", response_model=List[schemas.Project], response_model_exclude_unset=True) -async def get_projects( - current_user: schemas.User = Depends(get_current_active_user), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> List[schemas.Project]: +@router.get( + "", + response_model=List[schemas.Project], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Project.Audit"))] +) +async def get_projects() -> List[schemas.Project]: """ Return all projects. + + Required privilege: Project.Audit """ controller = Controller.instance() - if current_user.is_superadmin: - return [p.asdict() for p in controller.projects.values()] - else: - user_projects = [] - for project in controller.projects.values(): - if await rbac_repo.check_user_has_privilege( - current_user.user_id, - f"/projects/{project.id}", - "Project.Audit" - ): - user_projects.append(project.asdict()) - return user_projects + return [p.asdict() for p in controller.projects.values()] @router.post( @@ -107,6 +96,8 @@ async def create_project( ) -> schemas.Project: """ Create a new project. + + Required privilege: Project.Allocate """ controller = Controller.instance() @@ -115,9 +106,11 @@ async def create_project( @router.get("/{project_id}", response_model=schemas.Project, dependencies=[Depends(has_privilege("Project.Audit"))]) -async def get_project(project: Project = Depends(dep_project)) -> schemas.Project: +def get_project(project: Project = Depends(dep_project)) -> schemas.Project: """ Return a project. + + Required privilege: Project.Audit """ return project.asdict() @@ -135,6 +128,8 @@ async def update_project( ) -> schemas.Project: """ Update a project. + + Required privilege: Project.Modify """ await project.update(**jsonable_encoder(project_data, exclude_unset=True)) @@ -147,21 +142,27 @@ async def update_project( dependencies=[Depends(has_privilege("Project.Allocate"))] ) async def delete_project( - project: Project = Depends(dep_project) + project: Project = Depends(dep_project), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a project. + + Required privilege: Project.Allocate """ controller = Controller.instance() await project.delete() controller.remove_project(project) + await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}") @router.get("/{project_id}/stats", dependencies=[Depends(has_privilege("Project.Audit"))]) def get_project_stats(project: Project = Depends(dep_project)) -> dict: """ Return a project statistics. + + Required privilege: Project.Audit """ return project.stats() @@ -176,6 +177,8 @@ def get_project_stats(project: Project = Depends(dep_project)) -> dict: async def close_project(project: Project = Depends(dep_project)) -> None: """ Close a project. + + Required privilege: Project.Allocate """ await project.close() @@ -191,6 +194,8 @@ async def close_project(project: Project = Depends(dep_project)) -> None: async def open_project(project: Project = Depends(dep_project)) -> schemas.Project: """ Open a project. + + Required privilege: Project.Allocate """ await project.open() @@ -207,6 +212,8 @@ async def open_project(project: Project = Depends(dep_project)) -> schemas.Proje async def load_project(path: str = Body(..., embed=True)) -> schemas.Project: """ Load a project (local server only). + + Required privilege: Project.Allocate """ controller = Controller.instance() @@ -219,6 +226,8 @@ async def load_project(path: str = Body(..., embed=True)) -> schemas.Project: async def project_http_notifications(project_id: UUID) -> StreamingResponse: """ Receive project notifications about the controller from HTTP stream. + + Required privilege: Project.Audit """ from gns3server.api.server import app @@ -255,6 +264,8 @@ async def project_ws_notifications( ) -> None: """ Receive project notifications about the controller from WebSocket. + + Required privilege: Project.Audit """ if current_user is None: @@ -298,6 +309,8 @@ async def export_project( ) -> StreamingResponse: """ Export a project as a portable archive. + + Required privilege: Project.Audit """ compression_query = compression.lower() @@ -366,6 +379,8 @@ async def import_project( ) -> schemas.Project: """ Import a project from a portable archive. + + Required privilege: Project.Allocate """ controller = Controller.instance() @@ -401,6 +416,8 @@ async def duplicate_project( ) -> schemas.Project: """ Duplicate a project. + + Required privilege: Project.Allocate """ reset_mac_addresses = project_data.reset_mac_addresses @@ -413,7 +430,9 @@ async def duplicate_project( @router.get("/{project_id}/locked", dependencies=[Depends(has_privilege("Project.Audit"))]) async def locked_project(project: Project = Depends(dep_project)) -> bool: """ - Returns whether a project is locked or not + Returns whether a project is locked or not. + + Required privilege: Project.Audit """ return project.locked @@ -427,6 +446,8 @@ async def locked_project(project: Project = Depends(dep_project)) -> bool: async def lock_project(project: Project = Depends(dep_project)) -> None: """ Lock all drawings and nodes in a given project. + + Required privilege: Project.Audit """ project.lock() @@ -440,6 +461,8 @@ async def lock_project(project: Project = Depends(dep_project)) -> None: async def unlock_project(project: Project = Depends(dep_project)) -> None: """ Unlock all drawings and nodes in a given project. + + Required privilege: Project.Modify """ project.unlock() @@ -449,6 +472,8 @@ async def unlock_project(project: Project = Depends(dep_project)) -> None: async def get_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse: """ Return a file from a project. + + Required privilege: Project.Audit """ file_path = urllib.parse.unquote(file_path) @@ -473,6 +498,8 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)) -> F async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None: """ Write a file to a project. + + Required privilege: Project.Modify """ file_path = urllib.parse.unquote(file_path) @@ -511,6 +538,8 @@ async def create_node_from_template( ) -> schemas.Node: """ Create a new node from a template. + + Required privilege: Node.Allocate """ template = await TemplatesService(templates_repo).get_template(template_id) diff --git a/gns3server/api/routes/controller/roles.py b/gns3server/api/routes/controller/roles.py index f8c3b9e6..f1a5434f 100644 --- a/gns3server/api/routes/controller/roles.py +++ b/gns3server/api/routes/controller/roles.py @@ -19,7 +19,7 @@ API routes for roles. """ -from fastapi import APIRouter, Depends, Response, status +from fastapi import APIRouter, Depends, status from uuid import UUID from typing import List @@ -33,6 +33,7 @@ from gns3server.controller.controller_error import ( from gns3server.db.repositories.rbac import RbacRepository from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege import logging @@ -41,24 +42,37 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("", response_model=List[schemas.Role]) +@router.get( + "", + response_model=List[schemas.Role], + dependencies=[Depends(has_privilege("Role.Audit"))] +) async def get_roles( rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> List[schemas.Role]: """ Get all roles. + + Required privilege: Role.Audit """ return await rbac_repo.get_roles() -@router.post("", response_model=schemas.Role, status_code=status.HTTP_201_CREATED) +@router.post( + "", + response_model=schemas.Role, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Role.Allocate"))] +) async def create_role( role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> schemas.Role: """ Create a new role. + + Required privilege: Role.Allocate """ if await rbac_repo.get_role_by_name(role_create.name): @@ -67,13 +81,19 @@ async def create_role( return await rbac_repo.create_role(role_create) -@router.get("/{role_id}", response_model=schemas.Role) +@router.get( + "/{role_id}", + response_model=schemas.Role, + dependencies=[Depends(has_privilege("Role.Audit"))] +) async def get_role( role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> schemas.Role: """ Get a role. + + Required privilege: Role.Audit """ role = await rbac_repo.get_role(role_id) @@ -82,7 +102,11 @@ async def get_role( return role -@router.put("/{role_id}", response_model=schemas.Role) +@router.put( + "/{role_id}", + response_model=schemas.Role, + dependencies=[Depends(has_privilege("Role.Modify"))] +) async def update_role( role_id: UUID, role_update: schemas.RoleUpdate, @@ -90,6 +114,8 @@ async def update_role( ) -> schemas.Role: """ Update a role. + + Required privilege: Role.Modify """ role = await rbac_repo.get_role(role_id) @@ -102,13 +128,19 @@ async def update_role( return await rbac_repo.update_role(role_id, role_update) -@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{role_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Role.Allocate"))] +) async def delete_role( role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a role. + + Required privilege: Role.Allocate """ role = await rbac_repo.get_role(role_id) @@ -121,15 +153,22 @@ async def delete_role( success = await rbac_repo.delete_role(role_id) if not success: raise ControllerError(f"Role '{role_id}' could not be deleted") + await rbac_repo.delete_all_ace_starting_with_path(f"/roles/{role_id}") -@router.get("/{role_id}/privileges", response_model=List[schemas.Privilege]) +@router.get( + "/{role_id}/privileges", + response_model=List[schemas.Privilege], + dependencies=[Depends(has_privilege("Role.Audit"))] +) async def get_role_privileges( role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> List[schemas.Privilege]: """ Get all role privileges. + + Required privilege: Role.Audit """ return await rbac_repo.get_role_privileges(role_id) @@ -137,7 +176,8 @@ async def get_role_privileges( @router.put( "/{role_id}/privileges/{privilege_id}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Role.Modify"))] ) async def add_privilege_to_role( role_id: UUID, @@ -146,6 +186,8 @@ async def add_privilege_to_role( ) -> None: """ Add a privilege to a role. + + Required privilege: Role.Modify """ privilege = await rbac_repo.get_privilege(privilege_id) @@ -159,7 +201,8 @@ async def add_privilege_to_role( @router.delete( "/{role_id}/privileges/{privilege_id}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Role.Modify"))] ) async def remove_privilege_from_role( role_id: UUID, @@ -168,6 +211,8 @@ async def remove_privilege_from_role( ) -> None: """ Remove privilege from a role. + + Required privilege: Role.Modify """ privilege = await rbac_repo.get_privilege(privilege_id) diff --git a/gns3server/api/routes/controller/snapshots.py b/gns3server/api/routes/controller/snapshots.py index c372a8e8..37d9f777 100644 --- a/gns3server/api/routes/controller/snapshots.py +++ b/gns3server/api/routes/controller/snapshots.py @@ -23,14 +23,18 @@ import logging log = logging.getLogger() -from fastapi import APIRouter, Depends, Response, status +from fastapi import APIRouter, Depends, status from typing import List from uuid import UUID from gns3server.controller.project import Project +from gns3server.db.repositories.rbac import RbacRepository from gns3server import schemas from gns3server.controller import Controller +from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege + responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}} router = APIRouter(responses=responses) @@ -45,42 +49,74 @@ def dep_project(project_id: UUID) -> Project: return project -@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Snapshot) +@router.post( + "", + status_code=status.HTTP_201_CREATED, + response_model=schemas.Snapshot, + dependencies=[Depends(has_privilege("Snapshot.Allocate"))] +) async def create_snapshot( snapshot_data: schemas.SnapshotCreate, project: Project = Depends(dep_project) ) -> schemas.Snapshot: """ Create a new snapshot of a project. + + Required privilege: Snapshot.Allocate """ snapshot = await project.snapshot(snapshot_data.name) return snapshot.asdict() -@router.get("", response_model=List[schemas.Snapshot], response_model_exclude_unset=True) +@router.get( + "", + response_model=List[schemas.Snapshot], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Snapshot.Audit"))] +) def get_snapshots(project: Project = Depends(dep_project)) -> List[schemas.Snapshot]: """ Return all snapshots belonging to a given project. + + Required privilege: Snapshot.Audit """ snapshots = [s for s in project.snapshots.values()] return [s.asdict() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))] -@router.delete("/{snapshot_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> None: +@router.delete( + "/{snapshot_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Snapshot.Allocate"))] +) +async def delete_snapshot( + snapshot_id: UUID, + project: Project = Depends(dep_project), + rbac_repo=Depends(get_repository(RbacRepository)) +) -> None: """ Delete a snapshot. + + Required privilege: Snapshot.Allocate """ await project.delete_snapshot(str(snapshot_id)) + await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}/snapshots/{snapshot_id}") -@router.post("/{snapshot_id}/restore", status_code=status.HTTP_201_CREATED, response_model=schemas.Project) +@router.post( + "/{snapshot_id}/restore", + status_code=status.HTTP_201_CREATED, + response_model=schemas.Project, + dependencies=[Depends(has_privilege("Snapshot.Restore"))] +) async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> schemas.Project: """ Restore a snapshot. + + Required privilege: Snapshot.Restore """ snapshot = project.get_snapshot(str(snapshot_id)) diff --git a/gns3server/api/routes/controller/symbols.py b/gns3server/api/routes/controller/symbols.py index 10b39387..6d73c623 100644 --- a/gns3server/api/routes/controller/symbols.py +++ b/gns3server/api/routes/controller/symbols.py @@ -29,7 +29,7 @@ from gns3server.controller import Controller from gns3server import schemas from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError -from .dependencies.authentication import get_current_active_user +from .dependencies.rbac import has_privilege import logging @@ -39,19 +39,28 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get("") +@router.get("", dependencies=[Depends(has_privilege("Symbol.Audit"))]) def get_symbols() -> List[dict]: + """ + Return all symbols. + + Required privilege: Symbol.Audit + """ controller = Controller.instance() return controller.symbols.list() @router.get( - "/{symbol_id:path}/raw", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}} + "/{symbol_id:path}/raw", + responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}, + dependencies=[Depends(has_privilege("Symbol.Audit"))] ) async def get_symbol(symbol_id: str) -> FileResponse: """ Download a symbol file. + + Required privilege: Symbol.Audit """ controller = Controller.instance() @@ -65,10 +74,13 @@ async def get_symbol(symbol_id: str) -> FileResponse: @router.get( "/{symbol_id:path}/dimensions", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}, + dependencies=[Depends(has_privilege("Symbol.Audit"))] ) async def get_symbol_dimensions(symbol_id: str) -> dict: """ Get a symbol dimensions. + + Required privilege: Symbol.Audit """ controller = Controller.instance() @@ -80,10 +92,12 @@ async def get_symbol_dimensions(symbol_id: str) -> dict: raise ControllerNotFoundError(f"Could not get symbol file: {e}") -@router.get("/default_symbols") +@router.get("/default_symbols", dependencies=[Depends(has_privilege("Symbol.Audit"))]) def get_default_symbols() -> dict: """ Return all default symbols. + + Required privilege: Symbol.Audit """ controller = Controller.instance() @@ -92,12 +106,14 @@ def get_default_symbols() -> dict: @router.post( "/{symbol_id:path}/raw", - dependencies=[Depends(get_current_active_user)], - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Symbol.Allocate"))] ) async def upload_symbol(symbol_id: str, request: Request) -> None: """ Upload a symbol file. + + Required privilege: Symbol.Allocate """ controller = Controller.instance() @@ -111,4 +127,3 @@ async def upload_symbol(symbol_id: str, request: Request) -> None: # Reset the symbol list controller.symbols.list() - diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index fee359be..9d67a74d 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -36,6 +36,7 @@ from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.images import ImagesRepository from .dependencies.authentication import get_current_active_user +from .dependencies.rbac import has_privilege from .dependencies.database import get_repository responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}} @@ -43,20 +44,32 @@ responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find router = APIRouter(responses=responses) -@router.post("", response_model=schemas.Template, status_code=status.HTTP_201_CREATED) +@router.post( + "", + response_model=schemas.Template, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Template.Allocate"))] +) async def create_template( template_create: schemas.TemplateCreate, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) ) -> schemas.Template: """ Create a new template. + + Required privilege: Template.Allocate """ template = await TemplatesService(templates_repo).create_template(template_create) return template -@router.get("/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True) +@router.get( + "/{template_id}", + response_model=schemas.Template, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Template.Audit"))] +) async def get_template( template_id: UUID, request: Request, @@ -65,6 +78,8 @@ async def get_template( ) -> schemas.Template: """ Return a template. + + Required privilege: Template.Audit """ request_etag = request.headers.get("If-None-Match", "") @@ -78,7 +93,12 @@ async def get_template( return template -@router.put("/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True) +@router.put( + "/{template_id}", + response_model=schemas.Template, + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Template.Modify"))] +) async def update_template( template_id: UUID, template_update: schemas.TemplateUpdate, @@ -86,12 +106,18 @@ async def update_template( ) -> schemas.Template: """ Update a template. + + Required privilege: Template.Modify """ return await TemplatesService(templates_repo).update_template(template_id, template_update) -@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{template_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("Template.Allocate"))] +) async def delete_template( template_id: UUID, prune_images: Optional[bool] = False, @@ -101,15 +127,22 @@ async def delete_template( ) -> None: """ Delete a template. + + Required privilege: Template.Allocate """ await TemplatesService(templates_repo).delete_template(template_id) - #await rbac_repo.delete_all_permissions_with_path(f"/templates/{template_id}") + await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}") if prune_images: await images_repo.prune_images() -@router.get("", response_model=List[schemas.Template], response_model_exclude_unset=True) +@router.get( + "", + response_model=List[schemas.Template], + response_model_exclude_unset=True, + dependencies=[Depends(has_privilege("Template.Audit"))] +) async def get_templates( templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), current_user: schemas.User = Depends(get_current_active_user), @@ -117,6 +150,8 @@ async def get_templates( ) -> List[schemas.Template]: """ Return all templates. + + Required privilege: Template.Audit """ templates = await TemplatesService(templates_repo).get_templates() @@ -136,12 +171,19 @@ async def get_templates( return user_templates -@router.post("/{template_id}/duplicate", response_model=schemas.Template, status_code=status.HTTP_201_CREATED) +@router.post( + "/{template_id}/duplicate", + response_model=schemas.Template, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("Template.Allocate"))] +) async def duplicate_template( template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) ) -> schemas.Template: """ Duplicate a template. + + Required privilege: Template.Allocate """ template = await TemplatesService(templates_repo).duplicate_template(template_id) diff --git a/gns3server/api/routes/controller/users.py b/gns3server/api/routes/controller/users.py index 7dd9c2c8..9901dd5c 100644 --- a/gns3server/api/routes/controller/users.py +++ b/gns3server/api/routes/controller/users.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (C) 2020 GNS3 Technologies Inc. +# Copyright (C) 2023 GNS3 Technologies Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -38,6 +38,7 @@ from gns3server.services import auth_service from .dependencies.authentication import get_current_active_user from .dependencies.database import get_repository +from .dependencies.rbac import has_privilege import logging @@ -115,12 +116,18 @@ async def update_logged_in_user( return await users_repo.update_user(current_user.user_id, user_update) -@router.get("", response_model=List[schemas.User], dependencies=[Depends(get_current_active_user)]) +@router.get( + "", + response_model=List[schemas.User], + dependencies=[Depends(has_privilege("User.Audit"))] +) async def get_users( users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> List[schemas.User]: """ Get all users. + + Required privilege: User.Audit """ return await users_repo.get_users() @@ -129,8 +136,8 @@ async def get_users( @router.post( "", response_model=schemas.User, - dependencies=[Depends(get_current_active_user)], - status_code=status.HTTP_201_CREATED + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(has_privilege("User.Allocate"))] ) async def create_user( user_create: schemas.UserCreate, @@ -138,6 +145,8 @@ async def create_user( ) -> schemas.User: """ Create a new user. + + Required privilege: User.Allocate """ if await users_repo.get_user_by_username(user_create.username): @@ -149,13 +158,19 @@ async def create_user( return await users_repo.create_user(user_create) -@router.get("/{user_id}", dependencies=[Depends(get_current_active_user)], response_model=schemas.User) +@router.get( + "/{user_id}", + response_model=schemas.User, + dependencies=[Depends(has_privilege("User.Audit"))] +) async def get_user( user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.User: """ Get a user. + + Required privilege: User.Audit """ user = await users_repo.get_user(user_id) @@ -164,7 +179,11 @@ async def get_user( return user -@router.put("/{user_id}", dependencies=[Depends(get_current_active_user)], response_model=schemas.User) +@router.put( + "/{user_id}", + response_model=schemas.User, + dependencies=[Depends(has_privilege("User.Modify"))] +) async def update_user( user_id: UUID, user_update: schemas.UserUpdate, @@ -172,6 +191,8 @@ async def update_user( ) -> schemas.User: """ Update a user. + + Required privilege: User.Modify """ if user_update.username and await users_repo.get_user_by_username(user_update.username): @@ -188,15 +209,18 @@ async def update_user( @router.delete( "/{user_id}", - dependencies=[Depends(get_current_active_user)], - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(has_privilege("User.Allocate"))] ) async def delete_user( - user_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + user_id: UUID, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> None: """ Delete a user. + + Required privilege: User.Allocate """ user = await users_repo.get_user(user_id) @@ -209,12 +233,13 @@ async def delete_user( success = await users_repo.delete_user(user_id) if not success: raise ControllerError(f"User '{user_id}' could not be deleted") + await rbac_repo.delete_all_ace_starting_with_path(f"/users/{user_id}") @router.get( "/{user_id}/groups", - dependencies=[Depends(get_current_active_user)], - response_model=List[schemas.UserGroup] + response_model=List[schemas.UserGroup], + dependencies=[Depends(has_privilege("Group.Audit"))] ) async def get_user_memberships( user_id: UUID, @@ -222,6 +247,8 @@ async def get_user_memberships( ) -> List[schemas.UserGroup]: """ Get user memberships. + + Required privilege: Group.Audit """ return await users_repo.get_user_memberships(user_id) diff --git a/gns3server/db/models/acl.py b/gns3server/db/models/acl.py index fe033262..20df831d 100644 --- a/gns3server/db/models/acl.py +++ b/gns3server/db/models/acl.py @@ -30,10 +30,10 @@ class ACE(BaseTable): __tablename__ = "acl" ace_id = Column(GUID, primary_key=True, default=generate_uuid) + ace_type: str = Column(String) path = Column(String) propagate = Column(Boolean, default=True) allowed = Column(Boolean, default=True) - type: str = Column(String) user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE")) user = relationship("User", back_populates="acl_entries") group_id = Column(GUID, ForeignKey('user_groups.user_group_id', ondelete="CASCADE")) @@ -42,5 +42,5 @@ class ACE(BaseTable): role = relationship("Role", back_populates="acl_entries") __table_args__ = ( - CheckConstraint("(user_id IS NOT NULL AND type = 'user') OR (group_id IS NOT NULL AND type = 'group')"), + CheckConstraint("(user_id IS NOT NULL AND ace_type = 'user') OR (group_id IS NOT NULL AND ace_type = 'group')"), ) diff --git a/gns3server/db/models/privileges.py b/gns3server/db/models/privileges.py index 2bd746df..dcc141fc 100644 --- a/gns3server/db/models/privileges.py +++ b/gns3server/db/models/privileges.py @@ -71,6 +71,30 @@ def create_default_roles(target, connection, **kw): "description": "Update a group", "name": "Group.Modify" }, + { + "description": "Create or delete a role", + "name": "Role.Allocate" + }, + { + "description": "View a role", + "name": "Role.Audit" + }, + { + "description": "Update a role", + "name": "Role.Modify" + }, + { + "description": "Create or delete an ACE", + "name": "ACE.Allocate" + }, + { + "description": "View an ACE", + "name": "ACE.Audit" + }, + { + "description": "Update an ACE", + "name": "ACE.Modify" + }, { "description": "Create or delete a template", "name": "Template.Allocate" @@ -97,7 +121,15 @@ def create_default_roles(target, connection, **kw): }, { "description": "Create or delete project snapshots", - "name": "Project.Snapshot" + "name": "Snapshot.Allocate" + }, + { + "description": "Restore a snapshot", + "name": "Snapshot.Restore" + }, + { + "description": "View a snapshot", + "name": "Snapshot.Audit" }, { "description": "Create or delete a node", @@ -167,6 +199,10 @@ def create_default_roles(target, connection, **kw): "description": "Create or delete a compute", "name": "Compute.Allocate" }, + { + "description": "Update a compute", + "name": "Compute.Modify" + }, { "description": "View a compute", "name": "Compute.Audit" @@ -227,7 +263,9 @@ def add_privileges_to_default_roles(target, connection, **kw): "Project.Allocate", "Project.Audit", "Project.Modify", - "Project.Snapshot", + "Snapshot.Allocate", + "Snapshot.Audit", + "Snapshot.Restore", "Node.Allocate", "Node.Audit", "Node.Modify", @@ -253,6 +291,7 @@ def add_privileges_to_default_roles(target, connection, **kw): # add required privileges to the "Auditor" role auditor_privileges = ( "Project.Audit", + "Snapshot.Audit", "Node.Audit", "Link.Audit", "Drawing.Audit", diff --git a/gns3server/db/repositories/rbac.py b/gns3server/db/repositories/rbac.py index 418e8775..cf1aa9a8 100644 --- a/gns3server/db/repositories/rbac.py +++ b/gns3server/db/repositories/rbac.py @@ -276,22 +276,6 @@ class RbacRepository(BaseRepository): await self._db_session.commit() return result.rowcount > 0 - # async def prune_permissions(self) -> int: - # """ - # Prune orphaned permissions. - # """ - # - # query = select(models.Permission).\ - # filter((~models.Permission.roles.any()) & (models.Permission.user_id == null())) - # result = await self._db_session.execute(query) - # permissions = result.scalars().all() - # permissions_deleted = 0 - # for permission in permissions: - # if await self.delete_permission(permission.permission_id): - # permissions_deleted += 1 - # log.info(f"{permissions_deleted} orphaned permissions have been deleted") - # return permissions_deleted - async def delete_all_ace_starting_with_path(self, path: str) -> None: """ Delete all ACEs starting with path. @@ -304,7 +288,10 @@ class RbacRepository(BaseRepository): log.debug(f"{result.rowcount} ACE(s) have been deleted") @staticmethod - def _match_path_to_aces(path: str, aces) -> bool: + def _check_path_with_aces(path: str, aces) -> bool: + """ + Compare path with existing ACEs to check if the user has the required privilege on that path. + """ parsed_url = urlparse(path) original_path = path @@ -347,7 +334,7 @@ class RbacRepository(BaseRepository): aces = result.all() try: - if self._match_path_to_aces(path, aces): + if self._check_path_with_aces(path, aces): # the user has an ACE matching the path and privilege,there is no need to check group ACEs return True except PermissionError: @@ -366,6 +353,6 @@ class RbacRepository(BaseRepository): aces = result.all() try: - return self._match_path_to_aces(path, aces) + return self._check_path_with_aces(path, aces) except PermissionError: return False diff --git a/gns3server/schemas/controller/rbac.py b/gns3server/schemas/controller/rbac.py index 36cb2897..9d44e5c4 100644 --- a/gns3server/schemas/controller/rbac.py +++ b/gns3server/schemas/controller/rbac.py @@ -48,10 +48,10 @@ class ACEBase(BaseModel): Common ACE properties. """ + ace_type: ACEType = Field(..., description="Type of the ACE") path: str propagate: Optional[bool] = True allowed: Optional[bool] = True - type: ACEType = Field(..., description="Type of the ACE") user_id: Optional[UUID] = None group_id: Optional[UUID] = None role_id: UUID diff --git a/tests/api/routes/controller/test_acl.py b/tests/api/routes/controller/test_acl.py index bf67d1a9..a5461d89 100644 --- a/tests/api/routes/controller/test_acl.py +++ b/tests/api/routes/controller/test_acl.py @@ -17,7 +17,6 @@ import pytest import pytest_asyncio -import uuid from fastapi import FastAPI, status from httpx import AsyncClient @@ -25,58 +24,15 @@ from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.rbac import RbacRepository -from gns3server.controller import Controller -from gns3server.controller.project import Project from gns3server.schemas.controller.users import User from gns3server.schemas.controller.rbac import ACECreate +from gns3server.controller import Controller pytestmark = pytest.mark.asyncio class TestACLRoutes: - # @pytest_asyncio.fixture - # async def project( - # self, - # app: FastAPI, - # authorized_client: AsyncClient, - # test_user: User, - # db_session: AsyncSession, - # controller: Controller - # ) -> Project: - # - # # add an ACE to allow user to create a project - # user_id = test_user.user_id - # rbac_repo = RbacRepository(db_session) - # role_in_db = await rbac_repo.get_role_by_name("User") - # role_id = role_in_db.role_id - # ace = ACECreate( - # path="/projects", - # type="user", - # user_id=user_id, - # role_id=role_id - # ) - # await rbac_repo.create_ace(ace) - # project_uuid = str(uuid.uuid4()) - # params = {"name": "test", "project_id": project_uuid} - # response = await authorized_client.post(app.url_path_for("create_project"), json=params) - # assert response.status_code == status.HTTP_201_CREATED - # return controller.get_project(project_uuid) - - #@pytest_asyncio.fixture - # async def project( - # self, - # app: FastAPI, - # client: AsyncClient, - # controller: Controller - # ) -> Project: - # - # project_uuid = str(uuid.uuid4()) - # params = {"name": "test", "project_id": project_uuid} - # response = await client.post(app.url_path_for("create_project"), json=params) - # assert response.status_code == status.HTTP_201_CREATED - # return controller.get_project(project_uuid) - @pytest_asyncio.fixture async def group_id(self, db_session: AsyncSession) -> str: @@ -102,11 +58,22 @@ class TestACLRoutes: role_id: str ) -> None: + # allow the user to create an ACE + rbac_repo = RbacRepository(db_session) + admin_role_id = (await rbac_repo.get_role_by_name("Administrator")).role_id + ace = ACECreate( + path="/acl", + ace_type="user", + user_id=test_user.user_id, + role_id=admin_role_id + ) + await rbac_repo.create_ace(ace) + # add an ACE on /projects to allow user to create a project path = f"/projects" new_ace = { "path": path, - "type": "user", + "ace_type": "user", "user_id": str(test_user.user_id), "role_id": role_id } @@ -130,14 +97,14 @@ class TestACLRoutes: new_ace = { "path": "/projects/invalid", - "type": "group", + "ace_type": "group", "group_id": group_id, "role_id": role_id } response = await client.post(app.url_path_for("create_ace"), json=new_ace) assert response.status_code == status.HTTP_400_BAD_REQUEST - # async def test_create_ace_not_existing_resource( + # async def test_create_ace_non_existing_resource( # self, # app: FastAPI, # client: AsyncClient, @@ -147,6 +114,7 @@ class TestACLRoutes: # # new_ace = { # "path": f"/projects/{str(uuid.uuid4())}", + # "ace_type": "group", # "group_id": group_id, # "role_id": role_id # } @@ -165,7 +133,7 @@ class TestACLRoutes: response = await client.get(app.url_path_for("get_aces")) assert response.status_code == status.HTTP_200_OK - assert len(response.json()) == 1 + assert len(response.json()) == 2 async def test_update_ace( self, app: FastAPI, @@ -180,7 +148,7 @@ class TestACLRoutes: update_ace = { "path": f"/appliances", - "type": "user", + "ace_type": "user", "user_id": str(test_user.user_id), "role_id": role_id } @@ -204,11 +172,41 @@ class TestACLRoutes: response = await client.delete(app.url_path_for("delete_ace", ace_id=ace_in_db.ace_id)) assert response.status_code == status.HTTP_204_NO_CONTENT - # async def test_prune_permissions(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: - # - # response = await client.post(app.url_path_for("prune_permissions")) - # assert response.status_code == status.HTTP_204_NO_CONTENT - # - # rbac_repo = RbacRepository(db_session) - # permissions_in_db = await rbac_repo.get_permissions() - # assert len(permissions_in_db) == 10 # 6 default permissions + 4 custom permissions + async def test_ace_cleanup( + self, + app: FastAPI, + authorized_client: AsyncClient, + db_session: AsyncSession, + test_user: User, + role_id: str, + ) -> None: + + # allow the user to create projects + rbac_repo = RbacRepository(db_session) + ace = ACECreate( + path="/projects", + ace_type="user", + user_id=test_user.user_id, + role_id=role_id + ) + await rbac_repo.create_ace(ace) + + response = await authorized_client.post(app.url_path_for("create_project"), json={"name": "test2"}) + assert response.status_code == status.HTTP_201_CREATED + project_id = response.json()["project_id"] + + path = f"/projects/{project_id}" + ace = ACECreate( + path=path, + ace_type="user", + user_id=test_user.user_id, + role_id=role_id + ) + await rbac_repo.create_ace(ace) + assert await rbac_repo.get_ace_by_path(path) + + response = await authorized_client.delete(app.url_path_for("delete_project", project_id=project_id)) + assert response.status_code == status.HTTP_204_NO_CONTENT + + # the ACE should have been deleted after deleting the project + assert not await rbac_repo.get_ace_by_path(path) diff --git a/tests/api/routes/controller/test_roles.py b/tests/api/routes/controller/test_roles.py index 66778eae..f0c85856 100644 --- a/tests/api/routes/controller/test_roles.py +++ b/tests/api/routes/controller/test_roles.py @@ -125,7 +125,7 @@ class TestRolesPrivilegesRoutes: ) assert response.status_code == status.HTTP_204_NO_CONTENT privileges = await rbac_repo.get_role_privileges(role_in_db.role_id) - assert len(privileges) == 21 # 20 default privileges + 1 custom privilege + assert len(privileges) == 25 # 24 default privileges + 1 custom privilege async def test_get_role_privileges( self, @@ -143,7 +143,7 @@ class TestRolesPrivilegesRoutes: role_id=role_in_db.role_id) ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()) == 21 # 20 default privileges + 1 custom privilege + assert len(response.json()) == 25 # 24 default privileges + 1 custom privilege async def test_remove_privilege_from_role( self, @@ -165,4 +165,4 @@ class TestRolesPrivilegesRoutes: ) assert response.status_code == status.HTTP_204_NO_CONTENT privileges = await rbac_repo.get_role_privileges(role_in_db.role_id) - assert len(privileges) == 20 # 20 default privileges + assert len(privileges) == 24 # 24 default privileges diff --git a/tests/api/routes/controller/test_users.py b/tests/api/routes/controller/test_users.py index 2b33b52c..94262fdb 100644 --- a/tests/api/routes/controller/test_users.py +++ b/tests/api/routes/controller/test_users.py @@ -305,7 +305,7 @@ class TestUserLogin: assert response.status_code == status.HTTP_200_OK token = response.json().get("access_token") - response = await unauthorized_client.get(app.url_path_for("get_projects"), params={"token": token}) + response = await unauthorized_client.get(app.url_path_for("statistics"), params={"token": token}) assert response.status_code == status.HTTP_200_OK diff --git a/tests/controller/test_rbac.py b/tests/controller/test_rbac.py index 178e96fb..51339d83 100644 --- a/tests/controller/test_rbac.py +++ b/tests/controller/test_rbac.py @@ -16,51 +16,118 @@ # along with this program. If not, see . import pytest +import pytest_asyncio from fastapi import FastAPI, status from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from gns3server.db.repositories.rbac import RbacRepository +from gns3server.db.repositories.users import UsersRepository +from gns3server.schemas.controller.rbac import ACECreate from gns3server.db.models import User pytestmark = pytest.mark.asyncio - -# class TestPermissions: +# @pytest_asyncio.fixture +# async def project_ace(db_session: AsyncSession): # -# @pytest.mark.parametrize( -# "method, path, result", -# ( -# ("GET", "/users", False), -# ("GET", "/projects", True), -# ("GET", "/projects/e451ad73-2519-4f83-87fe-a8e821792d44", False), -# ("POST", "/projects", True), -# ("GET", "/templates", True), -# ("GET", "/templates/62e92cf1-244a-4486-8dae-b95439b54da9", False), -# ("POST", "/templates", True), -# ("GET", "/computes", True), -# ("GET", "/computes/local", True), -# ("GET", "/symbols", True), -# ("GET", "/symbols/default_symbols", True), -# ), +# group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id +# role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id +# ace = ACECreate( +# path="/projects", +# ace_type="group", +# propagate=False, +# group_id=str(group_id), +# role_id=str(role_id) # ) -# async def test_default_permissions_user_group( -# self, -# app: FastAPI, -# authorized_client: AsyncClient, -# test_user: User, -# db_session: AsyncSession, -# method: str, -# path: str, -# result: bool -# ) -> None: -# -# rbac_repo = RbacRepository(db_session) -# authorized = await rbac_repo.check_user_is_authorized(test_user.user_id, method, path) -# assert authorized == result -# -# +# await RbacRepository(db_session).create_ace(ace) + + +class TestPrivileges: + + @pytest.mark.parametrize( + "privilege, path, result", + ( + ("User.Allocate", "/users", False), + ("Project.Allocate", "/projects", False), + ("Project.Allocate", "/projects", True), + ("Project.Audit", "/projects/e451ad73-2519-4f83-87fe-a8e821792d44", True), + ("Project.Audit", "/templates", False), + ("Template.Audit", "/templates", True), + ("Template.Allocate", "/templates", False), + ("Compute.Audit", "/computes", True), + ("Compute.Audit", "/computes/local", True), + ("Symbol.Audit", "/symbols", True), + ("Symbol.Audit", "/symbols/default_symbols", True), + ), + ) + async def test_default_privileges_user_group( + self, + test_user: User, + db_session: AsyncSession, + privilege: str, + path: str, + result: bool + ) -> None: + + # add an ACE for path + if result: + group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id + role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id + ace = ACECreate( + path=path, + ace_type="group", + propagate=False, + group_id=str(group_id), + role_id=str(role_id) + ) + await RbacRepository(db_session).create_ace(ace) + + authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) + assert authorized == result + + async def test_propagate(self, test_user: User, db_session: AsyncSession): + + privilege = "Project.Audit" + path = "/projects/44929147-47bb-460a-90ae-c782c4dbb6ef" + authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) + assert authorized is False + + ace = await RbacRepository(db_session).get_ace_by_path("/projects") + ace.propagate = True + await db_session.commit() + + authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) + assert authorized is True + + async def test_allowed(self, test_user: User, db_session: AsyncSession): + + ace = await RbacRepository(db_session).get_ace_by_path("/projects") + ace.allowed = False + ace.propagate = True + await db_session.commit() + + privilege = "Project.Audit" + path = "/projects/44929147-47bb-460a-90ae-c782c4dbb6ef" + authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) + assert authorized is False + + # privileges on deeper levels replace those inherited from an upper level. + group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id + role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id + ace = ACECreate( + path=path, + ace_type="group", + propagate=False, + group_id=str(group_id), + role_id=str(role_id) + ) + await RbacRepository(db_session).create_ace(ace) + + authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) + assert authorized is True + # class TestProjectsWithRbac: # # async def test_admin_create_project(self, app: FastAPI, client: AsyncClient): @@ -73,6 +140,7 @@ pytestmark = pytest.mark.asyncio # self, # app: FastAPI, # authorized_client: AsyncClient, +# project_ace, # test_user: User, # db_session: AsyncSession # ) -> None: @@ -86,67 +154,67 @@ pytestmark = pytest.mark.asyncio # permissions_in_db = await rbac_repo.get_user_permissions(test_user.user_id) # assert len(permissions_in_db) == 1 # assert permissions_in_db[0].path == f"/projects/{project_id}/*" -# -# response = await authorized_client.get(app.url_path_for("get_projects")) -# assert response.status_code == status.HTTP_200_OK -# projects = response.json() -# assert len(projects) == 1 -# -# async def test_admin_access_all_projects(self, app: FastAPI, client: AsyncClient): -# -# response = await client.get(app.url_path_for("get_projects")) -# assert response.status_code == status.HTTP_200_OK -# projects = response.json() -# assert len(projects) == 2 -# -# async def test_admin_user_give_permission_on_project( -# self, -# app: FastAPI, -# client: AsyncClient, -# test_user: User -# ): -# -# response = await client.get(app.url_path_for("get_projects")) -# assert response.status_code == status.HTTP_200_OK -# projects = response.json() -# project_id = None -# for project in projects: -# if project["name"] == "Admin project": -# project_id = project["project_id"] -# break -# -# new_permission = { -# "methods": ["GET"], -# "path": f"/projects/{project_id}", -# "action": "ALLOW" -# } -# response = await client.post(app.url_path_for("create_permission"), json=new_permission) -# assert response.status_code == status.HTTP_201_CREATED -# permission_id = response.json()["permission_id"] -# -# response = await client.put( -# app.url_path_for( -# "add_permission_to_user", -# user_id=test_user.user_id, -# permission_id=permission_id -# ) -# ) -# assert response.status_code == status.HTTP_204_NO_CONTENT -# -# async def test_user_access_admin_project( -# self, -# app: FastAPI, -# authorized_client: AsyncClient, -# test_user: User, -# db_session: AsyncSession -# ) -> None: -# -# response = await authorized_client.get(app.url_path_for("get_projects")) -# assert response.status_code == status.HTTP_200_OK -# projects = response.json() -# assert len(projects) == 2 -# -# + # + # response = await authorized_client.get(app.url_path_for("get_projects")) + # assert response.status_code == status.HTTP_200_OK + # projects = response.json() + # assert len(projects) == 1 + + # async def test_admin_access_all_projects(self, app: FastAPI, client: AsyncClient): + # + # response = await client.get(app.url_path_for("get_projects")) + # assert response.status_code == status.HTTP_200_OK + # projects = response.json() + # assert len(projects) == 2 + # + # async def test_admin_user_give_permission_on_project( + # self, + # app: FastAPI, + # client: AsyncClient, + # test_user: User + # ): + # + # response = await client.get(app.url_path_for("get_projects")) + # assert response.status_code == status.HTTP_200_OK + # projects = response.json() + # project_id = None + # for project in projects: + # if project["name"] == "Admin project": + # project_id = project["project_id"] + # break + # + # new_permission = { + # "methods": ["GET"], + # "path": f"/projects/{project_id}", + # "action": "ALLOW" + # } + # response = await client.post(app.url_path_for("create_permission"), json=new_permission) + # assert response.status_code == status.HTTP_201_CREATED + # permission_id = response.json()["permission_id"] + # + # response = await client.put( + # app.url_path_for( + # "add_permission_to_user", + # user_id=test_user.user_id, + # permission_id=permission_id + # ) + # ) + # assert response.status_code == status.HTTP_204_NO_CONTENT + # + # async def test_user_access_admin_project( + # self, + # app: FastAPI, + # authorized_client: AsyncClient, + # test_user: User, + # db_session: AsyncSession + # ) -> None: + # + # response = await authorized_client.get(app.url_path_for("get_projects")) + # assert response.status_code == status.HTTP_200_OK + # projects = response.json() + # assert len(projects) == 2 + # + # class TestTemplatesWithRbac: # # async def test_admin_create_template(self, app: FastAPI, client: AsyncClient):