Merge branch '3.1' into base-configs-3.0

This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2026-06-01 11:39:40 +02:00
commit 12347b24aa
40 files changed
+3093 -204

No files matched your search

+15 -1
View File
@@ -226,6 +226,18 @@ def create_default_roles(target, connection, **kw):
{
"description": "View an appliance",
"name": "Appliance.Audit"
},
{
"description": "Create or delete an LLM model configuration",
"name": "LLMConfig.Allocate"
},
{
"description": "View an LLM model configuration",
"name": "LLMConfig.Audit"
},
{
"description": "Update an LLM model configuration",
"name": "LLMConfig.Modify"
}
]
@@ -295,7 +307,9 @@ def add_privileges_to_default_roles(target, connection, **kw):
"Image.Audit",
"Compute.Audit",
"Appliance.Allocate",
"Appliance.Audit"
"Appliance.Audit",
"LLMConfig.Audit",
"LLMConfig.Modify"
)
add_privileges_to_role(target, connection, "User", user_privileges)
+9
View File
@@ -156,6 +156,14 @@ class ResourcePoolsRepository(BaseRepository):
Delete a resource pool.
"""
# Get all resources in the pool first
resources = await self.get_pool_resources(resource_pool_id)
# Delete all resource records
for resource in resources:
await self.delete_resource(resource.resource_id)
# Now delete the resource pool
query = delete(models.ResourcePool).where(models.ResourcePool.resource_pool_id == resource_pool_id)
result = await self._db_session.execute(query)
await self._db_session.commit()
@@ -203,6 +211,7 @@ class ResourcePoolsRepository(BaseRepository):
resource_pool_db.resources.remove(resource)
await self._db_session.commit()
await self._db_session.refresh(resource_pool_db)
return resource_pool_db
async def get_pool_resources(self, resource_pool_id: UUID) -> List[models.Resource]:
+105 -5
View File
@@ -230,6 +230,25 @@ class RbacRepository(BaseRepository):
result = await self._db_session.execute(query)
return result.scalars().all()
async def get_aces_for_path(self, path: str) -> List[models.ACE]:
"""
Get all ACEs for a specific path (exact match or starting with path).
This method includes related user, group, and role information.
"""
query = select(models.ACE).\
where(
(models.ACE.path == path) | (models.ACE.path.startswith(path + "/"))
).\
options(
selectinload(models.ACE.user),
selectinload(models.ACE.group),
selectinload(models.ACE.role)
)
result = await self._db_session.execute(query)
return result.scalars().all()
async def check_ace_exists(self, path: str) -> bool:
"""
Check if an ACE exists.
@@ -385,6 +404,83 @@ class RbacRepository(BaseRepository):
pool_resources.extend(await self._get_resources_in_pools(group_aces))
return list(set(pool_resources))
async def get_accessible_project_ids(
self,
user_id: UUID,
privilege_name: str,
all_project_ids: List[str]
):
"""
Batch check which projects a user can access via direct ACE or resource pools.
Performs 3 fixed DB queries regardless of project count.
Returns:
(direct_ace_ids, pool_accessible_ids) where:
- direct_ace_ids: projects the user has a direct ACE on (needs created_by filter)
- pool_accessible_ids: projects in resource pools the user can access (no created_by filter)
"""
user_aces = await self._get_user_aces(user_id, privilege_name)
group_aces = await self._get_group_aces(user_id, privilege_name)
# Single query for all resources and their pool memberships
query = select(models.Resource).options(selectinload(models.Resource.resource_pools))
result = await self._db_session.execute(query)
all_resources = result.scalars().all()
# Precompute pool_id -> set of project_ids
pool_to_projects = {}
for r in all_resources:
if r.resource_type == "project":
for pool in r.resource_pools:
pool_to_projects.setdefault(str(pool.resource_pool_id), set()).add(str(r.resource_id))
# --- User ACE: direct path check ---
direct_ace_ids = set()
user_denied = set()
for pid in all_project_ids:
path = f"/projects/{pid}"
try:
if self._check_path_with_aces(path, user_aces):
direct_ace_ids.add(pid)
except PermissionError:
user_denied.add(pid)
# --- User ACE: pool check ---
user_pool_ids = {ace_path.split("/")[2] for ace_path, _, ace_allowed, _ in user_aces
if ace_path.startswith("/pools/") and ace_allowed}
pool_accessible_ids = set()
for pool_id, project_ids in pool_to_projects.items():
if pool_id in user_pool_ids:
for pid in project_ids:
if pid not in user_denied:
pool_accessible_ids.add(pid)
# --- Group ACE: direct path check (skip already accessible or denied) ---
for pid in all_project_ids:
if pid in direct_ace_ids or pid in user_denied:
continue
path = f"/projects/{pid}"
try:
if self._check_path_with_aces(path, group_aces):
direct_ace_ids.add(pid)
except PermissionError:
pass
# --- Group ACE: pool check ---
group_pool_ids = {ace_path.split("/")[2] for ace_path, _, ace_allowed, _ in group_aces
if ace_path.startswith("/pools/") and ace_allowed}
for pool_id, project_ids in pool_to_projects.items():
if pool_id in group_pool_ids:
for pid in project_ids:
if pid not in user_denied and pid not in pool_accessible_ids:
pool_accessible_ids.add(pid)
return direct_ace_ids, pool_accessible_ids
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
@@ -409,22 +505,26 @@ class RbacRepository(BaseRepository):
aces = await self._get_user_aces(user_id, privilege_name)
try:
# Check regular ACEs first
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
# Then check resource pool 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):
# 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:
# Check regular ACEs first
if self._check_path_with_aces(path, aces):
return True
# Then check resource pool 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