Base API and tables for RBAC support.

pull/1906/head
grossmj 3 years ago
parent eb0f8c6174
commit 6d4da98b8e

@ -30,6 +30,8 @@ from . import symbols
from . import templates
from . import users
from . import groups
from . import roles
from . import permissions
from .dependencies.authentication import get_current_active_user
@ -46,31 +48,36 @@ router.include_router(
)
router.include_router(
appliances.router,
roles.router,
dependencies=[Depends(get_current_active_user)],
prefix="/appliances",
tags=["Appliances"]
prefix="/roles",
tags=["Roles"]
)
router.include_router(
computes.router,
roles.router,
dependencies=[Depends(get_current_active_user)],
prefix="/computes",
tags=["Computes"]
prefix="/permissions",
tags=["Permissions"]
)
router.include_router(
drawings.router,
templates.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/drawings",
tags=["Drawings"])
tags=["Templates"]
)
router.include_router(
gns3vm.router,
deprecated=True,
projects.router,
dependencies=[Depends(get_current_active_user)],
prefix="/gns3vm",
tags=["GNS3 VM"]
prefix="/projects",
tags=["Projects"])
router.include_router(
nodes.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/nodes",
tags=["Nodes"]
)
router.include_router(
@ -81,38 +88,47 @@ router.include_router(
)
router.include_router(
nodes.router,
drawings.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/nodes",
tags=["Nodes"]
prefix="/projects/{project_id}/drawings",
tags=["Drawings"])
router.include_router(
symbols.router,
dependencies=[Depends(get_current_active_user)],
prefix="/symbols", tags=["Symbols"]
)
router.include_router(
notifications.router,
snapshots.router,
dependencies=[Depends(get_current_active_user)],
prefix="/notifications",
tags=["Notifications"])
prefix="/projects/{project_id}/snapshots",
tags=["Snapshots"])
router.include_router(
projects.router,
computes.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects",
tags=["Projects"])
prefix="/computes",
tags=["Computes"]
)
router.include_router(
snapshots.router,
notifications.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/snapshots",
tags=["Snapshots"])
prefix="/notifications",
tags=["Notifications"])
router.include_router(
symbols.router,
appliances.router,
dependencies=[Depends(get_current_active_user)],
prefix="/symbols", tags=["Symbols"]
prefix="/appliances",
tags=["Appliances"]
)
router.include_router(
templates.router,
gns3vm.router,
deprecated=True,
dependencies=[Depends(get_current_active_user)],
tags=["Templates"]
prefix="/gns3vm",
tags=["GNS3 VM"]
)

