Skip to content

Commit 80fa04a

Browse files
authored
Merge pull request #493 from araujof/feat/cpex_plugin
Feat: Add CPEX plugin
2 parents df01b1b + 821160f commit 80fa04a

72 files changed

Lines changed: 12594 additions & 6 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ jobs:
5656
build_args: |
5757
GO_BUILD_TAGS=exclude_plugin_a2aparser,exclude_plugin_ibac,exclude_plugin_inferenceparser,exclude_plugin_mcpparser,exclude_plugin_opa,exclude_plugin_sparc,exclude_plugin_tokenbroker
5858
59+
# AuthBridge proxy-sidecar CPEX image — authbridge-proxy built
60+
# with -tags cpex (links libcpex_ffi.a from a pinned CPEX
61+
# release) so the `cpex` plugin routes hooks through the CPEX
62+
# framework (APL DSL + named CPEX policy plugins). Needs the
63+
# CPEX_FFI_VERSION build-arg, read from the CPEX_FFI_VERSION
64+
# file in the build step below.
65+
- name: authbridge-cpex
66+
context: ./authbridge
67+
dockerfile: cmd/authbridge-cpex/Dockerfile
68+
5969
# SPARC reflection service — the backend the `sparc` plugin calls.
6070
# Deployed once per cluster via authbridge/sparc-service/deploy.
6171
- name: sparc-service
@@ -109,6 +119,27 @@ jobs:
109119
# Add 'latest' tag for version tags, workflow_dispatch, and pushes to main
110120
type=raw,value=latest,enable=${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
111121
122+
# 6b. Resolve build-args. authbridge-cpex needs CPEX_FFI_VERSION
123+
# (the release tag) and CPEX_FFI_ABI (the FFI ABI integer the
124+
# linked lib must report) — both read from the files next to its
125+
# Dockerfile and asserted against the tarball at build time. Other
126+
# images leave this empty (an undeclared build-arg is ignored).
127+
- name: Resolve build args
128+
id: buildargs
129+
run: |
130+
if [[ "${{ matrix.image_config.name }}" == "authbridge-cpex" ]]; then
131+
VERSION="$(tr -d '[:space:]' < authbridge/cmd/authbridge-cpex/CPEX_FFI_VERSION)"
132+
ABI="$(tr -d '[:space:]' < authbridge/cmd/authbridge-cpex/CPEX_FFI_ABI)"
133+
{
134+
echo "args<<EOF"
135+
echo "CPEX_FFI_VERSION=${VERSION}"
136+
echo "CPEX_FFI_ABI=${ABI}"
137+
echo "EOF"
138+
} >> "$GITHUB_OUTPUT"
139+
else
140+
echo "args=" >> "$GITHUB_OUTPUT"
141+
fi
142+
112143
# 7. Build and push image
113144
- name: Build and push ${{ matrix.image_config.name }}
114145
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
@@ -120,3 +151,4 @@ jobs:
120151
platforms: linux/amd64,linux/arm64
121152
tags: ${{ steps.meta.outputs.tags }}
122153
labels: ${{ steps.meta.outputs.labels }}
154+
build-args: ${{ steps.buildargs.outputs.args }}

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Thumbs.db
3737
*.bak
3838
*.orig
3939

40+
# Local scratch / working notes (never committed)
41+
.sketchpad/
42+
4043
# venv
4144
venv
4245
.venv
@@ -67,3 +70,11 @@ __pycache__/
6770
.mypy_cache/
6871
test-jwt-rotation.sh
6972
kagenti-webhook/bin/
73+
74+
# Go build artifacts — local `go build` outputs in cmd/* dirs
75+
# (binary name matches the package dir; trivially `git add`-ed by mistake).
76+
authbridge/cmd/abctl/abctl
77+
authbridge/cmd/authbridge-proxy/authbridge-proxy
78+
authbridge/cmd/authbridge-envoy/authbridge-envoy
79+
authbridge/cmd/authbridge-lite/authbridge-lite
80+
authbridge/cmd/authbridge-cpex/authbridge-cpex

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ All images are pushed to `ghcr.io/kagenti/kagenti-extensions/` from
171171
| **`authbridge`** | **`authbridge/cmd/authbridge-proxy/Dockerfile`** | **proxy-sidecar combined image (default mode): authbridge-proxy (full plugin set incl. parsers) + spiffe-helper. No Envoy.** |
172172
| `authbridge-envoy` | `authbridge/cmd/authbridge-envoy/Dockerfile` | envoy-sidecar combined image: Envoy + authbridge-envoy (ext_proc, full plugin set) + spiffe-helper |
173173
| `authbridge-lite` | `authbridge/cmd/authbridge-proxy/Dockerfile` (+ `GO_BUILD_TAGS=exclude_plugin_*`) | proxy-sidecar combined image built auth-only (jwt-validation + token-exchange; OPA + parsers dropped) + spiffe-helper. A build variant of `authbridge`, not a separate binary; not yet referenced by the operator's default config |
174+
| `authbridge-cpex` | `authbridge/cmd/authbridge-cpex/Dockerfile` | proxy-sidecar build with the CPEX plugin: authbridge-proxy built with `-tags cpex`, links `libcpex_ffi.a` from a pinned CPEX release (CGO_ENABLED=1). Routes hooks through the CPEX framework (APL DSL + named CPEX policy plugins). FFI ABI version is read from `authbridge/cmd/authbridge-cpex/CPEX_FFI_VERSION` |
174175
| `proxy-init` | `authbridge/proxy-init/Dockerfile.init` | Alpine + iptables init container (envoy-sidecar + proxy-sidecar enforce-redirect modes) |
175176

176177
In all three combined images, `spiffe-helper` is started conditionally

authbridge/CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ binaries with shared auth logic in `authlib/`:
2020
`-tags include_plugin_contextguru` to link it in.
2121
- `cmd/authbridge-envoy/` — envoy-sidecar mode. ext_proc gRPC server hooked
2222
into Envoy. Full plugin set. The `exclude_plugin_ibac` tag applies here too.
23+
- `cmd/authbridge-cpex/` — proxy-sidecar mode, full plugin set plus the
24+
`cpex` plugin. Built with `-tags cpex` and requires cgo (CGO_ENABLED=1):
25+
it links `libcpex_ffi.a` from a pinned CPEX release to route hooks through
26+
the CPEX framework (APL DSL + named CPEX policy plugins). The FFI ABI
27+
version lives in `cmd/authbridge-cpex/CPEX_FFI_VERSION`. The other
28+
binaries are pure-Go (CGO_ENABLED=0) and do not import the cpex package.
2329
- `authbridge-lite` (**image, not a separate binary**) — `cmd/authbridge-proxy`
2430
built with `exclude_plugin_*` tags so only jwt-validation + token-exchange
2531
compile in (OPA + parsers dropped). For size-optimized deployments that
@@ -64,6 +70,12 @@ authbridge/
6470
│ ├── Dockerfile # envoy-sidecar combined image (Envoy + authbridge-envoy)
6571
│ └── entrypoint.sh
6672
73+
├── cmd/authbridge-cpex/ # proxy-sidecar mode + cpex plugin. -tags cpex, cgo required.
74+
│ ├── main.go
75+
│ ├── Dockerfile # proxy-sidecar build linking libcpex_ffi.a
76+
│ ├── CPEX_FFI_VERSION # pinned CPEX FFI ABI version (build-arg source of truth)
77+
│ └── entrypoint.sh
78+
6779
├── proxy-init/ # iptables init container (envoy-sidecar + proxy-sidecar enforce-redirect modes)
6880
│ ├── init-iptables.sh # iptables setup script
6981
│ ├── Dockerfile.init # proxy-init container image
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package contracts
2+
3+
// ClaimsCarrier is an optional capability interface a pipeline.Identity
4+
// may implement to expose richer claim data than the minimal
5+
// pipeline.Identity surface (Subject / ClientID / Scopes).
6+
//
7+
// It exists so consumers that want issuer / audience / auth-method /
8+
// curated claims — chiefly the cpex plugin building a policy-input
9+
// document — can read them without growing pipeline.Identity (which
10+
// stays deliberately minimal so any auth shape can satisfy it). Mirrors
11+
// the ContentSource pattern: consumers type-assert to this interface and
12+
// simply skip the enrichment when the concrete identity doesn't
13+
// implement it.
14+
//
15+
// if cc, ok := pctx.Identity.(contracts.ClaimsCarrier); ok {
16+
// authMethod = cc.AuthMethod()
17+
// claims = cc.Claims()
18+
// }
19+
//
20+
// Producers (e.g. jwt-validation's claims adapter) curate what Claims
21+
// returns — a small, safe-to-forward set (issuer, audience, expiry), NOT
22+
// the full raw claim map. The session API and CPEX traces both surface
23+
// this data, so producers must keep it free of secrets.
24+
type ClaimsCarrier interface {
25+
// Issuer is the token issuer (`iss`), or "" when not applicable.
26+
Issuer() string
27+
28+
// Audience is the token audience list (`aud`), or nil.
29+
Audience() []string
30+
31+
// AuthMethod names how the caller authenticated — "jwt", "mtls",
32+
// "spiffe", etc. Drives policies that branch on authentication
33+
// strength. "" when the producer can't classify it.
34+
AuthMethod() string
35+
36+
// Claims returns a curated, string-valued claim set safe to forward
37+
// into policy context and observability surfaces. Producers pick the
38+
// keys (conventionally "issuer", "audience", "exp"); they MUST NOT
39+
// dump the full raw claim map, which may contain secrets or PII.
40+
// Values are strings so the set maps cleanly onto CPEX's
41+
// SubjectExtension.Claims (map[string]string) without lossy coercion.
42+
Claims() map[string]string
43+
}

authbridge/authlib/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/kagenti/kagenti-extensions/authbridge/authlib
33
go 1.26.4
44

55
require (
6+
github.com/contextforge-org/cpex/go/cpex v0.2.2
67
github.com/envoyproxy/go-control-plane/envoy v1.37.0
78
github.com/fsnotify/fsnotify v1.10.1
89
github.com/gobwas/glob v0.2.3
@@ -99,6 +100,8 @@ require (
99100
github.com/valyala/fasthttp v1.71.0 // indirect
100101
github.com/valyala/fastjson v1.6.10 // indirect
101102
github.com/vektah/gqlparser/v2 v2.5.34 // indirect
103+
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
104+
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
102105
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
103106
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
104107
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect

authbridge/authlib/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
5858
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
5959
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
6060
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
61+
github.com/contextforge-org/cpex/go/cpex v0.2.2 h1:rC0HciJa19nEz9LcjmgwyjoRF3EUFidNpAT6eYu1fhA=
62+
github.com/contextforge-org/cpex/go/cpex v0.2.2/go.mod h1:SuDkdY76asy9MP/72YKqhc2FBcFZ9t5PKe3CeXSO4/g=
6163
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6264
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6365
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -261,6 +263,10 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT
261263
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
262264
github.com/vektah/gqlparser/v2 v2.5.34 h1:MEea5P0qhdcqfBL45ghKE+qr9laidVHTMHjav5h7ckk=
263265
github.com/vektah/gqlparser/v2 v2.5.34/go.mod h1:mFdHLGCio7OGX1fby9ZjTW6FN+qxgmbnBcRIeeScE5s=
266+
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
267+
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
268+
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
269+
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
264270
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
265271
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
266272
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=

authbridge/authlib/pipeline/context.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,16 @@ func (c *Context) clearCurrent() {
289289
// them.
290290
func (c *Context) RejectingPlugin() string { return c.rejectingPlugin }
291291

292+
// CurrentPhase reports the phase of the plugin dispatch currently in
293+
// flight — InvocationPhaseRequest while Pipeline.Run is iterating
294+
// OnRequest, InvocationPhaseResponse while Pipeline.RunResponse is
295+
// iterating OnResponse, and "" outside a dispatch. Plugins read it to
296+
// distinguish request from response without inferring the phase from
297+
// body presence (an empty-bodied 204 response would otherwise look
298+
// like a request). Set by the framework via setCurrent before each
299+
// dispatch; see SetCurrentPlugin.
300+
func (c *Context) CurrentPhase() InvocationPhase { return c.currentPhase }
301+
292302
// setRejectingPlugin records the name of the plugin that returned
293303
// Reject. Framework-internal; callers in Pipeline.Run / RunResponse
294304
// set this once per request, never overwrite (first rejection wins,

authbridge/authlib/pipeline/extensions.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,28 @@ func (d *DelegationExtension) Depth() int {
391391
}
392392

393393
// DelegationHop represents one hop in the delegation chain.
394+
//
395+
// Audience, Strategy, and FromCache enrich a hop produced by an
396+
// AuthBridge auth plugin (today: token-exchange) so downstream policy
397+
// can reason about WHAT a token was minted for and HOW. They map onto
398+
// the corresponding CPEX DelegationHop fields. All three are optional —
399+
// a producer that only knows the subject leaves them zero.
394400
type DelegationHop struct {
395401
SubjectID string
396402
Scopes []string
397403
Timestamp time.Time
404+
405+
// Audience is the target audience the hop's token was minted for
406+
// (RFC 8693 `audience`). Empty when the hop isn't an exchange.
407+
Audience string
408+
409+
// Strategy names how the hop was produced — e.g. "token-exchange"
410+
// for an RFC 8693 exchange. Empty when unclassified.
411+
Strategy string
412+
413+
// FromCache reports the hop's token was served from the exchange
414+
// cache rather than freshly minted at the IdP.
415+
FromCache bool
398416
}
399417

400418
// AppendHop adds a hop to the delegation chain. This is the only way to extend

0 commit comments

Comments
 (0)