1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-24 17:28:08 +00:00

Add required privileges to all endpoints

This commit is contained in:
grossmj 2023-09-02 17:54:24 +07:00
parent f3a4ad49f4
commit 0077fd98aa
24 changed files with 1125 additions and 357 deletions

View File

@ -43,35 +43,30 @@ router.include_router(users.router, prefix="/users", tags=["Users"])
router.include_router( router.include_router(
groups.router, groups.router,
dependencies=[Depends(get_current_active_user)],
prefix="/groups", prefix="/groups",
tags=["Users groups"] tags=["Users groups"]
) )
router.include_router( router.include_router(
roles.router, roles.router,
dependencies=[Depends(get_current_active_user)],
prefix="/roles", prefix="/roles",
tags=["Roles"] tags=["Roles"]
) )
router.include_router( router.include_router(
acl.router, acl.router,
dependencies=[Depends(get_current_active_user)],
prefix="/acl", prefix="/acl",
tags=["ACL"] tags=["ACL"]
) )
router.include_router( router.include_router(
images.router, images.router,
dependencies=[Depends(get_current_active_user)],
prefix="/images", prefix="/images",
tags=["Images"] tags=["Images"]
) )
router.include_router( router.include_router(
templates.router, templates.router,
dependencies=[Depends(get_current_active_user)],
prefix="/templates", prefix="/templates",
tags=["Templates"] tags=["Templates"]
) )
@ -83,21 +78,18 @@ router.include_router(
router.include_router( router.include_router(
nodes.router, nodes.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/nodes", prefix="/projects/{project_id}/nodes",
tags=["Nodes"] tags=["Nodes"]
) )
router.include_router( router.include_router(
links.router, links.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/links", prefix="/projects/{project_id}/links",
tags=["Links"] tags=["Links"]
) )
router.include_router( router.include_router(
drawings.router, drawings.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/drawings", prefix="/projects/{project_id}/drawings",
tags=["Drawings"]) tags=["Drawings"])
@ -108,7 +100,6 @@ router.include_router(
router.include_router( router.include_router(
snapshots.router, snapshots.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/snapshots", prefix="/projects/{project_id}/snapshots",
tags=["Snapshots"]) tags=["Snapshots"])
@ -126,15 +117,14 @@ router.include_router(
router.include_router( router.include_router(
appliances.router, appliances.router,
dependencies=[Depends(get_current_active_user)],
prefix="/appliances", prefix="/appliances",
tags=["Appliances"] tags=["Appliances"]
) )
router.include_router( router.include_router(
gns3vm.router, gns3vm.router,
deprecated=True,
dependencies=[Depends(get_current_active_user)], dependencies=[Depends(get_current_active_user)],
deprecated=True,
prefix="/gns3vm", prefix="/gns3vm",
tags=["GNS3 VM"] tags=["GNS3 VM"]
) )

View File

@ -30,13 +30,16 @@ from typing import List
from gns3server import schemas from gns3server import schemas
from gns3server.controller.controller_error import ( from gns3server.controller.controller_error import (
ControllerBadRequestError, ControllerBadRequestError,
ControllerNotFoundError, ControllerNotFoundError
ControllerForbiddenError,
) )
from gns3server.controller import Controller
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository 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.database import get_repository
from .dependencies.authentication import get_current_active_user from .dependencies.rbac import has_privilege
import logging import logging
@ -45,26 +48,121 @@ log = logging.getLogger(__name__)
router = APIRouter() 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( async def get_aces(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.ACE]: ) -> List[schemas.ACE]:
""" """
Get all ACL entries. Get all ACL entries.
Required privilege: ACE.Audit
""" """
return await rbac_repo.get_aces() 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( async def create_ace(
request: Request, request: Request,
ace_create: schemas.ACECreate, ace_create: schemas.ACECreate,
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE: ) -> schemas.ACE:
""" """
Create a new ACL entry. Create a new ACL entry.
Required privilege: ACE.Allocate
""" """
for route in request.app.routes: 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") 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( async def get_ace(
ace_id: UUID, ace_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.ACE: ) -> schemas.ACE:
""" """
Get an ACL entry. Get an ACL entry.
Required privilege: ACE.Audit
""" """
ace = await rbac_repo.get_ace(ace_id) ace = await rbac_repo.get_ace(ace_id)
@ -99,7 +203,11 @@ async def get_ace(
return 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( async def update_ace(
ace_id: UUID, ace_id: UUID,
ace_update: schemas.ACEUpdate, ace_update: schemas.ACEUpdate,
@ -107,6 +215,8 @@ async def update_ace(
) -> schemas.ACE: ) -> schemas.ACE:
""" """
Update an ACL entry. Update an ACL entry.
Required privilege: ACE.Modify
""" """
ace = await rbac_repo.get_ace(ace_id) 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) 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( async def delete_ace(
ace_id: UUID, ace_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None: ) -> None:
""" """
Delete an ACL entry. Delete an ACL entry.
Required privilege: ACE.Allocate
""" """
ace = await rbac_repo.get_ace(ace_id) ace = await rbac_repo.get_ace(ace_id)
@ -132,14 +248,3 @@ async def delete_ace(
success = await rbac_repo.delete_ace(ace_id) success = await rbac_repo.delete_ace(ace_id)
if not success: if not success:
raise ControllerNotFoundError(f"ACL entry '{ace_id}' could not be deleted") 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()

View File

@ -20,7 +20,7 @@ API routes for appliances.
import logging import logging
from fastapi import APIRouter, Depends, Response, status from fastapi import APIRouter, Depends, status
from typing import Optional, List from typing import Optional, List
from uuid import UUID 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.authentication import get_current_active_user
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() 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( async def get_appliances(
update: Optional[bool] = False, update: Optional[bool] = False,
symbol_theme: Optional[str] = None symbol_theme: Optional[str] = None
) -> List[schemas.Appliance]: ) -> List[schemas.Appliance]:
""" """
Return all appliances known by the controller. Return all appliances known by the controller.
Required privilege: Appliance.Audit
""" """
controller = Controller.instance() controller = Controller.instance()
@ -60,10 +69,17 @@ async def get_appliances(
return [c.asdict() for c in controller.appliance_manager.appliances.values()] 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: def get_appliance(appliance_id: UUID) -> schemas.Appliance:
""" """
Get an appliance file. Get an appliance file.
Required privilege: Appliance.Audit
""" """
controller = Controller.instance() controller = Controller.instance()
@ -73,10 +89,16 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance:
return appliance.asdict() 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: 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() controller = Controller.instance()
@ -98,7 +120,11 @@ def add_appliance_version(appliance_id: UUID, appliance_version: schemas.Applian
return appliance.asdict() 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( async def install_appliance(
appliance_id: UUID, appliance_id: UUID,
version: Optional[str] = None, version: Optional[str] = None,
@ -109,6 +135,8 @@ async def install_appliance(
) -> None: ) -> None:
""" """
Install an appliance. Install an appliance.
Required privilege: Appliance.Allocate
""" """
controller = Controller.instance() controller = Controller.instance()

View File

@ -24,10 +24,12 @@ from uuid import UUID
from gns3server.controller import Controller from gns3server.controller import Controller
from gns3server.db.repositories.computes import ComputesRepository from gns3server.db.repositories.computes import ComputesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services.computes import ComputesService from gns3server.services.computes import ComputesService
from gns3server import schemas from gns3server import schemas
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}} 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"}, 409: {"model": schemas.ErrorMessage, "description": "Could not create compute"},
401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"}, 401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"},
}, },
dependencies=[Depends(has_privilege("Compute.Allocate"))]
) )
async def create_compute( async def create_compute(
compute_create: schemas.ComputeCreate, compute_create: schemas.ComputeCreate,
@ -51,15 +54,23 @@ async def create_compute(
) -> schemas.Compute: ) -> schemas.Compute:
""" """
Create a new compute on the controller. Create a new compute on the controller.
Required privilege: Compute.Allocate
""" """
return await ComputesService(computes_repo).create_compute(compute_create, connect) 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: async def connect_compute(compute_id: Union[str, UUID]) -> None:
""" """
Connect to compute on the controller. Connect to compute on the controller.
Required privilege: Compute.Audit
""" """
compute = Controller.instance().get_compute(str(compute_id)) 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) 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( async def get_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))
) -> schemas.Compute: ) -> schemas.Compute:
""" """
Return a compute from the controller. Return a compute from the controller.
Required privilege: Compute.Audit
""" """
return await ComputesService(computes_repo).get_compute(compute_id) 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( async def get_computes(
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
) -> List[schemas.Compute]: ) -> List[schemas.Compute]:
""" """
Return all computes known by the controller. Return all computes known by the controller.
Required privilege: Compute.Audit
""" """
return await ComputesService(computes_repo).get_computes() 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( async def update_compute(
compute_id: Union[str, UUID], compute_id: Union[str, UUID],
compute_update: schemas.ComputeUpdate, compute_update: schemas.ComputeUpdate,
@ -97,20 +127,31 @@ async def update_compute(
) -> schemas.Compute: ) -> schemas.Compute:
""" """
Update a compute on the controller. Update a compute on the controller.
Required privilege: Compute.Modify
""" """
return await ComputesService(computes_repo).update_compute(compute_id, compute_update) 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( 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: ) -> None:
""" """
Delete a compute from the controller. Delete a compute from the controller.
Required privilege: Compute.Allocate
""" """
await ComputesService(computes_repo).delete_compute(compute_id) 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]) @router.get("/{compute_id}/docker/images", response_model=List[schemas.ComputeDockerImage])

