Files
PTah 80320ae698 fix: harden SAC security per audit (0.4.15)
Close audit findings: anti-spoof client IP for login rate limit, JWT/CORS
enforce on startup, SSH host-key verification and sudo via stdin, optional
WinRM HTTPS, SSE httpOnly cookie auth, ingest body limit, ILIKE escaping,
and HMAC API key hashing with legacy SHA-256 fallback.
2026-07-07 19:32:12 +10:00

160 lines
4.8 KiB
Python

"""SQLite in-memory fixtures for API tests."""
import os
# Must be set before app.database imports create_engine
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SAC_BOOTSTRAP_API_KEY", "sac_test_key_for_pytest_only")
os.environ.setdefault("SAC_SECURITY_ENFORCE", "false")
os.environ.setdefault("JWT_SECRET", "pytest-jwt-secret-not-for-production-use")
os.environ.setdefault("SAC_SSH_AUTO_ADD_HOST_KEY", "true")
os.environ.setdefault("SAC_HOST_SILENCE_SCAN_ENABLED", "false")
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import JSON, create_engine, event
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.auth.api_key import hash_api_key
from app.auth.jwt_auth import create_access_token
from app.database import Base, get_db
from app.main import app as fastapi_app
import app.models # noqa: F401 — register all tables on Base.metadata
from app.models import ApiKey, User
from app.models.user import USER_ROLE_ADMIN, USER_ROLE_MONITOR
from app.services.user_auth import hash_password
TEST_API_KEY = os.environ["SAC_BOOTSTRAP_API_KEY"]
@event.listens_for(Base.metadata, "before_create")
def _sqlite_jsonb_as_json(metadata, connection, **_kwargs) -> None:
if connection.dialect.name != "sqlite":
return
for table in metadata.tables.values():
for column in table.columns:
if isinstance(column.type, JSONB):
column.type = JSON()
@pytest.fixture
def db_engine():
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def db_session(db_engine):
Session = sessionmaker(bind=db_engine, autocommit=False, autoflush=False)
session = Session()
session.add(
ApiKey(
name="test",
key_prefix=TEST_API_KEY[:12],
key_hash=hash_api_key(TEST_API_KEY),
is_active=True,
)
)
session.add(
User(
username="test-admin",
password_hash=hash_password("test-admin-password"),
role=USER_ROLE_ADMIN,
is_active=True,
)
)
session.add(
User(
username="test-monitor",
password_hash=hash_password("test-monitor-password"),
role=USER_ROLE_MONITOR,
is_active=True,
)
)
session.commit()
yield session
session.close()
@pytest.fixture(autouse=True)
def mock_agent_git_release(monkeypatch):
"""API tests must not clone real git repos."""
from app.services.agent_git_release import GitReleaseVersions
empty = GitReleaseVersions(versions={}, fetched_at=None, from_cache=True)
monkeypatch.setattr(
"app.services.agent_git_release.get_git_release_versions",
lambda cfg, **kwargs: empty,
)
monkeypatch.setattr(
"app.api.v1.hosts.get_git_release_versions",
lambda cfg, **kwargs: empty,
)
monkeypatch.setattr(
"app.api.v1.settings.get_git_release_versions",
lambda cfg, **kwargs: empty,
)
monkeypatch.setattr(
"app.services.agent_update.get_git_release_versions",
lambda cfg, **kwargs: empty,
)
@pytest.fixture(autouse=True)
def reset_remote_action_state():
from app.services import host_remote_actions
with host_remote_actions._lock:
host_remote_actions._running.clear()
yield
with host_remote_actions._lock:
host_remote_actions._running.clear()
@pytest.fixture
def client(db_session, db_engine, monkeypatch):
monkeypatch.setenv("SAC_REMOTE_ACTION_INLINE", "1")
monkeypatch.setattr("app.main.bootstrap_api_key", lambda: None)
monkeypatch.setattr("app.main.bootstrap_users", lambda: None)
monkeypatch.setattr("app.main.bootstrap_stale_remote_actions", lambda: None)
test_session_local = sessionmaker(bind=db_engine, autocommit=False, autoflush=False)
monkeypatch.setattr("app.services.host_remote_actions.SessionLocal", test_session_local)
def override_get_db():
session = test_session_local()
try:
yield session
finally:
session.close()
fastapi_app.dependency_overrides[get_db] = override_get_db
with TestClient(fastapi_app) as c:
yield c
fastapi_app.dependency_overrides.clear()
@pytest.fixture
def auth_headers():
return {"Authorization": f"Bearer {TEST_API_KEY}"}
@pytest.fixture
def jwt_headers():
token = create_access_token("test-admin", USER_ROLE_ADMIN)
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def jwt_monitor_headers():
token = create_access_token("test-monitor", USER_ROLE_MONITOR)
return {"Authorization": f"Bearer {token}"}