Skip to content

Commit 2f5df2f

Browse files
authored
Merge branch 'main' into bugfix-adk-sessions
2 parents 227910a + f4e34a1 commit 2f5df2f

33 files changed

Lines changed: 1196 additions & 1265 deletions

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ AGENT_REQUIRED_SCOPE=api.console,api.ocm
3535
# Tokens carrying scopes outside this list are rejected (HTTP 403).
3636
AGENT_ALLOWED_SCOPES=openid,profile,email,api.console,api.ocm
3737

38+
# GMA SSO API credentials (for DCR tenant creation when DCR_ENABLED=true)
39+
# Used to create OAuth tenant clients via the GMA API
40+
GMA_CLIENT_ID=your_gma_client_id
41+
GMA_CLIENT_SECRET=your_gma_client_secret
42+
# GMA_API_BASE_URL=https://sso.redhat.com/auth/realms/redhat-external/apis/beta/acs/v1/
43+
3844
# -----------------------------------------------------------------------------
3945
# Red Hat Lightspeed MCP Server Configuration
4046
# -----------------------------------------------------------------------------

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ The agent loads tools from a Red Hat Lightspeed MCP server running as a sidecar:
119119
### DCR (Dynamic Client Registration)
120120

121121
Two modes controlled by `DCR_ENABLED`:
122-
- **Real DCR** (`true`): Creates OAuth clients in Red Hat SSO (Keycloak) via `dcr/keycloak_client.py`
122+
- **Real DCR** (`true`): Creates OAuth tenant clients in Red Hat SSO via the GMA API (`dcr/gma_client.py`). Authenticates with `GMA_CLIENT_ID`/`GMA_CLIENT_SECRET` using `scope=api.iam.clients.gma`.
123123
- **Static credentials** (`false`): Accepts pre-seeded client_id/secret in DCR request body
124124

125125
Client secrets are Fernet-encrypted at rest (`DCR_ENCRYPTION_KEY`).
@@ -168,6 +168,7 @@ All configuration is via environment variables, managed through Pydantic setting
168168

169169
**DCR:**
170170
- `DCR_ENABLED`, `DCR_ENCRYPTION_KEY`
171+
- `GMA_CLIENT_ID`, `GMA_CLIENT_SECRET`, `GMA_API_BASE_URL`
171172

172173
**Agent:**
173174
- `AGENT_HOST`, `AGENT_PORT`

README.md

Lines changed: 5 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This agent provides AI-powered access to Red Hat Insights services, enabling nat
1818
- Built with Google ADK and Gemini 2.5 Flash
1919
- A2A protocol support with SSE streaming for multi-agent ecosystems
2020
- OAuth 2.0 authentication via Red Hat SSO
21-
- Dynamic Client Registration (DCR) with Red Hat SSO (Keycloak)
21+
- Dynamic Client Registration (DCR) with Red Hat SSO via GMA SSO API
2222
- Google Cloud Marketplace integration (Gemini Enterprise)
2323
- PostgreSQL persistence for production deployments
2424
- Usage tracking and reporting to Google Cloud Service Control
@@ -252,7 +252,7 @@ lightspeed_agent/
252252
├── core/ # Agent definition (ADK)
253253
├── db/ # Database models (SQLAlchemy)
254254
├── dcr/ # Dynamic Client Registration
255-
│ ├── keycloak_client.py # Red Hat SSO DCR client
255+
│ ├── gma_client.py # GMA SSO API client
256256
│ └── service.py # DCR business logic
257257
├── marketplace/ # Google Marketplace integration & handler service
258258
│ ├── app.py # Handler FastAPI app (port 8001)
@@ -537,7 +537,7 @@ For development without real tokens, set `SKIP_JWT_VALIDATION: "true"` in the co
537537
538538
### Testing DCR Locally
539539
540-
The Dynamic Client Registration (DCR) flow can be tested locally without admin access to the production Red Hat SSO. There are two modes: **static credentials** (no Keycloak needed) and **real DCR** against a local Keycloak instance.
540+
The Dynamic Client Registration (DCR) flow can be tested locally without admin access to the production Red Hat SSO. The **static credentials** mode allows testing without a real SSO connection.
541541
542542
Both modes require `SKIP_JWT_VALIDATION=true` on the marketplace handler so it accepts JWTs signed by your own GCP service account instead of Google's production `cloud-agentspace` account.
543543

@@ -585,9 +585,9 @@ Both modes require `SKIP_JWT_VALIDATION=true` on the marketplace handler so it a
585585
python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'
586586
```
587587

588-
#### Option A: Static Credentials (No Keycloak)
588+
#### Static Credentials Mode
589589

590-
This mode skips Keycloak client creation. Instead, the caller provides pre-registered `client_id` and `client_secret` in the DCR request body alongside the `software_statement`. The handler validates them (skipped with `SKIP_JWT_VALIDATION=true`), stores them linked to the order, and returns them.
590+
This mode skips OAuth client creation via the GMA SSO API. Instead, the caller provides pre-registered `client_id` and `client_secret` in the DCR request body alongside the `software_statement`. The handler validates them (skipped with `SKIP_JWT_VALIDATION=true`), stores them linked to the order, and returns them.
591591

592592
1. **Copy the secrets template and edit it:**
593593
```bash
@@ -638,142 +638,6 @@ This mode skips Keycloak client creation. Instead, the caller provides pre-regis
638638
podman kube down deploy/podman/marketplace-handler-pod.yaml
639639
```
640640

641-
#### Option B: Real DCR with Local Keycloak
642-
643-
This mode exercises the full DCR flow -- real OAuth client creation in a locally-controlled Keycloak instance.
644-
645-
1. **Start Keycloak in Podman:**
646-
```bash
647-
podman run -d \
648-
--name keycloak-test \
649-
-p 8180:8080 \
650-
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
651-
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
652-
-e KC_HTTP_ENABLED=true \
653-
-e KC_HOSTNAME=host.containers.internal \
654-
-e KC_HOSTNAME_PORT=8180 \
655-
-e KC_HOSTNAME_STRICT=true \
656-
quay.io/keycloak/keycloak:26.0 start-dev --http-port=8080
657-
```
658-
659-
> **Why these hostname settings?** The marketplace handler container reaches
660-
> Keycloak via `host.containers.internal:8180`, but you interact with Keycloak
661-
> from the host via `localhost:8180`. With `KC_HOSTNAME_STRICT=true`, Keycloak
662-
> uses a consistent issuer (`http://host.containers.internal:8180/...`) for all
663-
> tokens regardless of which hostname the request arrives on. Without this, the
664-
> IAT (Initial Access Token) would have a `localhost` issuer that mismatches
665-
> when the handler presents it via `host.containers.internal`, causing
666-
> "Failed decode token" errors.
667-
668-
2. **Disable SSL requirement and create the test realm:**
669-
670-
Since `KC_HOSTNAME_STRICT=true` treats `localhost` requests as external,
671-
you must disable the SSL requirement via `kcadm.sh` from inside the container:
672-
673-
```bash
674-
# Authenticate kcadm.sh (uses internal port 8080)
675-
podman exec keycloak-test /opt/keycloak/bin/kcadm.sh \
676-
config credentials --server http://localhost:8080 \
677-
--realm master --user admin --password admin
678-
679-
# Disable SSL on master realm
680-
podman exec keycloak-test /opt/keycloak/bin/kcadm.sh \
681-
update realms/master -s sslRequired=NONE
682-
683-
# Create test-realm with SSL disabled
684-
podman exec keycloak-test /opt/keycloak/bin/kcadm.sh \
685-
create realms -s realm=test-realm -s enabled=true -s sslRequired=NONE
686-
```
687-
688-
3. **Get an admin token:**
689-
```bash
690-
ADMIN_TOKEN=$(curl -s -X POST \
691-
"http://localhost:8180/realms/master/protocol/openid-connect/token" \
692-
-d "client_id=admin-cli" \
693-
-d "username=admin" \
694-
-d "password=admin" \
695-
-d "grant_type=password" \
696-
| python -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
697-
```
698-
699-
4. **Generate an Initial Access Token (IAT) for DCR:**
700-
```bash
701-
IAT=$(curl -s -X POST \
702-
"http://localhost:8180/admin/realms/test-realm/clients-initial-access" \
703-
-H "Authorization: Bearer $ADMIN_TOKEN" \
704-
-H "Content-Type: application/json" \
705-
-d '{"count": 100, "expiration": 86400}' \
706-
| python -c "import sys,json; print(json.load(sys.stdin)['token'])")
707-
echo "Initial Access Token: $IAT"
708-
```
709-
710-
5. **Copy the secrets template and configure for local Keycloak:**
711-
```bash
712-
cp deploy/podman/lightspeed-agent-secret.yaml deploy/podman/my-secrets.yaml
713-
```
714-
715-
Edit `deploy/podman/my-secrets.yaml`:
716-
```yaml
717-
stringData:
718-
RED_HAT_SSO_CLIENT_ID: "lightspeed-agent"
719-
RED_HAT_SSO_CLIENT_SECRET: "dummy"
720-
DCR_INITIAL_ACCESS_TOKEN: "<the IAT from step 4>"
721-
DCR_ENCRYPTION_KEY: "<your-fernet-key>"
722-
MARKETPLACE_DATABASE_URL: "postgresql+asyncpg://insights:insights@localhost:5432/lightspeed_agent"
723-
MARKETPLACE_DB_PASSWORD: "insights"
724-
```
725-
726-
6. **Update the configmap** in `deploy/podman/lightspeed-agent-configmap.yaml`:
727-
```yaml
728-
DCR_ENABLED: "true"
729-
SKIP_JWT_VALIDATION: "true"
730-
RED_HAT_SSO_ISSUER: "http://host.containers.internal:8180/realms/test-realm"
731-
```
732-
733-
Note: Use `host.containers.internal` so the handler container can reach Keycloak running on the host.
734-
735-
7. **Start the marketplace handler pod:**
736-
```bash
737-
podman kube play deploy/podman/my-secrets.yaml
738-
podman kube play \
739-
--configmap deploy/podman/lightspeed-agent-configmap.yaml \
740-
deploy/podman/marketplace-handler-pod.yaml
741-
```
742-
743-
8. **Run the test script:**
744-
```bash
745-
# Method A (key file):
746-
export TEST_SA_KEY_FILE=dcr-test-key.json
747-
python scripts/test_dcr.py
748-
749-
# Method B (IAM API):
750-
export TEST_SERVICE_ACCOUNT=dcr-test@<PROJECT>.iam.gserviceaccount.com
751-
python scripts/test_dcr.py
752-
```
753-
754-
The handler will create a real OAuth client in your local Keycloak. You can verify it at http://localhost:8180/admin -> test-realm -> Clients.
755-
756-
9. **You can also test Keycloak DCR directly** (bypassing the handler entirely):
757-
```bash
758-
curl -s -X POST \
759-
"http://localhost:8180/realms/test-realm/clients-registrations/openid-connect" \
760-
-H "Authorization: Bearer $IAT" \
761-
-H "Content-Type: application/json" \
762-
-d '{
763-
"client_name": "gemini-order-test-123",
764-
"redirect_uris": ["https://gemini.google.com/callback"],
765-
"grant_types": ["authorization_code", "refresh_token"],
766-
"token_endpoint_auth_method": "client_secret_basic",
767-
"application_type": "web"
768-
}'
769-
```
770-
771-
10. **Clean up:**
772-
```bash
773-
podman kube down deploy/podman/marketplace-handler-pod.yaml
774-
podman stop keycloak-test && podman rm keycloak-test
775-
```
776-
777641
#### Test Script Reference
778642

779643
The test script at `scripts/test_dcr.py` is configurable via environment variables:

0 commit comments

Comments
 (0)