View File

@ -37,10 +37,10 @@ def has_privilege(
): ):
if not current_user.is_superadmin: 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 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): 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)") 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 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}'") 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): 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)") 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 return get_user_and_check_privilege
# class PrivilegeChecker: # class PrivilegeChecker:

View File

@ -18,33 +18,51 @@
API routes for drawings. API routes for drawings.
""" """
from fastapi import APIRouter, Response, status from fastapi import APIRouter, Depends, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from typing import List from typing import List
from uuid import UUID from uuid import UUID
from gns3server.controller import Controller from gns3server.controller import Controller
from gns3server.db.repositories.rbac import RbacRepository
from gns3server import schemas 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"}} responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}}
router = APIRouter(responses=responses) 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]: async def get_drawings(project_id: UUID) -> List[schemas.Drawing]:
""" """
Return the list of all drawings for a given project. Return the list of all drawings for a given project.
Required privilege: Drawing.Audit
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) project = await Controller.instance().get_loaded_project(str(project_id))
return [v.asdict() for v in project.drawings.values()] 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: async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing:
""" """
Create a new drawing. Create a new drawing.
Required privilege: Drawing.Allocate
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) 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() 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: async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing:
""" """
Return a drawing. Return a drawing.
Required privilege: Drawing.Audit
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) 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() 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: async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing:
""" """
Update a drawing. Update a drawing.
Required privilege: Drawing.Modify
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) 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() return drawing.asdict()
@router.delete("/{drawing_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete(
async def delete_drawing(project_id: UUID, drawing_id: UUID) -> None: "/{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. Delete a drawing.
Required privilege: Drawing.Allocate
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_drawing(str(drawing_id)) await project.delete_drawing(str(drawing_id))
await rbac_repo.delete_all_ace_starting_with_path(f"/drawings/{drawing_id}")

View File

@ -19,7 +19,7 @@
API routes for user groups. API routes for user groups.
""" """
from fastapi import APIRouter, Depends, Response, status from fastapi import APIRouter, Depends, status
from uuid import UUID from uuid import UUID
from typing import List 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.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository from .dependencies.database import get_repository
import logging import logging
@ -42,12 +44,18 @@ log = logging.getLogger(__name__)
router = APIRouter() 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( async def get_user_groups(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)) users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.UserGroup]: ) -> List[schemas.UserGroup]:
""" """
Get all user groups. Get all user groups.
Required privilege: Group.Audit
""" """
return await users_repo.get_user_groups() return await users_repo.get_user_groups()
@ -56,7 +64,8 @@ async def get_user_groups(
@router.post( @router.post(
"", "",
response_model=schemas.UserGroup, 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( async def create_user_group(
user_group_create: schemas.UserGroupCreate, user_group_create: schemas.UserGroupCreate,
@ -64,6 +73,8 @@ async def create_user_group(
) -> schemas.UserGroup: ) -> schemas.UserGroup:
""" """
Create a new user group. Create a new user group.
Required privilege: Group.Allocate
""" """
if await users_repo.get_user_group_by_name(user_group_create.name): 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) 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( async def get_user_group(
user_group_id: UUID, user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.UserGroup: ) -> schemas.UserGroup:
""" """
Get a user group. Get a user group.
Required privilege: Group.Audit
""" """
user_group = await users_repo.get_user_group(user_group_id) user_group = await users_repo.get_user_group(user_group_id)
@ -87,7 +104,11 @@ async def get_user_group(
return 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( async def update_user_group(
user_group_id: UUID, user_group_id: UUID,
user_group_update: schemas.UserGroupUpdate, user_group_update: schemas.UserGroupUpdate,
@ -95,6 +116,8 @@ async def update_user_group(
) -> schemas.UserGroup: ) -> schemas.UserGroup:
""" """
Update a user group. Update a user group.
Required privilege: Group.Modify
""" """
user_group = await users_repo.get_user_group(user_group_id) user_group = await users_repo.get_user_group(user_group_id)
if not user_group: if not user_group:
@ -108,14 +131,18 @@ async def update_user_group(
@router.delete( @router.delete(
"/{user_group_id}", "/{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( async def delete_user_group(
user_group_id: UUID, user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None: ) -> None:
""" """
Delete a user group Delete a user group.
Required privilege: Group.Allocate
""" """
user_group = await users_repo.get_user_group(user_group_id) 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) success = await users_repo.delete_user_group(user_group_id)
if not success: if not success:
raise ControllerError(f"User group '{user_group_id}' could not be deleted") 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( async def get_user_group_members(
user_group_id: UUID, user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)) users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.User]: ) -> List[schemas.User]:
""" """
Get all user group members. Get all user group members.
Required privilege: Group.Audit
""" """
return await users_repo.get_user_group_members(user_group_id) return await users_repo.get_user_group_members(user_group_id)
@ -144,7 +178,8 @@ async def get_user_group_members(
@router.put( @router.put(
"/{user_group_id}/members/{user_id}", "/{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( async def add_member_to_group(
user_group_id: UUID, user_group_id: UUID,
@ -153,6 +188,8 @@ async def add_member_to_group(
) -> None: ) -> None:
""" """
Add member to a user group. Add member to a user group.
Required privilege: Group.Modify
""" """
user = await users_repo.get_user(user_id) user = await users_repo.get_user(user_id)
@ -166,7 +203,8 @@ async def add_member_to_group(
@router.delete( @router.delete(
"/{user_group_id}/members/{user_id}", "/{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( async def remove_member_from_group(
user_group_id: UUID, user_group_id: UUID,
@ -175,6 +213,8 @@ async def remove_member_from_group(
) -> None: ) -> None:
""" """
Remove member from a user group. Remove member from a user group.
Required privilege: Group.Modify
""" """
user = await users_repo.get_user(user_id) user = await users_repo.get_user(user_id)

View File

@ -22,7 +22,7 @@ import os
import logging import logging
import urllib.parse import urllib.parse
from fastapi import APIRouter, Request, Response, Depends, status from fastapi import APIRouter, Request, Depends, status
from starlette.requests import ClientDisconnect from starlette.requests import ClientDisconnect
from sqlalchemy.orm.exc import MultipleResultsFound from sqlalchemy.orm.exc import MultipleResultsFound
from typing import List, Optional 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.authentication import get_current_active_user
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() 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( async def get_images(
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
image_type: Optional[schemas.ImageType] = None image_type: Optional[schemas.ImageType] = None
) -> List[schemas.Image]: ) -> List[schemas.Image]:
""" """
Return all images. Return all images.
Required privilege: Image.Audit
""" """
return await images_repo.get_images(image_type) 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( async def upload_image(
image_path: str, image_path: str,
request: Request, request: Request,
@ -76,6 +88,8 @@ async def upload_image(
Example: curl -X POST http://host:port/v3/images/upload/my_image_name.qcow2 \ Example: curl -X POST http://host:port/v3/images/upload/my_image_name.qcow2 \
-H 'Authorization: Bearer <token>' --data-binary @"/path/to/image.qcow2" -H 'Authorization: Bearer <token>' --data-binary @"/path/to/image.qcow2"
Required privilege: Image.Allocate
""" """
image_path = urllib.parse.unquote(image_path) image_path = urllib.parse.unquote(image_path)
@ -110,13 +124,19 @@ async def upload_image(
return 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( async def get_image(
image_path: str, image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image: ) -> schemas.Image:
""" """
Return an image. Return an image.
Required privilege: Image.Audit
""" """
image_path = urllib.parse.unquote(image_path) image_path = urllib.parse.unquote(image_path)
@ -126,13 +146,19 @@ async def get_image(
return 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( async def delete_image(
image_path: str, image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> None: ) -> None:
""" """
Delete an image. Delete an image.
Required privilege: Image.Allocate
""" """
image_path = urllib.parse.unquote(image_path) 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") 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( async def prune_images(
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> None: ) -> None:
""" """
Prune images not attached to any template. Prune images not attached to any template.
Required privilege: Image.Allocate
""" """
await images_repo.prune_images() await images_repo.prune_images()

View File

@ -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 # 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 # it under the terms of the GNU General Public License as published by
@ -21,7 +21,7 @@ API routes for links.
import multidict import multidict
import aiohttp import aiohttp
from fastapi import APIRouter, Depends, Request, Response, status from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from typing import List from typing import List
@ -29,10 +29,14 @@ from uuid import UUID
from gns3server.controller import Controller from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError from gns3server.controller.controller_error import ControllerError
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.controller.link import Link from gns3server.controller.link import Link
from gns3server.utils.http_client import HTTPClient from gns3server.utils.http_client import HTTPClient
from gns3server import schemas from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -52,10 +56,17 @@ async def dep_link(project_id: UUID, link_id: UUID) -> Link:
return 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]: async def get_links(project_id: UUID) -> List[schemas.Link]:
""" """
Return all links for a given project. Return all links for a given project.
Required privilege: Link.Audit
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) 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"}, 404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
409: {"model": schemas.ErrorMessage, "description": "Could not create link"}, 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: async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link:
""" """
Create a new link. Create a new link.
Required privilege: Link.Allocate
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) 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() 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]: async def get_filters(link: Link = Depends(dep_link)) -> List[dict]:
""" """
Return all filters available for a given link. Return all filters available for a given link.
Required privilege: Link.Audit
""" """
return link.available_filters() 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: async def get_link(link: Link = Depends(dep_link)) -> schemas.Link:
""" """
Return a link. Return a link.
Required privilege: Link.Audit
""" """
return link.asdict() 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: async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
""" """
Update a link. Update a link.
Required privilege: Link.Modify
""" """
link_data = jsonable_encoder(link_data, exclude_unset=True) 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() return link.asdict()
@router.delete("/{link_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete(
async def delete_link(project_id: UUID, link: Link = Depends(dep_link)) -> None: "/{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. Delete a link.
Required privilege: Link.Allocate
""" """
project = await Controller.instance().get_loaded_project(str(project_id)) project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_link(link.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: async def reset_link(link: Link = Depends(dep_link)) -> schemas.Link:
""" """
Reset a link. Reset a link.
Required privilege: Link.Modify
""" """
await link.reset() await link.reset()
return link.asdict() 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: async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> schemas.Link:
""" """
Start packet capture on the link. Start packet capture on the link.
Required privilege: Link.Capture
""" """
await link.start_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() 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: async def stop_capture(link: Link = Depends(dep_link)) -> None:
""" """
Stop packet capture on the link. Stop packet capture on the link.
Required privilege: Link.Capture
""" """
await link.stop_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: async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> StreamingResponse:
""" """
Stream the PCAP capture file from compute. Stream the PCAP capture file from compute.
Required privilege: Link.Capture
""" """
if not link.capturing: if not link.capturing:

View File

@ -34,8 +34,12 @@ from gns3server.controller.project import Project
from gns3server.utils import force_unix_path from gns3server.utils import force_unix_path
from gns3server.utils.http_client import HTTPClient from gns3server.utils.http_client import HTTPClient
from gns3server.controller.controller_error import ControllerForbiddenError, ControllerBadRequestError from gns3server.controller.controller_error import ControllerForbiddenError, ControllerBadRequestError
from gns3server.db.repositories.rbac import RbacRepository
from gns3server import schemas from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
import logging import logging
log = logging.getLogger(__name__) 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"}, 404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
409: {"model": schemas.ErrorMessage, "description": "Could not create node"}, 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: async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
""" """
Create a new node. Create a new node.
Required privilege: Node.Allocate
""" """
controller = Controller.instance() controller = Controller.instance()
@ -121,65 +128,89 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
return node.asdict() return node.asdict()
@router.get("", response_model=List[schemas.Node], response_model_exclude_unset=True) @router.get(
async def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]: "",
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. Return all nodes belonging to a given project.
Required privilege: Node.Audit
""" """
return [v.asdict() for v in project.nodes.values()] 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: async def start_all_nodes(project: Project = Depends(dep_project)) -> None:
""" """
Start all nodes belonging to a given project. Start all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
""" """
await project.start_all() 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: async def stop_all_nodes(project: Project = Depends(dep_project)) -> None:
""" """
Stop all nodes belonging to a given project. Stop all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
""" """
await project.stop_all() 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: async def suspend_all_nodes(project: Project = Depends(dep_project)) -> None:
""" """
Suspend all nodes belonging to a given project. Suspend all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
""" """
await project.suspend_all() 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: async def reload_all_nodes(project: Project = Depends(dep_project)) -> None:
""" """
Reload all nodes belonging to a given project. Reload all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
""" """
await project.stop_all() await project.stop_all()
await project.start_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: def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
""" """
Return a node from a given project. Return a node from a given project.
Required privilege: Node.Audit
""" """
return node.asdict() 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: async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
""" """
Update a node. Update a node.
Required privilege: Node.Modify
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) 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}", "/{node_id}",
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}}, 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. Delete a node from a project.
Required privilege: Node.Allocate
""" """
await project.delete_node(str(node_id)) 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: async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)) -> schemas.Node:
""" """
Duplicate a 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) new_node = await node.project.duplicate_node(node, duplicate_data.x, duplicate_data.y, duplicate_data.z)
return new_node.asdict() 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: async def start_node(start_data: dict, node: Node = Depends(dep_node)) -> None:
""" """
Start a node. Start a node.
Required privilege: Node.PowerMgmt
""" """
await node.start(data=start_data) 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: async def stop_node(node: Node = Depends(dep_node)) -> None:
""" """
Stop a node. Stop a node.
Required privilege: Node.PowerMgmt
""" """
await node.stop() 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: async def suspend_node(node: Node = Depends(dep_node)) -> None:
""" """
Suspend a node. Suspend a node.
Required privilege: Node.PowerMgmt
""" """
await node.suspend() 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: async def reload_node(node: Node = Depends(dep_node)) -> None:
""" """
Reload a node. Reload a node.
Required privilege: Node.PowerMgmt
""" """
await node.reload() 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: async def isolate_node(node: Node = Depends(dep_node)) -> None:
""" """
Isolate a node (suspend all attached links). Isolate a node (suspend all attached links).
Required privilege: Link.Modify
""" """
for link in node.links: for link in node.links:
await link.update_suspend(True) 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: async def unisolate_node(node: Node = Depends(dep_node)) -> None:
""" """
Un-isolate a node (resume all attached suspended links). Un-isolate a node (resume all attached suspended links).
Required privilege: Link.Modify
""" """
for link in node.links: for link in node.links:
await link.update_suspend(False) 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]: async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]:
""" """
Return all the links connected to a node. Return all the links connected to a node.
Required privilege: Link.Audit
""" """
links = [] links = []
@ -284,10 +372,12 @@ async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]:
return links 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: async def auto_idlepc(node: Node = Depends(dep_node)) -> dict:
""" """
Compute an Idle-PC value for a Dynamips node Compute an Idle-PC value for a Dynamips node
Required privilege: Node.Audit
""" """
if node.node_type != "dynamips": 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() 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]: async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]:
""" """
Compute a list of potential idle-pc values for a Dynamips node Compute a list of potential idle-pc values for a Dynamips node
Required privilege: Node.Audit
""" """
if node.node_type != "dynamips": 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() 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( async def create_disk_image(
disk_name: str, disk_name: str,
disk_data: schemas.QemuDiskImageCreate, disk_data: schemas.QemuDiskImageCreate,
@ -314,6 +410,8 @@ async def create_disk_image(
) -> None: ) -> None:
""" """
Create a Qemu disk image. Create a Qemu disk image.
Required privilege: Node.Allocate
""" """
if node.node_type != "qemu": 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)) 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( async def update_disk_image(
disk_name: str, disk_name: str,
disk_data: schemas.QemuDiskImageUpdate, disk_data: schemas.QemuDiskImageUpdate,
@ -329,6 +431,8 @@ async def update_disk_image(
) -> None: ) -> None:
""" """
Update a Qemu disk image. Update a Qemu disk image.
Required privilege: Node.Allocate
""" """
if node.node_type != "qemu": 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)) 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( async def delete_disk_image(
disk_name: str, disk_name: str,
node: Node = Depends(dep_node) node: Node = Depends(dep_node)
) -> None: ) -> None:
""" """
Delete a Qemu disk image. Delete a Qemu disk image.
Required privilege: Node.Allocate
""" """
if node.node_type != "qemu": if node.node_type != "qemu":
@ -350,10 +460,12 @@ async def delete_disk_image(
await node.delete(f"/disk_image/{disk_name}") 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: 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) 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) 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)): async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
""" """
Write a file in the node directory. Write a file in the node directory.
Required privilege: Node.Modify
""" """
path = force_unix_path(file_path) 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) # 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: async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None:
""" """
WebSocket console. WebSocket console.
Required privilege: Node.Console
""" """
compute = node.compute 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}") 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: async def reset_console_all_nodes(project: Project = Depends(dep_project)) -> None:
""" """
Reset console for all nodes belonging to the project. Reset console for all nodes belonging to the project.
Required privilege: Node.Console
""" """
await project.reset_console_all() 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: 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") await node.post("/console/reset")

