From ea3fd0c51abe89437ca09efc2bc9a73612103ebb Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Fri, 17 Jul 2026 12:27:16 +0200 Subject: [PATCH] fix: document Producer Portal SA registration for multi-agent Procurement API access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Cloud Commerce Partner Procurement API does not use standard IAM roles. Access is managed through the GCP Marketplace Producer Portal Technical Integration, which is provider-level (shared across all products in the same project). When multiple agents share a project, all runtime SAs must be registered together — otherwise the Procurement API returns 403 and multi-agent product filtering silently drops events. Changes: - Upgrade 403 log messages from info to warning in router.py so the misconfiguration is visible in logs - Document Producer Portal requirement in setup.sh, GitOps README, and marketplace docs - Add troubleshooting entry for the 403 / silently dropped events scenario Co-Authored-By: Claude Opus 4.6 (1M context) --- deploy/cloudrun/setup.sh | 4 ++++ deploy/gitops/README.md | 10 ++++++---- docs/marketplace.md | 19 +++++++++++++++---- src/lightspeed_agent/marketplace/router.py | 10 ++++++---- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/deploy/cloudrun/setup.sh b/deploy/cloudrun/setup.sh index ffd6a538..cdf9bb85 100755 --- a/deploy/cloudrun/setup.sh +++ b/deploy/cloudrun/setup.sh @@ -213,6 +213,10 @@ roles=( "roles/cloudsql.client" "roles/serviceusage.serviceUsageConsumer" ) +# NOTE: The Cloud Commerce Partner Procurement API (cloudcommerceprocurement.googleapis.com) +# does not use standard IAM roles. To enable multi-agent product filtering (fetching +# entitlement product info from the API), register this SA in the GCP Marketplace +# Producer Portal → Technical Integration → Partner Procurement API. for role in "${roles[@]}"; do log_info " Granting $role..." diff --git a/deploy/gitops/README.md b/deploy/gitops/README.md index 35e9d815..47ec3ac9 100644 --- a/deploy/gitops/README.md +++ b/deploy/gitops/README.md @@ -419,12 +419,14 @@ Many `setup.sh` env vars must match the corresponding `values-override.yaml` fie #### 2. Register Service Accounts in the Google Cloud Marketplace Producer Portal -Each instance's service accounts must be registered in the [Producer Portal](https://console.cloud.google.com/producer-portal) under the **Technical Integration** section of your product: +**All** instance service accounts must be registered in the [Producer Portal](https://console.cloud.google.com/producer-portal) under the **Technical Integration** section: -- **Partner Procurement API integration** — add the runtime SA (e.g., `sa-lightspeed-agent-staging@.iam.gserviceaccount.com`). This authorizes the SA to call the Procurement API for entitlement management. -- **Cloud Pub/Sub integration** — add the Pub/Sub invoker SA (e.g., `pubsub-invoker-staging@.iam.gserviceaccount.com`). This authorizes the SA to create push subscriptions on the Marketplace entitlements topic. +- **Partner Procurement API integration** — add the runtime SA for **every** instance (e.g., both `sa-lightspeed-agent@.iam.gserviceaccount.com` and `sa-lightspeed-agent-staging@.iam.gserviceaccount.com`). This authorizes the SA to call the Procurement API for entitlement management and multi-agent product filtering. +- **Cloud Pub/Sub integration** — add the Pub/Sub invoker SA for **every** instance (e.g., both `pubsub-invoker@.iam.gserviceaccount.com` and `pubsub-invoker-staging@.iam.gserviceaccount.com`). This authorizes the SA to create push subscriptions on the Marketplace entitlements topic. -Without this step, the deployment will fail at the `configure-pubsub` Cloud Build step with `"User not authorized to perform this action"`. +> **Important:** The Technical Integration configuration is **provider-level** (per GCP project), not per-product. Updating the SA list on one product automatically updates all products under the same provider. This means you register all SAs in a single place, and all instances gain access to the Procurement API regardless of which product listing you edit. This is also why access to the Procurement API cannot be granted via standard IAM roles — it is managed exclusively through the Producer Portal. + +Without this step, the marketplace handler will get `403 Forbidden` when calling the Procurement API to resolve entitlement products, causing multi-agent product filtering to silently drop events. The deployment will also fail at the `configure-pubsub` Cloud Build step with `"User not authorized to perform this action"`. #### 3. Create Per-Instance GCP Deploy Service Accounts diff --git a/docs/marketplace.md b/docs/marketplace.md index 84dd3be9..236c282c 100644 --- a/docs/marketplace.md +++ b/docs/marketplace.md @@ -149,10 +149,20 @@ Marketplace sends procurement events via Pub/Sub: ### Multi-Agent Product Filtering In multi-agent deployments where multiple agents share the same Google Cloud -project and Pub/Sub topic, events are filtered by the `product` field in the -entitlement data. Each agent only processes events matching its -`SERVICE_CONTROL_SERVICE_NAME`. Account-only events (no product field) pass -through without filtering and are handled normally (e.g. account approval). +project and Pub/Sub topic, events are filtered by product. Each agent only +processes events matching its `SERVICE_CONTROL_SERVICE_NAME`. + +Since most Pub/Sub event types omit the `product` field, the handler fetches +it from the Procurement API using the entitlement ID. This requires the +Cloud Run runtime SA to be registered in the **Producer Portal → Technical +Integration → Partner Procurement API**. The Technical Integration is +provider-level (per GCP project, not per product), so all instance SAs must +be registered together — see [GitOps README: Step 2](../deploy/gitops/README.md#2-register-service-accounts-in-the-google-cloud-marketplace-producer-portal). + +If the SA is not registered, the Procurement API returns `403 Forbidden` +and the handler cannot determine the product, causing events to be silently +dropped. Account-only events (no entitlement data) pass through without +filtering and are handled normally (e.g., account approval). ### Handling Entitlements @@ -386,6 +396,7 @@ gcloud logging read \ | Missing events | Subscription not configured | Verify Pub/Sub subscription | | Event processing failed | Handler error | Check logs for exceptions | | Entitlement not found | Sync delay | Wait and retry | +| 403 on Procurement API / events silently dropped | Runtime SA not registered in Producer Portal | Register the Cloud Run SA in Producer Portal → Technical Integration → Partner Procurement API (this is provider-level, not per-product — register all instance SAs together) | ### Usage Reporting Issues diff --git a/src/lightspeed_agent/marketplace/router.py b/src/lightspeed_agent/marketplace/router.py index 04c6604b..ebc12b4d 100644 --- a/src/lightspeed_agent/marketplace/router.py +++ b/src/lightspeed_agent/marketplace/router.py @@ -201,8 +201,9 @@ async def _handle_pubsub_event(body: dict[str, Any]) -> JSONResponse: product = await _fetch_entitlement_product(entitlement_id, settings) if product == _NOT_AUTHORIZED: - logger.info( - "Entitlement %s belongs to a different product (SA not authorized)", + logger.warning( + "Entitlement %s skipped: SA returned 403 from Procurement API " + "(different product, or SA not registered in Producer Portal)", entitlement_data.get("id", "unknown") if entitlement_data else "unknown", ) return JSONResponse(content={"status": "ok", "message": "not for this product"}) @@ -379,8 +380,9 @@ async def _fetch_entitlement_product(entitlement_id: str, settings: Settings) -> ) return product elif response.status_code == 403: - logger.info( - "Not authorized to access entitlement %s — belongs to a different product", + logger.warning( + "403 fetching entitlement %s — either belongs to a different product " + "or this SA is not registered in the Producer Portal Technical Integration", entitlement_id, ) return _NOT_AUTHORIZED