diff --git a/lnd.go b/lnd.go index 2ed6f30de..d5bc77687 100644 --- a/lnd.go +++ b/lnd.go @@ -145,6 +145,12 @@ type RPCSubserverConfig struct { // per URI, they are all required. See rpcserver.go for a list of valid // action and entity values. Permissions map[string][]bakery.Op + + // MacaroonValidator is a custom macaroon validator that should be used + // instead of the default lnd validator. If specified, the custom + // validator is used for all URIs specified in the above Permissions + // map. + MacaroonValidator macaroons.MacaroonValidator } // ListenerWithSignal is a net.Listener that has an additional Ready channel that @@ -393,7 +399,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, shutdownChan <-chan struct{}) error { if !cfg.NoMacaroons { // Create the macaroon authentication/authorization service. macaroonService, err = macaroons.NewService( - cfg.networkDir, macaroons.IPLockChecker, + cfg.networkDir, "lnd", macaroons.IPLockChecker, ) if err != nil { err := fmt.Errorf("unable to set up macaroon "+ diff --git a/macaroons/service.go b/macaroons/service.go index 0e84ddeb8..8f3402d09 100644 --- a/macaroons/service.go +++ b/macaroons/service.go @@ -38,6 +38,17 @@ var ( PermissionEntityCustomURI = "uri" ) +// MacaroonValidator is an interface type that can check if macaroons are valid. +type MacaroonValidator interface { + // ValidateMacaroon extracts the macaroon from the context's gRPC + // metadata, checks its signature, makes sure all specified permissions + // for the called method are contained within and finally ensures all + // caveat conditions are met. A non-nil error is returned if any of the + // checks fail. + ValidateMacaroon(ctx context.Context, + requiredPermissions []bakery.Op, fullMethod string) error +} + // Service encapsulates bakery.Bakery and adds a Close() method that zeroes the // root key service encryption keys, as well as utility methods to validate a // macaroon against the bakery and gRPC middleware for macaroon-based auth. @@ -45,6 +56,12 @@ type Service struct { bakery.Bakery rks *RootKeyStorage + + // externalValidators is a map between an absolute gRPC URIs and the + // corresponding external macaroon validator to be used for that URI. + // If no external validator for an URI is specified, the service will + // use the internal validator. + externalValidators map[string]MacaroonValidator } // NewService returns a service backed by the macaroon Bolt DB stored in the @@ -54,7 +71,7 @@ type Service struct { // listing the same checker more than once is not harmful. Default checkers, // such as those for `allow`, `time-before`, `declared`, and `error` caveats // are registered automatically and don't need to be added. -func NewService(dir string, checks ...Checker) (*Service, error) { +func NewService(dir, location string, checks ...Checker) (*Service, error) { // Ensure that the path to the directory exists. if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0700); err != nil { @@ -77,7 +94,7 @@ func NewService(dir string, checks ...Checker) (*Service, error) { } macaroonParams := bakery.BakeryParams{ - Location: "lnd", + Location: location, RootKeyStore: rootKeyStore, // No third-party caveat support for now. // TODO(aakselrod): Add third-party caveat support. @@ -97,7 +114,11 @@ func NewService(dir string, checks ...Checker) (*Service, error) { } } - return &Service{*svc, rootKeyStore}, nil + return &Service{ + Bakery: *svc, + rks: rootKeyStore, + externalValidators: make(map[string]MacaroonValidator), + }, nil } // isRegistered checks to see if the required checker has already been @@ -118,6 +139,27 @@ func isRegistered(c *checkers.Checker, name string) bool { return false } +// RegisterExternalValidator registers a custom, external macaroon validator for +// the specified absolute gRPC URI. That validator is then fully responsible to +// make sure any macaroon passed for a request to that URI is valid and +// satisfies all conditions. +func (svc *Service) RegisterExternalValidator(fullMethod string, + validator MacaroonValidator) error { + + if validator == nil { + return fmt.Errorf("validator cannot be nil") + } + + _, ok := svc.externalValidators[fullMethod] + if ok { + return fmt.Errorf("external validator for method %s already "+ + "registered", fullMethod) + } + + svc.externalValidators[fullMethod] = validator + return nil +} + // UnaryServerInterceptor is a GRPC interceptor that checks whether the // request is authorized by the included macaroons. func (svc *Service) UnaryServerInterceptor( @@ -133,7 +175,15 @@ func (svc *Service) UnaryServerInterceptor( "required for method", info.FullMethod) } - err := svc.ValidateMacaroon( + // 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[info.FullMethod] + if !ok { + validator = svc + } + + // Now that we know what validator to use, let it do its work. + err := validator.ValidateMacaroon( ctx, uriPermissions, info.FullMethod, ) if err != nil { @@ -158,7 +208,15 @@ func (svc *Service) StreamServerInterceptor( "for method", info.FullMethod) } - err := svc.ValidateMacaroon( + // 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[info.FullMethod] + if !ok { + validator = svc + } + + // Now that we know what validator to use, let it do its work. + err := validator.ValidateMacaroon( ss.Context(), uriPermissions, info.FullMethod, ) if err != nil { diff --git a/macaroons/service_test.go b/macaroons/service_test.go index 90c9b5736..5f584240d 100644 --- a/macaroons/service_test.go +++ b/macaroons/service_test.go @@ -66,7 +66,9 @@ func TestNewService(t *testing.T) { // Second, create the new service instance, unlock it and pass in a // checker that we expect it to add to the bakery. - service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) + service, err := macaroons.NewService( + tempDir, "lnd", macaroons.IPLockChecker, + ) if err != nil { t.Fatalf("Error creating new service: %v", err) } @@ -115,7 +117,9 @@ func TestValidateMacaroon(t *testing.T) { // First, initialize the service and unlock it. tempDir := setupTestRootKeyStorage(t) defer os.RemoveAll(tempDir) - service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) + service, err := macaroons.NewService( + tempDir, "lnd", macaroons.IPLockChecker, + ) if err != nil { t.Fatalf("Error creating new service: %v", err) } @@ -173,7 +177,9 @@ func TestListMacaroonIDs(t *testing.T) { // Second, create the new service instance, unlock it and pass in a // checker that we expect it to add to the bakery. - service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) + service, err := macaroons.NewService( + tempDir, "lnd", macaroons.IPLockChecker, + ) require.NoError(t, err, "Error creating new service") defer service.Close() @@ -203,7 +209,9 @@ func TestDeleteMacaroonID(t *testing.T) { // Second, create the new service instance, unlock it and pass in a // checker that we expect it to add to the bakery. - service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) + service, err := macaroons.NewService( + tempDir, "lnd", macaroons.IPLockChecker, + ) require.NoError(t, err, "Error creating new service") defer service.Close() diff --git a/rpcserver.go b/rpcserver.go index be6e7a8e3..fde2d3b89 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -213,9 +213,9 @@ func stringInSlice(a string, slice []string) bool { return false } -// mainRPCServerPermissions returns a mapping of the main RPC server calls to +// MainRPCServerPermissions returns a mapping of the main RPC server calls to // the permissions they require. -func mainRPCServerPermissions() map[string][]bakery.Op { +func MainRPCServerPermissions() map[string][]bakery.Op { return map[string][]bakery.Op{ "/lnrpc.Lightning/SendCoins": {{ Entity: "onchain", @@ -640,7 +640,7 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, // Next, we need to merge the set of sub server macaroon permissions // with the main RPC server permissions so we can unite them under a // single set of interceptors. - permissions := mainRPCServerPermissions() + permissions := MainRPCServerPermissions() for _, subServerPerm := range subServerPerms { for method, ops := range subServerPerm { // For each new method:ops combo, we also ensure that @@ -661,10 +661,12 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, return nil, err } - // External subserver possibly need to register their own permissions. + // External subserver possibly need to register their own permissions + // and macaroon validator. for _, lis := range listeners { extSubserver := lis.ExternalRPCSubserverCfg if extSubserver != nil { + macValidator := extSubserver.MacaroonValidator for method, ops := range extSubserver.Permissions { // For each new method:ops combo, we also ensure // that non of the sub-servers try to override @@ -677,6 +679,23 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service, } permissions[method] = ops + + // Give the external subservers the possibility + // to also use their own validator to check any + // macaroons attached to calls to this method. + // This allows them to have their own root key + // ID database and permission entities. + if macValidator != nil { + err := macService.RegisterExternalValidator( + method, macValidator, + ) + if err != nil { + return nil, fmt.Errorf("could "+ + "not register "+ + "external macaroon "+ + "validator: %v", err) + } + } } } }