Files
microfish/backend/tests/test_authorization_policy.py
Kunthawat Greethong 8b84378fe1 feat: SaaS foundation for CrowdSight
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.
2026-08-31 13:05:21 +07:00

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()