Files
tatlock/tests/core/test_tenant_guard.py
T
jpmschweitzerandClaude Fable 5 e8e5d367b6 fix(tenant): guard against sanitization collisions with production tenant
The request-level tenant guard compared the raw user string exactly
(user == PRODUCTION_TENANT), but all local namespaces (Qdrant
collections, Redis keys) are derived through sanitize_user_id(), which
lowercases and strips/maps punctuation. Case or punctuation variants
("JPMSchweitzer", "jpmschweitzer.", " jpmschweitzer") therefore passed
the guard yet resolved to the production namespaces, letting a dev
instance on the shared services read/write production tenant data.

- context.py: compare sanitize_user_id(user) against the sanitized
  production tenant; expose the guard as public apply_tenant_guard()
- config.py: startup refusal validator uses the same sanitized
  comparison, so a colliding DEFAULT_USER refuses startup loudly
  instead of relying on the allowlist fallback
- tests: variant matrix at both config and request-context level,
  plus a non-colliding passthrough case

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:23:36 +02:00

301 lines
11 KiB
Python

"""
Tests for the tenant isolation guard.
Isolation is tenant-based: the production tenant ("jpmschweitzer") owns
real data in the shared services, and every non-production environment
must run under the reserved test tenant ("llm_tester") or an explicit
"test_"-prefixed namespace.
Guard matrix covered here: dev/test/prod x default/explicit user, at
both config level (effective_default_user) and request-context
resolution (get_user).
"""
import pytest
from pydantic import ValidationError
from src.core.config import (
PRODUCTION_TENANT,
TEST_TENANT,
Config,
Environment,
)
from src.core.context import RequestContext, get_user
def make_config(**overrides) -> Config:
"""Build a Config isolated from the local .env file."""
return Config(_env_file=None, **overrides)
@pytest.mark.unit
class TestEffectiveDefaultUserMatrix:
"""Config-level guard: effective_default_user per environment."""
# --- development ---
def test_dev_without_default_user_forces_test_tenant(self):
config = make_config(ENVIRONMENT=Environment.DEVELOPMENT)
assert config.effective_default_user == TEST_TENANT
assert config.tenant_forced is False
def test_dev_with_test_tenant_is_kept(self):
config = make_config(ENVIRONMENT=Environment.DEVELOPMENT, DEFAULT_USER=TEST_TENANT)
assert config.effective_default_user == TEST_TENANT
assert config.tenant_forced is False
def test_dev_with_test_prefixed_override_is_kept(self):
config = make_config(ENVIRONMENT=Environment.DEVELOPMENT, DEFAULT_USER="test_phase_b")
assert config.effective_default_user == "test_phase_b"
assert config.tenant_forced is False
def test_dev_with_misconfigured_user_is_forced_to_test_tenant(self):
config = make_config(ENVIRONMENT=Environment.DEVELOPMENT, DEFAULT_USER="alice")
assert config.effective_default_user == TEST_TENANT
assert config.tenant_forced is True
def test_dev_with_production_tenant_refuses_startup(self):
with pytest.raises(ValidationError) as exc_info:
make_config(
ENVIRONMENT=Environment.DEVELOPMENT,
DEFAULT_USER=PRODUCTION_TENANT,
)
assert "Refusing to start" in str(exc_info.value)
assert PRODUCTION_TENANT in str(exc_info.value)
@pytest.mark.parametrize(
"variant",
[
"JPMSchweitzer",
"JPMSCHWEITZER",
"jpmschweitzer.",
" jpmschweitzer",
"jpmschweitzer ",
"_jpmschweitzer_",
"jpmschweitzer!",
],
)
def test_dev_with_production_tenant_variant_refuses_startup(self, variant):
"""Sanitization collisions with the production tenant are refused too."""
with pytest.raises(ValidationError, match="Refusing to start"):
make_config(
ENVIRONMENT=Environment.DEVELOPMENT,
DEFAULT_USER=variant,
)
# --- testing ---
def test_testing_without_default_user_forces_test_tenant(self):
config = make_config(ENVIRONMENT=Environment.TESTING)
assert config.effective_default_user == TEST_TENANT
def test_testing_with_misconfigured_user_is_forced_to_test_tenant(self):
config = make_config(ENVIRONMENT=Environment.TESTING, DEFAULT_USER="bob")
assert config.effective_default_user == TEST_TENANT
assert config.tenant_forced is True
def test_testing_with_production_tenant_refuses_startup(self):
with pytest.raises(ValidationError, match="Refusing to start"):
make_config(
ENVIRONMENT=Environment.TESTING,
DEFAULT_USER=PRODUCTION_TENANT,
)
def test_testing_with_test_prefixed_override_is_kept(self):
config = make_config(ENVIRONMENT=Environment.TESTING, DEFAULT_USER="test_ci_run")
assert config.effective_default_user == "test_ci_run"
# --- production ---
def test_prod_without_default_user_uses_production_tenant(self):
config = make_config(ENVIRONMENT=Environment.PRODUCTION)
assert config.effective_default_user == PRODUCTION_TENANT
assert config.tenant_forced is False
def test_prod_with_explicit_production_tenant_is_kept(self):
config = make_config(ENVIRONMENT=Environment.PRODUCTION, DEFAULT_USER=PRODUCTION_TENANT)
assert config.effective_default_user == PRODUCTION_TENANT
def test_prod_with_explicit_other_user_is_kept(self):
config = make_config(ENVIRONMENT=Environment.PRODUCTION, DEFAULT_USER="household_guest")
assert config.effective_default_user == "household_guest"
assert config.tenant_forced is False
@pytest.mark.unit
class TestRequestContextGuard:
"""Request-context resolution guard: get_user() per environment."""
def _patch_environment(self, monkeypatch, environment: Environment):
from src.core import config as config_module
monkeypatch.setattr(config_module.config, "ENVIRONMENT", environment)
def test_dev_default_resolution_is_test_tenant(self, monkeypatch):
self._patch_environment(monkeypatch, Environment.DEVELOPMENT)
assert get_user() == TEST_TENANT
def test_dev_explicit_production_tenant_is_forced(self, monkeypatch):
self._patch_environment(monkeypatch, Environment.DEVELOPMENT)
with RequestContext(user=PRODUCTION_TENANT):
assert get_user() == TEST_TENANT
def test_testing_explicit_production_tenant_is_forced(self, monkeypatch):
self._patch_environment(monkeypatch, Environment.TESTING)
with RequestContext(user=PRODUCTION_TENANT):
assert get_user() == TEST_TENANT
@pytest.mark.parametrize(
"variant",
[
"JPMSchweitzer",
"JPMSCHWEITZER",
"jpmschweitzer.",
" jpmschweitzer",
"jpmschweitzer ",
"_jpmschweitzer_",
"jpmschweitzer!",
],
)
@pytest.mark.parametrize(
"environment", [Environment.DEVELOPMENT, Environment.TESTING]
)
def test_production_tenant_sanitization_variants_are_forced(
self, monkeypatch, environment, variant
):
"""
Any raw user that sanitizes to the production tenant would resolve
to the production namespaces (memories_jpmschweitzer,
session:jpmschweitzer:*) - the guard must force it to the test
tenant in non-production environments.
"""
from src.core.multi_tenancy import get_memory_collection_name
self._patch_environment(monkeypatch, environment)
with RequestContext(user=variant):
effective = get_user()
assert effective == TEST_TENANT
assert (
get_memory_collection_name(effective)
!= get_memory_collection_name(PRODUCTION_TENANT)
)
def test_dev_non_colliding_user_is_not_forced(self, monkeypatch):
"""A user that sanitizes to a different namespace passes through."""
self._patch_environment(monkeypatch, Environment.DEVELOPMENT)
with RequestContext(user="jpm.schweitzer"):
# sanitizes to jpm_schweitzer != jpmschweitzer
assert get_user() == "jpm.schweitzer"
def test_dev_explicit_other_user_passes_through(self, monkeypatch):
self._patch_environment(monkeypatch, Environment.DEVELOPMENT)
with RequestContext(user="testuser"):
assert get_user() == "testuser"
def test_prod_explicit_production_tenant_passes_through(self, monkeypatch):
self._patch_environment(monkeypatch, Environment.PRODUCTION)
with RequestContext(user=PRODUCTION_TENANT):
assert get_user() == PRODUCTION_TENANT
def test_prod_explicit_other_user_passes_through(self, monkeypatch):
self._patch_environment(monkeypatch, Environment.PRODUCTION)
with RequestContext(user="alice"):
assert get_user() == "alice"
@pytest.mark.unit
class TestSuiteRunsUnderTestTenant:
"""
The live test session itself must resolve to the test tenant.
The session guard in tests/conftest.py hard-fails the suite when the
effective tenant is the production tenant; these tests assert the
namespaces every shared-service touch would use (Qdrant memories
collection, Redis session keys) are the llm_tester ones.
"""
def test_effective_tenant_is_not_production(self):
from src.core.context import get_default_user
assert get_default_user() != PRODUCTION_TENANT
def test_effective_tenant_is_the_reserved_test_tenant(self):
from src.core.context import get_default_user
assert get_default_user() == TEST_TENANT
def test_memories_collection_namespace_is_test_tenant(self):
from src.core.context import get_default_user
from src.core.multi_tenancy import get_memory_collection_name
assert (
get_memory_collection_name(get_default_user())
== f"memories_{TEST_TENANT}"
)
def test_redis_session_namespace_is_test_tenant(self):
from src.core.context import get_default_user
from src.core.multi_tenancy import get_session_key
key = get_session_key(get_default_user(), "conv_test")
assert key.startswith(f"session:{TEST_TENANT}:")
@pytest.mark.unit
class TestStartupTenantGuardLog:
"""One loud startup log line states the effective tenant."""
def test_non_production_logs_forced_tenant(self, monkeypatch):
from src.core import startup as startup_module
events = []
class _Recorder:
def warning(self, event, **kw):
events.append((event, kw))
def info(self, event, **kw):
events.append((event, kw))
monkeypatch.setattr(startup_module, "logger", _Recorder())
monkeypatch.setattr(startup_module.config, "ENVIRONMENT", Environment.DEVELOPMENT)
startup_module.log_tenant_guard()
assert events == [
(
"tenant_guard_active",
{
"environment": "development",
"forced_tenant": TEST_TENANT,
"default_user_overridden": startup_module.config.tenant_forced,
"configured_default_user": startup_module.config.DEFAULT_USER,
},
)
]
def test_production_logs_production_tenant(self, monkeypatch):
from src.core import startup as startup_module
events = []
class _Recorder:
def warning(self, event, **kw):
events.append(("warning", event, kw))
def info(self, event, **kw):
events.append(("info", event, kw))
monkeypatch.setattr(startup_module, "logger", _Recorder())
monkeypatch.setattr(startup_module.config, "ENVIRONMENT", Environment.PRODUCTION)
startup_module.log_tenant_guard()
assert events == [
(
"info",
"tenant_guard_production",
{"environment": "production", "tenant": PRODUCTION_TENANT},
)
]