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
// 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 "+

@ -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 {

@ -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()

@ -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)
}
}
}
}
}