Skip to content
Open
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 @@ -209,11 +209,23 @@ func newPolicyMiddleware(policyClient policy.Authorizer, serviceName string, log
// 1. Extract the token (simple bearer token extraction)
token := ""
authHeader := r.Header.Get("Authorization")
_, hasJWTClaims := r.Context().Value(claimsContextKey).(jwt.MapClaims)
if strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
logger.InfoContext(traceCtx, "policy: token extracted", zap.String("token_length", strconv.Itoa(len(token))))
} else {
token = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
}
if token == "" && !hasJWTClaims {
// No usable credential (header absent, not Bearer-prefixed, or
// an empty/whitespace-only Bearer value): fail fast instead of
// asking the policy evaluator to deny an empty API key.
// hasJWTClaims covers the managed-mode JWT-then-policy chain,
// where jwtVerify already consumed and cleared this header
// after a successful local verification.
logger.WarnContext(traceCtx, "policy: no bearer token found in authorization header")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need policy: in the log message?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused. Does the endpoint accept both JWT and api-key as bearer tokens? If yes, should Event Ledger call UAM both when JWT and api-key are specified as bearer tokens? What does the rego policy do when a JWT is passed to UAM?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's redundant — the caller field already shows this came from policy.go:

sample log message:
2026-09-16T20:07:47.697340094Z {"level":"warn","ts":1789589267.697265,"msg":"policy: no bearer token found in authorization header","service.name":"nvcf-function-deployment-stages-api","service.version":"0.15.2","deployment.environment.name":"prd","trace_id":"f1649b1ce51391fbc568ce428ffcdaa2","span_id":"68366c5e65b604a5","otlp.trace_id":"f1649b1ce51391fbc568ce428ffcdaa2","caller":"src/control-plane-services/event-ledger/internal/middleware/policy.go:216 [newPolicyMiddleware.func2.1]"}

That said, this prefix convention is already used elsewhere in the file, so I'd rather keep it consistent than special-case this one line. Happy to drop it everywhere if you'd prefer.

@shelleyshen-0 shelleyshen-0 Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah both JWT and api-keys are sent to UAM. I assume UAM checks the scopes of JWT to evaluate whether or not it should be authorized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. UAM does not check scopes. For SAK, your rego policy can check the scopes in the key and return appropriate result. But, it is not how we do it typically. For JWT, there is no mechanism to check scopes.

http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if token != "" {
logger.InfoContext(traceCtx, "policy: token extracted", zap.String("token_length", strconv.Itoa(len(token))))
}

authCtx := map[string]interface{}{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,24 +335,29 @@ func TestPolicyAuthInputFields(t *testing.T) {

func TestNewPolicyMiddleware(t *testing.T) {
tests := []struct {
name string
client *stubPolicyClient
token string
expectedStatusCode int
expectedActorID string
expectedOrgName string
expectedActorType string
expectedRoles []string
name string
client *stubPolicyClient
token string
rawAuthHeader string
setRawAuthHeader bool
claims jwt.MapClaims
expectedStatusCode int
expectedClientCalled bool
expectedActorID string
expectedOrgName string
expectedActorType string
expectedRoles []string
}{
{
name: "Successful Authorization",
client: &stubPolicyClient{result: allowResult(nil)},
token: "valid-token",
expectedStatusCode: http.StatusOK,
expectedActorID: "user123",
expectedOrgName: "org123",
expectedActorType: "user",
expectedRoles: []string{"admin", "user"},
name: "Successful Authorization",
client: &stubPolicyClient{result: allowResult(nil)},
token: "valid-token",
expectedStatusCode: http.StatusOK,
expectedClientCalled: true,
expectedActorID: "user123",
expectedOrgName: "org123",
expectedActorType: "user",
expectedRoles: []string{"admin", "user"},
},
{
name: "Failed Authorization",
Expand All @@ -361,48 +366,92 @@ func TestNewPolicyMiddleware(t *testing.T) {
"statusCode": 403,
"reasons": []interface{}{"unauthorized access"},
}},
token: "invalid-token",
expectedStatusCode: http.StatusForbidden,
token: "invalid-token",
expectedStatusCode: http.StatusForbidden,
expectedClientCalled: true,
},
{
name: "Policy Service Error",
client: &stubPolicyClient{err: errors.New("service unavailable")},
token: "token",
expectedStatusCode: http.StatusUnauthorized,
name: "Policy Service Error",
client: &stubPolicyClient{err: errors.New("service unavailable")},
token: "token",
expectedStatusCode: http.StatusUnauthorized,
expectedClientCalled: true,
},
{
name: "Empty Result From Policy",
client: &stubPolicyClient{empty: true},
token: "token",
expectedStatusCode: http.StatusUnauthorized,
name: "Empty Result From Policy",
client: &stubPolicyClient{empty: true},
token: "token",
expectedStatusCode: http.StatusUnauthorized,
expectedClientCalled: true,
},
{
name: "No Token Provided",
// No Authorization header and no prior JWT verification: fail
// fast locally instead of sending an empty API key to the
// policy evaluator.
name: "No Token Provided",
client: &stubPolicyClient{result: allowResult(nil)},
token: "",
expectedStatusCode: http.StatusUnauthorized,
expectedClientCalled: false,
},
{
// "Bearer " with nothing after it is Bearer-prefixed, so it must
// not slip past the fail-fast check via the happy-path branch.
name: "Empty Bearer Token",
client: &stubPolicyClient{result: allowResult(nil)},
rawAuthHeader: "Bearer ",
setRawAuthHeader: true,
expectedStatusCode: http.StatusUnauthorized,
expectedClientCalled: false,
},
{
// Whitespace-only value after "Bearer " must be treated the same
// as an empty token, not forwarded to the policy evaluator.
name: "Whitespace-Only Bearer Token",
client: &stubPolicyClient{result: allowResult(nil)},
rawAuthHeader: "Bearer ",
setRawAuthHeader: true,
expectedStatusCode: http.StatusUnauthorized,
expectedClientCalled: false,
},
{
// Simulates the managed-mode JWT-then-policy chain: jwtVerify
// already validated the token and cleared the Authorization
// header, but stashed claims in the request context. The policy
// evaluator must still be consulted using those claims.
name: "No Header, But Already-Verified JWT Claims",
client: &stubPolicyClient{result: allowResult(map[string]interface{}{
"actorId": "anonymous",
"orgName": "anonymous",
"actorType": "anonymous",
"roles": []interface{}{"guest"},
"actorId": "user456",
"orgName": "org456",
"actorType": "user",
"roles": []interface{}{"user"},
})},
token: "",
expectedStatusCode: http.StatusOK,
expectedActorID: "anonymous",
expectedOrgName: "anonymous",
expectedActorType: "anonymous",
expectedRoles: []string{"guest"},
claims: jwt.MapClaims{"sub": "user456"},
token: "",
expectedStatusCode: http.StatusOK,
expectedClientCalled: true,
expectedActorID: "user456",
expectedOrgName: "org456",
expectedActorType: "user",
expectedRoles: []string{"user"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
if tt.token != "" {
if tt.setRawAuthHeader {
req.Header.Set("Authorization", tt.rawAuthHeader)
} else if tt.token != "" {
req.Header.Set("Authorization", "Bearer "+tt.token)
}
if tt.claims != nil {
req = req.WithContext(context.WithValue(req.Context(), claimsContextKey, tt.claims))
}

recorder, capturedCtx := servePolicy(t, tt.client, req)
assert.Equal(t, tt.expectedStatusCode, recorder.Code)
assert.True(t, tt.client.called)
assert.Equal(t, tt.expectedClientCalled, tt.client.called)

if tt.expectedStatusCode != http.StatusOK {
return
Expand Down
Loading