Complete resource pool support for projects

pull/2280/head
grossmj 9 months ago
parent d53ef175f8
commit a95dda0d1d

@ -38,6 +38,7 @@ from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
@ -57,7 +58,8 @@ async def endpoints(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[dict]:
"""
List all endpoints to be used in ACL entries.
@ -128,6 +130,11 @@ async def endpoints(
for template in templates:
add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template")
# resource pools
add_to_endpoints("/pools", "All resource pools", "pool")
pools = await pools_repo.get_resource_pools()
for pool in pools:
add_to_endpoints(f"/pools/{pool.resource_pool_id}", f'Resource pool "{pool.name}"', "pool")
return endpoints

@ -47,9 +47,11 @@ from gns3server.utils.asyncio import aiozipstream
from gns3server.utils.path import is_safe_path
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from gns3server.services.templates import TemplatesService
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
@ -69,10 +71,13 @@ def dep_project(project_id: UUID) -> Project:
@router.get(
"",
response_model=List[schemas.Project],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Project.Audit"))]
response_model_exclude_unset=True
)
async def get_projects() -> List[schemas.Project]:
async def get_projects(
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[schemas.Project]:
"""
Return all projects.
@ -80,7 +85,22 @@ async def get_projects() -> List[schemas.Project]:
"""
controller = Controller.instance()
return [p.asdict() for p in controller.projects.values()]
projects = []
if current_user.is_superadmin:
# super admin sees all projects
return [p.asdict() for p in controller.projects.values()]
elif await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Audit"):
# user with Project.Audit privilege on '/projects' sees all projects except those in resource pools
project_ids_in_pools = [str(r.resource_id) for r in await pools_repo.get_resources() if r.resource_type == "project"]
projects.extend([p.asdict() for p in controller.projects.values() if p.id not in project_ids_in_pools])
# user with Project.Audit privilege on resource pools sees the projects in these pools
user_pool_resources = await rbac_repo.get_user_pool_resources(current_user.user_id, "Project.Audit")
project_ids_in_pools = [str(r.resource_id) for r in user_pool_resources if r.resource_type == "project"]
projects.extend([p.asdict() for p in controller.projects.values() if p.id in project_ids_in_pools])
return projects
@router.post(

@ -81,6 +81,23 @@ async def create_role(
return await rbac_repo.create_role(role_create)
@router.get(
"/privileges",
response_model=List[schemas.Privilege],
dependencies=[Depends(has_privilege("Role.Audit"))]
)
async def get_privileges(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> List[schemas.Privilege]:
"""
Get all available privileges.
Required privilege: Role.Audit
"""
return await rbac_repo.get_privileges()
@router.get(
"/{role_id}",
response_model=schemas.Role,

@ -309,19 +309,37 @@ class RbacRepository(BaseRepository):
return True # only allow if the path is the original path or the ACE is set to propagate
return False
async def check_user_has_privilege(self, user_id: UUID, path: str, privilege_name: str) -> bool:
async def _get_resources_in_pools(self, aces, path: str = None) -> List[models.Resource]:
"""
Get all resources in pools.
"""
Resource paths form a file system like tree and privileges can be inherited by paths down that tree
(the propagate field is True by default)
The following inheritance rules are used:
pool_resources = []
for ace_path, ace_propagate, ace_allowed, ace_privilege in aces:
if ace_path.startswith("/pool"):
resource_pool_id = ace_path.split("/")[2]
query = select(models.Resource). \
join(models.Resource.resource_pools). \
filter(models.ResourcePool.resource_pool_id == resource_pool_id)
* Privileges for individual users always replace group privileges.
* Privileges for groups apply when the user is member of that group.
* Privileges on deeper levels replace those inherited from an upper level.
result = await self._db_session.execute(query)
resources = result.scalars().all()
for resource in resources:
# we only support projects in resource pools for now
if resource.resource_type == "project":
if path:
if path.startswith(f"/projects/{resource.resource_id}"):
pool_resources.append(resource)
else:
pool_resources.append(resource)
return pool_resources
async def _get_user_aces(self, user_id: UUID, privilege_name: str):
"""
Retrieve all user ACEs matching the user_id and privilege name.
"""
# retrieve all user ACEs matching the user_id and privilege name
query = select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name).\
join(models.Privilege.roles).\
join(models.Role.acl_entries).\
@ -331,16 +349,13 @@ class RbacRepository(BaseRepository):
order_by(models.ACE.path.desc())
result = await self._db_session.execute(query)
aces = result.all()
return result.all()
try:
if self._check_path_with_aces(path, aces):
# the user has an ACE matching the path and privilege,there is no need to check group ACEs
return True
except PermissionError:
return False
async def _get_group_aces(self, user_id: UUID, privilege_name: str):
"""
Retrieve all group ACEs matching the user_id and privilege name.
"""
# retrieve all group ACEs matching the user_id and privilege name
query = select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name). \
join(models.Privilege.roles). \
join(models.Role.acl_entries). \
@ -350,9 +365,59 @@ class RbacRepository(BaseRepository):
filter(models.Privilege.name == privilege_name)
result = await self._db_session.execute(query)
aces = result.all()
return result.all()
async def get_user_pool_resources(self, user_id: UUID, privilege_name: str) -> List[models.Resource]:
"""
Get all resources in pools belonging to a user and groups
"""
user_aces = await self._get_user_aces(user_id, privilege_name)
pool_resources = await self._get_resources_in_pools(user_aces)
group_aces = await self._get_group_aces(user_id, privilege_name)
pool_resources.extend(await self._get_resources_in_pools(group_aces))
return list(set(pool_resources))
async def check_user_has_privilege(self, user_id: UUID, path: str, privilege_name: str) -> bool:
"""
Resource paths form a file system like tree and privileges can be inherited by paths down that tree
(the propagate field is True by default)
The following inheritance rules are used:
* Privileges for individual users always replace group privileges.
* Privileges for groups apply when the user is member of that group.
* Privileges on deeper levels replace those inherited from an upper level.
"""
query = select(models.Resource)
result = await self._db_session.execute(query)
resources = result.scalars().all()
projects_in_pools = [f"/projects/{r.resource_id}" for r in resources if r.resource_type == "project"]
path_is_in_pool = False
for project_in_pool in projects_in_pools:
if path.startswith(project_in_pool):
path_is_in_pool = True
break
aces = await self._get_user_aces(user_id, privilege_name)
try:
if path_is_in_pool:
if await self._get_resources_in_pools(aces, path):
return True
elif self._check_path_with_aces(path, aces):
# the user has an ACE matching the path and privilege, there is no need to check group ACEs
return True
except PermissionError:
return False
aces = await self._get_group_aces(user_id, privilege_name)
try:
return self._check_path_with_aces(path, aces)
if path_is_in_pool:
if await self._get_resources_in_pools(aces, path):
return True
elif self._check_path_with_aces(path, aces):
return True
except PermissionError:
return False
return False

@ -42,6 +42,12 @@ class TestRolesRoutes:
assert response.status_code == status.HTTP_200_OK
assert response.json()["role_id"] == str(role_in_db.role_id)
async def test_get_privileges(self, app: FastAPI, client: AsyncClient):
response = await client.get(app.url_path_for("get_privileges"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 45 # 45 built-in privileges
async def test_list_roles(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_roles"))

@ -17,14 +17,18 @@
import pytest
import pytest_asyncio
import uuid
from fastapi import FastAPI, status
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.controller import Controller
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from gns3server.schemas.controller.rbac import ACECreate
from gns3server.schemas.controller.pools import ResourceCreate, ResourcePoolCreate
from gns3server.db.models import User
pytestmark = pytest.mark.asyncio
@ -128,6 +132,113 @@ class TestPrivileges:
authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege)
assert authorized is True
class TestResourcePools:
async def test_resource_pool(self, test_user: User, db_session: AsyncSession):
project_id = uuid.uuid4()
project_name = "project42"
pools_repo = ResourcePoolsRepository(db_session)
new_resource_pool = ResourcePoolCreate(name="pool1")
pool_in_db = await pools_repo.create_resource_pool(new_resource_pool)
resource_create = ResourceCreate(resource_id=project_id, resource_type="project", name=project_name)
resource = await pools_repo.create_resource(resource_create)
await pools_repo.add_resource_to_pool(pool_in_db.resource_pool_id, resource)
group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id
role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id
ace = ACECreate(
path=f"/pools/{pool_in_db.resource_pool_id}",
ace_type="group",
propagate=False,
group_id=str(group_id),
role_id=str(role_id)
)
await RbacRepository(db_session).create_ace(ace)
privilege = "Project.Audit"
path = f"/projects/{project_id}"
authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege)
assert authorized is True
async def test_list_projects_in_resource_pool(
self,
app: FastAPI,
controller: Controller,
authorized_client: AsyncClient,
db_session: AsyncSession
) -> None:
uuid1 = str(uuid.uuid4())
uuid2 = str(uuid.uuid4())
uuid3 = str(uuid.uuid4())
await controller.add_project(project_id=uuid1, name="Project1")
await controller.add_project(project_id=uuid2, name="Project2")
await controller.add_project(project_id=uuid3, name="Project3")
# user has no access to projects (no ACE on /projects or resource pools)
response = await authorized_client.get(app.url_path_for("get_projects"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 0
pools_repo = ResourcePoolsRepository(db_session)
new_resource_pool = ResourcePoolCreate(name="pool2")
pool_in_db = await pools_repo.create_resource_pool(new_resource_pool)
resource_create = ResourceCreate(resource_id=uuid2, resource_type="project", name="Project2")
resource = await pools_repo.create_resource(resource_create)
await pools_repo.add_resource_to_pool(pool_in_db.resource_pool_id, resource)
group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id
role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id
ace = ACECreate(
path=f"/pools/{pool_in_db.resource_pool_id}",
ace_type="group",
propagate=False,
group_id=str(group_id),
role_id=str(role_id)
)
await RbacRepository(db_session).create_ace(ace)
response = await authorized_client.get(app.url_path_for("get_project", project_id=uuid2))
assert response.status_code == status.HTTP_200_OK
assert response.json()["name"] == "Project2"
# user should only see one project because it is in the resource pool he has access to
response = await authorized_client.get(app.url_path_for("get_projects"))
assert response.status_code == status.HTTP_200_OK
projects = response.json()
assert len(projects) == 1
assert projects[0]["project_id"] == uuid2
ace = ACECreate(
path=f"/projects",
ace_type="group",
propagate=True,
group_id=str(group_id),
role_id=str(role_id)
)
await RbacRepository(db_session).create_ace(ace)
# now user should see all projects because he has access to /projects and the resource pool
response = await authorized_client.get(app.url_path_for("get_projects"))
assert response.status_code == status.HTTP_200_OK
projects = response.json()
assert len(projects) == 3
await RbacRepository(db_session).delete_all_ace_starting_with_path(f"/pools/{pool_in_db.resource_pool_id}")
response = await authorized_client.get(app.url_path_for("get_project", project_id=uuid2))
assert response.status_code == status.HTTP_403_FORBIDDEN
# now user should only see the projects that are not in a resource pool
response = await authorized_client.get(app.url_path_for("get_projects"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 2
# class TestProjectsWithRbac:
#
# async def test_admin_create_project(self, app: FastAPI, client: AsyncClient):

Loading…
Cancel
Save