package rpcperms import ( "context" "fmt" "sync" "github.com/btcsuite/btclog" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/monitoring" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" ) var ( // macaroonWhitelist defines methods that we don't require macaroons to // access. macaroonWhitelist = map[string]struct{}{ // We allow all calls to the WalletUnlocker without macaroons. "/lnrpc.WalletUnlocker/GenSeed": {}, "/lnrpc.WalletUnlocker/InitWallet": {}, "/lnrpc.WalletUnlocker/UnlockWallet": {}, "/lnrpc.WalletUnlocker/ChangePassword": {}, } ) // InterceptorChain is a struct that can be added to the running GRPC server, // intercepting API calls. This is useful for logging, enforcing permissions // etc. type InterceptorChain struct { // noMacaroons should be set true if we don't want to check macaroons. noMacaroons bool // svc is the macaroon service used to enforce permissions in case // macaroons are used. svc *macaroons.Service // permissionMap is the permissions to enforce if macaroons are used. permissionMap map[string][]bakery.Op // rpcsLog is the logger used to log calles to the RPCs intercepted. rpcsLog btclog.Logger sync.RWMutex } // NewInterceptorChain creates a new InterceptorChain. func NewInterceptorChain(log btclog.Logger, noMacaroons bool) *InterceptorChain { return &InterceptorChain{ noMacaroons: noMacaroons, permissionMap: make(map[string][]bakery.Op), rpcsLog: log, } } // AddMacaroonService adds a macaroon service to the interceptor. After this is // done every RPC call made will have to pass a valid macaroon to be accepted. func (r *InterceptorChain) AddMacaroonService(svc *macaroons.Service) { r.Lock() defer r.Unlock() r.svc = svc } // AddPermission adds a new macaroon rule for the given method. func (r *InterceptorChain) AddPermission(method string, ops []bakery.Op) error { r.Lock() defer r.Unlock() if _, ok := r.permissionMap[method]; ok { return fmt.Errorf("detected duplicate macaroon constraints "+ "for path: %v", method) } r.permissionMap[method] = ops return nil } // Permissions returns the current set of macaroon permissions. func (r *InterceptorChain) Permissions() map[string][]bakery.Op { r.RLock() defer r.RUnlock() // Make a copy under the read lock to avoid races. c := make(map[string][]bakery.Op) for k, v := range r.permissionMap { s := make([]bakery.Op, len(v)) copy(s, v) c[k] = s } return c } // CreateServerOpts creates the GRPC server options that can be added to a GRPC // server in order to add this InterceptorChain. func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption { macUnaryInterceptors := []grpc.UnaryServerInterceptor{} macStrmInterceptors := []grpc.StreamServerInterceptor{} // We'll add the macaroon interceptors. If macaroons aren't disabled, // then these interceptors will enforce macaroon authentication. unaryInterceptor := r.macaroonUnaryServerInterceptor() macUnaryInterceptors = append(macUnaryInterceptors, unaryInterceptor) strmInterceptor := r.macaroonStreamServerInterceptor() macStrmInterceptors = append(macStrmInterceptors, strmInterceptor) // Get interceptors for Prometheus to gather gRPC performance metrics. // If monitoring is disabled, GetPromInterceptors() will return empty // slices. promUnaryInterceptors, promStrmInterceptors := monitoring.GetPromInterceptors() // Concatenate the slices of unary and stream interceptors respectively. unaryInterceptors := append(macUnaryInterceptors, promUnaryInterceptors...) strmInterceptors := append(macStrmInterceptors, promStrmInterceptors...) // We'll also add our logging interceptors as well, so we can // automatically log all errors that happen during RPC calls. unaryInterceptors = append( unaryInterceptors, errorLogUnaryServerInterceptor(r.rpcsLog), ) strmInterceptors = append( strmInterceptors, errorLogStreamServerInterceptor(r.rpcsLog), ) // Create server options from the interceptors we just set up. chainedUnary := grpc_middleware.WithUnaryServerChain( unaryInterceptors..., ) chainedStream := grpc_middleware.WithStreamServerChain( strmInterceptors..., ) serverOpts := []grpc.ServerOption{chainedUnary, chainedStream} return serverOpts } // errorLogUnaryServerInterceptor is a simple UnaryServerInterceptor that will // automatically log any errors that occur when serving a client's unary // request. func errorLogUnaryServerInterceptor(logger btclog.Logger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { resp, err := handler(ctx, req) if err != nil { // TODO(roasbeef): also log request details? logger.Errorf("[%v]: %v", info.FullMethod, err) } return resp, err } } // errorLogStreamServerInterceptor is a simple StreamServerInterceptor that // will log any errors that occur while processing a client or server streaming // RPC. func errorLogStreamServerInterceptor(logger btclog.Logger) grpc.StreamServerInterceptor { return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { err := handler(srv, ss) if err != nil { logger.Errorf("[%v]: %v", info.FullMethod, err) } return err } } // checkMacaroon validates that the context contains the macaroon needed to // invoke the given RPC method. func (r *InterceptorChain) checkMacaroon(ctx context.Context, fullMethod string) error { // If noMacaroons is set, we'll always allow the call. if r.noMacaroons { return nil } // Check whether the method is whitelisted, if so we'll allow it // regardless of macaroons. _, ok := macaroonWhitelist[fullMethod] if ok { return nil } r.RLock() svc := r.svc r.RUnlock() // If the macaroon service is not yet active, we cannot allow // the call. if svc == nil { return fmt.Errorf("unable to determine macaroon permissions") } r.RLock() uriPermissions, ok := r.permissionMap[fullMethod] r.RUnlock() if !ok { return fmt.Errorf("%s: unknown permissions required for method", fullMethod) } // Find out if there is an external validator registered for // this method. Fall back to the internal one if there isn't. validator, ok := svc.ExternalValidators[fullMethod] if !ok { validator = svc } // Now that we know what validator to use, let it do its work. return validator.ValidateMacaroon(ctx, uriPermissions, fullMethod) } // macaroonUnaryServerInterceptor is a GRPC interceptor that checks whether the // request is authorized by the included macaroons. func (r *InterceptorChain) macaroonUnaryServerInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { // Check macaroons. if err := r.checkMacaroon(ctx, info.FullMethod); err != nil { return nil, err } return handler(ctx, req) } } // macaroonStreamServerInterceptor is a GRPC interceptor that checks whether // the request is authorized by the included macaroons. func (r *InterceptorChain) macaroonStreamServerInterceptor() grpc.StreamServerInterceptor { return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { // Check macaroons. err := r.checkMacaroon(ss.Context(), info.FullMethod) if err != nil { return err } return handler(srv, ss) } }