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