Add required privileges to all endpoints

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

No files matched your search

+1 -11
View File
@@ -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"]
)
+125 -20
View File
@@ -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()
+34 -6
View File
@@ -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()
+47 -6
View File
@@ -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:
+50 -7
View File
@@ -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}")
+52 -12
View File
@@ -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)
+38 -6
View File
@@ -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()
+80 -12
View File
@@ -1,5 +1,5 @@
#
# Copyright (C) 2016 GNS3 Technologies Inc.
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# 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:
+163 -28
View File
@@ -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")
+54 -25
View File
@@ -45,11 +45,10 @@ from gns3server.controller.import_project import import_project as import_contro
from gns3server.controller.export_project import export_project as export_controller_project
from gns3server.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)
+54 -9
View File
@@ -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)
+42 -6
View File
@@ -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))
+22 -7
View File
@@ -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()
+49 -7
View File
@@ -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)
+39 -12
View File
@@ -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)