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

+4 -8
View File
@@ -38,7 +38,6 @@ 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
@@ -58,8 +57,7 @@ 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)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
) -> List[dict]:
"""
List all endpoints to be used in ACL entries.
@@ -141,11 +139,9 @@ 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")
# Resource pools are not included in "all endpoints" to prevent accidental access
# They must be explicitly configured for team sharing
return endpoints
@@ -66,13 +66,13 @@ def _filter_api_key_from_config(config: dict) -> dict:
)
async def get_user_llm_model_configs(
user_id: UUID,
current_user: schemas.User = Depends(has_privilege("User.Audit")),
current_user: schemas.User = Depends(has_privilege("LLMConfig.Audit")),
llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository))
) -> schemas.LLMModelConfigInheritedResponse:
"""
Get user's effective LLM model configurations (own + inherited from groups).
Required privilege: User.Audit
Required privilege: LLMConfig.Audit
"""
try:
@@ -97,7 +97,7 @@ async def get_user_llm_model_configs(
@router.get(
"/users/{user_id}/llm-model-configs/own",
response_model=List[schemas.LLMModelConfigResponse],
dependencies=[Depends(has_privilege("User.Audit"))]
dependencies=[Depends(has_privilege("LLMConfig.Audit"))]
)
async def get_user_own_llm_model_configs(
user_id: UUID,
@@ -106,7 +106,7 @@ async def get_user_own_llm_model_configs(
"""
Get user's own LLM model configurations (excluding inherited ones).
Required privilege: User.Audit
Required privilege: LLMConfig.Audit
"""
try:
@@ -137,7 +137,7 @@ async def get_user_own_llm_model_configs(
@router.get(
"/users/{user_id}/llm-model-configs/default",
response_model=schemas.LLMModelConfigResponse,
dependencies=[Depends(has_privilege("User.Audit"))]
dependencies=[Depends(has_privilege("LLMConfig.Audit"))]
)
async def get_user_default_llm_model_config(
user_id: UUID,
@@ -146,7 +146,7 @@ async def get_user_default_llm_model_config(
"""
Get user's default LLM model configuration.
Required privilege: User.Audit
Required privilege: LLMConfig.Audit
"""
try:
@@ -183,7 +183,7 @@ async def get_user_default_llm_model_config(
"/users/{user_id}/llm-model-configs",
response_model=schemas.LLMModelConfigResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("User.Modify"))]
dependencies=[Depends(has_privilege("LLMConfig.Modify"))]
)
async def create_user_llm_model_config(
user_id: UUID,
@@ -194,7 +194,7 @@ async def create_user_llm_model_config(
"""
Create a new LLM model configuration for a user.
Required privilege: User.Modify
Required privilege: LLMConfig.Modify
IMPORTANT: context_limit is REQUIRED. Unit is K tokens (e.g., 128 = 128K = 128,000 tokens).
Please check your model provider's documentation for the current context window size.
@@ -249,7 +249,7 @@ async def create_user_llm_model_config(
@router.put(
"/users/{user_id}/llm-model-configs/{config_id}",
response_model=schemas.LLMModelConfigResponse,
dependencies=[Depends(has_privilege("User.Modify"))]
dependencies=[Depends(has_privilege("LLMConfig.Modify"))]
)
async def update_user_llm_model_config(
user_id: UUID,
@@ -261,7 +261,7 @@ async def update_user_llm_model_config(
Update a user's LLM model configuration.
Supports optimistic locking via expected_version field.
Required privilege: User.Modify
Required privilege: LLMConfig.Modify
"""
try:
@@ -317,7 +317,7 @@ async def update_user_llm_model_config(
@router.delete(
"/users/{user_id}/llm-model-configs/{config_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("User.Modify"))]
dependencies=[Depends(has_privilege("LLMConfig.Modify"))]
)
async def delete_user_llm_model_config(
user_id: UUID,
@@ -327,7 +327,7 @@ async def delete_user_llm_model_config(
"""
Delete a user's LLM model configuration.
Required privilege: User.Modify
Required privilege: LLMConfig.Modify
"""
try:
@@ -350,7 +350,7 @@ async def delete_user_llm_model_config(
@router.put(
"/users/{user_id}/llm-model-configs/default/{config_id}",
response_model=schemas.LLMModelConfigResponse,
dependencies=[Depends(has_privilege("User.Modify"))]
dependencies=[Depends(has_privilege("LLMConfig.Modify"))]
)
async def set_user_default_llm_model_config(
user_id: UUID,
@@ -360,7 +360,7 @@ async def set_user_default_llm_model_config(
"""
Set a user's default LLM model configuration.
Required privilege: User.Modify
Required privilege: LLMConfig.Modify
"""
try:
+29 -1
View File
@@ -147,10 +147,38 @@ async def delete_resource_pool(
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
# Check if there are any ACE configurations using this resource pool path
pool_path = f"/pools/{resource_pool_id}"
using_aces = await rbac_repo.get_aces_for_path(pool_path)
if using_aces:
# Build detailed error message with ACE information
ace_details = []
for ace in using_aces:
identifier = ""
if ace.ace_type == "user" and ace.user:
identifier = f"User '{ace.user.username}'"
elif ace.ace_type == "group" and ace.group:
identifier = f"Group '{ace.group.name}'"
else:
identifier = f"{ace.ace_type.capitalize()} '{ace.user_id or ace.group_id}'"
if ace.role:
identifier += f" with role '{ace.role.name}'"
ace_details.append(f"- {identifier}")
error_message = (
f"Resource pool '{resource_pool.name}' cannot be deleted because it is being used by {len(using_aces)} ACE configuration(s):\n"
+ "\n".join(ace_details)
+ f"\n\nPlease delete the ACE configuration(s) for resource pool '{resource_pool.name}' first."
)
raise ControllerBadRequestError(error_message)
success = await pools_repo.delete_resource_pool(resource_pool_id)
if not success:
raise ControllerError(f"Resource pool '{resource_pool_id}' could not be deleted")
await rbac_repo.delete_all_ace_starting_with_path(f"/pools/{resource_pool_id}")
await rbac_repo.delete_all_ace_starting_with_path(pool_path)
@router.get(
+21 -9
View File
@@ -76,7 +76,6 @@ def dep_project(project_id: UUID) -> 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.
@@ -86,19 +85,32 @@ async def get_projects(
controller = Controller.instance()
projects = []
seen_project_ids = set() # track seen projects to avoid duplicates
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])
# Batch ACE + resource pool check (3 DB queries regardless of project count)
all_project_ids = list(controller.projects.keys())
direct_ace_ids, pool_accessible_ids = await rbac_repo.get_accessible_project_ids(
current_user.user_id, "Project.Audit", all_project_ids
)
# Step 2: Filter direct ACE projects by created_by
# Direct project sharing is only available through resource pools
for p in controller.projects.values():
if p.id in direct_ace_ids and p.created_by == current_user.username:
if p.id not in seen_project_ids:
projects.append(p.asdict())
seen_project_ids.add(p.id)
# Step 3: Resource pool projects (no created_by filter)
for p in controller.projects.values():
if p.id in pool_accessible_ids:
if p.id not in seen_project_ids:
projects.append(p.asdict())
seen_project_ids.add(p.id)
return projects