@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
)
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository
import logging
@ -182,3 +183,61 @@ async def remove_member_from_group(
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")
@router.get("/{user_group_id}/roles", response_model=List[schemas.Role])
async def get_user_group_roles(
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.Role]:
"""
Get all user group roles.
"""
return await users_repo.get_user_group_roles(user_group_id)
@router.put(
"/{user_group_id}/roles/{role_id}",
status_code=status.HTTP_204_NO_CONTENT
)
async def add_role_to_group(
user_group_id: UUID,
role_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Add role to an user group.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
user_group = await users_repo.add_role_to_user_group(user_group_id, role)
if not user_group:
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
@router.delete(
"/{user_group_id}/roles/{role_id}",
status_code=status.HTTP_204_NO_CONTENT
)
async def remove_role_from_group(
user_group_id: UUID,
role_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Remove role from an user group.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
user_group = await users_repo.remove_role_from_user_group(user_group_id, role)
if not user_group:
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")

@ -0,0 +1,122 @@
#!/usr/bin/env python
#
# Copyright (C) 2021 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API routes for permissions.
"""
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.rbac import RbacRepository
from .dependencies.database import get_repository
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_model=List[schemas.Permission])
async def get_permissions(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Permission]:
"""
Get all permissions.
"""
return await rbac_repo.get_permissions()
@router.post("", response_model=schemas.Permission, status_code=status.HTTP_201_CREATED)
async def create_permission(
permission_create: schemas.PermissionCreate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Permission:
"""
Create a new permission.
"""
# if await rbac_repo.get_role_by_path(role_create.name):
# raise ControllerBadRequestError(f"Role '{role_create.name}' already exists")
return await rbac_repo.create_permission(permission_create)
@router.get("/{permission_id}", response_model=schemas.Permission)
async def get_permission(
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Permission:
"""
Get a permission.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
return permission
@router.put("/{permission_id}", response_model=schemas.Permission)
async def update_permission(
permission_id: UUID,
permission_update: schemas.PermissionUpdate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Permission:
"""
Update a permission.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
#if not user_group.is_updatable:
# raise ControllerForbiddenError(f"User group '{user_group_id}' cannot be updated")
return await rbac_repo.update_permission(permission_id, permission_update)
@router.delete("/{permission_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_permission(
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a permission.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
#if not user_group.is_updatable:
# raise ControllerForbiddenError(f"User group '{user_group_id}' cannot be deleted")
success = await rbac_repo.delete_permission(permission_id)
if not success:
raise ControllerNotFoundError(f"Permission '{permission_id}' could not be deleted")

@ -0,0 +1,178 @@
#!/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 roles.
"""
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.rbac import RbacRepository
from .dependencies.database import get_repository
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_model=List[schemas.Role])
async def get_roles(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Role]:
"""
Get all roles.
"""
return await rbac_repo.get_roles()
@router.post("", response_model=schemas.Role, status_code=status.HTTP_201_CREATED)
async def create_role(
role_create: schemas.RoleCreate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role:
"""
Create a new role.
"""
if await rbac_repo.get_role_by_name(role_create.name):
raise ControllerBadRequestError(f"Role '{role_create.name}' already exists")
return await rbac_repo.create_role(role_create)
@router.get("/{role_id}", response_model=schemas.Role)
async def get_role(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Role:
"""
Get a role.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
return role
@router.put("/{role_id}", response_model=schemas.Role)
async def update_role(
role_id: UUID,
role_update: schemas.RoleUpdate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role:
"""
Update a role.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
#if not user_group.is_updatable:
# raise ControllerForbiddenError(f"User group '{user_group_id}' cannot be updated")
return await rbac_repo.update_role(role_id, role_update)
@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_role(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a role.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
#if not user_group.is_updatable:
# raise ControllerForbiddenError(f"User group '{user_group_id}' cannot be deleted")
success = await rbac_repo.delete_role(role_id)
if not success:
raise ControllerNotFoundError(f"Role '{role_id}' could not be deleted")
@router.get("/{role_id}/permissions", response_model=List[schemas.Permission])
async def get_role_permissions(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Permission]:
"""
Get all role permissions.
"""
return await rbac_repo.get_role_permissions(role_id)
@router.put(
"/{role_id}/permissions/{permission_id}",
status_code=status.HTTP_204_NO_CONTENT
)
async def add_permission_to_role(
role_id: UUID,
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Add a permission to a role.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
role = await rbac_repo.add_permission_to_role(role_id, permission)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
@router.delete(
"/{role_id}/permissions/{permission_id}",
status_code=status.HTTP_204_NO_CONTENT
)
async def remove_permission_from_role(
role_id: UUID,
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Remove member from an user group.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
role = await rbac_repo.remove_permission_from_role(role_id, permission)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")

@ -17,6 +17,8 @@
from .base import Base
from .users import User, UserGroup
from .roles import Role
from .permissions import Permission
from .computes import Compute
from .templates import (
Template,

@ -19,7 +19,7 @@ import uuid
from fastapi.encoders import jsonable_encoder
from sqlalchemy import Column, DateTime, func, inspect
from sqlalchemy.types import TypeDecorator, CHAR
from sqlalchemy.types import TypeDecorator, CHAR, VARCHAR
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import as_declarative
@ -72,6 +72,37 @@ class GUID(TypeDecorator):
return value
class ListException(Exception):
pass
class ListType(TypeDecorator):
"""
Save/restore a Python list to/from a database column.
"""
impl = VARCHAR
cache_ok = True
def __init__(self, separator=',', *args, **kwargs):
self._separator = separator
super().__init__(*args, **kwargs)
def process_bind_param(self, value, dialect):
if value is not None:
if any(self._separator in str(item) for item in value):
raise ListException(f"List values cannot contain '{self._separator}'"
f"Please use a different separator.")
return self._separator.join(map(str, value))
def process_result_value(self, value, dialect):
if value is None:
return []
else:
return list(map(str, value.split(self._separator)))
class BaseTable(Base):
__abstract__ = True
@ -79,6 +110,8 @@ class BaseTable(Base):
created_at = Column(DateTime, server_default=func.current_timestamp())
updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
__mapper_args__ = {"eager_defaults": True}
def generate_uuid():
return str(uuid.uuid4())

@ -0,0 +1,45 @@
#!/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/>.
from sqlalchemy import Table, Column, String, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from .base import Base, BaseTable, generate_uuid, GUID, ListType
import logging
log = logging.getLogger(__name__)
permission_role_link = Table(
"permissions_roles_link",
Base.metadata,
Column("permission_id", GUID, ForeignKey("permissions.permission_id", ondelete="CASCADE")),
Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE"))
)
class Permission(BaseTable):
__tablename__ = "permissions"
permission_id = Column(GUID, primary_key=True, default=generate_uuid)
methods = Column(ListType)
path = Column(String)
action = Column(String)
roles = relationship("Role", secondary=permission_role_link, back_populates="permissions")

@ -0,0 +1,59 @@
#!/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/>.
from sqlalchemy import Table, Column, String, Boolean, ForeignKey, event
from sqlalchemy.orm import relationship
from .base import Base, BaseTable, generate_uuid, GUID
from .permissions import permission_role_link
import logging
log = logging.getLogger(__name__)
role_group_link = Table(
"roles_groups_link",
Base.metadata,
Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE")),
Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"))
)
class Role(BaseTable):
__tablename__ = "roles"
role_id = Column(GUID, primary_key=True, default=generate_uuid)
name = Column(String)
description = Column(String)
is_updatable = Column(Boolean, default=True)
permissions = relationship("Permission", secondary=permission_role_link, back_populates="roles")
groups = relationship("UserGroup", secondary=role_group_link, back_populates="roles")
@event.listens_for(Role.__table__, 'after_create')
def create_default_roles(target, connection, **kw):
default_roles = [
{"name": "Administrator", "description": "Administrator role", "is_updatable": False},
{"name": "User", "description": "User role", "is_updatable": False},
]
stmt = target.insert().values(default_roles)
connection.execute(stmt)
connection.commit()
log.info("The default roles have been created in the database")

@ -19,6 +19,8 @@ from sqlalchemy import Table, Boolean, Column, String, ForeignKey, event
from sqlalchemy.orm import relationship
from .base import Base, BaseTable, generate_uuid, GUID
from .roles import role_group_link
from gns3server.config import Config
from gns3server.services import auth_service
@ -26,11 +28,11 @@ import logging
log = logging.getLogger(__name__)
users_group_members = Table(
"users_group_members",
user_group_link = Table(
"users_groups_link",
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"))
Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"))
)
@ -45,7 +47,9 @@ 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")
groups = relationship("UserGroup", secondary=user_group_link, back_populates="users")
permission_id = Column(GUID, ForeignKey('permissions.permission_id', ondelete="CASCADE"))
permissions = relationship("Permission")
@event.listens_for(User.__table__, 'after_create')
@ -68,12 +72,13 @@ def create_default_super_admin(target, connection, **kw):
class UserGroup(BaseTable):
__tablename__ = "users_group"
__tablename__ = "user_groups"
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")
users = relationship("User", secondary=user_group_link, back_populates="groups")
roles = relationship("Role", secondary=role_group_link, back_populates="groups")
@event.listens_for(UserGroup.__table__, 'after_create')
@ -91,11 +96,11 @@ def create_default_user_groups(target, connection, **kw):
log.info("The default user groups have been created in the database")
@event.listens_for(users_group_members, 'after_create')
@event.listens_for(user_group_link, '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")
user_groups_table = UserGroup.__table__
stmt = user_groups_table.select().where(user_groups_table.c.name == "Administrators")
result = connection.execute(stmt)
user_group_id = result.first().user_group_id

@ -0,0 +1,196 @@
#!/usr/bin/env python
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from uuid import UUID
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
import gns3server.db.models as models
from gns3server import schemas
import logging
log = logging.getLogger(__name__)
class RbacRepository(BaseRepository):
def __init__(self, db_session: AsyncSession) -> None:
super().__init__(db_session)
async def get_role(self, role_id: UUID) -> Optional[models.Role]:
query = select(models.Role).\
options(selectinload(models.Role.permissions)).\
where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_role_by_name(self, name: str) -> Optional[models.Role]:
query = select(models.Role).\
options(selectinload(models.Role.permissions)).\
where(models.Role.name == name)
#query = select(models.Role).where(models.Role.name == name)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_roles(self) -> List[models.Role]:
query = select(models.Role).options(selectinload(models.Role.permissions))
result = await self._db_session.execute(query)
return result.scalars().all()
async def create_role(self, role_create: schemas.RoleCreate) -> models.Role:
db_role = models.Role(
name=role_create.name,
description=role_create.description,
)
self._db_session.add(db_role)
await self._db_session.commit()
#await self._db_session.refresh(db_role)
return await self.get_role(db_role.role_id)
async def update_role(
self,
role_id: UUID,
role_update: schemas.RoleUpdate
) -> Optional[models.Role]:
update_values = role_update.dict(exclude_unset=True)
query = update(models.Role).where(models.Role.role_id == role_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
return await self.get_role(role_id)
async def delete_role(self, role_id: UUID) -> bool:
query = delete(models.Role).where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
await self._db_session.commit()
return result.rowcount > 0
async def add_permission_to_role(
self,
role_id: UUID,
permission: models.Permission
) -> Union[None, models.Role]:
query = select(models.Role).\
options(selectinload(models.Role.permissions)).\
where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
role_db = result.scalars().first()
if not role_db:
return None
role_db.permissions.append(permission)
await self._db_session.commit()
await self._db_session.refresh(role_db)
return role_db
async def remove_permission_from_role(
self,
role_id: UUID,
permission: models.Permission
) -> Union[None, models.Role]:
query = select(models.Role).\
options(selectinload(models.Role.permissions)).\
where(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
role_db = result.scalars().first()
if not role_db:
return None
role_db.permissions.remove(permission)
await self._db_session.commit()
await self._db_session.refresh(role_db)
return role_db
async def get_role_permissions(self, role_id: UUID) -> List[models.Permission]:
query = select(models.Permission).\
join(models.Permission.roles).\
filter(models.Role.role_id == role_id)
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_permission(self, permission_id: UUID) -> Optional[models.Permission]:
query = select(models.Permission).where(models.Permission.permission_id == permission_id)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_permission_by_path(self, path: str) -> Optional[models.Permission]:
query = select(models.Permission).where(models.Permission.path == path)
result = await self._db_session.execute(query)
return result.scalars().first()
async def get_permissions(self) -> List[models.Permission]:
query = select(models.Permission)
result = await self._db_session.execute(query)
return result.scalars().all()
async def create_permission(self, permission_create: schemas.PermissionCreate) -> models.Permission:
create_values = permission_create.dict(exclude_unset=True)
# action = create_values.pop("action", "deny")
# is_allowed = False
# if action == "allow":
# is_allowed = True
db_permission = models.Permission(
methods=permission_create.methods,
path=permission_create.path,
action=permission_create.action,
)
self._db_session.add(db_permission)
await self._db_session.commit()
await self._db_session.refresh(db_permission)
return db_permission
async def update_permission(
self,
permission_id: UUID,
permission_update: schemas.PermissionUpdate
) -> Optional[models.Permission]:
update_values = permission_update.dict(exclude_unset=True)
query = update(models.Permission).where(models.Permission.permission_id == permission_id).values(update_values)
await self._db_session.execute(query)
await self._db_session.commit()
return await self.get_permission(permission_id)
async def delete_permission(self, permission_id: UUID) -> bool:
query = delete(models.Permission).where(models.Permission.permission_id == permission_id)
result = await self._db_session.execute(query)
await self._db_session.commit()
return result.rowcount > 0

@ -210,3 +210,51 @@ class UsersRepository(BaseRepository):
result = await self._db_session.execute(query)
return result.scalars().all()
async def add_role_to_user_group(
self,
user_group_id: UUID,
role: models.Role
) -> Union[None, models.UserGroup]:
query = select(models.UserGroup).\
options(selectinload(models.UserGroup.roles)).\
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.roles.append(role)
await self._db_session.commit()
await self._db_session.refresh(user_group_db)
return user_group_db
async def remove_role_from_user_group(
self,
user_group_id: UUID,
role: models.Role
) -> Union[None, models.UserGroup]:
query = select(models.UserGroup).\
options(selectinload(models.UserGroup.roles)).\
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.roles.remove(role)
await self._db_session.commit()
await self._db_session.refresh(user_group_db)
return user_group_db
async def get_user_group_roles(self, user_group_id: UUID) -> List[models.Role]:
query = select(models.Role). \
options(selectinload(models.Role.permissions)). \
join(models.UserGroup.roles). \
filter(models.UserGroup.user_group_id == user_group_id)
result = await self._db_session.execute(query)
return result.scalars().all()

@ -28,6 +28,7 @@ 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, UserGroupCreate, UserGroupUpdate, UserGroup
from .controller.rbac import RoleCreate, RoleUpdate, Role, PermissionCreate, PermissionUpdate, Permission
from .controller.tokens import Token
from .controller.snapshots import SnapshotCreate, Snapshot
from .controller.iou_license import IOULicense

@ -0,0 +1,120 @@
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, List
from pydantic import BaseModel, validator
from uuid import UUID
from enum import Enum
from .base import DateTimeModelMixin
class HTTPMethods(str, Enum):
"""
HTTP method type.
"""
get = "GET"
head = "HEAD"
post = "POST"
patch = "PATCH"
put = "PUT"
delete = "DELETE"
class PermissionAction(str, Enum):
"""
Action to perform when permission is matched.
"""
allow = "ALLOW"
deny = "DENY"
class PermissionBase(BaseModel):
"""
Common permission properties.
"""
methods: List[HTTPMethods]
path: str
action: PermissionAction
description: Optional[str] = None
class Config:
use_enum_values = True
@validator("action", pre=True)
def action_uppercase(cls, v):
return v.upper()
class PermissionCreate(PermissionBase):
"""
Properties to create a permission.
"""
pass
class PermissionUpdate(PermissionBase):
"""
Properties to update a role.
"""
pass
class Permission(DateTimeModelMixin, PermissionBase):
permission_id: UUID
class Config:
orm_mode = True
class RoleBase(BaseModel):
"""
Common role properties.
"""
name: Optional[str] = None
description: Optional[str] = None
class RoleCreate(RoleBase):
"""
Properties to create a role.
"""
name: str
class RoleUpdate(RoleBase):
"""
Properties to update a role.
"""
pass
class Role(DateTimeModelMixin, RoleBase):
role_id: UUID
permissions: List[Permission]
class Config:
orm_mode = True

@ -22,7 +22,10 @@ from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.schemas.controller.users import User
from gns3server.schemas.controller.rbac import Role
from gns3server import schemas
pytestmark = pytest.mark.asyncio
@ -103,6 +106,9 @@ class TestGroupRoutes:
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
class TestGroupMembersRoutes:
async def test_add_member_to_group(
self,
app: FastAPI,
@ -163,3 +169,81 @@ class TestGroupRoutes:
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
@pytest.fixture
async def test_role(db_session: AsyncSession) -> Role:
new_role = schemas.RoleCreate(
name="TestRole",
description="This is my test role"
)
rbac_repo = RbacRepository(db_session)
existing_role = await rbac_repo.get_role_by_name(new_role.name)
if existing_role:
return existing_role
return await rbac_repo.create_role(new_role)
class TestGroupRolesRoutes:
async def test_add_role_to_group(
self,
app: FastAPI,
client: AsyncClient,
test_role: Role,
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_role_to_group",
user_group_id=group_in_db.user_group_id,
role_id=str(test_role.role_id)
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
roles = await user_repo.get_user_group_roles(group_in_db.user_group_id)
assert len(roles) == 1
assert roles[0].name == test_role.name
async def test_get_user_group_roles(
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_roles",
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_role_from_group(
self,
app: FastAPI,
client: AsyncClient,
test_role: Role,
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_role_from_group",
user_group_id=group_in_db.user_group_id,
role_id=test_role.role_id
),
)
assert response.status_code == status.HTTP_204_NO_CONTENT
roles = await user_repo.get_user_group_roles(group_in_db.user_group_id)
assert len(roles) == 0

@ -0,0 +1,186 @@
#!/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.rbac import RbacRepository
from gns3server.schemas.controller.rbac import Permission, HTTPMethods, PermissionAction
from gns3server import schemas
pytestmark = pytest.mark.asyncio
class TestRolesRoutes:
async def test_create_role(self, app: FastAPI, client: AsyncClient) -> None:
new_role = {"name": "role1"}
response = await client.post(app.url_path_for("create_role"), json=new_role)
assert response.status_code == status.HTTP_201_CREATED
async def test_get_role(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("role1")
response = await client.get(app.url_path_for("get_role", role_id=role_in_db.role_id))
assert response.status_code == status.HTTP_200_OK
assert response.json()["role_id"] == str(role_in_db.role_id)
async def test_list_roles(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_roles"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 3 # 2 default roles + role1
async def test_update_role(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("role1")
update_role = {"name": "role42"}
response = await client.put(
app.url_path_for("update_role", role_id=role_in_db.role_id),
json=update_role
)
assert response.status_code == status.HTTP_200_OK
updated_role_in_db = await rbac_repo.get_role(role_in_db.role_id)
assert updated_role_in_db.name == "role42"
# 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_role(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("role42")
response = await client.delete(app.url_path_for("delete_role", role_id=role_in_db.role_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
@pytest.fixture
async def test_permission(db_session: AsyncSession) -> Permission:
new_permission = schemas.PermissionCreate(
methods=[HTTPMethods.get, HTTPMethods.post],
path="/projects",
action=PermissionAction.allow
)
rbac_repo = RbacRepository(db_session)
existing_permission = await rbac_repo.get_permission_by_path("/projects")
if existing_permission:
return existing_permission
return await rbac_repo.create_permission(new_permission)
class TestRolesPermissionsRoutes:
async def test_add_permission_to_role(
self,
app: FastAPI,
client: AsyncClient,
test_permission: Permission,
db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("User")
response = await client.put(
app.url_path_for(
"add_permission_to_role",
role_id=role_in_db.role_id,
permission_id=str(test_permission.permission_id)
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
permissions = await rbac_repo.get_role_permissions(role_in_db.role_id)
assert len(permissions) == 1
assert permissions[0].path == test_permission.path
async def test_get_role_permissions(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("User")
response = await client.get(
app.url_path_for(
"get_role_permissions",
role_id=role_in_db.role_id)
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
async def test_remove_role_from_group(
self,
app: FastAPI,
client: AsyncClient,
test_permission: Permission,
db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("User")
response = await client.delete(
app.url_path_for(
"remove_permission_from_role",
role_id=role_in_db.role_id,
permission_id=str(test_permission.permission_id)
),
)
assert response.status_code == status.HTTP_204_NO_CONTENT
permissions = await rbac_repo.get_role_permissions(role_in_db.role_id)
assert len(permissions) == 0
Loading…
Cancel
Save