ae67bff5a3
Self-hosted platform for building, testing, and shipping LangChain/LangGraph agents. Deep-agent sub-agents on the canvas, a live tracing/observability timeline, auto-provisioned built-in tools with import/export, per-environment tool variables, streamed evaluations, and per-user auth token forwarding.
117 lines
4.8 KiB
Python
117 lines
4.8 KiB
Python
"""Server-to-server integration primitives:
|
|
|
|
1. A static service API token (`FORGE_SERVICE_API_TOKEN`) that authenticates a trusted backend
|
|
as a least-privilege (editor) service identity - the outer "is this call from our backend"
|
|
barrier. Non-expiring, revoked by rotation.
|
|
2. An egress allow-private-hosts toggle (`FORGE_EGRESS_ALLOW_PRIVATE_HOSTS`) that lets specific
|
|
trusted internal hosts (localhost, on-prem services) be reached even while the SSRF guard's
|
|
private-address block stays on globally (default-deny, explicit-allow).
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from forge.config import settings
|
|
from forge.deps import get_current_user
|
|
from forge.util.ssrf import EgressBlocked, EgressPolicy, validate_url
|
|
|
|
# --- service token ---------------------------------------------------------------------------
|
|
|
|
|
|
class _State:
|
|
tenant_id = "t-seed"
|
|
|
|
|
|
class _App:
|
|
state = _State()
|
|
|
|
|
|
class _Req:
|
|
def __init__(self, authorization: str | None):
|
|
self.headers = {"authorization": authorization} if authorization else {}
|
|
self.app = _App()
|
|
|
|
|
|
async def test_service_token_authenticates_as_editor(monkeypatch):
|
|
monkeypatch.setattr(settings, "service_api_token", "svc-secret-abc123")
|
|
user = await get_current_user(_Req("Bearer svc-secret-abc123"))
|
|
assert user.role == "editor" # least privilege, but enough to assert end_user identity
|
|
assert user.tenant_id == "t-seed" # bound to the seeded workspace
|
|
assert user.is_fallback is False
|
|
|
|
|
|
async def test_wrong_service_token_is_rejected(monkeypatch):
|
|
# A present-but-wrong token must NOT fall back to an anonymous/owner identity: it fails the
|
|
# constant-time compare, then fails JWT decode -> 401.
|
|
monkeypatch.setattr(settings, "service_api_token", "svc-secret-abc123")
|
|
with pytest.raises(HTTPException) as e:
|
|
await get_current_user(_Req("Bearer not-the-token"))
|
|
assert e.value.status_code == 401
|
|
|
|
|
|
async def test_empty_service_token_setting_is_disabled(monkeypatch):
|
|
# With no service token configured, the branch is inert (a random bearer is treated as a
|
|
# would-be JWT and rejected).
|
|
monkeypatch.setattr(settings, "service_api_token", "")
|
|
with pytest.raises(HTTPException) as e:
|
|
await get_current_user(_Req("Bearer anything"))
|
|
assert e.value.status_code == 401
|
|
|
|
|
|
# --- egress allow-private-hosts --------------------------------------------------------------
|
|
|
|
|
|
async def test_private_host_blocked_by_default():
|
|
with pytest.raises(EgressBlocked):
|
|
await validate_url("http://127.0.0.1:9002/x", EgressPolicy(block_private=True))
|
|
|
|
|
|
async def test_allow_private_hosts_bypasses_block_by_ip():
|
|
url = await validate_url(
|
|
"http://127.0.0.1:9002/x",
|
|
EgressPolicy(block_private=True, allow_private_hosts=("127.0.0.1",)),
|
|
)
|
|
assert url == "http://127.0.0.1:9002/x"
|
|
|
|
|
|
async def test_allow_private_hosts_bypasses_block_by_name():
|
|
# `localhost` resolves to a loopback IP, but the host is explicitly allow-listed.
|
|
url = await validate_url(
|
|
"http://localhost:9002/orders/detail/1",
|
|
EgressPolicy(block_private=True, allow_private_hosts=("localhost",)),
|
|
)
|
|
assert url.startswith("http://localhost")
|
|
|
|
|
|
async def test_non_allowlisted_private_still_blocked():
|
|
with pytest.raises(EgressBlocked):
|
|
await validate_url(
|
|
"http://10.0.0.5/x",
|
|
EgressPolicy(block_private=True, allow_private_hosts=("localhost",)),
|
|
)
|
|
|
|
|
|
def test_from_settings_reads_global_allow_private(monkeypatch):
|
|
monkeypatch.setattr(settings, "egress_allow_private_hosts", ["localhost", "127.0.0.1"])
|
|
p = EgressPolicy.from_settings()
|
|
assert "localhost" in p.allow_private_hosts and "127.0.0.1" in p.allow_private_hosts
|
|
|
|
|
|
def test_from_settings_ignores_project_allow_private(monkeypatch):
|
|
# SECURITY (audit H1): project config is editable by any tenant member, so it must NOT be able
|
|
# to add a private-host bypass. allow_private_hosts is a deployment-level control only.
|
|
monkeypatch.setattr(settings, "egress_allow_private_hosts", [])
|
|
p = EgressPolicy.from_settings({"allow_private_hosts": ["10.0.0.5"]})
|
|
assert "10.0.0.5" not in p.allow_private_hosts
|
|
assert p.allow_private_hosts == ()
|
|
|
|
|
|
def test_from_settings_project_cannot_loosen_block_private(monkeypatch):
|
|
# SECURITY (audit H1): a project may only TIGHTEN block_private (False->True), never turn the
|
|
# SSRF guard off. With the guard on globally, project block_private:false is ignored.
|
|
monkeypatch.setattr(settings, "egress_block_private", True)
|
|
assert EgressPolicy.from_settings({"block_private": False}).block_private is True
|
|
# And a project can still tighten when the deployment default is off.
|
|
monkeypatch.setattr(settings, "egress_block_private", False)
|
|
assert EgressPolicy.from_settings({"block_private": True}).block_private is True
|