From c2eb98db386e53e8e8bb82fda93a3e9031f3df36 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 31 Aug 2022 10:43:26 +0200 Subject: [PATCH] multi: add PermissionsManager In this commit, a new PermissionsManager is added. It handles all the active permissions that Lit has access to. This moves us away from using global variables for permission lists. This change might seem overkill on its own but hugely simplifies the permission management once we add lnd subserver permissions. --- itest/litd_mode_integrated_test.go | 7 +- rpc_proxy.go | 42 ++++---- session_rpcserver.go | 3 +- subserver_permissions.go | 164 ++++++++++++++++++----------- terminal.go | 24 +++-- 5 files changed, 147 insertions(+), 93 deletions(-) diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index 0bb0594e..bc52570a 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -834,7 +834,12 @@ func bakeSuperMacaroon(cfg *LitNodeConfig, readOnly bool) (string, error) { lndAdminCtx := macaroonContext(ctxt, lndAdminMacBytes) lndConn := lnrpc.NewLightningClient(rawConn) - superMacPermissions := terminal.GetAllPermissions(readOnly) + permsMgr, err := terminal.NewPermissionsManager() + if err != nil { + return "", err + } + + superMacPermissions := permsMgr.ActivePermissions(readOnly) nullID := [4]byte{} superMacHex, err := terminal.BakeSuperMacaroon( lndAdminCtx, lndConn, session.NewSuperMacaroonRootKeyID(nullID), diff --git a/rpc_proxy.go b/rpc_proxy.go index 1662da77..3a05f5e0 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -24,7 +24,6 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" - "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon.v2" ) @@ -59,8 +58,7 @@ func (e *proxyErr) Unwrap() error { // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, superMacValidator session.SuperMacaroonValidator, - permissionMap map[string][]bakery.Op, - bufListener *bufconn.Listener) *rpcProxy { + permsMgr *PermissionsManager, bufListener *bufconn.Listener) *rpcProxy { // The gRPC web calls are protected by HTTP basic auth which is defined // by base64(username:password). Because we only have a password, we @@ -77,7 +75,7 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, p := &rpcProxy{ cfg: cfg, basicAuth: basicAuth, - permissionMap: permissionMap, + permsMgr: permsMgr, macValidator: validator, superMacValidator: superMacValidator, bufListener: bufListener, @@ -146,9 +144,9 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, // +---------------------+ // type rpcProxy struct { - cfg *Config - basicAuth string - permissionMap map[string][]bakery.Op + cfg *Config + basicAuth string + permsMgr *PermissionsManager macValidator macaroons.MacaroonValidator superMacValidator session.SuperMacaroonValidator @@ -345,17 +343,17 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, // handled by the integrated daemons that are hooking into lnd's // gRPC server. switch { - case isFaradayURI(requestURI) && p.cfg.faradayRemote: + case p.permsMgr.IsFaradayURI(requestURI) && p.cfg.faradayRemote: return outCtx, p.faradayConn, nil - case isLoopURI(requestURI) && p.cfg.loopRemote: + case p.permsMgr.IsLoopURI(requestURI) && p.cfg.loopRemote: return outCtx, p.loopConn, nil - case isPoolURI(requestURI) && p.cfg.poolRemote: + case p.permsMgr.IsPoolURI(requestURI) && p.cfg.poolRemote: return outCtx, p.poolConn, nil // Calls to LiT session RPC aren't allowed in some cases. - case isLitURI(requestURI) && !allowLitRPC: + case p.permsMgr.IsLitURI(requestURI) && !allowLitRPC: return outCtx, nil, status.Errorf( codes.Unimplemented, "unknown service %s", requestURI, @@ -373,7 +371,7 @@ func (p *rpcProxy) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - uriPermissions, ok := p.permissionMap[info.FullMethod] + uriPermissions, ok := p.permsMgr.URIPermissions(info.FullMethod) if !ok { return nil, fmt.Errorf("%s: unknown permissions "+ "required for method", info.FullMethod) @@ -414,7 +412,7 @@ func (p *rpcProxy) StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - uriPermissions, ok := p.permissionMap[info.FullMethod] + uriPermissions, ok := p.permsMgr.URIPermissions(info.FullMethod) if !ok { return fmt.Errorf("%s: unknown permissions required "+ "for method", info.FullMethod) @@ -503,31 +501,31 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, macData []byte ) switch { - case isLndURI(requestURI): + case p.permsMgr.IsLndURI(requestURI): _, _, _, macPath, macData = p.cfg.lndConnectParams() - case isFaradayURI(requestURI): + case p.permsMgr.IsFaradayURI(requestURI): if p.cfg.faradayRemote { macPath = p.cfg.Remote.Faraday.MacaroonPath } else { macPath = p.cfg.Faraday.MacaroonPath } - case isLoopURI(requestURI): + case p.permsMgr.IsLoopURI(requestURI): if p.cfg.loopRemote { macPath = p.cfg.Remote.Loop.MacaroonPath } else { macPath = p.cfg.Loop.MacaroonPath } - case isPoolURI(requestURI): + case p.permsMgr.IsPoolURI(requestURI): if p.cfg.poolRemote { macPath = p.cfg.Remote.Pool.MacaroonPath } else { macPath = p.cfg.Pool.MacaroonPath } - case isLitURI(requestURI): + case p.permsMgr.IsLitURI(requestURI): macPath = p.cfg.MacaroonPath default: @@ -580,7 +578,7 @@ func (p *rpcProxy) basicAuthToMacaroon(basicAuth, requestURI string, func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, fullMethod string) ([]byte, error) { - requiredPermissions, ok := p.permissionMap[fullMethod] + requiredPermissions, ok := p.permsMgr.URIPermissions(fullMethod) if !ok { return nil, fmt.Errorf("%s: unknown permissions required for "+ "method", fullMethod) @@ -605,17 +603,17 @@ func (p *rpcProxy) convertSuperMacaroon(ctx context.Context, macHex string, // Is this actually a request that goes to a daemon that is running // remotely? switch { - case isFaradayURI(fullMethod) && p.cfg.faradayRemote: + case p.permsMgr.IsFaradayURI(fullMethod) && p.cfg.faradayRemote: return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Faraday.MacaroonPath, )) - case isLoopURI(fullMethod) && p.cfg.loopRemote: + case p.permsMgr.IsLoopURI(fullMethod) && p.cfg.loopRemote: return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Loop.MacaroonPath, )) - case isPoolURI(fullMethod) && p.cfg.poolRemote: + case p.permsMgr.IsPoolURI(fullMethod) && p.cfg.poolRemote: return readMacaroon(lncfg.CleanAndExpandPath( p.cfg.Remote.Pool.MacaroonPath, )) diff --git a/session_rpcserver.go b/session_rpcserver.go index ed403215..1272ff03 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -39,6 +39,7 @@ type sessionRpcServerConfig struct { superMacBaker func(ctx context.Context, rootKeyID uint64, recipe *session.MacaroonRecipe) (string, error) firstConnectionDeadline time.Duration + permMgr *PermissionsManager } // newSessionRPCServer creates a new sessionRpcServer using the passed config. @@ -205,7 +206,7 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error { mac, err := s.cfg.superMacBaker( context.Background(), sess.MacaroonRootKey, &session.MacaroonRecipe{ - Permissions: GetAllPermissions(readOnly), + Permissions: s.cfg.permMgr.ActivePermissions(readOnly), Caveats: caveats, }, ) diff --git a/subserver_permissions.go b/subserver_permissions.go index f350f5e6..d809c3eb 100644 --- a/subserver_permissions.go +++ b/subserver_permissions.go @@ -26,9 +26,9 @@ var ( }}, } - // whiteListedMethods is a map of all lnd RPC methods that don't require - // any macaroon authentication. - whiteListedMethods = map[string][]bakery.Op{ + // whiteListedLNDMethods is a map of all lnd RPC methods that don't + // require any macaroon authentication. + whiteListedLNDMethods = map[string][]bakery.Op{ "/lnrpc.WalletUnlocker/GenSeed": {}, "/lnrpc.WalletUnlocker/InitWallet": {}, "/lnrpc.WalletUnlocker/UnlockWallet": {}, @@ -41,53 +41,71 @@ var ( } ) -// getSubserverPermissions returns a merged map of all subserver macaroon -// permissions. -func getSubserverPermissions() map[string][]bakery.Op { - mapSize := len(faraday.RequiredPermissions) + - len(loop.RequiredPermissions) + len(pool.RequiredPermissions) - result := make(map[string][]bakery.Op, mapSize) - for key, value := range faraday.RequiredPermissions { - result[key] = value - } - for key, value := range loop.RequiredPermissions { - result[key] = value - } - for key, value := range pool.RequiredPermissions { - result[key] = value - } - for key, value := range litPermissions { - result[key] = value - } - return result +// subServerName is a name used to identify a particular Lit sub-server. +type subServerName string + +const ( + poolPerms subServerName = "pool" + loopPerms subServerName = "loop" + faradayPerms subServerName = "faraday" + litPerms subServerName = "lit" + lndPerms subServerName = "lnd" +) + +// PermissionsManager manages the permission lists that Lit requires. +type PermissionsManager struct { + // fixedPerms is constructed once on creation of the PermissionsManager. + // It contains all the permissions that will not change throughout the + // lifetime of the manager. It maps sub-server name to uri to permission + // operations. + fixedPerms map[subServerName]map[string][]bakery.Op + + // perms is a map containing all permissions that the manager knows + // are available for use. + perms map[string][]bakery.Op } -// getAllMethodPermissions returns a merged map of lnd's and all subservers' -// method macaroon permissions. -func getAllMethodPermissions() map[string][]bakery.Op { - subserverPermissions := getSubserverPermissions() - lndPermissions := lnd.MainRPCServerPermissions() - mapSize := len(subserverPermissions) + len(lndPermissions) + - len(whiteListedMethods) - result := make(map[string][]bakery.Op, mapSize) - for key, value := range lndPermissions { - result[key] = value +// NewPermissionsManager constructs a new PermissionsManager instance and +// collects any of the fixed permissions. +func NewPermissionsManager() (*PermissionsManager, error) { + permissions := make(map[subServerName]map[string][]bakery.Op) + permissions[faradayPerms] = faraday.RequiredPermissions + permissions[loopPerms] = loop.RequiredPermissions + permissions[poolPerms] = pool.RequiredPermissions + permissions[litPerms] = litPermissions + permissions[lndPerms] = lnd.MainRPCServerPermissions() + for k, v := range whiteListedLNDMethods { + permissions[lndPerms][k] = v } - for key, value := range subserverPermissions { - result[key] = value + + allPerms := make(map[string][]bakery.Op) + for _, perms := range permissions { + for k, v := range perms { + allPerms[k] = v + } } - for key, value := range whiteListedMethods { - result[key] = value - } - return result + + return &PermissionsManager{ + fixedPerms: permissions, + perms: allPerms, + }, nil } -// GetAllPermissions retrieves all the permissions needed to bake a super -// macaroon. -func GetAllPermissions(readOnly bool) []bakery.Op { +// URIPermissions returns a list of permission operations for the given URI if +// the uri is known to the manager. The second return parameter will be false +// if the URI is unknown to the manager. +func (pm *PermissionsManager) URIPermissions(uri string) ([]bakery.Op, bool) { + ops, ok := pm.perms[uri] + return ops, ok +} + +// ActivePermissions returns all the available active permissions that the +// manager is aware of. Optionally, readOnly can be set to true if only the +// read-only permissions should be returned. +func (pm *PermissionsManager) ActivePermissions(readOnly bool) []bakery.Op { + // De-dup the permissions and optionally apply the read-only filter. dedupMap := make(map[string]map[string]bool) - - for _, methodPerms := range getAllMethodPermissions() { + for _, methodPerms := range pm.perms { for _, methodPerm := range methodPerms { if methodPerm.Action == "" || methodPerm.Entity == "" { continue @@ -119,32 +137,56 @@ func GetAllPermissions(readOnly bool) []bakery.Op { return result } -// isLndURI returns true if the given URI belongs to an RPC of lnd. -func isLndURI(uri string) bool { - _, ok := lnd.MainRPCServerPermissions()[uri] +// GetLitPerms returns a map of all permissions that the manager is aware of +// _except_ for any LND permissions. In other words, this returns permissions +// for which the external validator of Lit is responsible. +func (pm *PermissionsManager) GetLitPerms() map[string][]bakery.Op { + mapSize := len(pm.fixedPerms[litPerms]) + + len(pm.fixedPerms[faradayPerms]) + + len(pm.fixedPerms[loopPerms]) + len(pm.fixedPerms[poolPerms]) + + result := make(map[string][]bakery.Op, mapSize) + for key, value := range pm.fixedPerms[faradayPerms] { + result[key] = value + } + for key, value := range pm.fixedPerms[loopPerms] { + result[key] = value + } + for key, value := range pm.fixedPerms[poolPerms] { + result[key] = value + } + for key, value := range pm.fixedPerms[litPerms] { + result[key] = value + } + return result +} + +// IsLndURI returns true if the given URI belongs to an RPC of lnd. +func (pm *PermissionsManager) IsLndURI(uri string) bool { + _, lndCall := pm.fixedPerms[lndPerms][uri] + return lndCall +} + +// IsLoopURI returns true if the given URI belongs to an RPC of loopd. +func (pm *PermissionsManager) IsLoopURI(uri string) bool { + _, ok := pm.fixedPerms[loopPerms][uri] return ok } -// isLoopURI returns true if the given URI belongs to an RPC of loopd. -func isLoopURI(uri string) bool { - _, ok := loop.RequiredPermissions[uri] +// IsFaradayURI returns true if the given URI belongs to an RPC of faraday. +func (pm *PermissionsManager) IsFaradayURI(uri string) bool { + _, ok := pm.fixedPerms[faradayPerms][uri] return ok } -// isFaradayURI returns true if the given URI belongs to an RPC of faraday. -func isFaradayURI(uri string) bool { - _, ok := faraday.RequiredPermissions[uri] +// IsPoolURI returns true if the given URI belongs to an RPC of poold. +func (pm *PermissionsManager) IsPoolURI(uri string) bool { + _, ok := pm.fixedPerms[poolPerms][uri] return ok } -// isPoolURI returns true if the given URI belongs to an RPC of poold. -func isPoolURI(uri string) bool { - _, ok := pool.RequiredPermissions[uri] - return ok -} - -// isLitURI returns true if the given URI belongs to an RPC of LiT. -func isLitURI(uri string) bool { - _, ok := litPermissions[uri] +// IsLitURI returns true if the given URI belongs to an RPC of LiT. +func (pm *PermissionsManager) IsLitURI(uri string) bool { + _, ok := pm.fixedPerms[litPerms][uri] return ok } diff --git a/terminal.go b/terminal.go index 710266db..2a1e0ebc 100644 --- a/terminal.go +++ b/terminal.go @@ -135,6 +135,8 @@ type LightningTerminal struct { defaultImplCfg *lnd.ImplementationCfg + permsMgr *PermissionsManager + // lndInterceptorChain is a reference to lnd's interceptor chain that // guards all incoming calls. This is only set in integrated mode! lndInterceptorChain *rpcperms.InterceptorChain @@ -193,6 +195,12 @@ func (g *LightningTerminal) Run() error { // Show version at startup. log.Infof("LiT version: %s", Version()) + // Construct a new PermissionsManager. + g.permsMgr, err = NewPermissionsManager() + if err != nil { + return fmt.Errorf("could not create permissions manager") + } + // Create the instances of our subservers now so we can hook them up to // lnd once it's fully started. bufRpcListener := bufconn.Listen(100) @@ -200,8 +208,7 @@ func (g *LightningTerminal) Run() error { g.loopServer = loopd.New(g.cfg.Loop, nil) g.poolServer = pool.NewServer(g.cfg.Pool) g.rpcProxy = newRpcProxy( - g.cfg, g, g.validateSuperMacaroon, getAllMethodPermissions(), - bufRpcListener, + g.cfg, g, g.validateSuperMacaroon, g.permsMgr, bufRpcListener, ) g.sessionRpcServer, err = newSessionRPCServer(&sessionRpcServerConfig{ basicAuth: g.rpcProxy.basicAuth, @@ -233,6 +240,7 @@ func (g *LightningTerminal) Run() error { ) }, firstConnectionDeadline: g.cfg.FirstLNCConnDeadline, + permMgr: g.permsMgr, }) if err != nil { return fmt.Errorf("could not create new session rpc "+ @@ -497,7 +505,7 @@ func (g *LightningTerminal) startSubservers() error { ctx, g.basicClient, session.NewSuperMacaroonRootKeyID( [4]byte{}, ), - GetAllPermissions(false), nil, + g.permsMgr.ActivePermissions(false), nil, ) if err != nil { return err @@ -704,7 +712,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, // process. Calls that we proxy to a remote host don't need to be // checked as they'll have their own interceptor. switch { - case isFaradayURI(fullMethod): + case g.permsMgr.IsFaradayURI(fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. if g.cfg.faradayRemote { @@ -728,7 +736,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } } - case isLoopURI(fullMethod): + case g.permsMgr.IsLoopURI(fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. if g.cfg.loopRemote { @@ -752,7 +760,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } } - case isPoolURI(fullMethod): + case g.permsMgr.IsPoolURI(fullMethod): // In remote mode we just pass through the request, the remote // daemon will check the macaroon. if g.cfg.poolRemote { @@ -776,7 +784,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, } } - case isLitURI(fullMethod): + case g.permsMgr.IsLitURI(fullMethod): if !g.macaroonServiceStarted { return fmt.Errorf("the macaroon service has not " + "started yet") @@ -806,7 +814,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, // // NOTE: This is part of the lnd.ExternalValidator interface. func (g *LightningTerminal) Permissions() map[string][]bakery.Op { - return getSubserverPermissions() + return g.permsMgr.GetLitPerms() } // BuildWalletConfig is responsible for creating or unlocking and then