From 922608d84d89387b1dccfdd95474b4a12812ae9f Mon Sep 17 00:00:00 2001 From: user Date: Thu, 23 Apr 2026 19:15:13 -0400 Subject: [PATCH 1/2] fix(api-server): recognize OIDC service tokens as service callers for gRPC authz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The control-plane gives runner pods OIDC tokens (SSO JWTs) for gRPC auth. These tokens pass JWT verification but carry the OIDC client username (e.g. ocm-ams-service), not the session creator's username. WatchSessionMessages then rejects the call with PERMISSION_DENIED because the JWT username doesn't match session.CreatedByUserId. Add GRPC_SERVICE_ACCOUNT env var support: when the JWT username matches this configured service account name, the pre-auth interceptor sets CallerTypeService on the context, allowing the call to bypass the per-user ownership check — same as static AMBIENT_API_TOKEN calls. The env var is sourced from the ambient-api-server secret's clientId key (same OIDC client ID the CP uses), with optional: true so deployments without OIDC continue to work. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../pkg/middleware/bearer_token.go | 13 ++++++++++--- .../pkg/middleware/bearer_token_grpc.go | 10 ++++++++-- .../base/core/ambient-api-server-service.yml | 6 ++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/components/ambient-api-server/pkg/middleware/bearer_token.go b/components/ambient-api-server/pkg/middleware/bearer_token.go index 4d804df9b..b8501776e 100644 --- a/components/ambient-api-server/pkg/middleware/bearer_token.go +++ b/components/ambient-api-server/pkg/middleware/bearer_token.go @@ -11,7 +11,10 @@ import ( pkgserver "github.com/openshift-online/rh-trex-ai/pkg/server" ) -const ambientAPITokenEnv = "AMBIENT_API_TOKEN" +const ( + ambientAPITokenEnv = "AMBIENT_API_TOKEN" + grpcServiceAccountEnv = "GRPC_SERVICE_ACCOUNT" +) var httpBypassPaths = map[string]bool{ "/healthcheck": true, @@ -25,9 +28,13 @@ func init() { glog.Infof("Service token auth disabled: %s not set", ambientAPITokenEnv) return } + serviceAccount := os.Getenv(grpcServiceAccountEnv) glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv) - pkgserver.RegisterPreAuthGRPCUnaryInterceptor(bearerTokenGRPCUnaryInterceptor(token)) - pkgserver.RegisterPreAuthGRPCStreamInterceptor(bearerTokenGRPCStreamInterceptor(token)) + if serviceAccount != "" { + glog.Infof("OIDC service account username: %s", serviceAccount) + } + pkgserver.RegisterPreAuthGRPCUnaryInterceptor(bearerTokenGRPCUnaryInterceptor(token, serviceAccount)) + pkgserver.RegisterPreAuthGRPCStreamInterceptor(bearerTokenGRPCStreamInterceptor(token, serviceAccount)) } func extractBearerToken(header string) (string, error) { diff --git a/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go b/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go index d02708e57..7902784ca 100644 --- a/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go +++ b/components/ambient-api-server/pkg/middleware/bearer_token_grpc.go @@ -16,7 +16,7 @@ var grpcBypassMethods = map[string]bool{ "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo": true, } -func bearerTokenGRPCUnaryInterceptor(expectedToken string) grpc.UnaryServerInterceptor { +func bearerTokenGRPCUnaryInterceptor(expectedToken, serviceAccountUsername string) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if grpcBypassMethods[info.FullMethod] { return handler(ctx, req) @@ -29,6 +29,9 @@ func bearerTokenGRPCUnaryInterceptor(expectedToken string) grpc.UnaryServerInter return handler(withCallerType(ctx, CallerTypeService), req) } if username := usernameFromJWT(token); username != "" { + if serviceAccountUsername != "" && username == serviceAccountUsername { + ctx = withCallerType(ctx, CallerTypeService) + } return handler(auth.SetUsernameContext(ctx, username), req) } } @@ -39,7 +42,7 @@ func bearerTokenGRPCUnaryInterceptor(expectedToken string) grpc.UnaryServerInter } } -func bearerTokenGRPCStreamInterceptor(expectedToken string) grpc.StreamServerInterceptor { +func bearerTokenGRPCStreamInterceptor(expectedToken, serviceAccountUsername string) grpc.StreamServerInterceptor { return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if grpcBypassMethods[info.FullMethod] { return handler(srv, ss) @@ -53,6 +56,9 @@ func bearerTokenGRPCStreamInterceptor(expectedToken string) grpc.StreamServerInt } if username := usernameFromJWT(token); username != "" { ctx := auth.SetUsernameContext(ss.Context(), username) + if serviceAccountUsername != "" && username == serviceAccountUsername { + ctx = withCallerType(ctx, CallerTypeService) + } return handler(srv, &serviceCallerStream{ServerStream: ss, ctx: ctx}) } } diff --git a/components/manifests/base/core/ambient-api-server-service.yml b/components/manifests/base/core/ambient-api-server-service.yml index 3aad19313..859ebf23e 100644 --- a/components/manifests/base/core/ambient-api-server-service.yml +++ b/components/manifests/base/core/ambient-api-server-service.yml @@ -93,6 +93,12 @@ spec: env: - name: AMBIENT_ENV value: development + - name: GRPC_SERVICE_ACCOUNT + valueFrom: + secretKeyRef: + name: ambient-api-server + key: clientId + optional: true ports: - name: api containerPort: 8000 From 0d2c89edbb5cf22a477c237179beef8310e23df3 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 23 Apr 2026 20:41:42 -0400 Subject: [PATCH 2/2] fix(api-server): register pre-auth interceptors when only GRPC_SERVICE_ACCOUNT is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The init() guard required AMBIENT_API_TOKEN to be non-empty before registering gRPC pre-auth interceptors. On Stage/production with OIDC auth, AMBIENT_API_TOKEN is not set — only GRPC_SERVICE_ACCOUNT is. This meant the interceptors were never registered and OIDC service tokens were not recognized as service callers. Register interceptors when either AMBIENT_API_TOKEN or GRPC_SERVICE_ACCOUNT is set. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../ambient-api-server/pkg/middleware/bearer_token.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/ambient-api-server/pkg/middleware/bearer_token.go b/components/ambient-api-server/pkg/middleware/bearer_token.go index b8501776e..d7dc15cf9 100644 --- a/components/ambient-api-server/pkg/middleware/bearer_token.go +++ b/components/ambient-api-server/pkg/middleware/bearer_token.go @@ -24,12 +24,14 @@ var httpBypassPaths = map[string]bool{ func init() { token := os.Getenv(ambientAPITokenEnv) - if token == "" { - glog.Infof("Service token auth disabled: %s not set", ambientAPITokenEnv) + serviceAccount := os.Getenv(grpcServiceAccountEnv) + if token == "" && serviceAccount == "" { + glog.Infof("Service token auth disabled: neither %s nor %s set", ambientAPITokenEnv, grpcServiceAccountEnv) return } - serviceAccount := os.Getenv(grpcServiceAccountEnv) - glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv) + if token != "" { + glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv) + } if serviceAccount != "" { glog.Infof("OIDC service account username: %s", serviceAccount) }