Auth (middleware/iam_auth_middleware.py): local JWKS JWT validation against
Hanzo IAM (signature + exp + iss + aud=hanzo-studio; org from the `owner`
claim). Standard OIDC Authorization Code flow with PKCE and a /callback
session cookie for the standalone studio.hanzo.ai app; anonymous localhost
bypass unchanged. verify_jwt() extracted as a pure, unit-tested function.
Tenancy (folder_paths.py, app/user_manager.py): make get_public_user_directory
org-rooted so multi-tenant userdata resolves under user/orgs/{org}/... (it
previously always returned None). Scope execution outputs to output/orgs/{org}/
via set_execution_org(), bound by the prompt worker from the queue item org_id.
Also block a token subject from escaping into a System User namespace.
Federation (middleware/{worker_client,prompt_router}.py, server.py): move the
worker registry + execute surface to /v1/workers/register, /v1/workers,
/v1/worker/execute; add X-Worker-Token coordinator trust. Outbound pull channel
for NAT'd local boxes (GB10) specced in docs/federation.md.
Deploy: hanzo.yml + .github/workflows/cicd.yml (hanzoai/ci reusable) build
ghcr.io/hanzoai/studio and roll the studio operator Service CR at
studio.hanzo.ai. PyJWT[crypto] pinned.
Tests: tests-unit/middleware_test/iam_auth_test.py (RS256 sign/verify, state
CSRF, path exemptions) + folder_paths_test/org_scoping_test.py; 140 passing
across the auth/tenancy/user-manager/prompt-server suites.
96 lines
4.1 KiB
Python
96 lines
4.1 KiB
Python
"""Tests for multi-tenant org scoping in folder_paths.py
|
|
|
|
Covers:
|
|
- get_public_user_directory(user, org): org-rooted user dir + System User block
|
|
- The containment invariant user_manager relies on (regression for the bug
|
|
where org userdata always resolved outside the org root -> None)
|
|
- Execution-org context scoping get_output_directory / temp / input
|
|
- Single-tenant behavior is unchanged
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
import folder_paths
|
|
|
|
|
|
@pytest.fixture
|
|
def tenant_env(tmp_path, monkeypatch):
|
|
"""Point folder_paths at a temp base and enable multi-tenant mode."""
|
|
base = str(tmp_path)
|
|
monkeypatch.setattr(folder_paths, "base_path", base)
|
|
monkeypatch.setattr(folder_paths, "output_directory", os.path.join(base, "output"))
|
|
monkeypatch.setattr(folder_paths, "input_directory", os.path.join(base, "input"))
|
|
monkeypatch.setattr(folder_paths, "temp_directory", os.path.join(base, "temp"))
|
|
monkeypatch.setattr(folder_paths, "user_directory", os.path.join(base, "user"))
|
|
monkeypatch.setattr(folder_paths, "_org_id", None)
|
|
folder_paths.set_execution_org(None)
|
|
yield base
|
|
folder_paths.set_execution_org(None)
|
|
|
|
|
|
def test_single_tenant_userdir_unchanged(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", False)
|
|
assert folder_paths.get_public_user_directory("default") == \
|
|
os.path.join(tenant_env, "user", "default")
|
|
|
|
|
|
def test_single_tenant_output_unchanged(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", False)
|
|
folder_paths.set_execution_org("acme") # ignored when not multi-tenant
|
|
assert folder_paths.get_output_directory() == os.path.join(tenant_env, "output")
|
|
|
|
|
|
def test_system_user_blocked_with_org(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", True)
|
|
assert folder_paths.get_public_user_directory("__system", "acme") is None
|
|
assert folder_paths.get_public_user_directory("", "acme") is None
|
|
assert folder_paths.get_public_user_directory(None, "acme") is None
|
|
|
|
|
|
def test_org_user_dir_is_org_rooted(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", True)
|
|
path = folder_paths.get_public_user_directory("alice", "acme")
|
|
assert path == os.path.join(tenant_env, "orgs", "acme", "user", "alice")
|
|
|
|
|
|
def test_containment_invariant(tenant_env, monkeypatch):
|
|
"""user_manager requires user_root to be a child of the org user root.
|
|
|
|
This is the exact relation that regressed: get_org_user_directory(org) and
|
|
get_public_user_directory(user, org) must share the org root as commonpath.
|
|
"""
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", True)
|
|
root = folder_paths.get_org_user_directory("acme")
|
|
user_root = folder_paths.get_public_user_directory("alice", "acme")
|
|
assert os.path.commonpath((root, user_root)) == root
|
|
|
|
|
|
def test_two_orgs_isolated(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", True)
|
|
a = folder_paths.get_public_user_directory("alice", "acme")
|
|
b = folder_paths.get_public_user_directory("alice", "globex")
|
|
assert a != b
|
|
# Distinct org roots: they share only the orgs/ parent, never each other.
|
|
assert os.path.commonpath((a, b)) == os.path.join(tenant_env, "orgs")
|
|
|
|
|
|
def test_execution_org_scopes_output(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", True)
|
|
folder_paths.set_execution_org("acme")
|
|
assert folder_paths.get_output_directory() == \
|
|
os.path.join(tenant_env, "orgs", "acme", "output")
|
|
assert folder_paths.get_temp_directory() == \
|
|
os.path.join(tenant_env, "orgs", "acme", "temp")
|
|
assert folder_paths.get_input_directory() == \
|
|
os.path.join(tenant_env, "orgs", "acme", "input")
|
|
|
|
|
|
def test_execution_org_reset(tenant_env, monkeypatch):
|
|
monkeypatch.setattr(folder_paths, "_multi_tenant", True)
|
|
folder_paths.set_execution_org("acme")
|
|
assert "acme" in folder_paths.get_output_directory()
|
|
folder_paths.set_execution_org(None)
|
|
# No active execution org and no process org -> base output dir
|
|
assert folder_paths.get_output_directory() == os.path.join(tenant_env, "output")
|