mirror of
https://github.com/GNS3/gns3-server
synced 2025-02-18 02:52:00 +00:00
Merge remote-tracking branch 'origin/3.0' into gh-pages
This commit is contained in:
commit
d3b90c7cca
@ -1,5 +1,10 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 2.2.21 10/05/2021
|
||||||
|
|
||||||
|
* Release Web-Ui v2.2.21
|
||||||
|
* Improvements for get symbol dimensions endpoint. Ref #1885
|
||||||
|
|
||||||
## 2.2.20 09/04/2021
|
## 2.2.20 09/04/2021
|
||||||
|
|
||||||
* Release Web UI version 2.2.20
|
* Release Web UI version 2.2.20
|
||||||
|
@ -76,14 +76,16 @@ Finally these commands will install the server as well as the rest of the depend
|
|||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
cd gns3-server-master
|
cd gns3-server-master
|
||||||
|
python3 -m venv venv-gns3server
|
||||||
|
source venv-gns3server/bin/activate
|
||||||
sudo python3 setup.py install
|
sudo python3 setup.py install
|
||||||
gns3server
|
python3 -m gns3server --local
|
||||||
|
|
||||||
To run tests use:
|
To run tests use:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
py.test -v
|
python3 -m pytest tests
|
||||||
|
|
||||||
|
|
||||||
Docker container
|
Docker container
|
||||||
|
@ -64,6 +64,14 @@ user = gns3
|
|||||||
; Password for HTTP authentication.
|
; Password for HTTP authentication.
|
||||||
password = gns3
|
password = gns3
|
||||||
|
|
||||||
|
; Initial default super admin username
|
||||||
|
; It cannot be changed once the server has started once
|
||||||
|
default_admin_username = "admin"
|
||||||
|
|
||||||
|
; Initial default super admin username
|
||||||
|
; It cannot be changed once the server has started once
|
||||||
|
default_admin_password = "admin"
|
||||||
|
|
||||||
; Only allow these interfaces to be used by GNS3, for the Cloud node for example (Linux/OSX only)
|
; Only allow these interfaces to be used by GNS3, for the Cloud node for example (Linux/OSX only)
|
||||||
; Do not forget to allow virbr0 in order for the NAT node to work
|
; Do not forget to allow virbr0 in order for the NAT node to work
|
||||||
allowed_interfaces = eth0,eth1,virbr0
|
allowed_interfaces = eth0,eth1,virbr0
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
pytest==6.2.3
|
pytest==6.2.4
|
||||||
flake8==3.9.0
|
flake8==3.9.1
|
||||||
pytest-timeout==1.4.2
|
pytest-timeout==1.4.2
|
||||||
pytest-asyncio==0.14.0
|
pytest-asyncio==0.15.1
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
httpx==0.17.1
|
httpx==0.18.1
|
||||||
|
@ -69,13 +69,15 @@ async def download_dynamips_image(filename: str) -> FileResponse:
|
|||||||
Download a Dynamips IOS image.
|
Download a Dynamips IOS image.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dynamips_manager = Dynamips.instance()
|
|
||||||
filename = urllib.parse.unquote(filename)
|
filename = urllib.parse.unquote(filename)
|
||||||
image_path = dynamips_manager.get_abs_image_path(filename)
|
|
||||||
|
|
||||||
if filename[0] == ".":
|
# Raise error if user try to escape
|
||||||
|
if filename[0] == "." or os.path.sep in filename:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
dynamips_manager = Dynamips.instance()
|
||||||
|
image_path = dynamips_manager.get_abs_image_path(filename)
|
||||||
|
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
@ -108,13 +110,14 @@ async def download_iou_image(filename: str) -> FileResponse:
|
|||||||
Download an IOU image.
|
Download an IOU image.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
iou_manager = IOU.instance()
|
|
||||||
filename = urllib.parse.unquote(filename)
|
filename = urllib.parse.unquote(filename)
|
||||||
image_path = iou_manager.get_abs_image_path(filename)
|
|
||||||
|
|
||||||
if filename[0] == ".":
|
# Raise error if user try to escape
|
||||||
|
if filename[0] == "." or os.path.sep in filename:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
iou_manager = IOU.instance()
|
||||||
|
image_path = iou_manager.get_abs_image_path(filename)
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
@ -138,13 +141,13 @@ async def upload_qemu_image(filename: str, request: Request) -> None:
|
|||||||
@router.get("/qemu/images/{filename:path}")
|
@router.get("/qemu/images/{filename:path}")
|
||||||
async def download_qemu_image(filename: str) -> FileResponse:
|
async def download_qemu_image(filename: str) -> FileResponse:
|
||||||
|
|
||||||
qemu_manager = Qemu.instance()
|
|
||||||
filename = urllib.parse.unquote(filename)
|
filename = urllib.parse.unquote(filename)
|
||||||
|
|
||||||
# Raise error if user try to escape
|
# Raise error if user try to escape
|
||||||
if filename[0] == ".":
|
if filename[0] == "." or os.path.sep in filename:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
qemu_manager = Qemu.instance()
|
||||||
image_path = qemu_manager.get_abs_image_path(filename)
|
image_path = qemu_manager.get_abs_image_path(filename)
|
||||||
|
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
|
@ -19,6 +19,7 @@ API routes for projects.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from gns3server.compute.project_manager import ProjectManager
|
from gns3server.compute.project_manager import ProjectManager
|
||||||
from gns3server.compute.project import Project
|
from gns3server.compute.project import Project
|
||||||
|
from gns3server.utils.path import is_safe_path
|
||||||
from gns3server import schemas
|
from gns3server import schemas
|
||||||
|
|
||||||
|
|
||||||
@ -197,10 +199,11 @@ async def get_compute_project_file(file_path: str, project: Project = Depends(de
|
|||||||
Get a file from a project.
|
Get a file from a project.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
file_path = urllib.parse.unquote(file_path)
|
||||||
path = os.path.normpath(file_path)
|
path = os.path.normpath(file_path)
|
||||||
|
|
||||||
# Raise error if user try to escape
|
# Raise error if user try to escape
|
||||||
if path[0] == ".":
|
if not is_safe_path(path, project.path):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
path = os.path.join(project.path, path)
|
path = os.path.join(project.path, path)
|
||||||
@ -213,10 +216,11 @@ async def get_compute_project_file(file_path: str, project: Project = Depends(de
|
|||||||
@router.post("/projects/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.post("/projects/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def write_compute_project_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None:
|
async def write_compute_project_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None:
|
||||||
|
|
||||||
|
file_path = urllib.parse.unquote(file_path)
|
||||||
path = os.path.normpath(file_path)
|
path = os.path.normpath(file_path)
|
||||||
|
|
||||||
# Raise error if user try to escape
|
# Raise error if user try to escape
|
||||||
if path[0] == ".":
|
if not is_safe_path(path, project.path):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
path = os.path.join(project.path, path)
|
path = os.path.join(project.path, path)
|
||||||
|
@ -29,6 +29,7 @@ from . import snapshots
|
|||||||
from . import symbols
|
from . import symbols
|
||||||
from . import templates
|
from . import templates
|
||||||
from . import users
|
from . import users
|
||||||
|
from . import groups
|
||||||
|
|
||||||
from .dependencies.authentication import get_current_active_user
|
from .dependencies.authentication import get_current_active_user
|
||||||
|
|
||||||
@ -37,6 +38,13 @@ router = APIRouter()
|
|||||||
router.include_router(controller.router, tags=["Controller"])
|
router.include_router(controller.router, tags=["Controller"])
|
||||||
router.include_router(users.router, prefix="/users", tags=["Users"])
|
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(
|
router.include_router(
|
||||||
appliances.router,
|
appliances.router,
|
||||||
dependencies=[Depends(get_current_active_user)],
|
dependencies=[Depends(get_current_active_user)],
|
||||||
@ -59,6 +67,7 @@ router.include_router(
|
|||||||
|
|
||||||
router.include_router(
|
router.include_router(
|
||||||
gns3vm.router,
|
gns3vm.router,
|
||||||
|
deprecated=True,
|
||||||
dependencies=[Depends(get_current_active_user)],
|
dependencies=[Depends(get_current_active_user)],
|
||||||
prefix="/gns3vm",
|
prefix="/gns3vm",
|
||||||
tags=["GNS3 VM"]
|
tags=["GNS3 VM"]
|
||||||
|
@ -25,7 +25,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def get_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic") -> List[dict]:
|
async def get_appliances(update: Optional[bool] = False, symbol_theme: Optional[str] = "Classic") -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Return all appliances known by the controller.
|
Return all appliances known by the controller.
|
||||||
"""
|
"""
|
||||||
|
184
gns3server/api/routes/controller/groups.py
Normal file
184
gns3server/api/routes/controller/groups.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#!/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 user groups.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
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.users import UsersRepository
|
||||||
|
from .dependencies.database import get_repository
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=List[schemas.UserGroup])
|
||||||
|
async def get_user_groups(
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||||
|
) -> List[schemas.UserGroup]:
|
||||||
|
"""
|
||||||
|
Get all user groups.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return await users_repo.get_user_groups()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"",
|
||||||
|
response_model=schemas.UserGroup,
|
||||||
|
status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
async def create_user_group(
|
||||||
|
user_group_create: schemas.UserGroupCreate,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||||
|
) -> schemas.UserGroup:
|
||||||
|
"""
|
||||||
|
Create a new user group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if await users_repo.get_user_group_by_name(user_group_create.name):
|
||||||
|
raise ControllerBadRequestError(f"User group '{user_group_create.name}' already exists")
|
||||||
|
|
||||||
|
return await users_repo.create_user_group(user_group_create)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_group_id}", response_model=schemas.UserGroup)
|
||||||
|
async def get_user_group(
|
||||||
|
user_group_id: UUID,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||||
|
) -> schemas.UserGroup:
|
||||||
|
"""
|
||||||
|
Get an user group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_group = await users_repo.get_user_group(user_group_id)
|
||||||
|
if not user_group:
|
||||||
|
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||||
|
return user_group
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{user_group_id}", response_model=schemas.UserGroup)
|
||||||
|
async def update_user_group(
|
||||||
|
user_group_id: UUID,
|
||||||
|
user_group_update: schemas.UserGroupUpdate,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||||
|
) -> schemas.UserGroup:
|
||||||
|
"""
|
||||||
|
Update an user group.
|
||||||
|
"""
|
||||||
|
user_group = await users_repo.get_user_group(user_group_id)
|
||||||
|
if not user_group:
|
||||||
|
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||||
|
|
||||||
|
if not user_group.is_updatable:
|
||||||
|
raise ControllerForbiddenError(f"User group '{user_group_id}' cannot be updated")
|
||||||
|
|
||||||
|
return await users_repo.update_user_group(user_group_id, user_group_update)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{user_group_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT
|
||||||
|
)
|
||||||
|
async def delete_user_group(
|
||||||
|
user_group_id: UUID,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Delete an user group
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_group = await users_repo.get_user_group(user_group_id)
|
||||||
|
if not user_group:
|
||||||
|
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||||
|
|
||||||
|
if not user_group.is_updatable:
|
||||||
|
raise ControllerForbiddenError(f"User group '{user_group_id}' cannot be deleted")
|
||||||
|
|
||||||
|
success = await users_repo.delete_user_group(user_group_id)
|
||||||
|
if not success:
|
||||||
|
raise ControllerNotFoundError(f"User group '{user_group_id}' could not be deleted")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_group_id}/members", response_model=List[schemas.User])
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return await users_repo.get_user_group_members(user_group_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/{user_group_id}/members/{user_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT
|
||||||
|
)
|
||||||
|
async def add_member_to_group(
|
||||||
|
user_group_id: UUID,
|
||||||
|
user_id: UUID,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add member to an user group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = await users_repo.get_user(user_id)
|
||||||
|
if not user:
|
||||||
|
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||||
|
|
||||||
|
user_group = await users_repo.add_member_to_user_group(user_group_id, user)
|
||||||
|
if not user_group:
|
||||||
|
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{user_group_id}/members/{user_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT
|
||||||
|
)
|
||||||
|
async def remove_member_from_group(
|
||||||
|
user_group_id: UUID,
|
||||||
|
user_id: UUID,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Remove member from an user group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = await users_repo.get_user(user_id)
|
||||||
|
if not user:
|
||||||
|
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||||
|
|
||||||
|
user_group = await users_repo.remove_member_from_user_group(user_group_id, user)
|
||||||
|
if not user_group:
|
||||||
|
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
@ -305,11 +305,11 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
|
|||||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||||
|
|
||||||
res = await node.compute.http_query("GET", f"/projects/{node.project.id}/files{path}", timeout=None, raw=True)
|
res = await node.compute.http_query("GET", f"/projects/{node.project.id}/files{path}", timeout=None, raw=True)
|
||||||
return Response(res.body, media_type="application/octet-stream")
|
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)
|
||||||
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)) -> dict:
|
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
|
||||||
"""
|
"""
|
||||||
Write a file in the node directory.
|
Write a file in the node directory.
|
||||||
"""
|
"""
|
||||||
@ -324,8 +324,8 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
|
|||||||
path = f"/project-files/{node_type}/{node.id}/{path}"
|
path = f"/project-files/{node_type}/{node.id}/{path}"
|
||||||
|
|
||||||
data = await request.body() # FIXME: are we handling timeout or large files correctly?
|
data = await request.body() # FIXME: are we handling timeout or large files correctly?
|
||||||
|
|
||||||
await node.compute.http_query("POST", f"/projects/{node.project.id}/files{path}", data=data, timeout=None, raw=True)
|
await node.compute.http_query("POST", f"/projects/{node.project.id}/files{path}", data=data, timeout=None, raw=True)
|
||||||
|
# FIXME: response with correct status code (from compute)
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/{node_id}/console/ws")
|
@router.websocket("/{node_id}/console/ws")
|
||||||
|
@ -18,12 +18,15 @@
|
|||||||
API routes for controller notifications.
|
API routes for controller notifications.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect, HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||||
|
|
||||||
|
from gns3server.services import auth_service
|
||||||
from gns3server.controller import Controller
|
from gns3server.controller import Controller
|
||||||
|
|
||||||
|
from .dependencies.authentication import get_current_active_user
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -31,7 +34,7 @@ log = logging.getLogger(__name__)
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("", dependencies=[Depends(get_current_active_user)])
|
||||||
async def http_notification() -> StreamingResponse:
|
async def http_notification() -> StreamingResponse:
|
||||||
"""
|
"""
|
||||||
Receive controller notifications about the controller from HTTP stream.
|
Receive controller notifications about the controller from HTTP stream.
|
||||||
@ -41,18 +44,26 @@ async def http_notification() -> StreamingResponse:
|
|||||||
with Controller.instance().notification.controller_queue() as queue:
|
with Controller.instance().notification.controller_queue() as queue:
|
||||||
while True:
|
while True:
|
||||||
msg = await queue.get_json(5)
|
msg = await queue.get_json(5)
|
||||||
yield (f"{msg}\n").encode("utf-8")
|
yield f"{msg}\n".encode("utf-8")
|
||||||
|
|
||||||
return StreamingResponse(event_stream(), media_type="application/json")
|
return StreamingResponse(event_stream(), media_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/ws")
|
@router.websocket("/ws")
|
||||||
async def notification_ws(websocket: WebSocket) -> None:
|
async def notification_ws(websocket: WebSocket, token: str = Query(None)) -> None:
|
||||||
"""
|
"""
|
||||||
Receive project notifications about the controller from WebSocket.
|
Receive project notifications about the controller from WebSocket.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
|
if token:
|
||||||
|
try:
|
||||||
|
username = auth_service.get_username_from_token(token)
|
||||||
|
except HTTPException:
|
||||||
|
log.error("Invalid token received")
|
||||||
|
await websocket.close(code=1008)
|
||||||
|
return
|
||||||
|
|
||||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||||
try:
|
try:
|
||||||
with Controller.instance().notification.controller_queue() as queue:
|
with Controller.instance().notification.controller_queue() as queue:
|
||||||
|
@ -24,6 +24,7 @@ import tempfile
|
|||||||
import zipfile
|
import zipfile
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ from gns3server.controller.controller_error import ControllerError, ControllerFo
|
|||||||
from gns3server.controller.import_project import import_project as import_controller_project
|
from gns3server.controller.import_project import import_project as import_controller_project
|
||||||
from gns3server.controller.export_project import export_project as export_controller_project
|
from gns3server.controller.export_project import export_project as export_controller_project
|
||||||
from gns3server.utils.asyncio import aiozipstream
|
from gns3server.utils.asyncio import aiozipstream
|
||||||
|
from gns3server.utils.path import is_safe_path
|
||||||
from gns3server.config import Config
|
from gns3server.config import Config
|
||||||
|
|
||||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
|
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
|
||||||
@ -368,10 +370,11 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)) -> F
|
|||||||
Return a file from a project.
|
Return a file from a project.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = os.path.normpath(file_path).strip("/")
|
file_path = urllib.parse.unquote(file_path)
|
||||||
|
path = os.path.normpath(file_path)
|
||||||
|
|
||||||
# Raise error if user try to escape
|
# Raise error if user try to escape
|
||||||
if path[0] == ".":
|
if not is_safe_path(path, project.path):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
path = os.path.join(project.path, path)
|
path = os.path.join(project.path, path)
|
||||||
@ -387,10 +390,11 @@ async def write_file(file_path: str, request: Request, project: Project = Depend
|
|||||||
Write a file from a project.
|
Write a file from a project.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = os.path.normpath(file_path).strip("/")
|
file_path = urllib.parse.unquote(file_path)
|
||||||
|
path = os.path.normpath(file_path)
|
||||||
|
|
||||||
# Raise error if user try to escape
|
# Raise error if user try to escape
|
||||||
if path[0] == ".":
|
if not is_safe_path(path, project.path):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
path = os.path.join(project.path, path)
|
path = os.path.join(project.path, path)
|
||||||
|
@ -139,7 +139,7 @@ async def create_node_from_template(
|
|||||||
Create a new node from a template.
|
Create a new node from a template.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template = TemplatesService(templates_repo).get_template(template_id)
|
template = await TemplatesService(templates_repo).get_template(template_id)
|
||||||
controller = Controller.instance()
|
controller = Controller.instance()
|
||||||
project = controller.get_project(str(project_id))
|
project = controller.get_project(str(project_id))
|
||||||
node = await project.add_node_from_template(
|
node = await project.add_node_from_template(
|
||||||
|
@ -153,22 +153,29 @@ async def update_user(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete(
|
||||||
|
"/{user_id}",
|
||||||
|
dependencies=[Depends(get_current_active_user)],
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT
|
||||||
|
)
|
||||||
async def delete_user(
|
async def delete_user(
|
||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||||
current_user: schemas.User = Depends(get_current_active_user),
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Delete an user.
|
Delete an user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if current_user.is_superadmin:
|
user = await users_repo.get_user(user_id)
|
||||||
|
if not user:
|
||||||
|
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||||
|
|
||||||
|
if user.is_superadmin:
|
||||||
raise ControllerForbiddenError("The super admin cannot be deleted")
|
raise ControllerForbiddenError("The super admin cannot be deleted")
|
||||||
|
|
||||||
success = await users_repo.delete_user(user_id)
|
success = await users_repo.delete_user(user_id)
|
||||||
if not success:
|
if not success:
|
||||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
raise ControllerNotFoundError(f"User '{user_id}' could not be deleted")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me/", response_model=schemas.User)
|
@router.get("/me/", response_model=schemas.User)
|
||||||
@ -178,3 +185,19 @@ async def get_current_active_user(current_user: schemas.User = Depends(get_curre
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{user_id}/groups",
|
||||||
|
dependencies=[Depends(get_current_active_user)],
|
||||||
|
response_model=List[schemas.UserGroup]
|
||||||
|
)
|
||||||
|
async def get_user_memberships(
|
||||||
|
user_id: UUID,
|
||||||
|
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||||
|
) -> List[schemas.UserGroup]:
|
||||||
|
"""
|
||||||
|
Get user memberships.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return await users_repo.get_user_memberships(user_id)
|
||||||
|
@ -25,6 +25,7 @@ from fastapi import FastAPI, Request
|
|||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from uvicorn.main import Server as UvicornServer
|
from uvicorn.main import Server as UvicornServer
|
||||||
|
|
||||||
from gns3server.controller.controller_error import (
|
from gns3server.controller.controller_error import (
|
||||||
@ -55,6 +56,8 @@ def get_application() -> FastAPI:
|
|||||||
origins = [
|
origins = [
|
||||||
"http://127.0.0.1",
|
"http://127.0.0.1",
|
||||||
"http://localhost",
|
"http://localhost",
|
||||||
|
"http://localhost:4200",
|
||||||
|
"http://127.0.0.1:4200"
|
||||||
"http://127.0.0.1:8080",
|
"http://127.0.0.1:8080",
|
||||||
"http://localhost:8080",
|
"http://localhost:8080",
|
||||||
"http://127.0.0.1:3080",
|
"http://127.0.0.1:3080",
|
||||||
@ -158,6 +161,15 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(SQLAlchemyError)
|
||||||
|
async def sqlalchemry_error_handler(request: Request, exc: SQLAlchemyError):
|
||||||
|
log.error(f"Controller database error: {exc}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"message": "Database error detected, please check logs to find details"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def add_extra_headers(request: Request, call_next):
|
async def add_extra_headers(request: Request, call_next):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
@ -27,10 +27,10 @@
|
|||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
{
|
{
|
||||||
"filename": "vEOS-lab-4.25.0F.vmdk",
|
"filename": "vEOS-lab-4.25.3M.vmdk",
|
||||||
"version": "4.25.0F",
|
"version": "4.25.3M",
|
||||||
"md5sum": "d420763fdf3bc50e7e5b88418bd9d1fd",
|
"md5sum": "2f196969036b4d283e86f15118d59c26",
|
||||||
"filesize": 468779008,
|
"filesize": 451543040,
|
||||||
"download_url": "https://www.arista.com/en/support/software-download"
|
"download_url": "https://www.arista.com/en/support/software-download"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -211,10 +211,10 @@
|
|||||||
],
|
],
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"name": "4.25.0F",
|
"name": "4.25.3M",
|
||||||
"images": {
|
"images": {
|
||||||
"hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
|
"hda_disk_image": "Aboot-veos-serial-8.0.0.iso",
|
||||||
"hdb_disk_image": "vEOS-lab-4.25.0F.vmdk"
|
"hdb_disk_image": "vEOS-lab-4.25.3M.vmdk"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
55
gns3server/appliances/cisco-c8000v.gns3a
Normal file
55
gns3server/appliances/cisco-c8000v.gns3a
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "Cisco Catalyst 8000V",
|
||||||
|
"category": "router",
|
||||||
|
"description": "The Cisco Catalyst 8000V Edge Software is a virtual, form-factor router deployed on a virtual machine (VM) running on an x86 server hardware.",
|
||||||
|
"vendor_name": "Cisco",
|
||||||
|
"vendor_url": "http://www.cisco.com/",
|
||||||
|
"documentation_url": "https://www.cisco.com/c/en/us/td/docs/routers/C8000V/Configuration/c8000v-installation-configuration-guide.html",
|
||||||
|
"product_name": "c8000v",
|
||||||
|
"product_url": "https://www.cisco.com/c/en/us/support/routers/catalyst-8000v-edge-software/series.html",
|
||||||
|
"registry_version": 3,
|
||||||
|
"status": "stable",
|
||||||
|
"maintainer": "GNS3 Team",
|
||||||
|
"maintainer_email": "developers@gns3.net",
|
||||||
|
"usage": "There is no default password and enable password. A default configuration is present.",
|
||||||
|
"port_name_format": "Gi{port1}",
|
||||||
|
"qemu": {
|
||||||
|
"adapter_type": "vmxnet3",
|
||||||
|
"adapters": 4,
|
||||||
|
"ram": 4096,
|
||||||
|
"hda_disk_interface": "ide",
|
||||||
|
"arch": "x86_64",
|
||||||
|
"console_type": "telnet",
|
||||||
|
"kvm": "require"
|
||||||
|
},
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "c8000v-universalk9_8G_serial.17.04.01a.qcow2",
|
||||||
|
"version": "17.04.01a 8G",
|
||||||
|
"md5sum": "5c1dd1d3757ea43b5b02e0af7a010525",
|
||||||
|
"filesize": 1623130112,
|
||||||
|
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Bengaluru-17.4.1a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "c8000v-universalk9_8G_serial.17.04.01b.qcow2",
|
||||||
|
"version": "17.04.01b 8G",
|
||||||
|
"md5sum": "84aebb7f5f38bdd4df8e7607643027be",
|
||||||
|
"filesize": 1623130112,
|
||||||
|
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Bengaluru-17.4.1b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"name": "17.04.01a 8G",
|
||||||
|
"images": {
|
||||||
|
"hda_disk_image": "c8000v-universalk9_8G_serial.17.04.01a.qcow2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "17.04.01b 8G",
|
||||||
|
"images": {
|
||||||
|
"hda_disk_image": "c8000v-universalk9_8G_serial.17.04.01b.qcow2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -25,6 +25,20 @@
|
|||||||
"kvm": "require"
|
"kvm": "require"
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
|
{
|
||||||
|
"filename": "nexus9500v64.10.1.1.qcow2",
|
||||||
|
"version": "9500v 10.1.1",
|
||||||
|
"md5sum": "35672370b0f43e725d5b2d92488524f0",
|
||||||
|
"filesize": 1592000512,
|
||||||
|
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "nexus9500v.9.3.7.qcow2",
|
||||||
|
"version": "9500v 9.3.7",
|
||||||
|
"md5sum": "65f669e0dd379a05a8cdbb9d7592a064",
|
||||||
|
"filesize": 1986068480,
|
||||||
|
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(7)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"filename": "nexus9500v.9.3.3.qcow2",
|
"filename": "nexus9500v.9.3.3.qcow2",
|
||||||
"version": "9500v 9.3.3",
|
"version": "9500v 9.3.3",
|
||||||
@ -148,6 +162,20 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"name": "9500v 10.1.1",
|
||||||
|
"images": {
|
||||||
|
"bios_image": "OVMF-20160813.fd",
|
||||||
|
"hda_disk_image": "nexus9500v64.10.1.1.qcow2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "9500v 9.3.7",
|
||||||
|
"images": {
|
||||||
|
"bios_image": "OVMF-20160813.fd",
|
||||||
|
"hda_disk_image": "nexus9500v.9.3.7.qcow2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "9500v 9.3.3",
|
"name": "9500v 9.3.3",
|
||||||
"images": {
|
"images": {
|
||||||
|
@ -11,29 +11,29 @@
|
|||||||
"status": "stable",
|
"status": "stable",
|
||||||
"maintainer": "GNS3 Team",
|
"maintainer": "GNS3 Team",
|
||||||
"maintainer_email": "developers@gns3.net",
|
"maintainer_email": "developers@gns3.net",
|
||||||
"usage": "Default username/password is vyatta/vyatta. DANOS will live boot and drop into a shell. DANOS can then be installed inside the VM by typing install image. Defaults to using a telnet console, but the vnc console can provide additional help if it's not booting.",
|
"usage": "Default username / password is tmpuser / tmppwd. DANOS will live boot and drop into a shell. DANOS can then be installed inside the VM by typing install image. Defaults to using a telnet console, but the vnc console can provide additional help if it's not booting.",
|
||||||
"symbol": ":/symbols/affinity/circle/gray/router_cloud.svg",
|
"symbol": ":/symbols/affinity/circle/gray/router_cloud.svg",
|
||||||
"port_name_format": "dp0p{1}s{0}",
|
"port_name_format": "dp0s{3}",
|
||||||
"qemu": {
|
"qemu": {
|
||||||
"adapter_type": "virtio-net-pci",
|
"adapter_type": "virtio-net-pci",
|
||||||
"adapters": 3,
|
"adapters": 8,
|
||||||
"ram": 4096,
|
"ram": 4096,
|
||||||
"cpus": 2,
|
"cpus": 4,
|
||||||
"hda_disk_interface": "ide",
|
"hda_disk_interface": "ide",
|
||||||
"arch": "x86_64",
|
"arch": "x86_64",
|
||||||
"console_type": "telnet",
|
"console_type": "telnet",
|
||||||
"boot_priority": "dc",
|
"boot_priority": "cd",
|
||||||
"kvm": "allow",
|
"kvm": "require",
|
||||||
"options": "-cpu host"
|
"options": "-cpu host"
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
{
|
{
|
||||||
"filename": "danos-1908-amd64-vrouter.iso",
|
"filename": "danos-2012-base-amd64.iso",
|
||||||
"version": "1908",
|
"version": "2012",
|
||||||
"md5sum": "e850b6aa2859de1075c11b9149fa50f4",
|
"md5sum": "fb7a60dc9afecdb274464832b3ab1ccb",
|
||||||
"filesize": 409993216,
|
"filesize": 441450496,
|
||||||
"download_url": "https://danosproject.atlassian.net/wiki/spaces/DAN/pages/753667/DANOS+1908",
|
"download_url": "https://danosproject.atlassian.net/wiki/spaces/DAN/pages/892141595/DANOS+2012",
|
||||||
"direct_download_url": "http://repos.danosproject.org.s3-website-us-west-1.amazonaws.com/images/danos-1908-amd64-vrouter.iso"
|
"direct_download_url": "https://s3-us-west-1.amazonaws.com/2012.repos.danosproject.org/2012/iso/danos-2012-base-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "empty8G.qcow2",
|
"filename": "empty8G.qcow2",
|
||||||
@ -46,10 +46,10 @@
|
|||||||
],
|
],
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"name": "1908",
|
"name": "2012",
|
||||||
"images": {
|
"images": {
|
||||||
"hda_disk_image": "empty8G.qcow2",
|
"hda_disk_image": "empty8G.qcow2",
|
||||||
"cdrom_image": "danos-1908-amd64-vrouter.iso"
|
"cdrom_image": "danos-2012-base-amd64.iso"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -28,96 +28,96 @@
|
|||||||
"version": "2019.3",
|
"version": "2019.3",
|
||||||
"md5sum": "9c6fb00558f78ed06992d89f745ef975",
|
"md5sum": "9c6fb00558f78ed06992d89f745ef975",
|
||||||
"filesize": 3037736960,
|
"filesize": 3037736960,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2019.3",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.3/kali-linux-2019.3-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.3/kali-linux-2019.3-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2019.2-amd64.iso",
|
"filename": "kali-linux-2019.2-amd64.iso",
|
||||||
"version": "2019.2",
|
"version": "2019.2",
|
||||||
"md5sum": "0f89b6225d7ea9c18682f7cc541c1179",
|
"md5sum": "0f89b6225d7ea9c18682f7cc541c1179",
|
||||||
"filesize": 3353227264,
|
"filesize": 3353227264,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2019.2",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.2/kali-linux-2019.2-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.2/kali-linux-2019.2-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-mate-2019.2-amd64.iso",
|
"filename": "kali-linux-mate-2019.2-amd64.iso",
|
||||||
"version": "2019.2 (MATE)",
|
"version": "2019.2 (MATE)",
|
||||||
"md5sum": "fec8dd7009f932c51a74323df965a709",
|
"md5sum": "fec8dd7009f932c51a74323df965a709",
|
||||||
"filesize": 3313217536,
|
"filesize": 3313217536,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2019.2",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.2/kali-linux-mate-2019.2-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.2/kali-linux-mate-2019.2-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2019.1a-amd64.iso",
|
"filename": "kali-linux-2019.1a-amd64.iso",
|
||||||
"version": "2019.1a",
|
"version": "2019.1a",
|
||||||
"md5sum": "58c6111ed0be1919ea87267e7e65ab0f",
|
"md5sum": "58c6111ed0be1919ea87267e7e65ab0f",
|
||||||
"filesize": 3483873280,
|
"filesize": 3483873280,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2019.1a",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.1a/kali-linux-2019.1a-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2019.1a/kali-linux-2019.1a-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2018.4-amd64.iso",
|
"filename": "kali-linux-2018.4-amd64.iso",
|
||||||
"version": "2018.4",
|
"version": "2018.4",
|
||||||
"md5sum": "1b2d598bb8d2003e6207c119c0ba42fe",
|
"md5sum": "1b2d598bb8d2003e6207c119c0ba42fe",
|
||||||
"filesize": 3139436544,
|
"filesize": 3139436544,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2018.4",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2018.4/kali-linux-2018.4-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2018.4/kali-linux-2018.4-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2018.3a-amd64.iso",
|
"filename": "kali-linux-2018.3a-amd64.iso",
|
||||||
"version": "2018.3a",
|
"version": "2018.3a",
|
||||||
"md5sum": "2da675d016bd690c05e180e33aa98b94",
|
"md5sum": "2da675d016bd690c05e180e33aa98b94",
|
||||||
"filesize": 3192651776,
|
"filesize": 3192651776,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2018.3a",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2018.3a/kali-linux-2018.3a-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2018.3a/kali-linux-2018.3a-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2018.1-amd64.iso",
|
"filename": "kali-linux-2018.1-amd64.iso",
|
||||||
"version": "2018.1",
|
"version": "2018.1",
|
||||||
"md5sum": "a3feb90df5b71b3c7f4a02bdddf221d7",
|
"md5sum": "a3feb90df5b71b3c7f4a02bdddf221d7",
|
||||||
"filesize": 3028500480,
|
"filesize": 3028500480,
|
||||||
"download_url": "https://www.kali.org/downloads/",
|
"download_url": "http://old.kali.org/kali-images/kali-2018.1",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2018.1/kali-linux-2018.1-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2018.1/kali-linux-2018.1-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2017.3-amd64.iso",
|
"filename": "kali-linux-2017.3-amd64.iso",
|
||||||
"version": "2017.3",
|
"version": "2017.3",
|
||||||
"md5sum": "b465580c897e94675ac1daf031fa66b9",
|
"md5sum": "b465580c897e94675ac1daf031fa66b9",
|
||||||
"filesize": 2886402048,
|
"filesize": 2886402048,
|
||||||
"download_url": "http://cdimage.kali.org/kali-2017.3/",
|
"download_url": "http://old.kali.org/kali-images/kali-2017.3/",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2017.3/kali-linux-2017.3-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2017.3/kali-linux-2017.3-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2017.2-amd64.iso",
|
"filename": "kali-linux-2017.2-amd64.iso",
|
||||||
"version": "2017.2",
|
"version": "2017.2",
|
||||||
"md5sum": "541654f8f818450dc0db866a0a0f6eec",
|
"md5sum": "541654f8f818450dc0db866a0a0f6eec",
|
||||||
"filesize": 3020619776,
|
"filesize": 3020619776,
|
||||||
"download_url": "http://cdimage.kali.org/kali-2017.2/",
|
"download_url": "http://old.kali.org/kali-images/kali-2017.2/",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2017.2/kali-linux-2017.2-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2017.2/kali-linux-2017.2-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2017.1-amd64.iso",
|
"filename": "kali-linux-2017.1-amd64.iso",
|
||||||
"version": "2017.1",
|
"version": "2017.1",
|
||||||
"md5sum": "c8e742283929d7a12dbe7c58e398ff08",
|
"md5sum": "c8e742283929d7a12dbe7c58e398ff08",
|
||||||
"filesize": 2794307584,
|
"filesize": 2794307584,
|
||||||
"download_url": "http://cdimage.kali.org/kali-2017.1/",
|
"download_url": "http://old.kali.org/kali-images/kali-2017.1/",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2017.1/kali-linux-2017.1-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2017.1/kali-linux-2017.1-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2016.2-amd64.iso",
|
"filename": "kali-linux-2016.2-amd64.iso",
|
||||||
"version": "2016.2",
|
"version": "2016.2",
|
||||||
"md5sum": "3d163746bc5148e61ad689d94bc263f9",
|
"md5sum": "3d163746bc5148e61ad689d94bc263f9",
|
||||||
"filesize": 3076767744,
|
"filesize": 3076767744,
|
||||||
"download_url": "http://cdimage.kali.org/kali-2016.2/",
|
"download_url": "http://old.kali.org/kali-images/kali-2016.2/",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2016.2/kali-linux-2016.2-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2016.2/kali-linux-2016.2-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2016.1-amd64.iso",
|
"filename": "kali-linux-2016.1-amd64.iso",
|
||||||
"version": "2016.1",
|
"version": "2016.1",
|
||||||
"md5sum": "2e1230dc14036935b3279dfe3e49ad39",
|
"md5sum": "2e1230dc14036935b3279dfe3e49ad39",
|
||||||
"filesize": 2945482752,
|
"filesize": 2945482752,
|
||||||
"download_url": "http://cdimage.kali.org/kali-2016.1/",
|
"download_url": "http://old.kali.org/kali-images/kali-2016.1/",
|
||||||
"direct_download_url": "http://cdimage.kali.org/kali-2016.1/kali-linux-2016.1-amd64.iso"
|
"direct_download_url": "http://old.kali.org/kali-images/kali-2016.1/kali-linux-2016.1-amd64.iso"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename": "kali-linux-2.0-amd64.iso",
|
"filename": "kali-linux-2.0-amd64.iso",
|
||||||
|
18
gns3server/appliances/mcjoin.gns3a
Normal file
18
gns3server/appliances/mcjoin.gns3a
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "mcjoin",
|
||||||
|
"category": "guest",
|
||||||
|
"description": "mcjoin is a very simple and easy-to-use tool to test IPv4 and IPv6 multicast.",
|
||||||
|
"vendor_name": "Joachim Nilsson",
|
||||||
|
"vendor_url": "https://github.com/troglobit",
|
||||||
|
"product_name": "mcjoin",
|
||||||
|
"registry_version": 3,
|
||||||
|
"status": "stable",
|
||||||
|
"maintainer": "GNS3 Team",
|
||||||
|
"maintainer_email": "developers@gns3.net",
|
||||||
|
"symbol": "linux_guest.svg",
|
||||||
|
"docker": {
|
||||||
|
"adapters": 1,
|
||||||
|
"image": "troglobit/mcjoin:latest",
|
||||||
|
"console_type": "telnet"
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@
|
|||||||
"maintainer": "GNS3 Team",
|
"maintainer": "GNS3 Team",
|
||||||
"maintainer_email": "developers@gns3.net",
|
"maintainer_email": "developers@gns3.net",
|
||||||
"usage": "User root, password gns3",
|
"usage": "User root, password gns3",
|
||||||
"first_port_name": "fxp0",
|
"first_port_name": "em0",
|
||||||
"port_name_format": "em{0}",
|
"port_name_format": "em{0}",
|
||||||
"qemu": {
|
"qemu": {
|
||||||
"adapter_type": "e1000",
|
"adapter_type": "e1000",
|
||||||
|
@ -417,7 +417,7 @@ class BaseManager:
|
|||||||
|
|
||||||
if not path or path == ".":
|
if not path or path == ".":
|
||||||
return ""
|
return ""
|
||||||
orig_path = path
|
orig_path = os.path.normpath(path)
|
||||||
|
|
||||||
img_directory = self.get_images_directory()
|
img_directory = self.get_images_directory()
|
||||||
valid_directory_prefices = images_directories(self._NODE_TYPE)
|
valid_directory_prefices = images_directories(self._NODE_TYPE)
|
||||||
@ -431,7 +431,8 @@ class BaseManager:
|
|||||||
f"'{path}' is not allowed on this remote server. Please only use a file from '{img_directory}'"
|
f"'{path}' is not allowed on this remote server. Please only use a file from '{img_directory}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.isabs(path):
|
if not os.path.isabs(orig_path):
|
||||||
|
|
||||||
for directory in valid_directory_prefices:
|
for directory in valid_directory_prefices:
|
||||||
log.debug(f"Searching for image '{orig_path}' in '{directory}'")
|
log.debug(f"Searching for image '{orig_path}' in '{directory}'")
|
||||||
path = self._recursive_search_file_in_directory(directory, orig_path)
|
path = self._recursive_search_file_in_directory(directory, orig_path)
|
||||||
@ -475,7 +476,7 @@ class BaseManager:
|
|||||||
for root, dirs, files in os.walk(directory):
|
for root, dirs, files in os.walk(directory):
|
||||||
for file in files:
|
for file in files:
|
||||||
# If filename is the same
|
# If filename is the same
|
||||||
if s[1] == file and (s[0] == "" or s[0] == os.path.basename(root)):
|
if s[1] == file and (s[0] == '' or os.path.basename(s[0]) == os.path.basename(root)):
|
||||||
path = os.path.normpath(os.path.join(root, s[1]))
|
path = os.path.normpath(os.path.join(root, s[1]))
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return path
|
return path
|
||||||
|
@ -153,8 +153,6 @@ class Config:
|
|||||||
Return the settings.
|
Return the settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._settings is None:
|
|
||||||
return ServerConfig()
|
|
||||||
return self._settings
|
return self._settings
|
||||||
|
|
||||||
def listen_for_config_changes(self, callback):
|
def listen_for_config_changes(self, callback):
|
||||||
@ -273,6 +271,7 @@ class Config:
|
|||||||
return
|
return
|
||||||
if not parsed_files:
|
if not parsed_files:
|
||||||
log.warning("No configuration file could be found or read")
|
log.warning("No configuration file could be found or read")
|
||||||
|
self._settings = ServerConfig()
|
||||||
return
|
return
|
||||||
|
|
||||||
for file in parsed_files:
|
for file in parsed_files:
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import uuid
|
import uuid
|
||||||
import socket
|
import socket
|
||||||
import shutil
|
import shutil
|
||||||
@ -30,7 +29,6 @@ from .appliance_manager import ApplianceManager
|
|||||||
from .compute import Compute, ComputeError
|
from .compute import Compute, ComputeError
|
||||||
from .notification import Notification
|
from .notification import Notification
|
||||||
from .symbols import Symbols
|
from .symbols import Symbols
|
||||||
from ..version import __version__
|
|
||||||
from .topology import load_topology
|
from .topology import load_topology
|
||||||
from .gns3vm import GNS3VM
|
from .gns3vm import GNS3VM
|
||||||
from ..utils.get_resource import get_resource
|
from ..utils.get_resource import get_resource
|
||||||
@ -205,7 +203,7 @@ class Controller:
|
|||||||
if iou_config.iourc_path:
|
if iou_config.iourc_path:
|
||||||
iourc_path = iou_config.iourc_path
|
iourc_path = iou_config.iourc_path
|
||||||
else:
|
else:
|
||||||
os.makedirs(os.path.dirname(server_config.secrets_dir), exist_ok=True)
|
os.makedirs(server_config.secrets_dir, exist_ok=True)
|
||||||
iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license")
|
iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -215,9 +213,17 @@ class Controller:
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.error(f"Cannot write IOU license file '{iourc_path}': {e}")
|
log.error(f"Cannot write IOU license file '{iourc_path}': {e}")
|
||||||
|
|
||||||
# if self._appliance_manager.appliances_etag:
|
if self._appliance_manager.appliances_etag:
|
||||||
# config._config.set("Controller", "appliances_etag", self._appliance_manager.appliances_etag)
|
etag_directory = os.path.dirname(Config.instance().server_config)
|
||||||
# config.write_config()
|
os.makedirs(etag_directory, exist_ok=True)
|
||||||
|
etag_appliances_path = os.path.join(etag_directory, "gns3_appliances_etag")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(etag_appliances_path, "w+") as f:
|
||||||
|
f.write(self._appliance_manager.appliances_etag)
|
||||||
|
log.info(f"etag appliances file '{etag_appliances_path}' saved")
|
||||||
|
except OSError as e:
|
||||||
|
log.error(f"Cannot write Etag appliance file '{etag_appliances_path}': {e}")
|
||||||
|
|
||||||
def _load_controller_settings(self):
|
def _load_controller_settings(self):
|
||||||
"""
|
"""
|
||||||
@ -263,8 +269,19 @@ class Controller:
|
|||||||
log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
|
log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
|
||||||
|
|
||||||
self._iou_license_settings["license_check"] = iou_config.license_check
|
self._iou_license_settings["license_check"] = iou_config.license_check
|
||||||
# self._appliance_manager.appliances_etag = controller_config.get("appliances_etag", None)
|
|
||||||
# self._appliance_manager.load_appliances()
|
etag_directory = os.path.dirname(Config.instance().server_config)
|
||||||
|
etag_appliances_path = os.path.join(etag_directory, "gns3_appliances_etag")
|
||||||
|
self._appliance_manager.appliances_etag = None
|
||||||
|
if os.path.exists(etag_appliances_path):
|
||||||
|
try:
|
||||||
|
with open(etag_appliances_path) as f:
|
||||||
|
self._appliance_manager.appliances_etag = f.read()
|
||||||
|
log.info(f"etag appliances file '{etag_appliances_path}' loaded")
|
||||||
|
except OSError as e:
|
||||||
|
log.error(f"Cannot read Etag appliance file '{etag_appliances_path}': {e}")
|
||||||
|
|
||||||
|
self._appliance_manager.load_appliances()
|
||||||
self._config_loaded = True
|
self._config_loaded = True
|
||||||
|
|
||||||
async def load_projects(self):
|
async def load_projects(self):
|
||||||
|
@ -487,7 +487,7 @@ class Compute:
|
|||||||
|
|
||||||
# Try to reconnect after 1 second if server unavailable only if not during tests (otherwise we create a ressources usage bomb)
|
# Try to reconnect after 1 second if server unavailable only if not during tests (otherwise we create a ressources usage bomb)
|
||||||
from gns3server.api.server import app
|
from gns3server.api.server import app
|
||||||
if not app.state.exiting and not hasattr(sys, "_called_from_test") or not sys._called_from_test:
|
if not app.state.exiting and not hasattr(sys, "_called_from_test"):
|
||||||
log.info(f"Reconnecting to to compute '{self._id}' WebSocket '{ws_url}'")
|
log.info(f"Reconnecting to to compute '{self._id}' WebSocket '{ws_url}'")
|
||||||
asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect()))
|
asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect()))
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class DynamipsNodeValidation(DynamipsCreate):
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def _check_topology_schema(topo):
|
def _check_topology_schema(topo, path):
|
||||||
try:
|
try:
|
||||||
Topology.parse_obj(topo)
|
Topology.parse_obj(topo)
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ def _check_topology_schema(topo):
|
|||||||
DynamipsNodeValidation.parse_obj(node.get("properties", {}))
|
DynamipsNodeValidation.parse_obj(node.get("properties", {}))
|
||||||
|
|
||||||
except pydantic.ValidationError as e:
|
except pydantic.ValidationError as e:
|
||||||
error = f"Invalid data in topology file: {e}"
|
error = f"Invalid data in topology file {path}: {e}"
|
||||||
log.critical(error)
|
log.critical(error)
|
||||||
raise ControllerError(error)
|
raise ControllerError(error)
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ def project_to_topology(project):
|
|||||||
data["topology"]["computes"].append(compute)
|
data["topology"]["computes"].append(compute)
|
||||||
elif isinstance(compute, dict):
|
elif isinstance(compute, dict):
|
||||||
data["topology"]["computes"].append(compute)
|
data["topology"]["computes"].append(compute)
|
||||||
_check_topology_schema(data)
|
_check_topology_schema(data, project.path)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ def load_topology(path):
|
|||||||
topo["variables"] = [var for var in variables if var.get("name")]
|
topo["variables"] = [var for var in variables if var.get("name")]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_check_topology_schema(topo)
|
_check_topology_schema(topo, path)
|
||||||
except ControllerError as e:
|
except ControllerError as e:
|
||||||
log.error("Can't load the topology %s", path)
|
log.error("Can't load the topology %s", path)
|
||||||
raise e
|
raise e
|
||||||
|
@ -59,7 +59,7 @@ class CrashReport:
|
|||||||
Report crash to a third party service
|
Report crash to a third party service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DSN = "https://d8001b95f4f244fe82ea720c9d4f0c09:690d4fe9fe3b4aa9aab004bc9e76cb8a@o19455.ingest.sentry.io/38482"
|
DSN = "https://ccd9829f1391432c900aa835e7eb1050:83d10f4d74654e2b8428129a62cf31cf@o19455.ingest.sentry.io/38482"
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from .base import Base
|
from .base import Base
|
||||||
from .users import User
|
from .users import User, UserGroup
|
||||||
from .computes import Compute
|
from .computes import Compute
|
||||||
from .templates import (
|
from .templates import (
|
||||||
Template,
|
Template,
|
||||||
|
@ -43,6 +43,7 @@ class GUID(TypeDecorator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
impl = CHAR
|
impl = CHAR
|
||||||
|
cache_ok = True
|
||||||
|
|
||||||
def load_dialect_impl(self, dialect):
|
def load_dialect_impl(self, dialect):
|
||||||
if dialect.name == "postgresql":
|
if dialect.name == "postgresql":
|
||||||
@ -75,8 +76,8 @@ class BaseTable(Base):
|
|||||||
|
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.current_timestamp())
|
created_at = Column(DateTime, server_default=func.current_timestamp())
|
||||||
updated_at = Column(DateTime, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||||
|
|
||||||
|
|
||||||
def generate_uuid():
|
def generate_uuid():
|
||||||
|
@ -45,7 +45,7 @@ class CloudTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "cloud_templates"
|
__tablename__ = "cloud_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
ports_mapping = Column(PickleType)
|
ports_mapping = Column(PickleType)
|
||||||
remote_console_host = Column(String)
|
remote_console_host = Column(String)
|
||||||
remote_console_port = Column(Integer)
|
remote_console_port = Column(Integer)
|
||||||
@ -59,7 +59,7 @@ class DockerTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "docker_templates"
|
__tablename__ = "docker_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
image = Column(String)
|
image = Column(String)
|
||||||
adapters = Column(Integer)
|
adapters = Column(Integer)
|
||||||
start_command = Column(String)
|
start_command = Column(String)
|
||||||
@ -83,7 +83,7 @@ class DynamipsTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "dynamips_templates"
|
__tablename__ = "dynamips_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
platform = Column(String)
|
platform = Column(String)
|
||||||
chassis = Column(String)
|
chassis = Column(String)
|
||||||
image = Column(String)
|
image = Column(String)
|
||||||
@ -126,7 +126,7 @@ class EthernetHubTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "ethernet_hub_templates"
|
__tablename__ = "ethernet_hub_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
ports_mapping = Column(PickleType)
|
ports_mapping = Column(PickleType)
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"}
|
__mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"}
|
||||||
@ -136,7 +136,7 @@ class EthernetSwitchTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "ethernet_switch_templates"
|
__tablename__ = "ethernet_switch_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
ports_mapping = Column(PickleType)
|
ports_mapping = Column(PickleType)
|
||||||
console_type = Column(String)
|
console_type = Column(String)
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ class IOUTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "iou_templates"
|
__tablename__ = "iou_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
path = Column(String)
|
path = Column(String)
|
||||||
ethernet_adapters = Column(Integer)
|
ethernet_adapters = Column(Integer)
|
||||||
serial_adapters = Column(Integer)
|
serial_adapters = Column(Integer)
|
||||||
@ -167,7 +167,7 @@ class QemuTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "qemu_templates"
|
__tablename__ = "qemu_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
qemu_path = Column(String)
|
qemu_path = Column(String)
|
||||||
platform = Column(String)
|
platform = Column(String)
|
||||||
linked_clone = Column(Boolean)
|
linked_clone = Column(Boolean)
|
||||||
@ -213,7 +213,7 @@ class VirtualBoxTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "virtualbox_templates"
|
__tablename__ = "virtualbox_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
vmname = Column(String)
|
vmname = Column(String)
|
||||||
ram = Column(Integer)
|
ram = Column(Integer)
|
||||||
linked_clone = Column(Boolean)
|
linked_clone = Column(Boolean)
|
||||||
@ -236,7 +236,7 @@ class VMwareTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "vmware_templates"
|
__tablename__ = "vmware_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
vmx_path = Column(String)
|
vmx_path = Column(String)
|
||||||
linked_clone = Column(Boolean)
|
linked_clone = Column(Boolean)
|
||||||
first_port_name = Column(String)
|
first_port_name = Column(String)
|
||||||
@ -258,7 +258,7 @@ class VPCSTemplate(Template):
|
|||||||
|
|
||||||
__tablename__ = "vpcs_templates"
|
__tablename__ = "vpcs_templates"
|
||||||
|
|
||||||
template_id = Column(GUID, ForeignKey("templates.template_id"), primary_key=True)
|
template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
|
||||||
base_script_file = Column(String)
|
base_script_file = Column(String)
|
||||||
console_type = Column(String)
|
console_type = Column(String)
|
||||||
console_auto_start = Column(Boolean, default=False)
|
console_auto_start = Column(Boolean, default=False)
|
||||||
|
@ -15,15 +15,24 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Column, String, event
|
from sqlalchemy import Table, Boolean, Column, String, ForeignKey, event
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from .base import BaseTable, generate_uuid, GUID
|
from .base import Base, BaseTable, generate_uuid, GUID
|
||||||
|
from gns3server.config import Config
|
||||||
from gns3server.services import auth_service
|
from gns3server.services import auth_service
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
users_group_members = Table(
|
||||||
|
"users_group_members",
|
||||||
|
Base.metadata,
|
||||||
|
Column("user_id", GUID, ForeignKey("users.user_id", ondelete="CASCADE")),
|
||||||
|
Column("user_group_id", GUID, ForeignKey("users_group.user_group_id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class User(BaseTable):
|
class User(BaseTable):
|
||||||
|
|
||||||
@ -36,14 +45,18 @@ class User(BaseTable):
|
|||||||
hashed_password = Column(String)
|
hashed_password = Column(String)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
is_superadmin = Column(Boolean, default=False)
|
is_superadmin = Column(Boolean, default=False)
|
||||||
|
groups = relationship("UserGroup", secondary=users_group_members, back_populates="users")
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(User.__table__, 'after_create')
|
@event.listens_for(User.__table__, 'after_create')
|
||||||
def create_default_super_admin(target, connection, **kw):
|
def create_default_super_admin(target, connection, **kw):
|
||||||
|
|
||||||
hashed_password = auth_service.hash_password("admin")
|
config = Config.instance().settings
|
||||||
|
default_admin_username = config.Server.default_admin_username
|
||||||
|
default_admin_password = config.Server.default_admin_password.get_secret_value()
|
||||||
|
hashed_password = auth_service.hash_password(default_admin_password)
|
||||||
stmt = target.insert().values(
|
stmt = target.insert().values(
|
||||||
username="admin",
|
username=default_admin_username,
|
||||||
full_name="Super Administrator",
|
full_name="Super Administrator",
|
||||||
hashed_password=hashed_password,
|
hashed_password=hashed_password,
|
||||||
is_superadmin=True
|
is_superadmin=True
|
||||||
@ -51,3 +64,46 @@ def create_default_super_admin(target, connection, **kw):
|
|||||||
connection.execute(stmt)
|
connection.execute(stmt)
|
||||||
connection.commit()
|
connection.commit()
|
||||||
log.info("The default super admin account has been created in the database")
|
log.info("The default super admin account has been created in the database")
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroup(BaseTable):
|
||||||
|
|
||||||
|
__tablename__ = "users_group"
|
||||||
|
|
||||||
|
user_group_id = Column(GUID, primary_key=True, default=generate_uuid)
|
||||||
|
name = Column(String, unique=True, index=True)
|
||||||
|
is_updatable = Column(Boolean, default=True)
|
||||||
|
users = relationship("User", secondary=users_group_members, back_populates="groups")
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(UserGroup.__table__, 'after_create')
|
||||||
|
def create_default_user_groups(target, connection, **kw):
|
||||||
|
|
||||||
|
default_groups = [
|
||||||
|
{"name": "Administrators", "is_updatable": False},
|
||||||
|
{"name": "Editors", "is_updatable": False},
|
||||||
|
{"name": "Users", "is_updatable": False}
|
||||||
|
]
|
||||||
|
|
||||||
|
stmt = target.insert().values(default_groups)
|
||||||
|
connection.execute(stmt)
|
||||||
|
connection.commit()
|
||||||
|
log.info("The default user groups have been created in the database")
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(users_group_members, 'after_create')
|
||||||
|
def add_admin_to_group(target, connection, **kw):
|
||||||
|
|
||||||
|
users_group_table = UserGroup.__table__
|
||||||
|
stmt = users_group_table.select().where(users_group_table.c.name == "Administrators")
|
||||||
|
result = connection.execute(stmt)
|
||||||
|
user_group_id = result.first().user_group_id
|
||||||
|
|
||||||
|
users_table = User.__table__
|
||||||
|
stmt = users_table.select().where(users_table.c.is_superadmin.is_(True))
|
||||||
|
result = connection.execute(stmt)
|
||||||
|
user_id = result.first().user_id
|
||||||
|
|
||||||
|
stmt = target.insert().values(user_id=user_id, user_group_id=user_group_id)
|
||||||
|
connection.execute(stmt)
|
||||||
|
connection.commit()
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Union
|
||||||
from sqlalchemy import select, update, delete
|
from sqlalchemy import select, update, delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from .base import BaseRepository
|
from .base import BaseRepository
|
||||||
|
|
||||||
@ -107,3 +108,105 @@ class UsersRepository(BaseRepository):
|
|||||||
if not self._auth_service.verify_password(password, user.hashed_password):
|
if not self._auth_service.verify_password(password, user.hashed_password):
|
||||||
return None
|
return None
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
async def get_user_memberships(self, user_id: UUID) -> List[models.UserGroup]:
|
||||||
|
|
||||||
|
query = select(models.UserGroup).\
|
||||||
|
join(models.UserGroup.users).\
|
||||||
|
filter(models.User.user_id == user_id)
|
||||||
|
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def get_user_group(self, user_group_id: UUID) -> Optional[models.UserGroup]:
|
||||||
|
|
||||||
|
query = select(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id)
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_user_group_by_name(self, name: str) -> Optional[models.UserGroup]:
|
||||||
|
|
||||||
|
query = select(models.UserGroup).where(models.UserGroup.name == name)
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_user_groups(self) -> List[models.UserGroup]:
|
||||||
|
|
||||||
|
query = select(models.UserGroup)
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def create_user_group(self, user_group: schemas.UserGroupCreate) -> models.UserGroup:
|
||||||
|
|
||||||
|
db_user_group = models.UserGroup(name=user_group.name)
|
||||||
|
self._db_session.add(db_user_group)
|
||||||
|
await self._db_session.commit()
|
||||||
|
await self._db_session.refresh(db_user_group)
|
||||||
|
return db_user_group
|
||||||
|
|
||||||
|
async def update_user_group(
|
||||||
|
self,
|
||||||
|
user_group_id: UUID,
|
||||||
|
user_group_update: schemas.UserGroupUpdate
|
||||||
|
) -> Optional[models.UserGroup]:
|
||||||
|
|
||||||
|
update_values = user_group_update.dict(exclude_unset=True)
|
||||||
|
query = update(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id).values(update_values)
|
||||||
|
|
||||||
|
await self._db_session.execute(query)
|
||||||
|
await self._db_session.commit()
|
||||||
|
return await self.get_user_group(user_group_id)
|
||||||
|
|
||||||
|
async def delete_user_group(self, user_group_id: UUID) -> bool:
|
||||||
|
|
||||||
|
query = delete(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id)
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
await self._db_session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
async def add_member_to_user_group(
|
||||||
|
self,
|
||||||
|
user_group_id: UUID,
|
||||||
|
user: models.User
|
||||||
|
) -> Union[None, models.UserGroup]:
|
||||||
|
|
||||||
|
query = select(models.UserGroup).\
|
||||||
|
options(selectinload(models.UserGroup.users)).\
|
||||||
|
where(models.UserGroup.user_group_id == user_group_id)
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
user_group_db = result.scalars().first()
|
||||||
|
if not user_group_db:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user_group_db.users.append(user)
|
||||||
|
await self._db_session.commit()
|
||||||
|
await self._db_session.refresh(user_group_db)
|
||||||
|
return user_group_db
|
||||||
|
|
||||||
|
async def remove_member_from_user_group(
|
||||||
|
self,
|
||||||
|
user_group_id: UUID,
|
||||||
|
user: models.User
|
||||||
|
) -> Union[None, models.UserGroup]:
|
||||||
|
|
||||||
|
query = select(models.UserGroup).\
|
||||||
|
options(selectinload(models.UserGroup.users)).\
|
||||||
|
where(models.UserGroup.user_group_id == user_group_id)
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
user_group_db = result.scalars().first()
|
||||||
|
if not user_group_db:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user_group_db.users.remove(user)
|
||||||
|
await self._db_session.commit()
|
||||||
|
await self._db_session.refresh(user_group_db)
|
||||||
|
return user_group_db
|
||||||
|
|
||||||
|
async def get_user_group_members(self, user_group_id: UUID) -> List[models.User]:
|
||||||
|
|
||||||
|
query = select(models.User).\
|
||||||
|
join(models.User.groups).\
|
||||||
|
filter(models.UserGroup.user_group_id == user_group_id)
|
||||||
|
|
||||||
|
result = await self._db_session.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
@ -22,6 +22,8 @@ from fastapi.encoders import jsonable_encoder
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||||
from gns3server.db.repositories.computes import ComputesRepository
|
from gns3server.db.repositories.computes import ComputesRepository
|
||||||
@ -41,12 +43,22 @@ async def connect_to_db(app: FastAPI) -> None:
|
|||||||
db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}")
|
db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}")
|
||||||
engine = create_async_engine(db_url, connect_args={"check_same_thread": False}, future=True)
|
engine = create_async_engine(db_url, connect_args={"check_same_thread": False}, future=True)
|
||||||
try:
|
try:
|
||||||
async with engine.begin() as conn:
|
async with engine.connect() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
log.info(f"Successfully connected to database '{db_url}'")
|
log.info(f"Successfully connected to database '{db_url}'")
|
||||||
app.state._db_engine = engine
|
app.state._db_engine = engine
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
log.error(f"Error while connecting to database '{db_url}: {e}")
|
log.fatal(f"Error while connecting to database '{db_url}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(Engine, "connect")
|
||||||
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
|
||||||
|
# Enable SQL foreign key support for SQLite
|
||||||
|
# https://docs.sqlalchemy.org/en/14/dialects/sqlite.html#foreign-key-support
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
async def get_computes(app: FastAPI) -> List[dict]:
|
async def get_computes(app: FastAPI) -> List[dict]:
|
||||||
|
@ -111,7 +111,7 @@ class LogFilter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def filter(record):
|
def filter(record):
|
||||||
if "/settings" in record.msg and "200" in record.msg:
|
if isinstance(record.msg, str) and "/settings" in record.msg and "200" in record.msg:
|
||||||
return 0
|
return 0
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ from .controller.drawings import Drawing
|
|||||||
from .controller.gns3vm import GNS3VM
|
from .controller.gns3vm import GNS3VM
|
||||||
from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node
|
from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node
|
||||||
from .controller.projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile
|
from .controller.projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile
|
||||||
from .controller.users import UserCreate, UserUpdate, User, Credentials
|
from .controller.users import UserCreate, UserUpdate, User, Credentials, UserGroupCreate, UserGroupUpdate, UserGroup
|
||||||
from .controller.tokens import Token
|
from .controller.tokens import Token
|
||||||
from .controller.snapshots import SnapshotCreate, Snapshot
|
from .controller.snapshots import SnapshotCreate, Snapshot
|
||||||
from .controller.iou_license import IOULicense
|
from .controller.iou_license import IOULicense
|
||||||
|
@ -134,6 +134,8 @@ class ServerSettings(BaseModel):
|
|||||||
user: str = None
|
user: str = None
|
||||||
password: SecretStr = None
|
password: SecretStr = None
|
||||||
enable_http_auth: bool = False
|
enable_http_auth: bool = False
|
||||||
|
default_admin_username: str = "admin"
|
||||||
|
default_admin_password: SecretStr = SecretStr("admin")
|
||||||
allowed_interfaces: List[str] = Field(default_factory=list)
|
allowed_interfaces: List[str] = Field(default_factory=list)
|
||||||
default_nat_interface: str = None
|
default_nat_interface: str = None
|
||||||
allow_remote_console: bool = False
|
allow_remote_console: bool = False
|
||||||
|
@ -14,9 +14,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, SecretStr, validator
|
from pydantic import BaseModel, Field, SecretStr, validator
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
from uuid import UUID, uuid4
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from .nodes import NodeType
|
from .nodes import NodeType
|
||||||
@ -49,7 +50,7 @@ class ComputeCreate(ComputeBase):
|
|||||||
Data to create a compute.
|
Data to create a compute.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
compute_id: Union[str, UUID] = Field(default_factory=uuid4)
|
compute_id: Union[str, uuid.UUID] = None
|
||||||
password: Optional[SecretStr] = None
|
password: Optional[SecretStr] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -63,7 +64,18 @@ class ComputeCreate(ComputeBase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@validator("name", always=True)
|
@validator("compute_id", pre=True, always=True)
|
||||||
|
def default_compute_id(cls, v, values):
|
||||||
|
|
||||||
|
if v is not None:
|
||||||
|
return v
|
||||||
|
else:
|
||||||
|
protocol = values.get("protocol")
|
||||||
|
host = values.get("host")
|
||||||
|
port = values.get("port")
|
||||||
|
return uuid.uuid5(uuid.NAMESPACE_URL, f"{protocol}://{host}:{port}")
|
||||||
|
|
||||||
|
@validator("name", pre=True, always=True)
|
||||||
def generate_name(cls, name, values):
|
def generate_name(cls, name, values):
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
@ -119,7 +131,7 @@ class Compute(DateTimeModelMixin, ComputeBase):
|
|||||||
Data returned for a compute.
|
Data returned for a compute.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
compute_id: Union[str, UUID]
|
compute_id: Union[str, uuid.UUID]
|
||||||
name: str
|
name: str
|
||||||
connected: Optional[bool] = Field(None, description="Whether the controller is connected to the compute or not")
|
connected: Optional[bool] = Field(None, description="Whether the controller is connected to the compute or not")
|
||||||
cpu_usage_percent: Optional[float] = Field(None, description="CPU usage of the compute", ge=0, le=100)
|
cpu_usage_percent: Optional[float] = Field(None, description="CPU usage of the compute", ge=0, le=100)
|
||||||
|
@ -58,6 +58,39 @@ class User(DateTimeModelMixin, UserBase):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupBase(BaseModel):
|
||||||
|
"""
|
||||||
|
Common user group properties.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: Optional[str] = Field(None, min_length=3, regex="[a-zA-Z0-9_-]+$")
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupCreate(UserGroupBase):
|
||||||
|
"""
|
||||||
|
Properties to create an user group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: Optional[str] = Field(..., min_length=3, regex="[a-zA-Z0-9_-]+$")
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupUpdate(UserGroupBase):
|
||||||
|
"""
|
||||||
|
Properties to update an user group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroup(DateTimeModelMixin, UserGroupBase):
|
||||||
|
|
||||||
|
user_group_id: UUID
|
||||||
|
is_updatable: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class Credentials(BaseModel):
|
class Credentials(BaseModel):
|
||||||
|
|
||||||
username: str
|
username: str
|
||||||
|
@ -319,6 +319,7 @@ class Server:
|
|||||||
access_log=access_log,
|
access_log=access_log,
|
||||||
ssl_certfile=config.Server.certfile,
|
ssl_certfile=config.Server.certfile,
|
||||||
ssl_keyfile=config.Server.certkey,
|
ssl_keyfile=config.Server.certkey,
|
||||||
|
lifespan="on"
|
||||||
)
|
)
|
||||||
|
|
||||||
# overwrite uvicorn loggers with our own logger
|
# overwrite uvicorn loggers with our own logger
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
// Github Pages redirection
|
// Github Pages redirection
|
||||||
(function() {
|
(function () {
|
||||||
var redirect = sessionStorage.redirect;
|
var redirect = sessionStorage.redirect;
|
||||||
delete sessionStorage.redirect;
|
delete sessionStorage.redirect;
|
||||||
if (redirect && redirect != location.href) {
|
if (redirect && redirect != location.href) {
|
||||||
@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="styles.9eb8c986af611de926ea.css"></head>
|
<link rel="stylesheet" href="styles.333203d05669b9ad3942.css"></head>
|
||||||
<!-- <body class="mat-app-background" oncontextmenu="return false;"> -->
|
<!-- <body class="mat-app-background" oncontextmenu="return false;"> -->
|
||||||
<body class="mat-app-background" oncontextmenu="return false;">
|
<body class="mat-app-background" oncontextmenu="return false;">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
@ -41,12 +41,12 @@
|
|||||||
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-5D6FZL9923"></script>
|
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-5D6FZL9923"></script>
|
||||||
<script>
|
<script>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag()
|
function gtag() {
|
||||||
|
dataLayer.push(arguments);
|
||||||
{dataLayer.push(arguments);}
|
}
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config', 'G-5D6FZL9923');
|
gtag('config', 'G-5D6FZL9923');
|
||||||
</script>
|
</script>
|
||||||
<script src="runtime.7425f237727658da0a30.js" defer></script><script src="polyfills-es5.c354ceb948246ee3c02e.js" nomodule defer></script><script src="polyfills.c1fadfb88d7fb5b7f9ac.js" defer></script><script src="main.a3d9cbf7065d44d2dc40.js" defer></script></body>
|
<script src="runtime.7425f237727658da0a30.js" defer></script><script src="polyfills-es5.c354ceb948246ee3c02e.js" nomodule defer></script><script src="polyfills.c1fadfb88d7fb5b7f9ac.js" defer></script><script src="main.2f0314a517dded67879c.js" defer></script></body>
|
||||||
</html>
|
</html>
|
||||||
|
1
gns3server/static/web-ui/main.2f0314a517dded67879c.js
Normal file
1
gns3server/static/web-ui/main.2f0314a517dded67879c.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
|
|
||||||
@ -37,6 +38,16 @@ def get_default_project_directory():
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def is_safe_path(file_path: str, basedir: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check that file path is safe.
|
||||||
|
(the file is stored inside directory or one of its sub-directory)
|
||||||
|
"""
|
||||||
|
|
||||||
|
test_path = (Path(basedir) / file_path).resolve()
|
||||||
|
return Path(basedir).resolve() in test_path.resolve().parents
|
||||||
|
|
||||||
|
|
||||||
def check_path_allowed(path):
|
def check_path_allowed(path):
|
||||||
"""
|
"""
|
||||||
If the server is non local raise an error if
|
If the server is non local raise an error if
|
||||||
|
@ -29,7 +29,6 @@ if "dev" in __version__:
|
|||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
|
if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
|
||||||
r = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
|
r = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
|
||||||
__version__ = f"{__version__}-{r}"
|
__version__ = f"{__version__}-{r}"
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
uvicorn==0.13.4
|
uvicorn==0.13.4
|
||||||
fastapi==0.63.0
|
fastapi==0.64.0
|
||||||
websockets==8.1
|
websockets==9.0.1
|
||||||
python-multipart==0.0.5
|
python-multipart==0.0.5
|
||||||
aiohttp==3.7.4.post0
|
aiohttp==3.7.4.post0
|
||||||
aiofiles==0.6.0
|
aiofiles==0.6.0
|
||||||
Jinja2==2.11.3
|
Jinja2==2.11.3
|
||||||
sentry-sdk==1.0.0
|
sentry-sdk==1.1.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
async-timeout==3.0.1
|
async-timeout==3.0.1
|
||||||
distro==1.5.0
|
distro==1.5.0
|
||||||
py-cpuinfo==7.0.0
|
py-cpuinfo==8.0.0
|
||||||
sqlalchemy==1.4.5
|
sqlalchemy==1.4.14
|
||||||
aiosqlite===0.17.0
|
aiosqlite===0.17.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
python-jose==3.2.0
|
python-jose==3.2.0
|
||||||
|
@ -186,6 +186,29 @@ async def test_upload_image(app: FastAPI, client: AsyncClient, images_dir: str)
|
|||||||
assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf"
|
assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_upload_image_forbidden_location(app: FastAPI, client: AsyncClient) -> None:
|
||||||
|
|
||||||
|
file_path = "%2e%2e/hello"
|
||||||
|
response = await client.post(app.url_path_for("upload_dynamips_image", filename=file_path), content=b"TEST")
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download_image(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
||||||
|
|
||||||
|
response = await client.post(app.url_path_for("upload_dynamips_image", filename="test3"), content=b"TEST")
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
|
response = await client.get(app.url_path_for("download_dynamips_image", filename="test3"))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download_image_forbidden(app: FastAPI, client: AsyncClient, tmpdir) -> None:
|
||||||
|
|
||||||
|
file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
|
||||||
|
response = await client.get(app.url_path_for("download_dynamips_image", filename=file_path))
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not sys.platform.startswith("win") and os.getuid() == 0, reason="Root can delete any image")
|
@pytest.mark.skipif(not sys.platform.startswith("win") and os.getuid() == 0, reason="Root can delete any image")
|
||||||
async def test_upload_image_permission_denied(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
async def test_upload_image_permission_denied(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
||||||
|
|
||||||
|
@ -401,10 +401,10 @@ async def test_images(app: FastAPI, client: AsyncClient, fake_iou_bin: str) -> N
|
|||||||
assert response.json() == [{"filename": "iou.bin", "path": "iou.bin", "filesize": 7, "md5sum": "e573e8f5c93c6c00783f20c7a170aa6c"}]
|
assert response.json() == [{"filename": "iou.bin", "path": "iou.bin", "filesize": 7, "md5sum": "e573e8f5c93c6c00783f20c7a170aa6c"}]
|
||||||
|
|
||||||
|
|
||||||
async def test_image_vm(app: FastAPI, client: AsyncClient, tmpdir) -> None:
|
async def test_upload_image(app: FastAPI, client: AsyncClient, tmpdir) -> None:
|
||||||
|
|
||||||
with patch("gns3server.compute.IOU.get_images_directory", return_value=str(tmpdir)):
|
with patch("gns3server.compute.IOU.get_images_directory", return_value=str(tmpdir)):
|
||||||
response = await client.post(app.url_path_for("download_iou_image", filename="test2"), content=b"TEST")
|
response = await client.post(app.url_path_for("upload_iou_image", filename="test2"), content=b"TEST")
|
||||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
with open(str(tmpdir / "test2")) as f:
|
with open(str(tmpdir / "test2")) as f:
|
||||||
@ -415,6 +415,29 @@ async def test_image_vm(app: FastAPI, client: AsyncClient, tmpdir) -> None:
|
|||||||
assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf"
|
assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_upload_image_forbidden_location(app: FastAPI, client: AsyncClient) -> None:
|
||||||
|
|
||||||
|
file_path = "%2e%2e/hello"
|
||||||
|
response = await client.post(app.url_path_for("upload_dynamips_image", filename=file_path), content=b"TEST")
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download_image(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
||||||
|
|
||||||
|
response = await client.post(app.url_path_for("upload_dynamips_image", filename="test3"), content=b"TEST")
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
|
response = await client.get(app.url_path_for("download_dynamips_image", filename="test3"))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download_image_forbidden(app: FastAPI, client: AsyncClient, tmpdir) -> None:
|
||||||
|
|
||||||
|
file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
|
||||||
|
response = await client.get(app.url_path_for("download_iou_image", filename=file_path))
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
async def test_iou_duplicate(app: FastAPI, client: AsyncClient, vm: dict, base_params: dict) -> None:
|
async def test_iou_duplicate(app: FastAPI, client: AsyncClient, vm: dict, base_params: dict) -> None:
|
||||||
|
|
||||||
# create destination node first
|
# create destination node first
|
||||||
|
@ -179,6 +179,21 @@ async def test_get_file(app: FastAPI, client: AsyncClient, config, tmpdir) -> No
|
|||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_file_forbidden_location(app: FastAPI, client: AsyncClient, config, tmpdir) -> None:
|
||||||
|
|
||||||
|
config.settings.Server.projects_path = str(tmpdir)
|
||||||
|
project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b")
|
||||||
|
file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
|
||||||
|
response = await client.get(
|
||||||
|
app.url_path_for(
|
||||||
|
"get_compute_project_file",
|
||||||
|
project_id=project.id,
|
||||||
|
file_path=file_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
async def test_write_file(app: FastAPI, client: AsyncClient, config, tmpdir) -> None:
|
async def test_write_file(app: FastAPI, client: AsyncClient, config, tmpdir) -> None:
|
||||||
|
|
||||||
config.settings.Server.projects_path = str(tmpdir)
|
config.settings.Server.projects_path = str(tmpdir)
|
||||||
@ -196,3 +211,15 @@ async def test_write_file(app: FastAPI, client: AsyncClient, config, tmpdir) ->
|
|||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
file_path="../hello"))
|
file_path="../hello"))
|
||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
async def test_write_file_forbidden_location(app: FastAPI, client: AsyncClient, config, tmpdir) -> None:
|
||||||
|
|
||||||
|
config.settings.Server.projects_path = str(tmpdir)
|
||||||
|
project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b")
|
||||||
|
|
||||||
|
file_path = "%2e%2e/hello"
|
||||||
|
response = await client.post(app.url_path_for("write_compute_project_file",
|
||||||
|
project_id=project.id,
|
||||||
|
file_path=file_path), content=b"world")
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
@ -388,14 +388,29 @@ async def test_upload_image_ova(app: FastAPI, client: AsyncClient, tmpdir:str) -
|
|||||||
assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf"
|
assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf"
|
||||||
|
|
||||||
|
|
||||||
async def test_upload_image_forbiden_location(app: FastAPI, client: AsyncClient, tmpdir: str) -> None:
|
async def test_upload_image_forbidden_location(app: FastAPI, client: AsyncClient, tmpdir: str) -> None:
|
||||||
|
|
||||||
with patch("gns3server.compute.Qemu.get_images_directory", return_value=str(tmpdir)):
|
|
||||||
response = await client.post(app.url_path_for("upload_qemu_image",
|
response = await client.post(app.url_path_for("upload_qemu_image",
|
||||||
filename="/qemu/images/../../test2"), content=b"TEST")
|
filename="/qemu/images/../../test2"), content=b"TEST")
|
||||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download_image(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
||||||
|
|
||||||
|
response = await client.post(app.url_path_for("upload_qemu_image", filename="test3"), content=b"TEST")
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
|
response = await client.get(app.url_path_for("download_qemu_image", filename="test3"))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download_image_forbidden_location(app: FastAPI, client: AsyncClient, tmpdir) -> None:
|
||||||
|
|
||||||
|
file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
|
||||||
|
response = await client.get(app.url_path_for("download_qemu_image", filename=file_path))
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not sys.platform.startswith("win") and os.getuid() == 0, reason="Root can delete any image")
|
@pytest.mark.skipif(not sys.platform.startswith("win") and os.getuid() == 0, reason="Root can delete any image")
|
||||||
async def test_upload_image_permission_denied(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
async def test_upload_image_permission_denied(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class TestComputeRoutes:
|
|||||||
params = {
|
params = {
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 84,
|
"port": 42,
|
||||||
"user": "julien",
|
"user": "julien",
|
||||||
"password": "secure"
|
"password": "secure"
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ class TestComputeFeatures:
|
|||||||
params = {
|
params = {
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 84,
|
"port": 4242,
|
||||||
"user": "julien",
|
"user": "julien",
|
||||||
"password": "secure"
|
"password": "secure"
|
||||||
}
|
}
|
||||||
@ -151,7 +151,7 @@ class TestComputeFeatures:
|
|||||||
params = {
|
params = {
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 84,
|
"port": 4284,
|
||||||
"user": "julien",
|
"user": "julien",
|
||||||
"password": "secure"
|
"password": "secure"
|
||||||
}
|
}
|
||||||
|
165
tests/api/routes/controller/test_groups.py
Normal file
165
tests/api/routes/controller/test_groups.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#!/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/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fastapi import FastAPI, status
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from gns3server.db.repositories.users import UsersRepository
|
||||||
|
from gns3server.schemas.controller.users import User
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroupRoutes:
|
||||||
|
|
||||||
|
async def test_create_group(self, app: FastAPI, client: AsyncClient) -> None:
|
||||||
|
|
||||||
|
new_group = {"name": "group1"}
|
||||||
|
response = await client.post(app.url_path_for("create_user_group"), json=new_group)
|
||||||
|
assert response.status_code == status.HTTP_201_CREATED
|
||||||
|
|
||||||
|
async def test_get_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("group1")
|
||||||
|
response = await client.get(app.url_path_for("get_user_group", user_group_id=group_in_db.user_group_id))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert response.json()["user_group_id"] == str(group_in_db.user_group_id)
|
||||||
|
|
||||||
|
async def test_list_groups(self, app: FastAPI, client: AsyncClient) -> None:
|
||||||
|
|
||||||
|
response = await client.get(app.url_path_for("get_user_groups"))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert len(response.json()) == 4 # 3 default groups + group1
|
||||||
|
|
||||||
|
async def test_update_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("group1")
|
||||||
|
|
||||||
|
update_group = {"name": "group42"}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id),
|
||||||
|
json=update_group
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
updated_group_in_db = await user_repo.get_user_group(group_in_db.user_group_id)
|
||||||
|
assert updated_group_in_db.name == "group42"
|
||||||
|
|
||||||
|
async def test_cannot_update_admin_group(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("Administrators")
|
||||||
|
update_group = {"name": "Hackers"}
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id),
|
||||||
|
json=update_group
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
async def test_delete_group(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("group42")
|
||||||
|
response = await client.delete(app.url_path_for("delete_user_group", user_group_id=group_in_db.user_group_id))
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
|
async def test_cannot_delete_admin_group(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("Administrators")
|
||||||
|
response = await client.delete(app.url_path_for("delete_user_group", user_group_id=group_in_db.user_group_id))
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
async def test_add_member_to_group(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_user: User,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("Users")
|
||||||
|
response = await client.put(
|
||||||
|
app.url_path_for(
|
||||||
|
"add_member_to_group",
|
||||||
|
user_group_id=group_in_db.user_group_id,
|
||||||
|
user_id=str(test_user.user_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
members = await user_repo.get_user_group_members(group_in_db.user_group_id)
|
||||||
|
assert len(members) == 1
|
||||||
|
assert members[0].username == test_user.username
|
||||||
|
|
||||||
|
async def test_get_user_group_members(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("Users")
|
||||||
|
response = await client.get(
|
||||||
|
app.url_path_for(
|
||||||
|
"get_user_group_members",
|
||||||
|
user_group_id=group_in_db.user_group_id)
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
|
||||||
|
async def test_remove_member_from_group(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
test_user: User,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
group_in_db = await user_repo.get_user_group_by_name("Users")
|
||||||
|
|
||||||
|
response = await client.delete(
|
||||||
|
app.url_path_for(
|
||||||
|
"remove_member_from_group",
|
||||||
|
user_group_id=group_in_db.user_group_id,
|
||||||
|
user_id=str(test_user.user_id)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
|
members = await user_repo.get_user_group_members(group_in_db.user_group_id)
|
||||||
|
assert len(members) == 0
|
@ -240,6 +240,7 @@ async def test_get_file(app: FastAPI, client: AsyncClient, project: Project, com
|
|||||||
|
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.body = b"world"
|
response.body = b"world"
|
||||||
|
response.status = status.HTTP_200_OK
|
||||||
compute.http_query = AsyncioMagicMock(return_value=response)
|
compute.http_query = AsyncioMagicMock(return_value=response)
|
||||||
|
|
||||||
response = await client.get(app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="hello"))
|
response = await client.get(app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="hello"))
|
||||||
|
@ -330,6 +330,13 @@ async def test_get_file(app: FastAPI, client: AsyncClient, project: Project) ->
|
|||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_file_forbidden_location(app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
|
file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
|
||||||
|
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path=file_path))
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
async def test_write_file(app: FastAPI, client: AsyncClient, project: Project) -> None:
|
async def test_write_file(app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="hello"),
|
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="hello"),
|
||||||
@ -343,6 +350,14 @@ async def test_write_file(app: FastAPI, client: AsyncClient, project: Project) -
|
|||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
async def test_write_file_forbidden_location(app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||||
|
|
||||||
|
file_path = "%2e%2e/hello"
|
||||||
|
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path=file_path),
|
||||||
|
content=b"world")
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
async def test_write_and_get_file_with_leading_slashes_in_filename(
|
async def test_write_and_get_file_with_leading_slashes_in_filename(
|
||||||
app: FastAPI,
|
app: FastAPI,
|
||||||
client: AsyncClient,
|
client: AsyncClient,
|
||||||
@ -350,11 +365,10 @@ async def test_write_and_get_file_with_leading_slashes_in_filename(
|
|||||||
|
|
||||||
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="//hello"),
|
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="//hello"),
|
||||||
content=b"world")
|
content=b"world")
|
||||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="//hello"))
|
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="//hello"))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
assert response.content == b"world"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import(app: FastAPI, client: AsyncClient, tmpdir, controller: Controller) -> None:
|
async def test_import(app: FastAPI, client: AsyncClient, tmpdir, controller: Controller) -> None:
|
||||||
|
@ -97,7 +97,7 @@ class TestTemplateRoutes:
|
|||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.json()["name"] == "VPCS_TEST_RENAMED"
|
assert response.json()["name"] == "VPCS_TEST_RENAMED"
|
||||||
|
|
||||||
async def test_template_delete(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None:
|
async def test_template_delete(self, app: FastAPI, client: AsyncClient) -> None:
|
||||||
|
|
||||||
template_id = str(uuid.uuid4())
|
template_id = str(uuid.uuid4())
|
||||||
params = {"template_id": template_id,
|
params = {"template_id": template_id,
|
||||||
|
@ -56,8 +56,8 @@ class TestUserRoutes:
|
|||||||
assert user_in_db is None
|
assert user_in_db is None
|
||||||
|
|
||||||
# register the user
|
# register the user
|
||||||
res = await client.post(app.url_path_for("create_user"), json=params)
|
response = await client.post(app.url_path_for("create_user"), json=params)
|
||||||
assert res.status_code == status.HTTP_201_CREATED
|
assert response.status_code == status.HTTP_201_CREATED
|
||||||
|
|
||||||
# make sure the user does exists in the database now
|
# make sure the user does exists in the database now
|
||||||
user_in_db = await user_repo.get_user_by_username(params["username"])
|
user_in_db = await user_repo.get_user_by_username(params["username"])
|
||||||
@ -66,7 +66,7 @@ class TestUserRoutes:
|
|||||||
assert user_in_db.username == params["username"]
|
assert user_in_db.username == params["username"]
|
||||||
|
|
||||||
# check that the user returned in the response is equal to the user in the database
|
# check that the user returned in the response is equal to the user in the database
|
||||||
created_user = User(**res.json()).json()
|
created_user = User(**response.json()).json()
|
||||||
assert created_user == User.from_orm(user_in_db).json()
|
assert created_user == User.from_orm(user_in_db).json()
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -91,8 +91,8 @@ class TestUserRoutes:
|
|||||||
|
|
||||||
new_user = {"email": "not_taken@email.com", "username": "not_taken_username", "password": "test_password"}
|
new_user = {"email": "not_taken@email.com", "username": "not_taken_username", "password": "test_password"}
|
||||||
new_user[attr] = value
|
new_user[attr] = value
|
||||||
res = await client.post(app.url_path_for("create_user"), json=new_user)
|
response = await client.post(app.url_path_for("create_user"), json=new_user)
|
||||||
assert res.status_code == status_code
|
assert response.status_code == status_code
|
||||||
|
|
||||||
async def test_users_saved_password_is_hashed(
|
async def test_users_saved_password_is_hashed(
|
||||||
self,
|
self,
|
||||||
@ -105,8 +105,8 @@ class TestUserRoutes:
|
|||||||
new_user = {"username": "user3", "email": "user3@email.com", "password": "test_password"}
|
new_user = {"username": "user3", "email": "user3@email.com", "password": "test_password"}
|
||||||
|
|
||||||
# send post request to create user and ensure it is successful
|
# send post request to create user and ensure it is successful
|
||||||
res = await client.post(app.url_path_for("create_user"), json=new_user)
|
response = await client.post(app.url_path_for("create_user"), json=new_user)
|
||||||
assert res.status_code == status.HTTP_201_CREATED
|
assert response.status_code == status.HTTP_201_CREATED
|
||||||
|
|
||||||
# ensure that the users password is hashed in the db
|
# ensure that the users password is hashed in the db
|
||||||
# and that we can verify it using our auth service
|
# and that we can verify it using our auth service
|
||||||
@ -156,7 +156,6 @@ class TestAuthTokens:
|
|||||||
username = auth_service.get_username_from_token(token)
|
username = auth_service.get_username_from_token(token)
|
||||||
assert username == test_user.username
|
assert username == test_user.username
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"wrong_secret, wrong_token",
|
"wrong_secret, wrong_token",
|
||||||
(
|
(
|
||||||
@ -200,19 +199,19 @@ class TestUserLogin:
|
|||||||
"username": test_user.username,
|
"username": test_user.username,
|
||||||
"password": "user1_password",
|
"password": "user1_password",
|
||||||
}
|
}
|
||||||
res = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||||
assert res.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
# check that token exists in response and has user encoded within it
|
# check that token exists in response and has user encoded within it
|
||||||
token = res.json().get("access_token")
|
token = response.json().get("access_token")
|
||||||
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"])
|
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"])
|
||||||
assert "sub" in payload
|
assert "sub" in payload
|
||||||
username = payload.get("sub")
|
username = payload.get("sub")
|
||||||
assert username == test_user.username
|
assert username == test_user.username
|
||||||
|
|
||||||
# check that token is proper type
|
# check that token is proper type
|
||||||
assert "token_type" in res.json()
|
assert "token_type" in response.json()
|
||||||
assert res.json().get("token_type") == "bearer"
|
assert response.json().get("token_type") == "bearer"
|
||||||
|
|
||||||
async def test_user_can_authenticate_using_json(
|
async def test_user_can_authenticate_using_json(
|
||||||
self,
|
self,
|
||||||
@ -226,16 +225,16 @@ class TestUserLogin:
|
|||||||
"username": test_user.username,
|
"username": test_user.username,
|
||||||
"password": "user1_password",
|
"password": "user1_password",
|
||||||
}
|
}
|
||||||
res = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
|
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
|
||||||
assert res.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert res.json().get("access_token")
|
assert response.json().get("access_token")
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"username, password, status_code",
|
"username, password, status_code",
|
||||||
(
|
(
|
||||||
("wrong_username", "user1_password", status.HTTP_401_UNAUTHORIZED),
|
("wrong_username", "user1_password", status.HTTP_401_UNAUTHORIZED),
|
||||||
("user1", "wrong_password", status.HTTP_401_UNAUTHORIZED),
|
("user1", "wrong_password", status.HTTP_401_UNAUTHORIZED),
|
||||||
("user1", None, status.HTTP_401_UNAUTHORIZED),
|
("user1", None, status.HTTP_422_UNPROCESSABLE_ENTITY),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_user_with_wrong_creds_doesnt_receive_token(
|
async def test_user_with_wrong_creds_doesnt_receive_token(
|
||||||
@ -253,9 +252,9 @@ class TestUserLogin:
|
|||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
}
|
}
|
||||||
res = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||||
assert res.status_code == status_code
|
assert response.status_code == status_code
|
||||||
assert "access_token" not in res.json()
|
assert "access_token" not in response.json()
|
||||||
|
|
||||||
|
|
||||||
class TestUserMe:
|
class TestUserMe:
|
||||||
@ -267,9 +266,9 @@ class TestUserMe:
|
|||||||
test_user: User,
|
test_user: User,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
res = await authorized_client.get(app.url_path_for("get_current_active_user"))
|
response = await authorized_client.get(app.url_path_for("get_current_active_user"))
|
||||||
assert res.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
user = User(**res.json())
|
user = User(**response.json())
|
||||||
assert user.username == test_user.username
|
assert user.username == test_user.username
|
||||||
assert user.email == test_user.email
|
assert user.email == test_user.email
|
||||||
assert user.user_id == test_user.user_id
|
assert user.user_id == test_user.user_id
|
||||||
@ -280,8 +279,8 @@ class TestUserMe:
|
|||||||
test_user: User,
|
test_user: User,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
res = await unauthorized_client.get(app.url_path_for("get_current_active_user"))
|
response = await unauthorized_client.get(app.url_path_for("get_current_active_user"))
|
||||||
assert res.status_code == status.HTTP_401_UNAUTHORIZED
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
class TestSuperAdmin:
|
class TestSuperAdmin:
|
||||||
@ -307,8 +306,8 @@ class TestSuperAdmin:
|
|||||||
|
|
||||||
user_repo = UsersRepository(db_session)
|
user_repo = UsersRepository(db_session)
|
||||||
admin_in_db = await user_repo.get_user_by_username("admin")
|
admin_in_db = await user_repo.get_user_by_username("admin")
|
||||||
res = await client.delete(app.url_path_for("delete_user", user_id=admin_in_db.user_id))
|
response = await client.delete(app.url_path_for("delete_user", user_id=admin_in_db.user_id))
|
||||||
assert res.status_code == status.HTTP_403_FORBIDDEN
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
async def test_admin_can_login_after_password_recovery(
|
async def test_admin_can_login_after_password_recovery(
|
||||||
self,
|
self,
|
||||||
@ -327,5 +326,18 @@ class TestSuperAdmin:
|
|||||||
"username": "admin",
|
"username": "admin",
|
||||||
"password": "whatever",
|
"password": "whatever",
|
||||||
}
|
}
|
||||||
res = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||||
assert res.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
async def test_super_admin_belongs_to_admin_group(
|
||||||
|
self,
|
||||||
|
app: FastAPI,
|
||||||
|
client: AsyncClient,
|
||||||
|
db_session: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
user_repo = UsersRepository(db_session)
|
||||||
|
admin_in_db = await user_repo.get_user_by_username("admin")
|
||||||
|
response = await client.get(app.url_path_for("get_user_memberships", user_id=admin_in_db.user_id))
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert len(response.json()) == 1
|
||||||
|
@ -78,7 +78,7 @@ async def db_session(db_engine):
|
|||||||
# recreate database tables for each class
|
# recreate database tables for each class
|
||||||
# preferred and faster way would be to rollback the session/transaction
|
# preferred and faster way would be to rollback the session/transaction
|
||||||
# but it doesn't work for some reason
|
# but it doesn't work for some reason
|
||||||
async with db_engine.begin() as conn:
|
async with db_engine.connect() as conn:
|
||||||
# Speed up tests by avoiding to hash the 'admin' password everytime the default super admin is added
|
# Speed up tests by avoiding to hash the 'admin' password everytime the default super admin is added
|
||||||
# to the database using the "after_create" sqlalchemy event
|
# to the database using the "after_create" sqlalchemy event
|
||||||
hashed_password = "$2b$12$jPsNU9IS7.EWEqXahtDfo.26w6VLOLCuFEHKNvDpOjxs5e0WpqJfa"
|
hashed_password = "$2b$12$jPsNU9IS7.EWEqXahtDfo.26w6VLOLCuFEHKNvDpOjxs5e0WpqJfa"
|
||||||
@ -86,7 +86,7 @@ async def db_session(db_engine):
|
|||||||
await conn.run_sync(Base.metadata.drop_all)
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
session = AsyncSession(db_engine)
|
session = AsyncSession(db_engine, expire_on_commit=False)
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
finally:
|
finally:
|
||||||
|
Loading…
Reference in New Issue
Block a user