fix(P2.2-sec): visibility-gate cross-tenant custom-library resolution
The compile scope resolved a project OWNER's per-user custom libraries for ANY project_id a requester supplied, with no visibility check — so a requester who knew a victim's PRIVATE project_id + custom-lib name could compile a binary against the victim's private uploaded library. Replace the ungated get_project_owner hook with resolve_compile_owner(project_id, requester_id), which returns the owner ONLY when the requester IS the owner OR the project is shareable (public/unlisted); otherwise None -> the caller falls back to the requester's OWN store. Fails closed on any error. Also gate the server-side manifest fallback by the same rule (symmetry): a private project's declared library NAMES are no longer read into a non-owner's compile scope. The owner's own compile and shared/embed compiles of public/ unlisted projects keep their backend-authoritative scope unchanged.
This commit is contained in:
parent
d3f06265ac
commit
1aae505aa7
|
|
@ -11,7 +11,7 @@ from pydantic import BaseModel
|
|||
from app.core.hooks import (
|
||||
get_current_user_id,
|
||||
get_project_libraries,
|
||||
get_project_owner,
|
||||
resolve_compile_owner,
|
||||
record_compile,
|
||||
)
|
||||
from app.services.arduino_cli import ArduinoCLIService
|
||||
|
|
@ -234,19 +234,29 @@ async def _resolve_compile_scope(
|
|||
|
||||
Owner = whose per-user custom libraries the manifest may reference: the
|
||||
project OWNER for a saved project (so a shared/embed compile finds that
|
||||
owner's libs), else the REQUESTER for an unsaved compile (the libs they just
|
||||
uploaded are their own). None for anon.
|
||||
owner's libs) — but ONLY when the requester is the owner or the project is
|
||||
shareable (public/unlisted), so a private project's custom libs are never
|
||||
reachable by another user (resolve_compile_owner enforces this gate). Falls
|
||||
back to the REQUESTER for an unsaved / private-non-owner / anon compile (the
|
||||
libs they just uploaded are their own).
|
||||
"""
|
||||
# Resolve the visibility-gated owner FIRST: a non-None result means the
|
||||
# requester may read THIS project's server-side state (it is their own, or
|
||||
# public/unlisted). That same gate decides whether the saved-project manifest
|
||||
# may be honored — so a PRIVATE project's declared library NAMES are never
|
||||
# exposed to a non-owner via the server-side fallback (symmetry with the
|
||||
# owner-bytes gate; P2.2-sec).
|
||||
gated_owner = await resolve_compile_owner(request.project_id, requester_id)
|
||||
|
||||
allowed_libraries: set[str] | None = None
|
||||
if request.libraries:
|
||||
allowed_libraries = set(request.libraries)
|
||||
else:
|
||||
elif gated_owner is not None:
|
||||
project_libs = await get_project_libraries(request.project_id)
|
||||
if project_libs:
|
||||
allowed_libraries = set(project_libs)
|
||||
owner_id = await get_project_owner(request.project_id)
|
||||
if owner_id is None:
|
||||
owner_id = requester_id
|
||||
|
||||
owner_id = gated_owner if gated_owner is not None else requester_id
|
||||
return allowed_libraries, owner_id
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -175,30 +175,39 @@ def materialize_library_scope(
|
|||
return None
|
||||
|
||||
|
||||
# ── get_project_owner ─────────────────────────────────────────────────────────
|
||||
# Resolve a project's OWNER user-id from its id (the value stored as user_id on
|
||||
# the project record). Used so a compile resolves the OWNER's per-user custom
|
||||
# libraries, not the requester's. Returns None for an unknown project or when no
|
||||
# overlay is loaded.
|
||||
# ── resolve_compile_owner ─────────────────────────────────────────────────────
|
||||
# Resolve WHOSE per-user custom libraries a compile may resolve for a project —
|
||||
# applying a VISIBILITY gate. A compile of a saved project resolves the OWNER's
|
||||
# custom libraries (so a shared / embed compile of someone's PUBLIC project still
|
||||
# finds that owner's uploaded libs), but a requester must NOT be able to pull
|
||||
# another user's PRIVATE custom libraries by supplying that user's project_id.
|
||||
# The overlay returns the project owner ONLY when the requester IS the owner OR
|
||||
# the project is shareable (public / unlisted); otherwise None, and the caller
|
||||
# falls back to the requester's OWN store. `requester_id` is the authenticated
|
||||
# caller (None for anon). Returns None for an unknown project or no overlay.
|
||||
|
||||
GetProjectOwnerHook = Callable[[str], Awaitable[Optional[str]]]
|
||||
ResolveCompileOwnerHook = Callable[[str, Optional[str]], Awaitable[Optional[str]]]
|
||||
|
||||
_get_project_owner_hook: Optional[GetProjectOwnerHook] = None
|
||||
_resolve_compile_owner_hook: Optional[ResolveCompileOwnerHook] = None
|
||||
|
||||
|
||||
def register_get_project_owner(hook: GetProjectOwnerHook) -> None:
|
||||
"""Install the project-owner resolver. Called by overlays in register_pro."""
|
||||
global _get_project_owner_hook
|
||||
_get_project_owner_hook = hook
|
||||
def register_resolve_compile_owner(hook: ResolveCompileOwnerHook) -> None:
|
||||
"""Install the visibility-gated compile-owner resolver. Called in register_pro."""
|
||||
global _resolve_compile_owner_hook
|
||||
_resolve_compile_owner_hook = hook
|
||||
|
||||
|
||||
async def get_project_owner(project_id: Optional[str]) -> Optional[str]:
|
||||
if _get_project_owner_hook is None or not project_id:
|
||||
async def resolve_compile_owner(
|
||||
project_id: Optional[str], requester_id: Optional[str]
|
||||
) -> Optional[str]:
|
||||
if _resolve_compile_owner_hook is None or not project_id:
|
||||
return None
|
||||
try:
|
||||
return await _get_project_owner_hook(project_id)
|
||||
return await _resolve_compile_owner_hook(project_id, requester_id)
|
||||
except Exception:
|
||||
logger.exception("get_project_owner hook failed (treating as no owner)")
|
||||
# Fail closed: on any error resolve no foreign owner (caller falls back
|
||||
# to the requester's own store), never leak another user's libraries.
|
||||
logger.exception("resolve_compile_owner hook failed (treating as requester-only)")
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue