mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-28 19:28:07 +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
|
||||
|
||||
## 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
|
||||
|
||||
* 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
|
||||
|
||||
cd gns3-server-master
|
||||
python3 -m venv venv-gns3server
|
||||
source venv-gns3server/bin/activate
|
||||
sudo python3 setup.py install
|
||||
gns3server
|
||||
python3 -m gns3server --local
|
||||
|
||||
To run tests use:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
py.test -v
|
||||
python3 -m pytest tests
|
||||
|
||||
|
||||
Docker container
|
||||
|
@ -64,6 +64,14 @@ user = gns3
|
||||
; Password for HTTP authentication.
|
||||
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)
|
||||
; Do not forget to allow virbr0 in order for the NAT node to work
|
||||
allowed_interfaces = eth0,eth1,virbr0
|
||||
|
@ -1,8 +1,8 @@
|
||||
-r requirements.txt
|
||||
|
||||
pytest==6.2.3
|
||||
flake8==3.9.0
|
||||
pytest==6.2.4
|
||||
flake8==3.9.1
|
||||
pytest-timeout==1.4.2
|
||||
pytest-asyncio==0.14.0
|
||||
pytest-asyncio==0.15.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.
|
||||
"""
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
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)
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
image_path = dynamips_manager.get_abs_image_path(filename)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
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.
|
||||
"""
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
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)
|
||||
|
||||
iou_manager = IOU.instance()
|
||||
image_path = iou_manager.get_abs_image_path(filename)
|
||||
if not os.path.exists(image_path):
|
||||
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}")
|
||||
async def download_qemu_image(filename: str) -> FileResponse:
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
filename = urllib.parse.unquote(filename)
|
||||
|
||||
# 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)
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
image_path = qemu_manager.get_abs_image_path(filename)
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
|
@ -19,6 +19,7 @@ API routes for projects.
|
||||
"""
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
import logging
|
||||
|
||||
@ -32,6 +33,7 @@ from uuid import UUID
|
||||
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.project import Project
|
||||
from gns3server.utils.path import is_safe_path
|
||||
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.
|
||||
"""
|
||||
|
||||
file_path = urllib.parse.unquote(file_path)
|
||||
path = os.path.normpath(file_path)
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
path = os.path.join(project.path, path)
|
||||
|
@ -29,6 +29,7 @@ from . import snapshots
|
||||
from . import symbols
|
||||
from . import templates
|
||||
from . import users
|
||||
from . import groups
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
@ -37,6 +38,13 @@ router = APIRouter()
|
||||
router.include_router(controller.router, tags=["Controller"])
|
||||
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(
|
||||
appliances.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
@ -59,6 +67,7 @@ router.include_router(
|
||||
|
||||
router.include_router(
|
||||
gns3vm.router,
|
||||
deprecated=True,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/gns3vm",
|
||||
tags=["GNS3 VM"]
|
||||
|
@ -25,7 +25,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
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}"
|
||||
|
||||
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)
|
||||
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.
|
||||
"""
|
||||
@ -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}"
|
||||
|
||||
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)
|
||||
# FIXME: response with correct status code (from compute)
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
|
@ -18,12 +18,15 @@
|
||||
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 websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from gns3server.services import auth_service
|
||||
from gns3server.controller import Controller
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -31,7 +34,7 @@ log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
@router.get("", dependencies=[Depends(get_current_active_user)])
|
||||
async def http_notification() -> StreamingResponse:
|
||||
"""
|
||||
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:
|
||||
while True:
|
||||
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")
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
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")
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
|
@ -24,6 +24,7 @@ import tempfile
|
||||
import zipfile
|
||||
import aiofiles
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
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.export_project import export_project as export_controller_project
|
||||
from gns3server.utils.asyncio import aiozipstream
|
||||
from gns3server.utils.path import is_safe_path
|
||||
from gns3server.config import Config
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
if path[0] == ".":
|
||||
if not is_safe_path(path, project.path):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
if path[0] == ".":
|
||||
if not is_safe_path(path, project.path):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
path = os.path.join(project.path, path)
|
||||
|
@ -139,7 +139,7 @@ async def create_node_from_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()
|
||||
project = controller.get_project(str(project_id))
|
||||
node = await project.add_node_from_template(
|
||||
|
@ -153,22 +153,29 @@ async def update_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(
|
||||
user_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
) -> None:
|
||||
"""
|
||||
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")
|
||||
|
||||
success = await users_repo.delete_user(user_id)
|
||||
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)
|
||||
@ -178,3 +185,19 @@ async def get_current_active_user(current_user: schemas.User = Depends(get_curre
|
||||
"""
|
||||
|
||||
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 fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from uvicorn.main import Server as UvicornServer
|
||||
|
||||
from gns3server.controller.controller_error import (
|
||||
@ -55,6 +56,8 @@ def get_application() -> FastAPI:
|
||||
origins = [
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
"http://localhost:4200",
|
||||
"http://127.0.0.1:4200"
|
||||
"http://127.0.0.1:8080",
|
||||
"http://localhost:8080",
|
||||
"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")
|
||||
async def add_extra_headers(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
|
@ -27,10 +27,10 @@
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "vEOS-lab-4.25.0F.vmdk",
|
||||
"version": "4.25.0F",
|
||||
"md5sum": "d420763fdf3bc50e7e5b88418bd9d1fd",
|
||||
"filesize": 468779008,
|
||||
"filename": "vEOS-lab-4.25.3M.vmdk",
|
||||
"version": "4.25.3M",
|
||||
"md5sum": "2f196969036b4d283e86f15118d59c26",
|
||||
"filesize": 451543040,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
@ -211,10 +211,10 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "4.25.0F",
|
||||
"name": "4.25.3M",
|
||||
"images": {
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"version": "9500v 9.3.3",
|
||||
@ -148,6 +162,20 @@
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"images": {
|
||||
|
@ -11,29 +11,29 @@
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"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",
|
||||
"port_name_format": "dp0p{1}s{0}",
|
||||
"port_name_format": "dp0s{3}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 3,
|
||||
"adapters": 8,
|
||||
"ram": 4096,
|
||||
"cpus": 2,
|
||||
"cpus": 4,
|
||||
"hda_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "dc",
|
||||
"kvm": "allow",
|
||||
"boot_priority": "cd",
|
||||
"kvm": "require",
|
||||
"options": "-cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "danos-1908-amd64-vrouter.iso",
|
||||
"version": "1908",
|
||||
"md5sum": "e850b6aa2859de1075c11b9149fa50f4",
|
||||
"filesize": 409993216,
|
||||
"download_url": "https://danosproject.atlassian.net/wiki/spaces/DAN/pages/753667/DANOS+1908",
|
||||
"direct_download_url": "http://repos.danosproject.org.s3-website-us-west-1.amazonaws.com/images/danos-1908-amd64-vrouter.iso"
|
||||
"filename": "danos-2012-base-amd64.iso",
|
||||
"version": "2012",
|
||||
"md5sum": "fb7a60dc9afecdb274464832b3ab1ccb",
|
||||
"filesize": 441450496,
|
||||
"download_url": "https://danosproject.atlassian.net/wiki/spaces/DAN/pages/892141595/DANOS+2012",
|
||||
"direct_download_url": "https://s3-us-west-1.amazonaws.com/2012.repos.danosproject.org/2012/iso/danos-2012-base-amd64.iso"
|
||||
},
|
||||
{
|
||||
"filename": "empty8G.qcow2",
|
||||
@ -46,10 +46,10 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "1908",
|
||||
"name": "2012",
|
||||
"images": {
|
||||
"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",
|
||||
"md5sum": "9c6fb00558f78ed06992d89f745ef975",
|
||||
"filesize": 3037736960,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.3/kali-linux-2019.3-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2019.3",
|
||||
"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",
|
||||
"version": "2019.2",
|
||||
"md5sum": "0f89b6225d7ea9c18682f7cc541c1179",
|
||||
"filesize": 3353227264,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.2/kali-linux-2019.2-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2019.2",
|
||||
"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",
|
||||
"version": "2019.2 (MATE)",
|
||||
"md5sum": "fec8dd7009f932c51a74323df965a709",
|
||||
"filesize": 3313217536,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.2/kali-linux-mate-2019.2-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2019.2",
|
||||
"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",
|
||||
"version": "2019.1a",
|
||||
"md5sum": "58c6111ed0be1919ea87267e7e65ab0f",
|
||||
"filesize": 3483873280,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2019.1a/kali-linux-2019.1a-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2019.1a",
|
||||
"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",
|
||||
"version": "2018.4",
|
||||
"md5sum": "1b2d598bb8d2003e6207c119c0ba42fe",
|
||||
"filesize": 3139436544,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2018.4/kali-linux-2018.4-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2018.4",
|
||||
"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",
|
||||
"version": "2018.3a",
|
||||
"md5sum": "2da675d016bd690c05e180e33aa98b94",
|
||||
"filesize": 3192651776,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2018.3a/kali-linux-2018.3a-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2018.3a",
|
||||
"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",
|
||||
"version": "2018.1",
|
||||
"md5sum": "a3feb90df5b71b3c7f4a02bdddf221d7",
|
||||
"filesize": 3028500480,
|
||||
"download_url": "https://www.kali.org/downloads/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2018.1/kali-linux-2018.1-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2018.1",
|
||||
"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",
|
||||
"version": "2017.3",
|
||||
"md5sum": "b465580c897e94675ac1daf031fa66b9",
|
||||
"filesize": 2886402048,
|
||||
"download_url": "http://cdimage.kali.org/kali-2017.3/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2017.3/kali-linux-2017.3-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2017.3/",
|
||||
"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",
|
||||
"version": "2017.2",
|
||||
"md5sum": "541654f8f818450dc0db866a0a0f6eec",
|
||||
"filesize": 3020619776,
|
||||
"download_url": "http://cdimage.kali.org/kali-2017.2/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2017.2/kali-linux-2017.2-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2017.2/",
|
||||
"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",
|
||||
"version": "2017.1",
|
||||
"md5sum": "c8e742283929d7a12dbe7c58e398ff08",
|
||||
"filesize": 2794307584,
|
||||
"download_url": "http://cdimage.kali.org/kali-2017.1/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2017.1/kali-linux-2017.1-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2017.1/",
|
||||
"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",
|
||||
"version": "2016.2",
|
||||
"md5sum": "3d163746bc5148e61ad689d94bc263f9",
|
||||
"filesize": 3076767744,
|
||||
"download_url": "http://cdimage.kali.org/kali-2016.2/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2016.2/kali-linux-2016.2-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2016.2/",
|
||||
"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",
|
||||
"version": "2016.1",
|
||||
"md5sum": "2e1230dc14036935b3279dfe3e49ad39",
|
||||
"filesize": 2945482752,
|
||||
"download_url": "http://cdimage.kali.org/kali-2016.1/",
|
||||
"direct_download_url": "http://cdimage.kali.org/kali-2016.1/kali-linux-2016.1-amd64.iso"
|
||||
"download_url": "http://old.kali.org/kali-images/kali-2016.1/",
|
||||
"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",
|
||||
|
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_email": "developers@gns3.net",
|
||||
"usage": "User root, password gns3",
|
||||
"first_port_name": "fxp0",
|
||||
"first_port_name": "em0",
|
||||
"port_name_format": "em{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
|
@ -417,7 +417,7 @@ class BaseManager:
|
||||
|
||||
if not path or path == ".":
|
||||
return ""
|
||||
orig_path = path
|
||||
orig_path = os.path.normpath(path)
|
||||
|
||||
img_directory = self.get_images_directory()
|
||||
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}'"
|
||||
)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
if not os.path.isabs(orig_path):
|
||||
|
||||
for directory in valid_directory_prefices:
|
||||
log.debug(f"Searching for image '{orig_path}' in '{directory}'")
|
||||
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 file in files:
|
||||
# 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]))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
@ -153,8 +153,6 @@ class Config:
|
||||
Return the settings.
|
||||
"""
|
||||
|
||||
if self._settings is None:
|
||||
return ServerConfig()
|
||||
return self._settings
|
||||
|
||||
def listen_for_config_changes(self, callback):
|
||||
@ -273,6 +271,7 @@ class Config:
|
||||
return
|
||||
if not parsed_files:
|
||||
log.warning("No configuration file could be found or read")
|
||||
self._settings = ServerConfig()
|
||||
return
|
||||
|
||||
for file in parsed_files:
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import uuid
|
||||
import socket
|
||||
import shutil
|
||||
@ -30,7 +29,6 @@ from .appliance_manager import ApplianceManager
|
||||
from .compute import Compute, ComputeError
|
||||
from .notification import Notification
|
||||
from .symbols import Symbols
|
||||
from ..version import __version__
|
||||
from .topology import load_topology
|
||||
from .gns3vm import GNS3VM
|
||||
from ..utils.get_resource import get_resource
|
||||
@ -205,7 +203,7 @@ class Controller:
|
||||
if iou_config.iourc_path:
|
||||
iourc_path = iou_config.iourc_path
|
||||
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")
|
||||
|
||||
try:
|
||||
@ -215,9 +213,17 @@ class Controller:
|
||||
except OSError as e:
|
||||
log.error(f"Cannot write IOU license file '{iourc_path}': {e}")
|
||||
|
||||
# if self._appliance_manager.appliances_etag:
|
||||
# config._config.set("Controller", "appliances_etag", self._appliance_manager.appliances_etag)
|
||||
# config.write_config()
|
||||
if self._appliance_manager.appliances_etag:
|
||||
etag_directory = os.path.dirname(Config.instance().server_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):
|
||||
"""
|
||||
@ -263,8 +269,19 @@ class Controller:
|
||||
log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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}'")
|
||||
asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect()))
|
||||
|
||||
|
@ -50,7 +50,7 @@ class DynamipsNodeValidation(DynamipsCreate):
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
def _check_topology_schema(topo):
|
||||
def _check_topology_schema(topo, path):
|
||||
try:
|
||||
Topology.parse_obj(topo)
|
||||
|
||||
@ -60,7 +60,7 @@ def _check_topology_schema(topo):
|
||||
DynamipsNodeValidation.parse_obj(node.get("properties", {}))
|
||||
|
||||
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)
|
||||
raise ControllerError(error)
|
||||
|
||||
@ -117,7 +117,7 @@ def project_to_topology(project):
|
||||
data["topology"]["computes"].append(compute)
|
||||
elif isinstance(compute, dict):
|
||||
data["topology"]["computes"].append(compute)
|
||||
_check_topology_schema(data)
|
||||
_check_topology_schema(data, project.path)
|
||||
return data
|
||||
|
||||
|
||||
@ -187,7 +187,7 @@ def load_topology(path):
|
||||
topo["variables"] = [var for var in variables if var.get("name")]
|
||||
|
||||
try:
|
||||
_check_topology_schema(topo)
|
||||
_check_topology_schema(topo, path)
|
||||
except ControllerError as e:
|
||||
log.error("Can't load the topology %s", path)
|
||||
raise e
|
||||
|
@ -59,7 +59,7 @@ class CrashReport:
|
||||
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
|
||||
|
||||
def __init__(self):
|
||||
|
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .base import Base
|
||||
from .users import User
|
||||
from .users import User, UserGroup
|
||||
from .computes import Compute
|
||||
from .templates import (
|
||||
Template,
|
||||
|
@ -43,6 +43,7 @@ class GUID(TypeDecorator):
|
||||
"""
|
||||
|
||||
impl = CHAR
|
||||
cache_ok = True
|
||||
|
||||
def load_dialect_impl(self, dialect):
|
||||
if dialect.name == "postgresql":
|
||||
@ -75,8 +76,8 @@ class BaseTable(Base):
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
created_at = Column(DateTime, default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
created_at = Column(DateTime, server_default=func.current_timestamp())
|
||||
updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
|
@ -45,7 +45,7 @@ class CloudTemplate(Template):
|
||||
|
||||
__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)
|
||||
remote_console_host = Column(String)
|
||||
remote_console_port = Column(Integer)
|
||||
@ -59,7 +59,7 @@ class DockerTemplate(Template):
|
||||
|
||||
__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)
|
||||
adapters = Column(Integer)
|
||||
start_command = Column(String)
|
||||
@ -83,7 +83,7 @@ class DynamipsTemplate(Template):
|
||||
|
||||
__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)
|
||||
chassis = Column(String)
|
||||
image = Column(String)
|
||||
@ -126,7 +126,7 @@ class EthernetHubTemplate(Template):
|
||||
|
||||
__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)
|
||||
|
||||
__mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"}
|
||||
@ -136,7 +136,7 @@ class EthernetSwitchTemplate(Template):
|
||||
|
||||
__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)
|
||||
console_type = Column(String)
|
||||
|
||||
@ -147,7 +147,7 @@ class IOUTemplate(Template):
|
||||
|
||||
__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)
|
||||
ethernet_adapters = Column(Integer)
|
||||
serial_adapters = Column(Integer)
|
||||
@ -167,7 +167,7 @@ class QemuTemplate(Template):
|
||||
|
||||
__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)
|
||||
platform = Column(String)
|
||||
linked_clone = Column(Boolean)
|
||||
@ -213,7 +213,7 @@ class VirtualBoxTemplate(Template):
|
||||
|
||||
__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)
|
||||
ram = Column(Integer)
|
||||
linked_clone = Column(Boolean)
|
||||
@ -236,7 +236,7 @@ class VMwareTemplate(Template):
|
||||
|
||||
__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)
|
||||
linked_clone = Column(Boolean)
|
||||
first_port_name = Column(String)
|
||||
@ -258,7 +258,7 @@ class VPCSTemplate(Template):
|
||||
|
||||
__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)
|
||||
console_type = Column(String)
|
||||
console_auto_start = Column(Boolean, default=False)
|
||||
|
@ -15,15 +15,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
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):
|
||||
|
||||
@ -36,14 +45,18 @@ class User(BaseTable):
|
||||
hashed_password = Column(String)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_superadmin = Column(Boolean, default=False)
|
||||
groups = relationship("UserGroup", secondary=users_group_members, back_populates="users")
|
||||
|
||||
|
||||
@event.listens_for(User.__table__, 'after_create')
|
||||
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(
|
||||
username="admin",
|
||||
username=default_admin_username,
|
||||
full_name="Super Administrator",
|
||||
hashed_password=hashed_password,
|
||||
is_superadmin=True
|
||||
@ -51,3 +64,46 @@ def create_default_super_admin(target, connection, **kw):
|
||||
connection.execute(stmt)
|
||||
connection.commit()
|
||||
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/>.
|
||||
|
||||
from uuid import UUID
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Union
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from .base import BaseRepository
|
||||
|
||||
@ -107,3 +108,105 @@ class UsersRepository(BaseRepository):
|
||||
if not self._auth_service.verify_password(password, user.hashed_password):
|
||||
return None
|
||||
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 typing import List
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
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}")
|
||||
engine = create_async_engine(db_url, connect_args={"check_same_thread": False}, future=True)
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
async with engine.connect() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
log.info(f"Successfully connected to database '{db_url}'")
|
||||
app.state._db_engine = engine
|
||||
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]:
|
||||
|
@ -111,7 +111,7 @@ class LogFilter:
|
||||
"""
|
||||
|
||||
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 1
|
||||
|
||||
|
@ -27,7 +27,7 @@ from .controller.drawings import Drawing
|
||||
from .controller.gns3vm import GNS3VM
|
||||
from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node
|
||||
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.snapshots import SnapshotCreate, Snapshot
|
||||
from .controller.iou_license import IOULicense
|
||||
|
@ -134,6 +134,8 @@ class ServerSettings(BaseModel):
|
||||
user: str = None
|
||||
password: SecretStr = None
|
||||
enable_http_auth: bool = False
|
||||
default_admin_username: str = "admin"
|
||||
default_admin_password: SecretStr = SecretStr("admin")
|
||||
allowed_interfaces: List[str] = Field(default_factory=list)
|
||||
default_nat_interface: str = None
|
||||
allow_remote_console: bool = False
|
||||
|
@ -14,9 +14,10 @@
|
||||
# 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 uuid
|
||||
|
||||
from pydantic import BaseModel, Field, SecretStr, validator
|
||||
from typing import List, Optional, Union
|
||||
from uuid import UUID, uuid4
|
||||
from enum import Enum
|
||||
|
||||
from .nodes import NodeType
|
||||
@ -49,7 +50,7 @@ class ComputeCreate(ComputeBase):
|
||||
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
|
||||
|
||||
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):
|
||||
|
||||
if name is not None:
|
||||
@ -119,7 +131,7 @@ class Compute(DateTimeModelMixin, ComputeBase):
|
||||
Data returned for a compute.
|
||||
"""
|
||||
|
||||
compute_id: Union[str, UUID]
|
||||
compute_id: Union[str, uuid.UUID]
|
||||
name: str
|
||||
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)
|
||||
|
@ -58,6 +58,39 @@ class User(DateTimeModelMixin, UserBase):
|
||||
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):
|
||||
|
||||
username: str
|
||||
|
@ -319,6 +319,7 @@ class Server:
|
||||
access_log=access_log,
|
||||
ssl_certfile=config.Server.certfile,
|
||||
ssl_keyfile=config.Server.certkey,
|
||||
lifespan="on"
|
||||
)
|
||||
|
||||
# overwrite uvicorn loggers with our own logger
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
<script type="application/javascript">
|
||||
// Github Pages redirection
|
||||
(function() {
|
||||
(function () {
|
||||
var redirect = sessionStorage.redirect;
|
||||
delete sessionStorage.redirect;
|
||||
if (redirect && redirect != location.href) {
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
})();
|
||||
</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;">
|
||||
<app-root></app-root>
|
||||
@ -41,12 +41,12 @@
|
||||
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-5D6FZL9923"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag()
|
||||
|
||||
{dataLayer.push(arguments);}
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-5D6FZL9923');
|
||||
</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>
|
||||
|
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
|
||||
|
||||
from pathlib import Path
|
||||
from fastapi import HTTPException, status
|
||||
from ..config import Config
|
||||
|
||||
@ -37,6 +38,16 @@ def get_default_project_directory():
|
||||
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):
|
||||
"""
|
||||
If the server is non local raise an error if
|
||||
|
@ -29,7 +29,6 @@ if "dev" in __version__:
|
||||
try:
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
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()
|
||||
__version__ = f"{__version__}-{r}"
|
||||
|
@ -1,16 +1,16 @@
|
||||
uvicorn==0.13.4
|
||||
fastapi==0.63.0
|
||||
websockets==8.1
|
||||
fastapi==0.64.0
|
||||
websockets==9.0.1
|
||||
python-multipart==0.0.5
|
||||
aiohttp==3.7.4.post0
|
||||
aiofiles==0.6.0
|
||||
Jinja2==2.11.3
|
||||
sentry-sdk==1.0.0
|
||||
sentry-sdk==1.1.0
|
||||
psutil==5.8.0
|
||||
async-timeout==3.0.1
|
||||
distro==1.5.0
|
||||
py-cpuinfo==7.0.0
|
||||
sqlalchemy==1.4.5
|
||||
py-cpuinfo==8.0.0
|
||||
sqlalchemy==1.4.14
|
||||
aiosqlite===0.17.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-jose==3.2.0
|
||||
|
@ -186,6 +186,29 @@ async def test_upload_image(app: FastAPI, client: AsyncClient, images_dir: str)
|
||||
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")
|
||||
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"}]
|
||||
|
||||
|
||||
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)):
|
||||
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
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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:
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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:
|
||||
|
||||
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,
|
||||
file_path="../hello"))
|
||||
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"
|
||||
|
||||
|
||||
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",
|
||||
filename="/qemu/images/../../test2"), 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_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")
|
||||
async def test_upload_image_permission_denied(app: FastAPI, client: AsyncClient, images_dir: str) -> None:
|
||||
|
||||
|
@ -84,7 +84,7 @@ class TestComputeRoutes:
|
||||
params = {
|
||||
"protocol": "http",
|
||||
"host": "localhost",
|
||||
"port": 84,
|
||||
"port": 42,
|
||||
"user": "julien",
|
||||
"password": "secure"
|
||||
}
|
||||
@ -133,7 +133,7 @@ class TestComputeFeatures:
|
||||
params = {
|
||||
"protocol": "http",
|
||||
"host": "localhost",
|
||||
"port": 84,
|
||||
"port": 4242,
|
||||
"user": "julien",
|
||||
"password": "secure"
|
||||
}
|
||||
@ -151,7 +151,7 @@ class TestComputeFeatures:
|
||||
params = {
|
||||
"protocol": "http",
|
||||
"host": "localhost",
|
||||
"port": 84,
|
||||
"port": 4284,
|
||||
"user": "julien",
|
||||
"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.body = b"world"
|
||||
response.status = status.HTTP_200_OK
|
||||
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"))
|
||||
|
@ -330,6 +330,13 @@ async def test_get_file(app: FastAPI, client: AsyncClient, project: Project) ->
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
app: FastAPI,
|
||||
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"),
|
||||
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"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.content == b"world"
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
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.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())
|
||||
params = {"template_id": template_id,
|
||||
|
@ -56,8 +56,8 @@ class TestUserRoutes:
|
||||
assert user_in_db is None
|
||||
|
||||
# register the user
|
||||
res = await client.post(app.url_path_for("create_user"), json=params)
|
||||
assert res.status_code == status.HTTP_201_CREATED
|
||||
response = await client.post(app.url_path_for("create_user"), json=params)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# make sure the user does exists in the database now
|
||||
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"]
|
||||
|
||||
# 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()
|
||||
|
||||
@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[attr] = value
|
||||
res = await client.post(app.url_path_for("create_user"), json=new_user)
|
||||
assert res.status_code == status_code
|
||||
response = await client.post(app.url_path_for("create_user"), json=new_user)
|
||||
assert response.status_code == status_code
|
||||
|
||||
async def test_users_saved_password_is_hashed(
|
||||
self,
|
||||
@ -105,8 +105,8 @@ class TestUserRoutes:
|
||||
new_user = {"username": "user3", "email": "user3@email.com", "password": "test_password"}
|
||||
|
||||
# send post request to create user and ensure it is successful
|
||||
res = await client.post(app.url_path_for("create_user"), json=new_user)
|
||||
assert res.status_code == status.HTTP_201_CREATED
|
||||
response = await client.post(app.url_path_for("create_user"), json=new_user)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# ensure that the users password is hashed in the db
|
||||
# and that we can verify it using our auth service
|
||||
@ -156,7 +156,6 @@ class TestAuthTokens:
|
||||
username = auth_service.get_username_from_token(token)
|
||||
assert username == test_user.username
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wrong_secret, wrong_token",
|
||||
(
|
||||
@ -200,19 +199,19 @@ class TestUserLogin:
|
||||
"username": test_user.username,
|
||||
"password": "user1_password",
|
||||
}
|
||||
res = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||
assert res.status_code == status.HTTP_200_OK
|
||||
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# 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"])
|
||||
assert "sub" in payload
|
||||
username = payload.get("sub")
|
||||
assert username == test_user.username
|
||||
|
||||
# check that token is proper type
|
||||
assert "token_type" in res.json()
|
||||
assert res.json().get("token_type") == "bearer"
|
||||
assert "token_type" in response.json()
|
||||
assert response.json().get("token_type") == "bearer"
|
||||
|
||||
async def test_user_can_authenticate_using_json(
|
||||
self,
|
||||
@ -226,16 +225,16 @@ class TestUserLogin:
|
||||
"username": test_user.username,
|
||||
"password": "user1_password",
|
||||
}
|
||||
res = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
|
||||
assert res.status_code == status.HTTP_200_OK
|
||||
assert res.json().get("access_token")
|
||||
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json().get("access_token")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username, password, status_code",
|
||||
(
|
||||
("wrong_username", "user1_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(
|
||||
@ -253,9 +252,9 @@ class TestUserLogin:
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
res = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||
assert res.status_code == status_code
|
||||
assert "access_token" not in res.json()
|
||||
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||
assert response.status_code == status_code
|
||||
assert "access_token" not in response.json()
|
||||
|
||||
|
||||
class TestUserMe:
|
||||
@ -267,9 +266,9 @@ class TestUserMe:
|
||||
test_user: User,
|
||||
) -> None:
|
||||
|
||||
res = await authorized_client.get(app.url_path_for("get_current_active_user"))
|
||||
assert res.status_code == status.HTTP_200_OK
|
||||
user = User(**res.json())
|
||||
response = await authorized_client.get(app.url_path_for("get_current_active_user"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
user = User(**response.json())
|
||||
assert user.username == test_user.username
|
||||
assert user.email == test_user.email
|
||||
assert user.user_id == test_user.user_id
|
||||
@ -280,8 +279,8 @@ class TestUserMe:
|
||||
test_user: User,
|
||||
) -> None:
|
||||
|
||||
res = await unauthorized_client.get(app.url_path_for("get_current_active_user"))
|
||||
assert res.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
response = await unauthorized_client.get(app.url_path_for("get_current_active_user"))
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestSuperAdmin:
|
||||
@ -307,8 +306,8 @@ class TestSuperAdmin:
|
||||
|
||||
user_repo = UsersRepository(db_session)
|
||||
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))
|
||||
assert res.status_code == status.HTTP_403_FORBIDDEN
|
||||
response = await client.delete(app.url_path_for("delete_user", user_id=admin_in_db.user_id))
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
async def test_admin_can_login_after_password_recovery(
|
||||
self,
|
||||
@ -327,5 +326,18 @@ class TestSuperAdmin:
|
||||
"username": "admin",
|
||||
"password": "whatever",
|
||||
}
|
||||
res = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||
assert res.status_code == status.HTTP_200_OK
|
||||
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
|
||||
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
|
||||
# preferred and faster way would be to rollback the session/transaction
|
||||
# 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
|
||||
# to the database using the "after_create" sqlalchemy event
|
||||
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.create_all)
|
||||
|
||||
session = AsyncSession(db_engine)
|
||||
session = AsyncSession(db_engine, expire_on_commit=False)
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
|
Loading…
Reference in New Issue
Block a user