1
0
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:
github-actions 2021-05-17 09:31:01 +00:00
commit d3b90c7cca
59 changed files with 1075 additions and 182 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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"]

View File

@ -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.
"""

View 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")

View File

@ -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")

View File

@ -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:

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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()

View File

@ -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"
}
},
{

View 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"
}
}
]
}

View File

@ -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": {

View File

@ -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"
}
}
]

View File

@ -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",

View 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"
}
}

View File

@ -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",

View File

@ -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

View File

@ -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:

View File

@ -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):

View File

@ -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()))

View File

@ -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

View File

@ -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):

View File

@ -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,

View File

@ -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():

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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]:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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}"

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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"
}

View 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

View File

@ -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"))

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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: