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.
This commit is contained in:
Elle Mouton 2022-08-31 10:43:26 +02:00
parent 824e94a08e
commit c2eb98db38
No known key found for this signature in database
GPG key ID: D7D916376026F177
5 changed files with 147 additions and 93 deletions

View file

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

View file

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

View file

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

View file

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

View file

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