Merge pull request #4464 from guggero/macaroon-interceptor

Advanced macaroons 2/2: Custom macaroon validator for external subservers
This commit is contained in:
Olaoluwa Osuntokun 2020-09-10 12:49:34 -07:00 committed by GitHub
commit f80549d6e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 14 deletions

8
lnd.go

@ -145,6 +145,12 @@ type RPCSubserverConfig struct {
// per URI, they are all required. See rpcserver.go for a list of valid // per URI, they are all required. See rpcserver.go for a list of valid
// action and entity values. // action and entity values.
Permissions map[string][]bakery.Op 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 // 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 { if !cfg.NoMacaroons {
// Create the macaroon authentication/authorization service. // Create the macaroon authentication/authorization service.
macaroonService, err = macaroons.NewService( macaroonService, err = macaroons.NewService(
cfg.networkDir, macaroons.IPLockChecker, cfg.networkDir, "lnd", macaroons.IPLockChecker,
) )
if err != nil { if err != nil {
err := fmt.Errorf("unable to set up macaroon "+ err := fmt.Errorf("unable to set up macaroon "+

@ -38,6 +38,17 @@ var (
PermissionEntityCustomURI = "uri" 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 // 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 // root key service encryption keys, as well as utility methods to validate a
// macaroon against the bakery and gRPC middleware for macaroon-based auth. // macaroon against the bakery and gRPC middleware for macaroon-based auth.
@ -45,6 +56,12 @@ type Service struct {
bakery.Bakery bakery.Bakery
rks *RootKeyStorage 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 // 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, // listing the same checker more than once is not harmful. Default checkers,
// such as those for `allow`, `time-before`, `declared`, and `error` caveats // such as those for `allow`, `time-before`, `declared`, and `error` caveats
// are registered automatically and don't need to be added. // 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. // Ensure that the path to the directory exists.
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0700); err != nil { if err := os.MkdirAll(dir, 0700); err != nil {
@ -77,7 +94,7 @@ func NewService(dir string, checks ...Checker) (*Service, error) {
} }
macaroonParams := bakery.BakeryParams{ macaroonParams := bakery.BakeryParams{
Location: "lnd", Location: location,
RootKeyStore: rootKeyStore, RootKeyStore: rootKeyStore,
// No third-party caveat support for now. // No third-party caveat support for now.
// TODO(aakselrod): Add third-party caveat support. // 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 // 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 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 // UnaryServerInterceptor is a GRPC interceptor that checks whether the
// request is authorized by the included macaroons. // request is authorized by the included macaroons.
func (svc *Service) UnaryServerInterceptor( func (svc *Service) UnaryServerInterceptor(
@ -133,7 +175,15 @@ func (svc *Service) UnaryServerInterceptor(
"required for method", info.FullMethod) "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, ctx, uriPermissions, info.FullMethod,
) )
if err != nil { if err != nil {
@ -158,7 +208,15 @@ func (svc *Service) StreamServerInterceptor(
"for method", info.FullMethod) "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, ss.Context(), uriPermissions, info.FullMethod,
) )
if err != nil { if err != nil {

@ -66,7 +66,9 @@ func TestNewService(t *testing.T) {
// Second, create the new service instance, unlock it and pass in a // Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery. // 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 { if err != nil {
t.Fatalf("Error creating new service: %v", err) t.Fatalf("Error creating new service: %v", err)
} }
@ -115,7 +117,9 @@ func TestValidateMacaroon(t *testing.T) {
// First, initialize the service and unlock it. // First, initialize the service and unlock it.
tempDir := setupTestRootKeyStorage(t) tempDir := setupTestRootKeyStorage(t)
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempDir)
service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker) service, err := macaroons.NewService(
tempDir, "lnd", macaroons.IPLockChecker,
)
if err != nil { if err != nil {
t.Fatalf("Error creating new service: %v", err) 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 // Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery. // 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") require.NoError(t, err, "Error creating new service")
defer service.Close() defer service.Close()
@ -203,7 +209,9 @@ func TestDeleteMacaroonID(t *testing.T) {
// Second, create the new service instance, unlock it and pass in a // Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery. // 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") require.NoError(t, err, "Error creating new service")
defer service.Close() defer service.Close()

@ -213,9 +213,9 @@ func stringInSlice(a string, slice []string) bool {
return false 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. // the permissions they require.
func mainRPCServerPermissions() map[string][]bakery.Op { func MainRPCServerPermissions() map[string][]bakery.Op {
return map[string][]bakery.Op{ return map[string][]bakery.Op{
"/lnrpc.Lightning/SendCoins": {{ "/lnrpc.Lightning/SendCoins": {{
Entity: "onchain", 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 // 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 // with the main RPC server permissions so we can unite them under a
// single set of interceptors. // single set of interceptors.
permissions := mainRPCServerPermissions() permissions := MainRPCServerPermissions()
for _, subServerPerm := range subServerPerms { for _, subServerPerm := range subServerPerms {
for method, ops := range subServerPerm { for method, ops := range subServerPerm {
// For each new method:ops combo, we also ensure that // 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 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 { for _, lis := range listeners {
extSubserver := lis.ExternalRPCSubserverCfg extSubserver := lis.ExternalRPCSubserverCfg
if extSubserver != nil { if extSubserver != nil {
macValidator := extSubserver.MacaroonValidator
for method, ops := range extSubserver.Permissions { for method, ops := range extSubserver.Permissions {
// For each new method:ops combo, we also ensure // For each new method:ops combo, we also ensure
// that non of the sub-servers try to override // 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 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)
}
}
} }
} }
} }