Skip to content

Commit 5e43f77

Browse files
fix(auth): preserve hosted dashboard principals
1 parent 872581f commit 5e43f77

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

engraphis/dashboard_app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1292,10 +1292,15 @@ def cancel_obsidian_job_alias(job_id: str, request: Request, workspace: str = Fo
12921292
@app.middleware("http")
12931293
async def _auth_gate(request: Request, call_next):
12941294
from engraphis.service import set_current_user
1295+
from engraphis.service_context import bound_service
12951296

12961297
# The open runtime has no hosted identity model. Clear any context inherited from
12971298
# embedding applications and authorize the whole local instance as one principal.
1298-
set_current_user(None)
1299+
# An embedding host may instead bind a tenant service and validated principal for
1300+
# this request; preserve that identity so personal-workspace enforcement remains
1301+
# active through dashboard and mounted MCP dispatch.
1302+
if bound_service() is None:
1303+
set_current_user(None)
12991304
path = request.url.path
13001305
if request.method == "OPTIONS":
13011306
return await call_next(request)

tests/test_dashboard_v2.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,57 @@ def test_dashboard_create_workspace_succeeds_when_unbound(monkeypatch, tmp_path)
165165
assert response.json()["created"] is True
166166

167167

168+
def test_dashboard_preserves_hosted_principal_for_personal_access(
169+
monkeypatch, tmp_path,
170+
):
171+
import anyio
172+
import httpx
173+
174+
from engraphis.dashboard_app import create_app
175+
from engraphis.service import current_user, set_current_user
176+
from engraphis.service_context import bind_service
177+
178+
monkeypatch.setattr(settings, "db_path", str(tmp_path / "hosted-dashboard.db"))
179+
monkeypatch.setattr(settings, "embed_model", "")
180+
monkeypatch.setattr(settings, "embed_dim", 384)
181+
monkeypatch.setattr(settings, "allowed_workspaces", [])
182+
monkeypatch.setattr(settings, "api_token", "")
183+
app = create_app()
184+
hosted = MemoryService.create(":memory:", extractor="none")
185+
owner = {"id": "member_bob", "email": "bob@example.test", "role": "member"}
186+
principal = {"id": "member_alice", "email": "alice@example.test", "role": "member"}
187+
set_current_user(owner)
188+
hosted.create_workspace("bob-private", visibility="personal")
189+
set_current_user(None)
190+
191+
@app.get("/api/test-hosted-personal", include_in_schema=False)
192+
def hosted_personal_probe():
193+
from engraphis.routes.v2_api import service
194+
from engraphis.service import ValidationError
195+
196+
try:
197+
service()._enforce_personal_access("bob-private")
198+
except ValidationError:
199+
allowed = False
200+
else:
201+
allowed = True
202+
return {"allowed": allowed, "user": current_user()}
203+
204+
async def request_probe():
205+
transport = httpx.ASGITransport(app=app, client=("127.0.0.1", 50000))
206+
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
207+
return await client.get("/api/test-hosted-personal")
208+
209+
try:
210+
with bind_service(hosted, principal=principal):
211+
response = anyio.run(request_probe)
212+
assert response.status_code == 200, response.text
213+
assert response.json() == {"allowed": False, "user": principal}
214+
finally:
215+
hosted.close()
216+
app.state.service.close()
217+
218+
168219
def test_dashboard_ignores_legacy_workspace_binding_setting(monkeypatch, tmp_path):
169220
monkeypatch.setattr(settings, "db_path", str(tmp_path / "legacy-binding.db"))
170221
monkeypatch.setattr(settings, "embed_model", "")

0 commit comments

Comments
 (0)