Files
notifier/internal/auth/grpc_middleware.go
T
igodwin eda033ff9b
CI / Lint (push) Successful in 2m29s
Build and Publish Container / build-and-publish (push) Successful in 2m58s
CI / Vulnerability scan (push) Successful in 44s
CI / Test (push) Successful in 1m45s
fix: clear golangci-lint backlog and make lint job blocking
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').

With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 10:32:51 -07:00

151 lines
4.8 KiB
Go

package auth
import (
"context"
"strings"
"github.com/igodwin/notifier/internal/logging"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// GRPCAuthMiddleware provides authentication for gRPC APIs
type GRPCAuthMiddleware struct {
store *APIKeyStore
logger *logging.Logger
}
// NewGRPCAuthMiddleware creates a new gRPC auth middleware
func NewGRPCAuthMiddleware(store *APIKeyStore, logger *logging.Logger) *GRPCAuthMiddleware {
return &GRPCAuthMiddleware{
store: store,
logger: logger,
}
}
// UnaryInterceptor returns a unary server interceptor for gRPC authentication
func (m *GRPCAuthMiddleware) UnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Extract API key from metadata
apiKey := m.extractAPIKey(ctx)
if apiKey == "" {
m.logger.Warnf("gRPC: Missing API key in request for method=%s", info.FullMethod)
return nil, status.Error(codes.Unauthenticated, "Missing or invalid Authorization header")
}
// Validate API key
key, err := m.store.ValidateKey(apiKey)
if err != nil {
m.logger.Warnf("gRPC: Invalid API key for method=%s - error=%v", info.FullMethod, err)
return nil, status.Error(codes.Unauthenticated, "Invalid API key")
}
// Check rate limit
allowed, err := m.store.CheckRateLimit(apiKey)
if err != nil || !allowed {
m.logger.Warnf("gRPC: Rate limit exceeded for client=%s method=%s", key.ClientID, info.FullMethod)
return nil, status.Error(codes.ResourceExhausted, "Rate limit exceeded")
}
// Update last used timestamp
if err := m.store.UpdateLastUsed(apiKey); err != nil {
m.logger.Errorf("gRPC: Failed to update last used time for client=%s - error=%v", key.ClientID, err)
}
// Create auth context and attach to request
authCtx := &Context{
APIKey: key,
ClientID: key.ClientID,
Roles: key.Roles,
}
// Add auth context to request context
newCtx := ContextWithAuth(ctx, authCtx)
m.logger.Debugf("gRPC: Authenticated request from client=%s method=%s with roles=%v", key.ClientID, info.FullMethod, key.Roles)
return handler(newCtx, req)
}
}
// StreamInterceptor returns a stream server interceptor for gRPC authentication
func (m *GRPCAuthMiddleware) StreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// Extract API key from metadata
apiKey := m.extractAPIKey(ss.Context())
if apiKey == "" {
m.logger.Warnf("gRPC: Missing API key in stream for method=%s", info.FullMethod)
return status.Error(codes.Unauthenticated, "Missing or invalid Authorization header")
}
// Validate API key
key, err := m.store.ValidateKey(apiKey)
if err != nil {
m.logger.Warnf("gRPC: Invalid API key for stream method=%s - error=%v", info.FullMethod, err)
return status.Error(codes.Unauthenticated, "Invalid API key")
}
// Check rate limit
allowed, err := m.store.CheckRateLimit(apiKey)
if err != nil || !allowed {
m.logger.Warnf("gRPC: Rate limit exceeded for client=%s stream method=%s", key.ClientID, info.FullMethod)
return status.Error(codes.ResourceExhausted, "Rate limit exceeded")
}
// Update last used timestamp
if err := m.store.UpdateLastUsed(apiKey); err != nil {
m.logger.Errorf("gRPC: Failed to update last used time for client=%s - error=%v", key.ClientID, err)
}
// Create auth context and attach to request
authCtx := &Context{
APIKey: key,
ClientID: key.ClientID,
Roles: key.Roles,
}
// Add auth context to request context
newCtx := ContextWithAuth(ss.Context(), authCtx)
m.logger.Debugf("gRPC: Authenticated stream from client=%s method=%s with roles=%v", key.ClientID, info.FullMethod, key.Roles)
// Create wrapped server stream with new context
wrappedStream := &wrappedServerStream{ServerStream: ss, ctx: newCtx}
return handler(srv, wrappedStream)
}
}
// wrappedServerStream wraps grpc.ServerStream to override context
type wrappedServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (w *wrappedServerStream) Context() context.Context {
return w.ctx
}
// extractAPIKey extracts API key from gRPC metadata
func (m *GRPCAuthMiddleware) extractAPIKey(ctx context.Context) string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ""
}
// Try authorization header first
if authHeaders := md.Get("authorization"); len(authHeaders) > 0 {
authHeader := authHeaders[0]
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
return parts[1]
}
}
// Try x-api-key header
if keyHeaders := md.Get("x-api-key"); len(keyHeaders) > 0 {
return keyHeaders[0]
}
return ""
}