mirror of https://github.com/GNS3/gns3-server
commit
c3e7b97014
@ -0,0 +1,16 @@
|
||||
name: Add new issues to GNS3 project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.4.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/GNS3/projects/3
|
||||
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}
|
@ -1,11 +1,7 @@
|
||||
include README.rst
|
||||
include AUTHORS
|
||||
include README.md
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include conf/*.conf
|
||||
recursive-include tests *
|
||||
recursive-exclude docs *
|
||||
include CHANGELOG
|
||||
recursive-include gns3server *
|
||||
recursive-exclude docs *
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
@ -1,8 +1,6 @@
|
||||
-r requirements.txt
|
||||
|
||||
pytest==7.2.0
|
||||
pytest==7.4.0
|
||||
flake8==5.0.4 # v5.0.4 is the last to support Python 3.7
|
||||
pytest-timeout==2.1.0
|
||||
pytest-asyncio==0.20.3
|
||||
requests==2.28.1
|
||||
httpx==0.23.1
|
||||
pytest-asyncio==0.21.1
|
||||
requests==2.31.0
|
||||
httpx==0.24.1
|
||||
|
@ -0,0 +1,103 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = db_migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to db_migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:db_migrations/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url =
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for ACL.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.routing import APIRoute
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerBadRequestError,
|
||||
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.rbac import has_privilege
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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("/access/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("/access/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("/access/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,
|
||||
dependencies=[Depends(has_privilege("ACE.Allocate"))]
|
||||
)
|
||||
async def create_ace(
|
||||
request: Request,
|
||||
ace_create: schemas.ACECreate,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.ACE:
|
||||
"""
|
||||
Create a new ACL entry.
|
||||
|
||||
Required privilege: ACE.Allocate
|
||||
"""
|
||||
|
||||
for route in request.app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
|
||||
# remove the prefix (e.g. "/v3") from the route path
|
||||
route_path = re.sub(r"^/v[0-9]", "", route.path)
|
||||
# replace route path ID parameters by a UUID regex
|
||||
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
|
||||
# replace remaining route path parameters by a word matching regex
|
||||
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
|
||||
|
||||
if re.fullmatch(route_path, ace_create.path):
|
||||
log.info("Creating ACE for route path", ace_create.path, route_path)
|
||||
return await rbac_repo.create_ace(ace_create)
|
||||
|
||||
raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint")
|
||||
|
||||
|
||||
@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)
|
||||
if not ace:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
|
||||
return 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,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.ACE:
|
||||
"""
|
||||
Update an ACL entry.
|
||||
|
||||
Required privilege: ACE.Modify
|
||||
"""
|
||||
|
||||
ace = await rbac_repo.get_ace(ace_id)
|
||||
if not ace:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
|
||||
|
||||
return await rbac_repo.update_ace(ace_id, ace_update)
|
||||
|
||||
|
||||
@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)
|
||||
if not ace:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
|
||||
|
||||
success = await rbac_repo.delete_ace(ace_id)
|
||||
if not success:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' could not be deleted")
|
@ -0,0 +1,78 @@
|
||||
#
|
||||
# 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import Request, WebSocket, Depends, HTTPException
|
||||
from gns3server import schemas
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .authentication import get_current_active_user, get_current_active_user_from_websocket
|
||||
from .database import get_repository
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def has_privilege(
|
||||
privilege_name: str
|
||||
):
|
||||
async def get_user_and_check_privilege(
|
||||
request: Request,
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
):
|
||||
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
|
||||
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 get_user_and_check_privilege
|
||||
|
||||
|
||||
def has_privilege_on_websocket(
|
||||
privilege_name: str
|
||||
):
|
||||
async def get_user_and_check_privilege(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(get_current_active_user_from_websocket),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
):
|
||||
if not current_user.is_superadmin:
|
||||
path = re.sub(r"^/v[0-9]", "", websocket.url.path) # remove the prefix (e.g. "/v3") from URL 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 get_user_and_check_privilege
|
||||
|
||||
# class PrivilegeChecker:
|
||||
#
|
||||
# def __init__(self, required_privilege: str) -> None:
|
||||
# self._required_privilege = required_privilege
|
||||
#
|
||||
# async def __call__(
|
||||
# self,
|
||||
# current_user: schemas.User = Depends(get_current_active_user),
|
||||
# rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
# ) -> bool:
|
||||
#
|
||||
# if not await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", self._required_privilege):
|
||||
# raise HTTPException(status_code=403, detail=f"Permission denied (privilege {self._required_privilege} is required)")
|
||||
# return True
|
||||
|
||||
# Depends(PrivilegeChecker("Project.Audit"))
|
@ -1,85 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2020 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for controller notifications.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import StreamingResponse
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", dependencies=[Depends(get_current_active_user)])
|
||||
async def controller_http_notifications(request: Request) -> StreamingResponse:
|
||||
"""
|
||||
Receive controller notifications about the controller from HTTP stream.
|
||||
"""
|
||||
|
||||
from gns3server.api.server import app
|
||||
log.info(f"New client {request.client.host}:{request.client.port} has connected to controller HTTP "
|
||||
f"notification stream")
|
||||
|
||||
async def event_stream():
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while not app.state.exiting:
|
||||
msg = await queue.get_json(5)
|
||||
yield f"{msg}\n".encode("utf-8")
|
||||
finally:
|
||||
log.info(f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP "
|
||||
f"notification stream")
|
||||
return StreamingResponse(event_stream(), media_type="application/json")
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def controller_ws_notifications(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
|
||||
) -> None:
|
||||
"""
|
||||
Receive project notifications about the controller from WebSocket.
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
finally:
|
||||
try:
|
||||
await websocket.close()
|
||||
except OSError:
|
||||
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
|
@ -1,161 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2021 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
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for permissions.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, Request, status
|
||||
from fastapi.routing import APIRoute
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerBadRequestError,
|
||||
ControllerNotFoundError,
|
||||
ControllerForbiddenError,
|
||||
)
|
||||
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Permission])
|
||||
async def get_permissions(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Permission]:
|
||||
"""
|
||||
Get all permissions.
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_permissions()
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.Permission, status_code=status.HTTP_201_CREATED)
|
||||
async def create_permission(
|
||||
request: Request,
|
||||
permission_create: schemas.PermissionCreate,
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Permission:
|
||||
"""
|
||||
Create a new permission.
|
||||
"""
|
||||
|
||||
# TODO: should we prevent having multiple permissions with same methods/path?
|
||||
#if await rbac_repo.check_permission_exists(permission_create):
|
||||
# raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path} "
|
||||
# f"{permission_create.action}' already exists")
|
||||
|
||||
for route in request.app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
|
||||
# remove the prefix (e.g. "/v3") from the route path
|
||||
route_path = re.sub(r"^/v[0-9]", "", route.path)
|
||||
# replace route path ID parameters by an UUID regex
|
||||
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
|
||||
# replace remaining route path parameters by an word matching regex
|
||||
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
|
||||
|
||||
# the permission can match multiple routes
|
||||
if permission_create.path.endswith("/*"):
|
||||
route_path += r"/.*"
|
||||
|
||||
if re.fullmatch(route_path, permission_create.path):
|
||||
for method in permission_create.methods:
|
||||
if method in list(route.methods):
|
||||
# check user has the right to add the permission (i.e has already to right on the path)
|
||||
if not await rbac_repo.check_user_is_authorized(current_user.user_id, method, permission_create.path):
|
||||
raise ControllerForbiddenError(f"User '{current_user.username}' doesn't have the rights to "
|
||||
f"add a permission on {method} {permission_create.path} or "
|
||||
f"the endpoint doesn't exist")
|
||||
return await rbac_repo.create_permission(permission_create)
|
||||
|
||||
raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path}' "
|
||||
f"doesn't match any existing endpoint")
|
||||
|
||||
|
||||
@router.get("/{permission_id}", response_model=schemas.Permission)
|
||||
async def get_permission(
|
||||
permission_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> schemas.Permission:
|
||||
"""
|
||||
Get a permission.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
return permission
|
||||
|
||||
|
||||
@router.put("/{permission_id}", response_model=schemas.Permission)
|
||||
async def update_permission(
|
||||
permission_id: UUID,
|
||||
permission_update: schemas.PermissionUpdate,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Permission:
|
||||
"""
|
||||
Update a permission.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
|
||||
return await rbac_repo.update_permission(permission_id, permission_update)
|
||||
|
||||
|
||||
@router.delete("/{permission_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_permission(
|
||||
permission_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a permission.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
|
||||
success = await rbac_repo.delete_permission(permission_id)
|
||||
if not success:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_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()
|
@ -0,0 +1,43 @@
|
||||
{
|
||||
"appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7",
|
||||
"name": "BIRD2",
|
||||
"category": "router",
|
||||
"description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.",
|
||||
"vendor_name": "CZ.NIC Labs",
|
||||
"vendor_url": "https://bird.network.cz",
|
||||
"documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20",
|
||||
"product_name": "BIRD internet routing daemon",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "Bernhard Ehlers",
|
||||
"maintainer_email": "dev-ehlers@mailbox.org",
|
||||
"usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 4,
|
||||
"ram": 512,
|
||||
"hda_disk_interface": "scsi",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "bird2-debian-2.0.12.qcow2",
|
||||
"version": "2.0.12",
|
||||
"md5sum": "435218a2e90cba921cc7fde1d64a9419",
|
||||
"filesize": 287965184,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "http://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "2.0.12",
|
||||
"images": {
|
||||
"hda_disk_image": "bird2-debian-2.0.12.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"appliance_id": "57a85f0e-b8ae-4820-bd2b-816b2cceb842",
|
||||
"name": "Cisco CAT IOS-XE 9000v",
|
||||
"category": "multilayer_switch",
|
||||
"description": "Cisco IOS-XE 9000v. This appliance requires 16GB of memory to run! Recommend 2 or more vCPUs for faster boot performance",
|
||||
"vendor_name": "Cisco",
|
||||
"vendor_url": "http://www.cisco.com/",
|
||||
"documentation_url": "https://developer.cisco.com/docs/modeling-labs/2-5/#!cml-release-notes",
|
||||
"product_name": "Cisco CAT IOS-XE 9000v",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "There is no default configuration present. Virtual Switch and Interfaces may take several minutes to be usable after appliance boot.",
|
||||
"first_port_name": "GigabitEthernet0/0",
|
||||
"port_name_format": "GigabitEthernet1/0/{port1}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 9,
|
||||
"ram": 16384,
|
||||
"cpus": 2,
|
||||
"hda_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "cat9kv-prd-17.10.01prd7.qcow2",
|
||||
"version": "17.10(1)",
|
||||
"md5sum": "ffdbace33d31deae33e2a920a96b79ef",
|
||||
"filesize": 2155806720,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "17.10(1)",
|
||||
"images": {
|
||||
"hda_disk_image": "cat9kv-prd-17.10.01prd7.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"appliance_id": "d9ce131e-ecdc-49d2-be7d-d883d3919a06",
|
||||
"name": "Cisco PyATS",
|
||||
"category": "guest",
|
||||
"description": "pyATS is an end-to-end DevOps automation ecosystem. Agnostic by design, pyATS enable network engineers to automate their day-to-day DevOps activities, perform stateful validation of their device operational status, build a safety-net of scalable, data-driven and reusable tests around their network, and visualize everything in a modern, easy to use dashboard.",
|
||||
"vendor_name": "Cisco",
|
||||
"vendor_url": "https://cisco.com",
|
||||
"product_name": "PyATS",
|
||||
"product_url": "https://developer.cisco.com/pyats/",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "Xander Petty",
|
||||
"maintainer_email": "Xander.Petty@protonmail.com",
|
||||
"docker": {
|
||||
"adapters": 1,
|
||||
"image": "gns3/pyats:latest",
|
||||
"console_type": "telnet"
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"appliance_id": "f59a5cf6-baaa-45a6-9685-989a2c3f3f4a",
|
||||
"name": "endhost",
|
||||
"category": "guest",
|
||||
"description": "General purpose alpine-based endhost",
|
||||
"vendor_name": "endhost",
|
||||
"vendor_url": "https://www.alpinelinux.org",
|
||||
"product_name": "endhost",
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"docker": {
|
||||
"adapters": 1,
|
||||
"image": "gns3/endhost:latest"
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue