Skip to content

Commit ae9539a

Browse files
feat(#3647): auth pivot, dev scaffold, entity provider fixes
Pivot Kagenti authentication from per-user RFC 8693 token exchange to service-account OAuth2 Client Credentials Grant. Add dev scaffold (app-config.yaml, packages/backend, load-secrets.sh). Fix entity provider API URLs and response handling. Auth pivot (config.d.ts, schemas.ts, schemas.test.ts): - Replace tokenExchange.{enabled,audience,userTokenHeader} with flat tokenEndpoint, clientId, clientSecret, tokenExpiryBufferSeconds - Bump BOOST_CONFIG_SCHEMA_VERSION to 2 - clientSecret marked sensitive: true with @visibility secret - tokenExpiryBufferSeconds: .number().int().min(0).optional() - Add validation tests for new fields Entity provider fixes (kagenti-entity-provider): - New KeycloakAuthClient: OAuth2 Client Credentials Grant with token caching and configurable expiry buffer - Fix API URLs from /a2a/ to /api/v1/ for agents and tools - Add unwrapItems helper for {items:T[]} and T[] responses - Pass optional authClient to entity providers - Module reads boost.kagenti.auth.* and constructs auth client - Add tests for auth, URL patterns, response unwrapping Dev scaffold: - app-config.yaml with guest auth, SQLite, dev providers - packages/backend host with metrics shim and boost plugins - scripts/load-secrets.sh for K8s dev secret loading Spec updates across 14 specification/openspec files: - Rename "Per-User Identity Delegation" to "Service-Account Keycloak Authentication" throughout - Add boost-responses-api-toolkit and boost-toolscope to workspace structure docs - Add Local Development section to boost-context.md - Update staged-issues.md issue 13 and add issue 16 URL TODO comments added to mcp/routes.ts and skills/routes.ts for future catalog cross-reference. Closes #3647
1 parent e2df0a3 commit ae9539a

32 files changed

Lines changed: 944 additions & 148 deletions

File tree

workspaces/boost/app-config.yaml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
app:
2+
title: Boost Dev
3+
baseUrl: http://localhost:3000
4+
5+
organization:
6+
name: Red Hat
7+
8+
backend:
9+
baseUrl: http://localhost:7007
10+
listen:
11+
port: 7007
12+
database:
13+
client: better-sqlite3
14+
connection: ':memory:'
15+
auth:
16+
dangerouslyDisableDefaultAuthPolicy: true
17+
18+
auth:
19+
providers:
20+
guest:
21+
dangerouslyAllowOutsideDevelopment: true
22+
23+
catalog:
24+
rules:
25+
- allow:
26+
- Component
27+
- Resource
28+
- API
29+
- System
30+
- Domain
31+
- Location
32+
- Template
33+
- Group
34+
- User
35+
36+
boost:
37+
security:
38+
mode: 'development-only-no-auth'
39+
40+
model:
41+
baseUrl: ${BOOST_MODEL_BASE_URL:-http://localhost:8080/v1}
42+
name: ${BOOST_MODEL:-default}
43+
44+
providers:
45+
llamastack:
46+
baseUrl: ${LLAMASTACK_BASE_URL:-http://localhost:8321}
47+
48+
kagenti:
49+
baseUrl: ${KAGENTI_BASE_URL:-http://localhost:8080}
50+
namespaces:
51+
- ${KAGENTI_NAMESPACE:-default}
52+
53+
kagenti:
54+
auth:
55+
tokenEndpoint: ${KAGENTI_TOKEN_ENDPOINT:-}
56+
clientId: ${KAGENTI_CLIENT_ID:-}
57+
clientSecret: ${KAGENTI_CLIENT_SECRET:-}
58+
59+
entityProviders:
60+
kagenti:
61+
baseUrl: ${KAGENTI_BASE_URL:-http://localhost:8080}
62+
namespaces:
63+
- ${KAGENTI_NAMESPACE:-default}
64+
agentRefreshIntervalSeconds: 300
65+
toolRefreshIntervalSeconds: 300

workspaces/boost/openspec/changes/platform-operations-deployment/specs/runtime-config/spec.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,14 @@ The following new features MUST have runtime configuration fields as specified b
103103

104104
#### Scenario: Token exchange configuration
105105

106-
- **WHEN** the admin configures per-user Kagenti auth
106+
- **WHEN** the admin configures Kagenti service-account auth
107107
- **THEN** the following fields are available:
108108
| Field | Scope | Description |
109109
|---|---|---|
110-
| `boost.kagenti.auth.tokenExchange.enabled` | yaml-only | Enable RFC 8693 token exchange |
111-
| `boost.kagenti.auth.tokenExchange.audience` | yaml-only | Target audience for exchanged token |
112-
| `boost.kagenti.auth.tokenExchange.userTokenHeader` | yaml-only | Header containing user OIDC token |
110+
| `boost.kagenti.auth.tokenEndpoint` | yaml-only | Keycloak token endpoint URL |
111+
| `boost.kagenti.auth.clientId` | yaml-only | OAuth2 client ID |
112+
| `boost.kagenti.auth.clientSecret` | yaml-only | OAuth2 client secret (sensitive) |
113+
| `boost.kagenti.auth.tokenExpiryBufferSeconds` | yaml-only | Seconds before expiry to refresh (default: 60) |
113114

114115
#### Scenario: Credential encryption
115116

workspaces/boost/openspec/changes/platform-operations-deployment/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
- [ ] 2.2 Generate `config.d.ts` types from Zod schemas
2020
- [ ] 2.3 Validate all config writes (YAML and DB) via Zod `.parse()` — no hand-written validators
2121
- [ ] 2.4 Annotate each field with `configScope`: `yaml-only`, `db-overridable`, or `db-only`
22-
- [ ] 2.5 Add Zod schemas for new config fields: agentApproval, skillsMarketplace, tokenExchange, DevSpaces credentials
22+
- [ ] 2.5 Add Zod schemas for new config fields: agentApproval, skillsMarketplace, Keycloak service-account auth, DevSpaces credentials
2323
- [ ] 2.6 Admin UI shows only DB-overridable and DB-only fields
2424
- [ ] 2.7 Implement credential encryption for sensitive DB-stored values (DevSpaces tokens)
2525
- [ ] 2.8 Implement schema version tracking: store schema version alongside DB values, re-validate on startup

workspaces/boost/openspec/changes/pluggable-ai-platform-architecture/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Boost implements the provider abstraction as modular RHDH dynamic plugins from t
1818
- Modifying chat interaction behavior or message rendering
1919
- Rewriting the ADK orchestration library
2020
- Creating catalog entities for models/agents (covered in agent-creation-discovery change)
21-
- Per-user token exchange (covered in security-safety-governance change)
21+
- Kagenti service-account auth (covered in security-safety-governance change — OAuth2 Client Credentials Grant adopted)
2222

2323
## Decisions
2424

workspaces/boost/openspec/changes/security-safety-governance/design.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
Boost builds the security and governance layer with Backstage fine-grained permissions as the sole authorization mechanism from day one. The Augment reference prototype's governance system grew into a parallel authorization layer where authorization decisions bypassed `permissions.authorize()`. Boost avoids this entirely.
66

7-
Boost also implements RFC 8693 token exchange for per-user identity delegation to Kagenti from the start, enabling per-user audit trails.
7+
Boost also implements OAuth2 Client Credentials Grant via `KeycloakTokenManager` for service-account authentication to Kagenti, with user identity propagated via `X-Backstage-User` header for audit trails.
88

99
## Goals
1010

1111
- Implement 16 fine-grained Backstage permissions as the sole authorization mechanism
1212
- Add conditional permission rules for ownership (IS_OWNER), separation of duties (IS_NOT_CREATOR), and lifecycle stage gating (HAS_LIFECYCLE_STAGE)
13-
- Implement RFC 8693 token exchange for per-user Kagenti identity
13+
- Implement OAuth2 Client Credentials Grant for Kagenti service-account auth
1414
- Use `development-only-no-auth` as the only dev security mode name (no legacy aliases)
1515
- Add CSRF protection and credential encryption
1616
- Export all permissions from `boost-common`
@@ -40,20 +40,20 @@ These rules are evaluated against loaded resources via `createConditionalDecisio
4040

4141
The `IS_NOT_CREATOR` permission rule is the primary enforcement mechanism. A route-level guard remains as defense-in-depth (belt and suspenders). Both layers are active in `security.mode === 'full'`.
4242

43-
### Decision 4: Per-user token exchange is backend-only with graceful fallback
43+
### Decision 4: Service-account auth with KeycloakTokenManager
4444

45-
`TokenExchangeManager` reads the user's OIDC token from a configurable request header (injected by auth proxy), exchanges it via RFC 8693, caches per-user, and deduplicates concurrent exchanges. All failures fall back silently to the shared service-account token — no request is ever blocked by token exchange issues. This is deliberately conservative: token exchange enhances audit trails and per-user authorization but must never degrade availability.
45+
`KeycloakTokenManager` acquires tokens via OAuth2 Client Credentials Grant, caches them with a configurable expiry buffer (`tokenExpiryBufferSeconds`, default: 60), and automatically refreshes before expiry. On 401 responses, the token is refreshed and the request retried once (max-1-retry). User identity is propagated via `X-Backstage-User` header for audit trails. This is deliberately simple: service-account auth provides consistent authentication without per-user token management complexity.
4646

4747
### Decision 5: Separation of authorization concerns
4848

4949
Three non-overlapping authorization layers:
5050

5151
- **Backstage** governs: UI visibility, agent lifecycle governance, ownership, approval workflows, admin operations
52-
- **Kagenti** governs: agent specs, tools, runtime operations — via per-user exchanged token when enabled
52+
- **Kagenti** governs: agent specs, tools, runtime operations — via service-account token with user identity in `X-Backstage-User` header
5353
- **Kubernetes** governs: pod/deployment operations, namespace scoping, SPIRE mTLS
5454

5555
## Risks
5656

5757
- **RBAC policy complexity:** 16 permissions with conditions is more complex than 2. Mitigated by sensible defaults — `boost.access` as top-level gate and `boost.admin` available for coarse control.
58-
- **Token exchange reliability:** Keycloak availability becomes a dependency. Mitigated by graceful fallback to service-account token on any failure.
58+
- **Keycloak availability:** Keycloak becoming unavailable blocks Kagenti API calls. Mitigated by token caching with configurable expiry buffer, reducing the number of token requests.
5959
- **SonataFlow trust boundary:** Callbacks bypass self-approval prevention via header. Callback identity verification should be implemented to close this gap.

workspaces/boost/openspec/changes/security-safety-governance/proposal.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Enterprise AI platforms must treat security, safety, and governance as foundatio
2121
### Identity & Authentication
2222

2323
- RBAC via Keycloak OIDC + Backstage permissions (`boost.access`, `boost.admin` as top-level gates)
24-
- RFC 8693 token exchange for per-user Kagenti identity delegation
25-
- Graceful fallback to service-account token on any exchange failure
24+
- OAuth2 Client Credentials Grant for Kagenti service-account authentication via `KeycloakTokenManager`
25+
- Token caching with configurable expiry buffer and max-1-retry on 401
2626
- MCP 4-level auth chain
2727
- Kagenti SPIRE integration for infrastructure mTLS
2828

@@ -45,4 +45,4 @@ Enterprise AI platforms must treat security, safety, and governance as foundatio
4545
- `plugins/boost-common/src/permissions.ts` — 16 permission definitions with resource types
4646
- `plugins/boost-backend/src/middleware/security.ts``authorizeLifecycleAction` middleware
4747
- `plugins/boost-frontend/src/components/SecurityGate.tsx` — granular permission checks
48-
- `plugins/boost-backend/src/services/TokenExchangeManager.ts`RFC 8693 implementation
48+
- `plugins/kagenti-entity-provider/src/providers/kagentiAuth.ts``KeycloakAuthClient` (OAuth2 Client Credentials Grant)

workspaces/boost/openspec/changes/security-safety-governance/specs/access-control/spec.md

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,41 +81,62 @@ Inference responses are not stored on the server when ZDR is enabled.
8181

8282
## ADDED Requirements
8383

84-
### Requirement: Per-User Kagenti Identity via Token Exchange
84+
### Requirement: Service-Account Keycloak Authentication for Kagenti
8585

86-
User identity MUST be delegated to Kagenti via RFC 8693 OAuth2 Token Exchange so agent operations are authorized per-user.
86+
Kagenti API calls MUST be authenticated via OAuth2 Client Credentials Grant using `KeycloakTokenManager` for service-account authentication. User identity is propagated via the `X-Backstage-User` header for audit purposes.
8787

88-
#### Scenario: Token exchange enabled
88+
#### Scenario: Token acquisition
8989

90-
- **WHEN** `boost.kagenti.auth.tokenExchange.enabled` is `true`
91-
- **AND** a user's OIDC token is available via the configured header (default: `X-Forwarded-Access-Token`)
92-
- **THEN** `TokenExchangeManager` exchanges the user's token for a Kagenti-scoped token via RFC 8693
93-
- **AND** the exchanged token is cached per-user with TTL from token expiry
94-
- **AND** concurrent exchanges for the same user are deduplicated
95-
- **AND** the per-user token is used for all Kagenti API calls on behalf of that user
90+
- **WHEN** `boost.kagenti.auth.tokenEndpoint`, `clientId`, and `clientSecret` are all configured
91+
- **THEN** `KeycloakAuthClient` acquires a bearer token via OAuth2 Client Credentials Grant
92+
- **AND** the token is cached until `expires_in - tokenExpiryBufferSeconds` seconds
93+
- **AND** the bearer token is included in all Kagenti API requests as `Authorization: Bearer <token>`
9694

97-
#### Scenario: Token exchange graceful fallback
95+
#### Scenario: Streaming lifecycle with token refresh
9896

99-
- **WHEN** token exchange fails (missing header, Keycloak error, exchange failure, disabled config)
100-
- **THEN** the system silently falls back to the shared service-account token
101-
- **AND** no request is blocked due to token exchange failure
102-
- **AND** the fallback is logged for debugging
97+
- **WHEN** a cached token is about to expire (within `tokenExpiryBufferSeconds`)
98+
- **THEN** a fresh token is acquired before the next API call
99+
- **AND** in-flight requests continue with the previously cached token
103100

104-
#### Scenario: Token exchange configuration
101+
#### Scenario: 401 retry with max-1-retry constraint
105102

106-
- **WHEN** token exchange is configured
107-
- **THEN** the following config is used:
103+
- **WHEN** a Kagenti API call returns HTTP 401
104+
- **THEN** the cached token is invalidated and a fresh token is acquired
105+
- **AND** the request is retried with the new token
106+
- **AND** if the retried request also returns 401, the error is propagated to the caller
107+
108+
#### Scenario: User identity propagation
109+
110+
- **WHEN** a Kagenti API call is made on behalf of a user
111+
- **THEN** the `X-Backstage-User` header is set to the user's identity
112+
- **AND** the service-account bearer token is used for authentication (not the user's token)
113+
114+
#### Scenario: Service-account auth configuration
115+
116+
- **WHEN** Kagenti auth is configured
117+
- **THEN** the following config keys are used:
108118
| Key | Default | Description |
109119
|---|---|---|
110-
| `boost.kagenti.auth.tokenExchange.enabled` | `false` | Enable per-user token exchange |
111-
| `boost.kagenti.auth.tokenExchange.audience` || Target audience for exchanged token |
112-
| `boost.kagenti.auth.tokenExchange.userTokenHeader` | `X-Forwarded-Access-Token` | Header containing user's OIDC token |
120+
| `boost.kagenti.auth.tokenEndpoint` || Keycloak token endpoint URL |
121+
| `boost.kagenti.auth.clientId` || OAuth2 client ID |
122+
| `boost.kagenti.auth.clientSecret` || OAuth2 client secret (visibility: secret) |
123+
| `boost.kagenti.auth.tokenExpiryBufferSeconds` | `60` | Seconds before expiry to refresh |
124+
125+
#### Scenario: Kagenti REST API endpoints
126+
127+
- **WHEN** fetching agents from Kagenti
128+
- **THEN** the URL pattern is `GET /api/v1/agents?namespace={ns}` (not `/a2a/`)
129+
- **AND** the response is unwrapped via `unwrapItems` to handle both `{ items: T[] }` and `T[]` shapes
130+
131+
- **WHEN** fetching tools from Kagenti
132+
- **THEN** the URL pattern is `GET /api/v1/tools?namespace={ns}` (not `/a2a/`)
133+
- **AND** the response is unwrapped via `unwrapItems` to handle both response shapes
113134

114135
#### Scenario: LlamaStack provider unaffected
115136

116-
- **WHEN** token exchange is configured
137+
- **WHEN** Kagenti auth is configured
117138
- **THEN** `ResponsesApiProvider` is not modified — `setUserContext` is optional and not implemented
118-
- **AND** token exchange is Kagenti-specific
139+
- **AND** Keycloak auth is Kagenti-specific
119140

120141
### Requirement: CSRF Protection
121142

workspaces/boost/openspec/changes/security-safety-governance/tasks.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,14 @@
4343
- [ ] 6.4 Gate MCP panel with `boost.mcp.manage`
4444
- [ ] 6.5 Gate config panel with `boost.config.manage`
4545

46-
## 7. Token Exchange (P1)
46+
## 7. Keycloak Service-Account Auth (P1)
4747

48-
- [ ] 7.1 Create `TokenExchangeManager` implementing RFC 8693 exchange
49-
- [ ] 7.2 Add per-user token caching with TTL from token expiry
50-
- [ ] 7.3 Add concurrent exchange deduplication
51-
- [ ] 7.4 Add graceful fallback to service-account token on all failures
52-
- [ ] 7.5 Add config schema: `boost.kagenti.auth.tokenExchange.{enabled, audience, userTokenHeader}`
53-
- [ ] 7.6 Integrate into `KagentiApiClient.requestCore()` — inject per-user token when available
54-
- [ ] 7.7 Extract user OIDC token from configurable request header in route handlers
48+
- [ ] 7.1 Create `KeycloakAuthClient` implementing OAuth2 Client Credentials Grant
49+
- [ ] 7.2 Add token caching with configurable expiry buffer (`tokenExpiryBufferSeconds`, default: 60)
50+
- [ ] 7.3 Add max-1-retry on 401 (refresh token and retry once)
51+
- [ ] 7.4 Add config schema: `boost.kagenti.auth.{tokenEndpoint, clientId, clientSecret, tokenExpiryBufferSeconds}`
52+
- [ ] 7.5 Integrate into entity providers and `KagentiApiClient` — inject bearer token
53+
- [ ] 7.6 Propagate user identity via `X-Backstage-User` header for audit
5554

5655
## 8. CSRF and Credential Security (P2)
5756

@@ -70,7 +69,7 @@
7069
- [ ] 10.2 Verify IS_OWNER blocks non-owner promote/delete/withdraw
7170
- [ ] 10.3 Verify IS_NOT_CREATOR blocks self-approval
7271
- [ ] 10.4 Verify `boost.admin` works as coarse-grained alternative to fine-grained permissions
73-
- [ ] 10.5 Verify token exchange fallback: disabled config → service-account token
74-
- [ ] 10.6 Verify token exchange fallback: Keycloak error → service-account token
75-
- [ ] 10.7 Verify token exchange fallback: missing header → service-account token
72+
- [ ] 10.5 Verify KeycloakAuthClient: token acquisition and caching
73+
- [ ] 10.6 Verify KeycloakAuthClient: max-1-retry on 401
74+
- [ ] 10.7 Verify entity providers include bearer token when auth is configured
7675
- [ ] 10.8 Verify `none` is rejected with a clear error pointing to `development-only-no-auth`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint.backend');
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "backend",
3+
"version": "0.0.0",
4+
"private": true,
5+
"main": "dist/index.cjs.js",
6+
"types": "src/index.ts",
7+
"backstage": {
8+
"role": "backend"
9+
},
10+
"scripts": {
11+
"start": "backstage-cli package start",
12+
"build": "backstage-cli package build",
13+
"clean": "backstage-cli package clean",
14+
"test": "backstage-cli package test --passWithNoTests",
15+
"lint": "backstage-cli package lint",
16+
"tsc": "tsc"
17+
},
18+
"dependencies": {
19+
"@backstage/backend-defaults": "^0.17.3",
20+
"@backstage/backend-plugin-api": "^1.9.2",
21+
"@red-hat-developer-hub/backstage-plugin-boost-backend": "workspace:^",
22+
"@red-hat-developer-hub/backstage-plugin-boost-backend-module-kagenti": "workspace:^",
23+
"@red-hat-developer-hub/backstage-plugin-boost-backend-module-llamastack": "workspace:^",
24+
"@red-hat-developer-hub/backstage-plugin-kagenti-entity-provider": "workspace:^",
25+
"@red-hat-developer-hub/backstage-plugin-llamastack-entity-provider": "workspace:^"
26+
},
27+
"devDependencies": {
28+
"@backstage/cli": "^0.36.3"
29+
}
30+
}

0 commit comments

Comments
 (0)