View File

@ -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.controller.export_project import export_project as export_controller_project
from gns3server.utils.asyncio import aiozipstream from gns3server.utils.asyncio import aiozipstream
from gns3server.utils.path import is_safe_path 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.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services.templates import TemplatesService 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.rbac import has_privilege, has_privilege_on_websocket
from .dependencies.database import get_repository from .dependencies.database import get_repository
@ -67,31 +66,21 @@ def dep_project(project_id: UUID) -> Project:
return project return project
CHUNK_SIZE = 1024 * 8 # 8KB @router.get(
"",
response_model=List[schemas.Project],
@router.get("", response_model=List[schemas.Project], response_model_exclude_unset=True) response_model_exclude_unset=True,
async def get_projects( dependencies=[Depends(has_privilege("Project.Audit"))]
current_user: schemas.User = Depends(get_current_active_user), )
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) async def get_projects() -> List[schemas.Project]:
) -> List[schemas.Project]:
""" """
Return all projects. Return all projects.
Required privilege: Project.Audit
""" """
controller = Controller.instance() controller = Controller.instance()
if current_user.is_superadmin: return [p.asdict() for p in controller.projects.values()]
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
@router.post( @router.post(
@ -107,6 +96,8 @@ async def create_project(
) -> schemas.Project: ) -> schemas.Project:
""" """
Create a new project. Create a new project.
Required privilege: Project.Allocate
""" """
controller = Controller.instance() 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"))]) @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. Return a project.
Required privilege: Project.Audit
""" """
return project.asdict() return project.asdict()
@ -135,6 +128,8 @@ async def update_project(
) -> schemas.Project: ) -> schemas.Project:
""" """
Update a project. Update a project.
Required privilege: Project.Modify
""" """
await project.update(**jsonable_encoder(project_data, exclude_unset=True)) await project.update(**jsonable_encoder(project_data, exclude_unset=True))
@ -147,21 +142,27 @@ async def update_project(
dependencies=[Depends(has_privilege("Project.Allocate"))] dependencies=[Depends(has_privilege("Project.Allocate"))]
) )
async def delete_project( async def delete_project(
project: Project = Depends(dep_project) project: Project = Depends(dep_project),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None: ) -> None:
""" """
Delete a project. Delete a project.
Required privilege: Project.Allocate
""" """
controller = Controller.instance() controller = Controller.instance()
await project.delete() await project.delete()
controller.remove_project(project) 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"))]) @router.get("/{project_id}/stats", dependencies=[Depends(has_privilege("Project.Audit"))])
def get_project_stats(project: Project = Depends(dep_project)) -> dict: def get_project_stats(project: Project = Depends(dep_project)) -> dict:
""" """
Return a project statistics. Return a project statistics.
Required privilege: Project.Audit
""" """
return project.stats() 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: async def close_project(project: Project = Depends(dep_project)) -> None:
""" """
Close a project. Close a project.
Required privilege: Project.Allocate
""" """
await project.close() 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: async def open_project(project: Project = Depends(dep_project)) -> schemas.Project:
""" """
Open a project. Open a project.
Required privilege: Project.Allocate
""" """
await project.open() 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: async def load_project(path: str = Body(..., embed=True)) -> schemas.Project:
""" """
Load a project (local server only). Load a project (local server only).
Required privilege: Project.Allocate
""" """
controller = Controller.instance() 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: async def project_http_notifications(project_id: UUID) -> StreamingResponse:
""" """
Receive project notifications about the controller from HTTP stream. Receive project notifications about the controller from HTTP stream.
Required privilege: Project.Audit
""" """
from gns3server.api.server import app from gns3server.api.server import app
@ -255,6 +264,8 @@ async def project_ws_notifications(
) -> None: ) -> None:
""" """
Receive project notifications about the controller from WebSocket. Receive project notifications about the controller from WebSocket.
Required privilege: Project.Audit
""" """
if current_user is None: if current_user is None:
@ -298,6 +309,8 @@ async def export_project(
) -> StreamingResponse: ) -> StreamingResponse:
""" """
Export a project as a portable archive. Export a project as a portable archive.
Required privilege: Project.Audit
""" """
compression_query = compression.lower() compression_query = compression.lower()
@ -366,6 +379,8 @@ async def import_project(
) -> schemas.Project: ) -> schemas.Project:
""" """
Import a project from a portable archive. Import a project from a portable archive.
Required privilege: Project.Allocate
""" """
controller = Controller.instance() controller = Controller.instance()
@ -401,6 +416,8 @@ async def duplicate_project(
) -> schemas.Project: ) -> schemas.Project:
""" """
Duplicate a project. Duplicate a project.
Required privilege: Project.Allocate
""" """
reset_mac_addresses = project_data.reset_mac_addresses 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"))]) @router.get("/{project_id}/locked", dependencies=[Depends(has_privilege("Project.Audit"))])
async def locked_project(project: Project = Depends(dep_project)) -> bool: 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 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: async def lock_project(project: Project = Depends(dep_project)) -> None:
""" """
Lock all drawings and nodes in a given project. Lock all drawings and nodes in a given project.
Required privilege: Project.Audit
""" """
project.lock() 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: async def unlock_project(project: Project = Depends(dep_project)) -> None:
""" """
Unlock all drawings and nodes in a given project. Unlock all drawings and nodes in a given project.
Required privilege: Project.Modify
""" """
project.unlock() 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: async def get_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
""" """
Return a file from a project. Return a file from a project.
Required privilege: Project.Audit
""" """
file_path = urllib.parse.unquote(file_path) 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: async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None:
""" """
Write a file to a project. Write a file to a project.
Required privilege: Project.Modify
""" """
file_path = urllib.parse.unquote(file_path) file_path = urllib.parse.unquote(file_path)
@ -511,6 +538,8 @@ async def create_node_from_template(
) -> schemas.Node: ) -> schemas.Node:
""" """
Create a new node from a template. Create a new node from a template.
Required privilege: Node.Allocate
""" """
template = await TemplatesService(templates_repo).get_template(template_id) template = await TemplatesService(templates_repo).get_template(template_id)

