[09d0185a] Backend: fix the roboco-api CI regression (make make quality green on slave) (#808)

* [c2c29256] Fix make quality CI regression on roboco-api slave (#804)

* [c2c29256] fix: mock _chown_entry to simulate chown failure in test_marker_written_only_on_zero_failure_pass

* [c2c29256] docs(changelog): add Unreleased Fixed entry for make quality slave CI regression fix

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [b5bc53ea] Fix security middleware trusted_proxies + run full make quality green (#811)

* [b5bc53ea] fix(security): remove LAN ranges from trusted_proxies so forwarded LAN IPs are rejected

* [b5bc53ea] refactor(task): extract private helpers from cancel() to clear xenon rank C gate

* [b5bc53ea] docs(security): document trusted_proxies/whitelist lockstep invariant and the #811 LAN-IP fix

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [13193140] Strengthen security middleware IP-resolution test + document trusted_proxies invariant (#813)

* [13193140] test(security): add direct IP-resolution unit tests and document trusted_proxies invariant

* [13193140] docs(security): cross-reference trusted_proxies invariant proof tests and inline comment

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

---------

Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
This commit is contained in:
roboco-app[bot]
2026-08-02 11:33:40 +00:00
committed by GitHub
co-authored by Backend Developer 1 Backend Documenter roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
parent 628b925f40
commit d829979b6a
6 changed files with 164 additions and 27 deletions
@@ -333,13 +333,18 @@ def test_full_walk_when_marker_absent(
assert str(tmp_path / "README.md") in _record_touched
def test_marker_written_only_on_zero_failure_pass(tmp_path: Path) -> None:
def test_marker_written_only_on_zero_failure_pass(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_build_workspace(tmp_path)
marker = tmp_path / ".git" / "roboco-owned"
assert not marker.exists()
# A real pass: chown to uid 1000 fails under the test's real (non-root)
# uid, exactly like a rootless/userns host — so no marker should land.
# Simulate a rootless/userns host where chown is rejected — the marker
# must NOT land when any entry's chown fails. Mocking _chown_entry avoids
# relying on the process uid (the test runs as uid 1000 and files are
# already owned by uid 1000, so a real chown would be a no-op success).
monkeypatch.setattr(workspace_module, "_chown_entry", lambda _entry, _st: False)
_ensure_agent_owned(tmp_path)
assert not marker.exists()
+49 -1
View File
@@ -20,14 +20,17 @@ from __future__ import annotations
import contextlib
from http import HTTPStatus
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from guard import SecurityMiddleware
from guard.adapters import StarletteGuardRequest
from guard.lifespan import make_lifespan
from guard_core.utils import extract_client_ip, is_ip_allowed
from roboco import security
from starlette.requests import Request
if TYPE_CHECKING:
from collections.abc import AsyncIterator
@@ -218,3 +221,48 @@ class TestDecoyPaths:
with _client(_guarded_app(passive=True)) as client:
resp = client.get("/.git/config")
assert resp.status_code == HTTPStatus.OK
# ---------------------------------------------------------------------------
# Direct unit tests for the IP-resolution path the stale PR-review finding
# keeps questioning. These call guard_core's extract_client_ip / is_ip_allowed
# directly (no running server) to make the security boundary self-evident.
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_extract_client_ip_forwarded_lan_not_peeled() -> None:
"""With trusted_proxies narrowed to the docker-bridge/loopback mesh (no
LAN ranges), a docker-bridge peer forwarding a LAN client's IP via
X-Forwarded-For resolves to the real LAN IP — not peeled to the peer."""
scope = {
"type": "http",
"method": "POST",
"path": "/task",
"query_string": b"",
"headers": [(b"x-forwarded-for", b"192.168.1.50")],
"client": ("172.18.0.5", 12345),
}
request = StarletteGuardRequest(Request(scope))
class _Cfg:
trusted_proxies: ClassVar[list[str]] = ["127.0.0.1", "::1", "172.16.0.0/12"]
trusted_proxy_depth = 1
assert await extract_client_ip(request, _Cfg()) == "192.168.1.50"
@pytest.mark.asyncio
async def test_is_ip_allowed_rejects_lan_ranges() -> None:
"""The narrowed whitelist (loopback + docker-bridge only) does NOT cover
RFC1918 LAN ranges, so a resolved LAN client IP is rejected."""
_WHITELIST = ["127.0.0.1", "::1", "172.16.0.0/12"]
class _Cfg:
whitelist: ClassVar[list[str]] = _WHITELIST
blacklist: ClassVar[list[str]] = []
blocked_countries: ClassVar[list[str]] = []
block_cloud_providers: ClassVar[list[str]] = []
assert await is_ip_allowed("192.168.1.50", _Cfg()) is False
assert await is_ip_allowed("10.0.0.5", _Cfg()) is False