Add required privileges to all endpoints

pull/2275/head
grossmj 9 months ago
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.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),
# ),
# @pytest_asyncio.fixture
# async def project_ace(db_session: AsyncSession):
#
# 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…
Cancel
Save