|
1 | 1 | # Copyright The Marin Authors |
2 | 2 | # SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
4 | | -"""GAE reverse proxy for the Iris controller, protected by GCP IAP. |
| 4 | +"""Cloud Run reverse proxy for the Iris controller. |
5 | 5 |
|
6 | 6 | All requests are forwarded to the controller VM discovered via GCE labels. |
7 | | -IAP handles authentication at the Google infrastructure layer; this app |
8 | | -re-validates the IAP JWT for defense-in-depth and extracts the caller's |
9 | | -email for audit logging. |
10 | | -
|
11 | | -CLI / programmatic callers send: |
12 | | - - ``Authorization: Bearer <iap-oidc-token>`` (consumed by IAP) |
13 | | - - ``X-Iris-Token: <controller-jwt>`` (forwarded as Authorization to controller) |
14 | | -
|
15 | | -Browser callers authenticate via IAP's OAuth flow; their controller session |
16 | | -cookie (``iris_session``) is forwarded transparently. |
| 7 | +When IAP is enabled, the proxy validates the ``X-Goog-IAP-JWT-Assertion`` |
| 8 | +header for defense-in-depth. When ``REQUIRE_IAP=false`` (the default), |
| 9 | +IAP validation is skipped and all requests are forwarded without auth — |
| 10 | +useful for initial testing before the LB + IAP stack is configured. |
17 | 11 | """ |
18 | 12 |
|
19 | 13 | import logging |
|
27 | 21 | from starlette.routing import Route |
28 | 22 |
|
29 | 23 | from discovery import get_controller_url |
30 | | -from iap import IapValidationError, validate_iap_jwt |
31 | 24 |
|
32 | 25 | logger = logging.getLogger(__name__) |
33 | 26 |
|
| 27 | +REQUIRE_IAP = os.environ.get("REQUIRE_IAP", "false").lower() in ("true", "1", "yes") |
| 28 | + |
34 | 29 | _IRIS_TOKEN_HEADER = "x-iris-token" |
35 | 30 |
|
36 | 31 | # Headers that should not be forwarded to the controller. |
@@ -126,15 +121,16 @@ def _build_upstream_headers(request: Request, iap_email: str | None) -> dict[str |
126 | 121 |
|
127 | 122 | async def _proxy(request: Request) -> Response: |
128 | 123 | """Forward any request to the controller.""" |
129 | | - # Validate IAP JWT (defense-in-depth). |
130 | 124 | iap_email: str | None = None |
131 | | - try: |
132 | | - iap_email = validate_iap_jwt(dict(request.headers)) |
133 | | - except IapValidationError as exc: |
134 | | - logger.warning("IAP validation failed: %s", exc) |
135 | | - # In production IAP is enforced at the infra layer, so this should |
136 | | - # not happen. Return 401 rather than silently forwarding. |
137 | | - return JSONResponse({"error": str(exc)}, status_code=401) |
| 125 | + |
| 126 | + if REQUIRE_IAP: |
| 127 | + from iap import IapValidationError, validate_iap_jwt |
| 128 | + |
| 129 | + try: |
| 130 | + iap_email = validate_iap_jwt(dict(request.headers)) |
| 131 | + except IapValidationError as exc: |
| 132 | + logger.warning("IAP validation failed: %s", exc) |
| 133 | + return JSONResponse({"error": str(exc)}, status_code=401) |
138 | 134 |
|
139 | 135 | client = await _get_client() |
140 | 136 | path = request.url.path |
|
0 commit comments