Merge pull request #4464 from guggero/macaroon-interceptor
Advanced macaroons 2/2: Custom macaroon validator for external subservers
This commit is contained in:
commit
f80549d6e5
8
lnd.go
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()
|
||||||
|
|
||||||
|
27
rpcserver.go
27
rpcserver.go
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user