package grpcserver import ( "context" "strings" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "git.wntrmute.dev/kyle/metacrypt/internal/auth" "git.wntrmute.dev/kyle/metacrypt/internal/seal" ) type contextKey int const tokenInfoKey contextKey = iota func tokenInfoFromContext(ctx context.Context) *auth.TokenInfo { v, _ := ctx.Value(tokenInfoKey).(*auth.TokenInfo) return v } // authInterceptor validates the Bearer token from gRPC metadata and injects // *auth.TokenInfo into the context. The set of method full names that require // auth is passed in; all others pass through without validation. func authInterceptor(authenticator *auth.Authenticator, methods map[string]bool) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if !methods[info.FullMethod] { return handler(ctx, req) } token := extractToken(ctx) if token == "" { return nil, status.Error(codes.Unauthenticated, "missing authorization token") } tokenInfo, err := authenticator.ValidateToken(token) if err != nil { return nil, status.Error(codes.Unauthenticated, "invalid token") } ctx = context.WithValue(ctx, tokenInfoKey, tokenInfo) return handler(ctx, req) } } // adminInterceptor requires IsAdmin on the token info for the listed methods. // Must run after authInterceptor. func adminInterceptor(methods map[string]bool) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if !methods[info.FullMethod] { return handler(ctx, req) } ti := tokenInfoFromContext(ctx) if ti == nil || !ti.IsAdmin { return nil, status.Error(codes.PermissionDenied, "admin required") } return handler(ctx, req) } } // sealInterceptor rejects calls with FailedPrecondition when the vault is // sealed, for the listed methods. func sealInterceptor(sealMgr *seal.Manager, methods map[string]bool) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if !methods[info.FullMethod] { return handler(ctx, req) } if sealMgr.State() != seal.StateUnsealed { return nil, status.Error(codes.FailedPrecondition, "vault is sealed") } return handler(ctx, req) } } // chainInterceptors reduces a slice of interceptors into a single one. func chainInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { chain := handler for i := len(interceptors) - 1; i >= 0; i-- { next := chain ic := interceptors[i] chain = func(ctx context.Context, req interface{}) (interface{}, error) { return ic(ctx, req, info, next) } } return chain(ctx, req) } } func extractToken(ctx context.Context) string { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "" } vals := md.Get("authorization") if len(vals) == 0 { return "" } v := vals[0] if strings.HasPrefix(v, "Bearer ") { return strings.TrimPrefix(v, "Bearer ") } return v }