View File

@ -19,7 +19,7 @@
API routes for roles. API routes for roles.
""" """
from fastapi import APIRouter, Depends, Response, status from fastapi import APIRouter, Depends, status
from uuid import UUID from uuid import UUID
from typing import List from typing import List
@ -33,6 +33,7 @@ from gns3server.controller.controller_error import (
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging import logging
@ -41,24 +42,37 @@ log = logging.getLogger(__name__)
router = APIRouter() 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( async def get_roles(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Role]: ) -> List[schemas.Role]:
""" """
Get all roles. Get all roles.
Required privilege: Role.Audit
""" """
return await rbac_repo.get_roles() 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( async def create_role(
role_create: schemas.RoleCreate, role_create: schemas.RoleCreate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role: ) -> schemas.Role:
""" """
Create a new role. Create a new role.
Required privilege: Role.Allocate
""" """
if await rbac_repo.get_role_by_name(role_create.name): 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) 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( async def get_role(
role_id: UUID, role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Role: ) -> schemas.Role:
""" """
Get a role. Get a role.
Required privilege: Role.Audit
""" """
role = await rbac_repo.get_role(role_id) role = await rbac_repo.get_role(role_id)
@ -82,7 +102,11 @@ async def get_role(
return 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( async def update_role(
role_id: UUID, role_id: UUID,
role_update: schemas.RoleUpdate, role_update: schemas.RoleUpdate,
@ -90,6 +114,8 @@ async def update_role(
) -> schemas.Role: ) -> schemas.Role:
""" """
Update a role. Update a role.
Required privilege: Role.Modify
""" """
role = await rbac_repo.get_role(role_id) 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) 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( async def delete_role(
role_id: UUID, role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None: ) -> None:
""" """
Delete a role. Delete a role.
Required privilege: Role.Allocate
""" """
role = await rbac_repo.get_role(role_id) role = await rbac_repo.get_role(role_id)
@ -121,15 +153,22 @@ async def delete_role(
success = await rbac_repo.delete_role(role_id) success = await rbac_repo.delete_role(role_id)
if not success: if not success:
raise ControllerError(f"Role '{role_id}' could not be deleted") 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( async def get_role_privileges(
role_id: UUID, role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Privilege]: ) -> List[schemas.Privilege]:
""" """
Get all role privileges. Get all role privileges.
Required privilege: Role.Audit
""" """
return await rbac_repo.get_role_privileges(role_id) return await rbac_repo.get_role_privileges(role_id)
@ -137,7 +176,8 @@ async def get_role_privileges(
@router.put( @router.put(
"/{role_id}/privileges/{privilege_id}", "/{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( async def add_privilege_to_role(
role_id: UUID, role_id: UUID,
@ -146,6 +186,8 @@ async def add_privilege_to_role(
) -> None: ) -> None:
""" """
Add a privilege to a role. Add a privilege to a role.
Required privilege: Role.Modify
""" """
privilege = await rbac_repo.get_privilege(privilege_id) privilege = await rbac_repo.get_privilege(privilege_id)
@ -159,7 +201,8 @@ async def add_privilege_to_role(
@router.delete( @router.delete(
"/{role_id}/privileges/{privilege_id}", "/{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( async def remove_privilege_from_role(
role_id: UUID, role_id: UUID,
@ -168,6 +211,8 @@ async def remove_privilege_from_role(
) -> None: ) -> None:
""" """
Remove privilege from a role. Remove privilege from a role.
Required privilege: Role.Modify
""" """
privilege = await rbac_repo.get_privilege(privilege_id) privilege = await rbac_repo.get_privilege(privilege_id)

View File

@ -23,14 +23,18 @@ import logging
log = logging.getLogger() log = logging.getLogger()
from fastapi import APIRouter, Depends, Response, status from fastapi import APIRouter, Depends, status
from typing import List from typing import List
from uuid import UUID from uuid import UUID
from gns3server.controller.project import Project from gns3server.controller.project import Project
from gns3server.db.repositories.rbac import RbacRepository
from gns3server import schemas from gns3server import schemas
from gns3server.controller import Controller 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"}} responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}}
router = APIRouter(responses=responses) router = APIRouter(responses=responses)
@ -45,42 +49,74 @@ def dep_project(project_id: UUID) -> Project:
return 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( async def create_snapshot(
snapshot_data: schemas.SnapshotCreate, snapshot_data: schemas.SnapshotCreate,
project: Project = Depends(dep_project) project: Project = Depends(dep_project)
) -> schemas.Snapshot: ) -> schemas.Snapshot:
""" """
Create a new snapshot of a project. Create a new snapshot of a project.
Required privilege: Snapshot.Allocate
""" """
snapshot = await project.snapshot(snapshot_data.name) snapshot = await project.snapshot(snapshot_data.name)
return snapshot.asdict() 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]: def get_snapshots(project: Project = Depends(dep_project)) -> List[schemas.Snapshot]:
""" """
Return all snapshots belonging to a given project. Return all snapshots belonging to a given project.
Required privilege: Snapshot.Audit
""" """
snapshots = [s for s in project.snapshots.values()] snapshots = [s for s in project.snapshots.values()]
return [s.asdict() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))] 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) @router.delete(
async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> None: "/{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. Delete a snapshot.
Required privilege: Snapshot.Allocate
""" """
await project.delete_snapshot(str(snapshot_id)) 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: async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> schemas.Project:
""" """
Restore a snapshot. Restore a snapshot.
Required privilege: Snapshot.Restore
""" """
snapshot = project.get_snapshot(str(snapshot_id)) snapshot = project.get_snapshot(str(snapshot_id))

View File

@ -29,7 +29,7 @@ from gns3server.controller import Controller
from gns3server import schemas from gns3server import schemas
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
from .dependencies.authentication import get_current_active_user from .dependencies.rbac import has_privilege
import logging import logging
@ -39,19 +39,28 @@ log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("") @router.get("", dependencies=[Depends(has_privilege("Symbol.Audit"))])
def get_symbols() -> List[dict]: def get_symbols() -> List[dict]:
"""
Return all symbols.
Required privilege: Symbol.Audit
"""
controller = Controller.instance() controller = Controller.instance()
return controller.symbols.list() return controller.symbols.list()
@router.get( @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: async def get_symbol(symbol_id: str) -> FileResponse:
""" """
Download a symbol file. Download a symbol file.
Required privilege: Symbol.Audit
""" """
controller = Controller.instance() controller = Controller.instance()
@ -65,10 +74,13 @@ async def get_symbol(symbol_id: str) -> FileResponse:
@router.get( @router.get(
"/{symbol_id:path}/dimensions", "/{symbol_id:path}/dimensions",
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}, 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: async def get_symbol_dimensions(symbol_id: str) -> dict:
""" """
Get a symbol dimensions. Get a symbol dimensions.
Required privilege: Symbol.Audit
""" """
controller = Controller.instance() 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}") 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: def get_default_symbols() -> dict:
""" """
Return all default symbols. Return all default symbols.
Required privilege: Symbol.Audit
""" """
controller = Controller.instance() controller = Controller.instance()
@ -92,12 +106,14 @@ def get_default_symbols() -> dict:
@router.post( @router.post(
"/{symbol_id:path}/raw", "/{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: async def upload_symbol(symbol_id: str, request: Request) -> None:
""" """
Upload a symbol file. Upload a symbol file.
Required privilege: Symbol.Allocate
""" """
controller = Controller.instance() controller = Controller.instance()
@ -111,4 +127,3 @@ async def upload_symbol(symbol_id: str, request: Request) -> None:
# Reset the symbol list # Reset the symbol list
controller.symbols.list() controller.symbols.list()

View File

@ -36,6 +36,7 @@ from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository from gns3server.db.repositories.images import ImagesRepository
from .dependencies.authentication import get_current_active_user from .dependencies.authentication import get_current_active_user
from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}} 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 = 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( async def create_template(
template_create: schemas.TemplateCreate, template_create: schemas.TemplateCreate,
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
) -> schemas.Template: ) -> schemas.Template:
""" """
Create a new template. Create a new template.
Required privilege: Template.Allocate
""" """
template = await TemplatesService(templates_repo).create_template(template_create) template = await TemplatesService(templates_repo).create_template(template_create)
return template 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( async def get_template(
template_id: UUID, template_id: UUID,
request: Request, request: Request,
@ -65,6 +78,8 @@ async def get_template(
) -> schemas.Template: ) -> schemas.Template:
""" """
Return a template. Return a template.
Required privilege: Template.Audit
""" """
request_etag = request.headers.get("If-None-Match", "") request_etag = request.headers.get("If-None-Match", "")
@ -78,7 +93,12 @@ async def get_template(
return 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( async def update_template(
template_id: UUID, template_id: UUID,
template_update: schemas.TemplateUpdate, template_update: schemas.TemplateUpdate,
@ -86,12 +106,18 @@ async def update_template(
) -> schemas.Template: ) -> schemas.Template:
""" """
Update a template. Update a template.
Required privilege: Template.Modify
""" """
return await TemplatesService(templates_repo).update_template(template_id, template_update) 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( async def delete_template(
template_id: UUID, template_id: UUID,
prune_images: Optional[bool] = False, prune_images: Optional[bool] = False,
@ -101,15 +127,22 @@ async def delete_template(
) -> None: ) -> None:
""" """
Delete a template. Delete a template.
Required privilege: Template.Allocate
""" """
await TemplatesService(templates_repo).delete_template(template_id) 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: if prune_images:
await images_repo.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( async def get_templates(
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
current_user: schemas.User = Depends(get_current_active_user), current_user: schemas.User = Depends(get_current_active_user),
@ -117,6 +150,8 @@ async def get_templates(
) -> List[schemas.Template]: ) -> List[schemas.Template]:
""" """
Return all templates. Return all templates.
Required privilege: Template.Audit
""" """
templates = await TemplatesService(templates_repo).get_templates() templates = await TemplatesService(templates_repo).get_templates()
@ -136,12 +171,19 @@ async def get_templates(
return user_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( async def duplicate_template(
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
) -> schemas.Template: ) -> schemas.Template:
""" """
Duplicate a template. Duplicate a template.
Required privilege: Template.Allocate
""" """
template = await TemplatesService(templates_repo).duplicate_template(template_id) template = await TemplatesService(templates_repo).duplicate_template(template_id)

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/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 # 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 # 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.authentication import get_current_active_user
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging import logging
@ -115,12 +116,18 @@ async def update_logged_in_user(
return await users_repo.update_user(current_user.user_id, user_update) 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( async def get_users(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)) users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.User]: ) -> List[schemas.User]:
""" """
Get all users. Get all users.
Required privilege: User.Audit
""" """
return await users_repo.get_users() return await users_repo.get_users()
@ -129,8 +136,8 @@ async def get_users(
@router.post( @router.post(
"", "",
response_model=schemas.User, 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( async def create_user(
user_create: schemas.UserCreate, user_create: schemas.UserCreate,
@ -138,6 +145,8 @@ async def create_user(
) -> schemas.User: ) -> schemas.User:
""" """
Create a new user. Create a new user.
Required privilege: User.Allocate
""" """
if await users_repo.get_user_by_username(user_create.username): 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) 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( async def get_user(
user_id: UUID, user_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.User: ) -> schemas.User:
""" """
Get a user. Get a user.
Required privilege: User.Audit
""" """
user = await users_repo.get_user(user_id) user = await users_repo.get_user(user_id)
@ -164,7 +179,11 @@ async def get_user(
return 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( async def update_user(
user_id: UUID, user_id: UUID,
user_update: schemas.UserUpdate, user_update: schemas.UserUpdate,
@ -172,6 +191,8 @@ async def update_user(
) -> schemas.User: ) -> schemas.User:
""" """
Update a user. Update a user.
Required privilege: User.Modify
""" """
if user_update.username and await users_repo.get_user_by_username(user_update.username): 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( @router.delete(
"/{user_id}", "/{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( async def delete_user(
user_id: UUID, user_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None: ) -> None:
""" """
Delete a user. Delete a user.
Required privilege: User.Allocate
""" """
user = await users_repo.get_user(user_id) user = await users_repo.get_user(user_id)
@ -209,12 +233,13 @@ async def delete_user(
success = await users_repo.delete_user(user_id) success = await users_repo.delete_user(user_id)
if not success: if not success:
raise ControllerError(f"User '{user_id}' could not be deleted") 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( @router.get(
"/{user_id}/groups", "/{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( async def get_user_memberships(
user_id: UUID, user_id: UUID,
@ -222,6 +247,8 @@ async def get_user_memberships(
) -> List[schemas.UserGroup]: ) -> List[schemas.UserGroup]:
""" """
Get user memberships. Get user memberships.
Required privilege: Group.Audit
""" """
return await users_repo.get_user_memberships(user_id) return await users_repo.get_user_memberships(user_id)

View File

@ -30,10 +30,10 @@ class ACE(BaseTable):
__tablename__ = "acl" __tablename__ = "acl"
ace_id = Column(GUID, primary_key=True, default=generate_uuid) ace_id = Column(GUID, primary_key=True, default=generate_uuid)
ace_type: str = Column(String)
path = Column(String) path = Column(String)
propagate = Column(Boolean, default=True) propagate = Column(Boolean, default=True)
allowed = Column(Boolean, default=True) allowed = Column(Boolean, default=True)
type: str = Column(String)
user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE")) user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE"))
user = relationship("User", back_populates="acl_entries") user = relationship("User", back_populates="acl_entries")
group_id = Column(GUID, ForeignKey('user_groups.user_group_id', ondelete="CASCADE")) 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") role = relationship("Role", back_populates="acl_entries")
__table_args__ = ( __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')"),
) )

View File

@ -71,6 +71,30 @@ def create_default_roles(target, connection, **kw):
"description": "Update a group", "description": "Update a group",
"name": "Group.Modify" "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", "description": "Create or delete a template",
"name": "Template.Allocate" "name": "Template.Allocate"
@ -97,7 +121,15 @@ def create_default_roles(target, connection, **kw):
}, },
{ {
"description": "Create or delete project snapshots", "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", "description": "Create or delete a node",
@ -167,6 +199,10 @@ def create_default_roles(target, connection, **kw):
"description": "Create or delete a compute", "description": "Create or delete a compute",
"name": "Compute.Allocate" "name": "Compute.Allocate"
}, },
{
"description": "Update a compute",
"name": "Compute.Modify"
},
{ {
"description": "View a compute", "description": "View a compute",
"name": "Compute.Audit" "name": "Compute.Audit"
@ -227,7 +263,9 @@ def add_privileges_to_default_roles(target, connection, **kw):
"Project.Allocate", "Project.Allocate",
"Project.Audit", "Project.Audit",
"Project.Modify", "Project.Modify",
"Project.Snapshot", "Snapshot.Allocate",
"Snapshot.Audit",
"Snapshot.Restore",
"Node.Allocate", "Node.Allocate",
"Node.Audit", "Node.Audit",
"Node.Modify", "Node.Modify",
@ -253,6 +291,7 @@ def add_privileges_to_default_roles(target, connection, **kw):
# add required privileges to the "Auditor" role # add required privileges to the "Auditor" role
auditor_privileges = ( auditor_privileges = (
"Project.Audit", "Project.Audit",
"Snapshot.Audit",
"Node.Audit", "Node.Audit",
"Link.Audit", "Link.Audit",
"Drawing.Audit", "Drawing.Audit",

View File

@ -276,22 +276,6 @@ class RbacRepository(BaseRepository):
await self._db_session.commit() await self._db_session.commit()
return result.rowcount > 0 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: async def delete_all_ace_starting_with_path(self, path: str) -> None:
""" """
Delete all ACEs starting with path. Delete all ACEs starting with path.
@ -304,7 +288,10 @@ class RbacRepository(BaseRepository):
log.debug(f"{result.rowcount} ACE(s) have been deleted") log.debug(f"{result.rowcount} ACE(s) have been deleted")
@staticmethod @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) parsed_url = urlparse(path)
original_path = path original_path = path
@ -347,7 +334,7 @@ class RbacRepository(BaseRepository):
aces = result.all() aces = result.all()
try: 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 # the user has an ACE matching the path and privilege,there is no need to check group ACEs
return True return True
except PermissionError: except PermissionError:
@ -366,6 +353,6 @@ class RbacRepository(BaseRepository):
aces = result.all() aces = result.all()
try: try:
return self._match_path_to_aces(path, aces) return self._check_path_with_aces(path, aces)
except PermissionError: except PermissionError:
return False return False

View File

@ -48,10 +48,10 @@ class ACEBase(BaseModel):
Common ACE properties. Common ACE properties.
""" """
ace_type: ACEType = Field(..., description="Type of the ACE")
path: str path: str
propagate: Optional[bool] = True propagate: Optional[bool] = True
allowed: Optional[bool] = True allowed: Optional[bool] = True
type: ACEType = Field(..., description="Type of the ACE")
user_id: Optional[UUID] = None user_id: Optional[UUID] = None
group_id: Optional[UUID] = None group_id: Optional[UUID] = None
role_id: UUID role_id: UUID

View File

@ -17,7 +17,6 @@
import pytest import pytest
import pytest_asyncio import pytest_asyncio
import uuid
from fastapi import FastAPI, status from fastapi import FastAPI, status
from httpx import AsyncClient from httpx import AsyncClient
@ -25,58 +24,15 @@ from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository 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.users import User
from gns3server.schemas.controller.rbac import ACECreate from gns3server.schemas.controller.rbac import ACECreate
from gns3server.controller import Controller
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
class TestACLRoutes: 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 @pytest_asyncio.fixture
async def group_id(self, db_session: AsyncSession) -> str: async def group_id(self, db_session: AsyncSession) -> str:
@ -102,11 +58,22 @@ class TestACLRoutes:
role_id: str role_id: str
) -> None: ) -> 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 # add an ACE on /projects to allow user to create a project
path = f"/projects" path = f"/projects"
new_ace = { new_ace = {
"path": path, "path": path,
"type": "user", "ace_type": "user",
"user_id": str(test_user.user_id), "user_id": str(test_user.user_id),
"role_id": role_id "role_id": role_id
} }
@ -130,14 +97,14 @@ class TestACLRoutes:
new_ace = { new_ace = {
"path": "/projects/invalid", "path": "/projects/invalid",
"type": "group", "ace_type": "group",
"group_id": group_id, "group_id": group_id,
"role_id": role_id "role_id": role_id
} }
response = await client.post(app.url_path_for("create_ace"), json=new_ace) response = await client.post(app.url_path_for("create_ace"), json=new_ace)
assert response.status_code == status.HTTP_400_BAD_REQUEST 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, # self,
# app: FastAPI, # app: FastAPI,
# client: AsyncClient, # client: AsyncClient,
@ -147,6 +114,7 @@ class TestACLRoutes:
# #
# new_ace = { # new_ace = {
# "path": f"/projects/{str(uuid.uuid4())}", # "path": f"/projects/{str(uuid.uuid4())}",
# "ace_type": "group",
# "group_id": group_id, # "group_id": group_id,
# "role_id": role_id # "role_id": role_id
# } # }
@ -165,7 +133,7 @@ class TestACLRoutes:
response = await client.get(app.url_path_for("get_aces")) response = await client.get(app.url_path_for("get_aces"))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1 assert len(response.json()) == 2
async def test_update_ace( async def test_update_ace(
self, app: FastAPI, self, app: FastAPI,
@ -180,7 +148,7 @@ class TestACLRoutes:
update_ace = { update_ace = {
"path": f"/appliances", "path": f"/appliances",
"type": "user", "ace_type": "user",
"user_id": str(test_user.user_id), "user_id": str(test_user.user_id),
"role_id": role_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)) 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 assert response.status_code == status.HTTP_204_NO_CONTENT
# async def test_prune_permissions(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: async def test_ace_cleanup(
# self,
# response = await client.post(app.url_path_for("prune_permissions")) app: FastAPI,
# assert response.status_code == status.HTTP_204_NO_CONTENT authorized_client: AsyncClient,
# db_session: AsyncSession,
# rbac_repo = RbacRepository(db_session) test_user: User,
# permissions_in_db = await rbac_repo.get_permissions() role_id: str,
# assert len(permissions_in_db) == 10 # 6 default permissions + 4 custom permissions ) -> 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)

View File

@ -125,7 +125,7 @@ class TestRolesPrivilegesRoutes:
) )
assert response.status_code == status.HTTP_204_NO_CONTENT assert response.status_code == status.HTTP_204_NO_CONTENT
privileges = await rbac_repo.get_role_privileges(role_in_db.role_id) 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( async def test_get_role_privileges(
self, self,
@ -143,7 +143,7 @@ class TestRolesPrivilegesRoutes:
role_id=role_in_db.role_id) role_id=role_in_db.role_id)
) )
assert response.status_code == status.HTTP_200_OK 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( async def test_remove_privilege_from_role(
self, self,
@ -165,4 +165,4 @@ class TestRolesPrivilegesRoutes:
) )
assert response.status_code == status.HTTP_204_NO_CONTENT assert response.status_code == status.HTTP_204_NO_CONTENT
privileges = await rbac_repo.get_role_privileges(role_in_db.role_id) 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

View File

@ -305,7 +305,7 @@ class TestUserLogin:
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
token = response.json().get("access_token") 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 assert response.status_code == status.HTTP_200_OK

View File

@ -16,51 +16,118 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest import pytest
import pytest_asyncio
from fastapi import FastAPI, status from fastapi import FastAPI, status
from httpx import AsyncClient from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.db.repositories.rbac import RbacRepository 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 from gns3server.db.models import User
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
# @pytest_asyncio.fixture
# class TestPermissions: # async def project_ace(db_session: AsyncSession):
# #
# @pytest.mark.parametrize( # group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id
# "method, path, result", # role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id
# ( # ace = ACECreate(
# ("GET", "/users", False), # path="/projects",
# ("GET", "/projects", True), # ace_type="group",
# ("GET", "/projects/e451ad73-2519-4f83-87fe-a8e821792d44", False), # propagate=False,
# ("POST", "/projects", True), # group_id=str(group_id),
# ("GET", "/templates", True), # role_id=str(role_id)
# ("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),
# ),
# ) # )
# async def test_default_permissions_user_group( # await RbacRepository(db_session).create_ace(ace)
# self,
# app: FastAPI,
# authorized_client: AsyncClient, class TestPrivileges:
# test_user: User,
# db_session: AsyncSession, @pytest.mark.parametrize(
# method: str, "privilege, path, result",
# path: str, (
# result: bool ("User.Allocate", "/users", False),
# ) -> None: ("Project.Allocate", "/projects", False),
# ("Project.Allocate", "/projects", True),
# rbac_repo = RbacRepository(db_session) ("Project.Audit", "/projects/e451ad73-2519-4f83-87fe-a8e821792d44", True),
# authorized = await rbac_repo.check_user_is_authorized(test_user.user_id, method, path) ("Project.Audit", "/templates", False),
# assert authorized == result ("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: # class TestProjectsWithRbac:
# #
# async def test_admin_create_project(self, app: FastAPI, client: AsyncClient): # async def test_admin_create_project(self, app: FastAPI, client: AsyncClient):
@ -73,6 +140,7 @@ pytestmark = pytest.mark.asyncio
# self, # self,
# app: FastAPI, # app: FastAPI,
# authorized_client: AsyncClient, # authorized_client: AsyncClient,
# project_ace,
# test_user: User, # test_user: User,
# db_session: AsyncSession # db_session: AsyncSession
# ) -> None: # ) -> None:
@ -86,67 +154,67 @@ pytestmark = pytest.mark.asyncio
# permissions_in_db = await rbac_repo.get_user_permissions(test_user.user_id) # permissions_in_db = await rbac_repo.get_user_permissions(test_user.user_id)
# assert len(permissions_in_db) == 1 # assert len(permissions_in_db) == 1
# assert permissions_in_db[0].path == f"/projects/{project_id}/*" # assert permissions_in_db[0].path == f"/projects/{project_id}/*"
# #
# response = await authorized_client.get(app.url_path_for("get_projects")) # response = await authorized_client.get(app.url_path_for("get_projects"))
# assert response.status_code == status.HTTP_200_OK # assert response.status_code == status.HTTP_200_OK
# projects = response.json() # projects = response.json()
# assert len(projects) == 1 # assert len(projects) == 1
#
# async def test_admin_access_all_projects(self, app: FastAPI, client: AsyncClient): # async def test_admin_access_all_projects(self, app: FastAPI, client: AsyncClient):
# #
# response = await client.get(app.url_path_for("get_projects")) # response = await client.get(app.url_path_for("get_projects"))
# assert response.status_code == status.HTTP_200_OK # assert response.status_code == status.HTTP_200_OK
# projects = response.json() # projects = response.json()
# assert len(projects) == 2 # assert len(projects) == 2
# #
# async def test_admin_user_give_permission_on_project( # async def test_admin_user_give_permission_on_project(
# self, # self,
# app: FastAPI, # app: FastAPI,
# client: AsyncClient, # client: AsyncClient,
# test_user: User # test_user: User
# ): # ):
# #
# response = await client.get(app.url_path_for("get_projects")) # response = await client.get(app.url_path_for("get_projects"))
# assert response.status_code == status.HTTP_200_OK # assert response.status_code == status.HTTP_200_OK
# projects = response.json() # projects = response.json()
# project_id = None # project_id = None
# for project in projects: # for project in projects:
# if project["name"] == "Admin project": # if project["name"] == "Admin project":
# project_id = project["project_id"] # project_id = project["project_id"]
# break # break
# #
# new_permission = { # new_permission = {
# "methods": ["GET"], # "methods": ["GET"],
# "path": f"/projects/{project_id}", # "path": f"/projects/{project_id}",
# "action": "ALLOW" # "action": "ALLOW"
# } # }
# response = await client.post(app.url_path_for("create_permission"), json=new_permission) # response = await client.post(app.url_path_for("create_permission"), json=new_permission)
# assert response.status_code == status.HTTP_201_CREATED # assert response.status_code == status.HTTP_201_CREATED
# permission_id = response.json()["permission_id"] # permission_id = response.json()["permission_id"]
# #
# response = await client.put( # response = await client.put(
# app.url_path_for( # app.url_path_for(
# "add_permission_to_user", # "add_permission_to_user",
# user_id=test_user.user_id, # user_id=test_user.user_id,
# permission_id=permission_id # permission_id=permission_id
# ) # )
# ) # )
# assert response.status_code == status.HTTP_204_NO_CONTENT # assert response.status_code == status.HTTP_204_NO_CONTENT
# #
# async def test_user_access_admin_project( # async def test_user_access_admin_project(
# self, # self,
# app: FastAPI, # app: FastAPI,
# authorized_client: AsyncClient, # authorized_client: AsyncClient,
# test_user: User, # test_user: User,
# db_session: AsyncSession # db_session: AsyncSession
# ) -> None: # ) -> None:
# #
# response = await authorized_client.get(app.url_path_for("get_projects")) # response = await authorized_client.get(app.url_path_for("get_projects"))
# assert response.status_code == status.HTTP_200_OK # assert response.status_code == status.HTTP_200_OK
# projects = response.json() # projects = response.json()
# assert len(projects) == 2 # assert len(projects) == 2
# #
#
# class TestTemplatesWithRbac: # class TestTemplatesWithRbac:
# #
# async def test_admin_create_template(self, app: FastAPI, client: AsyncClient): # async def test_admin_create_template(self, app: FastAPI, client: AsyncClient):