Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ spec:
MaxDelegationDepth caps how many nested RFC 8693 "act" entries are
recorded in an audit event's delegationChain. Deeper chains are
truncated (marked with truncated=true). Defaults to 10 when unset.
minimum: 1
type: integer
type: object
backends:
Expand Down Expand Up @@ -4644,6 +4645,7 @@ spec:
MaxDelegationDepth caps how many nested RFC 8693 "act" entries are
recorded in an audit event's delegationChain. Deeper chains are
truncated (marked with truncated=true). Defaults to 10 when unset.
minimum: 1
type: integer
type: object
backends:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,7 @@ spec:
MaxDelegationDepth caps how many nested RFC 8693 "act" entries are
recorded in an audit event's delegationChain. Deeper chains are
truncated (marked with truncated=true). Defaults to 10 when unset.
minimum: 1
type: integer
type: object
backends:
Expand Down Expand Up @@ -4647,6 +4648,7 @@ spec:
MaxDelegationDepth caps how many nested RFC 8693 "act" entries are
recorded in an audit event's delegationChain. Deeper chains are
truncated (marked with truncated=true). Defaults to 10 when unset.
minimum: 1
type: integer
type: object
backends:
Expand Down
2 changes: 1 addition & 1 deletion docs/arch/02-core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ See `pkg/audit/mcp_events.go` for complete list of event types.
**Event data:**
- Timestamp, source, outcome
- Subjects (user, session)
- Delegation chain (`delegationChain`) — RFC 8693 `act` claim actors, outermost
- Delegation chain (`delegation_chain`) — RFC 8693 `act` claim actors, outermost
first, when the caller used a delegated token
- Target (endpoint, method, resource)
- Request/response data (configurable)
Expand Down
18 changes: 16 additions & 2 deletions docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Multiple webhook definitions of the same type run in configuration order. When m

Configuration files may be written in YAML or JSON. Duration values such as `timeout` accept strings like `5s`, and omitted timeouts default to `10s`.

When the caller authenticated with an RFC 8693 delegated token, the request payload sent to webhook receivers includes a `delegation_chain` field on the principal object, with the same shape as the `delegation_chain` field documented under [Audit Middleware](#9-audit-middleware) below.

Example:

```bash
Expand Down Expand Up @@ -527,14 +529,26 @@ Audit events are logged as structured JSON objects:
- `delegation_chain`: RFC 8693 delegation chain, present only when the caller
authenticated with a delegated token (i.e. the JWT carries an `act` claim)
- `actors`: Acting parties, outermost (most recent) first — `actors[0]` is
the direct delegate that presented the token
the direct delegate that presented the token. Empty array (not omitted)
when `malformed` is `true` and no actor could be parsed at all
- `sub`: Acting party identifier (from the `act` claim's `sub` member)
- `act_claims`: Additional `act` claim members (e.g. `iss`), when present
- `act_claims`: Additional `act` claim members (e.g. `iss`), when present.
May also carry `sub` when the token's `act.sub` was present but not a
string (RFC 7519 §4.1.2 requires a string); such a value is
deliberately not reported as the actor's `sub` and must not be treated
as an actor identifier
- `truncated`: `true` when the chain exceeded the configured maximum depth
(`maxDelegationDepth` in the audit config, default 10) and trailing
actors were dropped
- `dropped_count`: Number of actors dropped due to the depth cap, present
only when `truncated` is `true`
- `malformed`: `true` when the token's `act` claim (at the top level or at
some nesting depth) was present and non-null but not a JSON object, per
RFC 8693 §4.1; present only when `true`. Actors already parsed from
shallower levels of the chain (if any) are still reported alongside it.
When the malformed value sits immediately past the depth cap,
`dropped_count` is deliberately 0 and `truncated` is `false` — unreadable
data past the cap is not a countable actor
- `target`: Information about the operation target
- `endpoint`: HTTP endpoint path
- `method`: HTTP method
Expand Down
2 changes: 1 addition & 1 deletion docs/operator/crd-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/server/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 20 additions & 24 deletions pkg/audit/auditor.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,6 @@ func (a *Auditor) Close() error {
return nil
}

// SetLogWriterForTest redirects the auditor's log output to w. It is intended
// for tests in other packages (e.g. integration tests) that need to capture
// emitted audit events without reimplementing the auditor.
//
// Deprecated: this is test-only surface on a production type. A future
// NewAuditorWithWriter constructor accepting an explicit io.Writer should
// replace it; keep usage confined to tests.
func (a *Auditor) SetLogWriterForTest(w io.Writer) {
a.auditLogger = NewAuditLogger(w)
a.logWriter = w
}

// isSSETransport checks if the current transport is SSE
func (a *Auditor) isSSETransport() bool {
return a.transportType == types.TransportTypeSSE.String()
Expand Down Expand Up @@ -558,24 +546,32 @@ func extractSubjectsFromIdentity(identity *auth.Identity) map[string]string {
}

// extractDelegationChainFromIdentity returns the RFC 8693 delegation chain
// carried by the identity, re-bounded to maxDepth, or nil when the identity
// has no chain. An already-parsed chain is re-bounded by applying maxDepth to
// its actor list directly; an identity carrying only the raw "act" claim is
// parsed once via auth.ParseDelegationChain. In both cases the configured cap
// is enforced — there is no uncapped "honor as-is" fallback.
// carried by the identity, bound to maxDepth, or nil when there is none.
// The identity layer (auth.claimsToIdentity) always parses at
// auth.DefaultMaxDelegationDepth, so a larger maxDepth requires re-parsing the
// raw "act" claim — rebinding an already-parsed chain can only shrink it. The
// re-parse is only trusted when it yields at least as many actors as are
// already parsed, so it can widen the chain but never narrow it.
func extractDelegationChainFromIdentity(identity *auth.Identity, maxDepth int) *auth.DelegationChain {
if identity == nil {
return nil
}
if chain := identity.DelegationChain; chain != nil && len(chain.Actors) > 0 {
return rebindDelegationChain(chain, maxDepth)
}
if act, ok := identity.Claims["act"]; ok && act != nil {
chain := auth.ParseDelegationChain(act, maxDepth)
if len(chain.Actors) > 0 {
return &chain
rawAct := identity.Claims["act"]
chain := identity.DelegationChain
existingActors := 0
if chain != nil {
existingActors = len(chain.Actors)
}
if rawAct != nil && (chain == nil || len(chain.Actors) == 0 ||
(chain.Truncated && maxDepth > len(chain.Actors))) {
if parsed := auth.ParseDelegationChain(rawAct, maxDepth); (len(parsed.Actors) > 0 || parsed.Malformed) &&
len(parsed.Actors) >= existingActors {
return &parsed
}
}
if chain != nil && (len(chain.Actors) > 0 || chain.Malformed) {
return rebindDelegationChain(chain, maxDepth)
}
return nil
}

Expand Down
Loading
Loading