Elevate MiroFish/CrowdSight from single-container dev to a SaaS foundation: - Local memory backend (Zep-compatible): memory services/models, local graph builder + updater, AgentActivity seam, import-boundary isolation; Zep stays default, local is opt-in behind MEMORY_BACKEND. Semantic parity not yet proven. - Durable product persistence: projects/simulations/reports schema (migration 0007) + tenant/owner-scoped ProductRepository + dual-write + scoped_project read-first + ArtifactStore abstraction; durable JobQueue + worker.py. - SaaS hardening: durable RateLimiter (wired to login), UsageService (LLM accounting), redacted AuditService, idempotency, CORS allowlist, safe API errors, single-use PasswordResetService + endpoints (covers invite-pending). - Exactly 3 roles (super_admin/admin/user) with tenant authz policy. - Admin UI: GET/POST/PATCH /api/admin/users + GET/PUT /api/admin/settings (super-admin only, encrypted/masked); AdminView.vue + SettingsView.vue with admin/super-admin route guards, th/en i18n. - Production deploy topology: multi-stage Dockerfile (frontend build + gunicorn wsgi + nginx SPA-proxy + supervisord worker), backend/wsgi.py, gunicorn dep. Backend 197 passed; frontend 10 tests + build green. ruff unavailable (gap). No commit of credentials; secrets handled via env/.env.example. Deferred: Zep semantic A/B parity, object storage cutover, mobile QA, EasyPanel container build of deploy topology.
127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
import sys
|
|
import unittest
|
|
|
|
|
|
_MODULE_PATH = Path(__file__).parents[1] / "app" / "security" / "policy.py"
|
|
_SPEC = importlib.util.spec_from_file_location("authorization_policy_under_test", _MODULE_PATH)
|
|
assert _SPEC is not None and _SPEC.loader is not None
|
|
_POLICY = importlib.util.module_from_spec(_SPEC)
|
|
sys.modules[_SPEC.name] = _POLICY
|
|
_SPEC.loader.exec_module(_POLICY)
|
|
|
|
Actor = _POLICY.Actor
|
|
AuthorizationError = _POLICY.AuthorizationError
|
|
Role = _POLICY.Role
|
|
assert_can_access_resource = _POLICY.assert_can_access_resource
|
|
assert_can_manage_llm_settings = _POLICY.assert_can_manage_llm_settings
|
|
assert_can_manage_user = _POLICY.assert_can_manage_user
|
|
|
|
|
|
class AuthorizationPolicyTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.admin = Actor(user_id="admin-1", organization_id="org-a", role=Role.ADMIN)
|
|
self.user = Actor(user_id="user-1", organization_id="org-a", role=Role.USER)
|
|
self.other_user = Actor(user_id="user-2", organization_id="org-a", role=Role.USER)
|
|
self.super_admin = Actor(
|
|
user_id="root-1", organization_id="platform", role=Role.SUPER_ADMIN
|
|
)
|
|
|
|
def test_user_can_access_owned_resource_in_own_organization(self):
|
|
assert_can_access_resource(
|
|
self.user,
|
|
resource_organization_id="org-a",
|
|
owner_user_id="user-1",
|
|
)
|
|
|
|
def test_user_cannot_access_another_users_resource_in_same_organization(self):
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_access_resource(
|
|
self.user,
|
|
resource_organization_id="org-a",
|
|
owner_user_id="user-2",
|
|
)
|
|
|
|
def test_user_cannot_access_resource_from_another_organization(self):
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_access_resource(
|
|
self.user,
|
|
resource_organization_id="org-b",
|
|
owner_user_id="user-1",
|
|
)
|
|
|
|
def test_admin_can_access_resources_in_own_organization(self):
|
|
assert_can_access_resource(
|
|
self.admin,
|
|
resource_organization_id="org-a",
|
|
owner_user_id="user-2",
|
|
)
|
|
|
|
def test_admin_cannot_cross_organization_boundary(self):
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_access_resource(
|
|
self.admin,
|
|
resource_organization_id="org-b",
|
|
owner_user_id="user-9",
|
|
)
|
|
|
|
def test_super_admin_requires_explicit_platform_scope_for_cross_tenant_access(self):
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_access_resource(
|
|
self.super_admin,
|
|
resource_organization_id="org-a",
|
|
owner_user_id="user-9",
|
|
)
|
|
|
|
assert_can_access_resource(
|
|
self.super_admin,
|
|
resource_organization_id="org-a",
|
|
owner_user_id="user-9",
|
|
platform_scope=True,
|
|
)
|
|
|
|
def test_admin_can_manage_user_but_not_grant_admin_or_super_admin(self):
|
|
assert_can_manage_user(
|
|
self.admin,
|
|
target_organization_id="org-a",
|
|
target_role=Role.USER,
|
|
)
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_manage_user(
|
|
self.admin,
|
|
target_organization_id="org-a",
|
|
target_role=Role.ADMIN,
|
|
)
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_manage_user(
|
|
self.admin,
|
|
target_organization_id="org-a",
|
|
target_role=Role.SUPER_ADMIN,
|
|
)
|
|
|
|
def test_super_admin_can_manage_any_role_only_with_platform_scope(self):
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_manage_user(
|
|
self.super_admin,
|
|
target_organization_id="org-a",
|
|
target_role=Role.ADMIN,
|
|
)
|
|
assert_can_manage_user(
|
|
self.super_admin,
|
|
target_organization_id="org-a",
|
|
target_role=Role.ADMIN,
|
|
platform_scope=True,
|
|
)
|
|
|
|
def test_only_super_admin_can_manage_llm_settings(self):
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_manage_llm_settings(self.admin)
|
|
with self.assertRaises(AuthorizationError):
|
|
assert_can_manage_llm_settings(self.user)
|
|
assert_can_manage_llm_settings(self.super_admin, platform_scope=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|