diff --git a/cmd/litcli/sessions.go b/cmd/litcli/sessions.go index d16e27e03..595919327 100644 --- a/cmd/litcli/sessions.go +++ b/cmd/litcli/sessions.go @@ -73,7 +73,11 @@ var addSessionCommand = cli.Command{ "this flag will only be used if the 'type' " + "flag is set to 'custom'. This flag can be " + "specified multiple times if multiple URIs " + - "should be included", + "should be included. Note that a regex can " + + "also be specified which will then result in " + + "all URIs matching the regex to be included. " + + "For example, '/lnrpc\\..*' will result in " + + "all `lnrpc` permissions being included.", }, }, } diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index fe152d600..e646cbe3b 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -21,6 +21,7 @@ import ( "github.com/lightninglabs/lightning-node-connect/mailbox" terminal "github.com/lightninglabs/lightning-terminal" "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/pool/poolrpc" @@ -945,7 +946,7 @@ func bakeSuperMacaroon(cfg *LitNodeConfig, readOnly bool) (string, error) { lndAdminCtx := macaroonContext(ctxt, lndAdminMacBytes) lndConn := lnrpc.NewLightningClient(rawConn) - permsMgr, err := terminal.NewPermissionsManager() + permsMgr, err := perms.NewManager() if err != nil { return "", err } diff --git a/subserver_permissions.go b/perms/permissions.go similarity index 85% rename from subserver_permissions.go rename to perms/permissions.go index 6a92d3505..51d2fa12f 100644 --- a/subserver_permissions.go +++ b/perms/permissions.go @@ -1,7 +1,8 @@ -package terminal +package perms import ( "net" + "regexp" "strings" "sync" @@ -30,9 +31,9 @@ import ( ) var ( - // litPermissions is a map of all LiT RPC methods and their required + // LitPermissions is a map of all LiT RPC methods and their required // macaroon permissions to access the session service. - litPermissions = map[string][]bakery.Op{ + LitPermissions = map[string][]bakery.Op{ "/litrpc.Sessions/AddSession": {{ Entity: "sessions", Action: "write", @@ -93,15 +94,15 @@ const ( lndPerms subServerName = "lnd" ) -// PermissionsManager manages the permission lists that Lit requires. -type PermissionsManager struct { +// Manager manages the permission lists that Lit requires. +type Manager struct { // lndSubServerPerms is a map from LND subserver name to permissions // map. This is used once the manager receives a list of build tags // that LND has been compiled with so that the correct permissions can // be extracted based on subservers that LND has been compiled with. lndSubServerPerms map[string]map[string][]bakery.Op - // fixedPerms is constructed once on creation of the PermissionsManager. + // fixedPerms is constructed once on creation of the Manager. // 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. @@ -117,14 +118,14 @@ type PermissionsManager struct { permsMu sync.RWMutex } -// NewPermissionsManager constructs a new PermissionsManager instance and -// collects any of the fixed permissions. -func NewPermissionsManager() (*PermissionsManager, error) { +// NewManager constructs a new Manager instance and collects any of the fixed +// permissions. +func NewManager() (*Manager, 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[litPerms] = LitPermissions permissions[lndPerms] = lnd.MainRPCServerPermissions() for k, v := range whiteListedLNDMethods { permissions[lndPerms][k] = v @@ -163,7 +164,7 @@ func NewPermissionsManager() (*PermissionsManager, error) { } } - return &PermissionsManager{ + return &Manager{ lndSubServerPerms: lndSubServerPerms, fixedPerms: permissions, perms: allPerms, @@ -174,7 +175,7 @@ func NewPermissionsManager() (*PermissionsManager, error) { // obtained. It then uses those build tags to decide which of the LND sub-server // permissions to add to the main permissions list. This method should only // be called once. -func (pm *PermissionsManager) OnLNDBuildTags(lndBuildTags []string) { +func (pm *Manager) OnLNDBuildTags(lndBuildTags []string) { pm.permsMu.Lock() defer pm.permsMu.Unlock() @@ -202,7 +203,7 @@ func (pm *PermissionsManager) OnLNDBuildTags(lndBuildTags []string) { // 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) { +func (pm *Manager) URIPermissions(uri string) ([]bakery.Op, bool) { pm.permsMu.RLock() defer pm.permsMu.RUnlock() @@ -210,10 +211,44 @@ func (pm *PermissionsManager) URIPermissions(uri string) ([]bakery.Op, bool) { return ops, ok } +// MatchRegexURI first checks that the given URI is in fact a regex. If it is, +// then it is used to match on the perms that the manager has. The return values +// are a list of URIs that match the regex and the boolean represents whether +// the given uri is in fact a regex. +func (pm *Manager) MatchRegexURI(uriRegex string) ([]string, bool) { + pm.permsMu.RLock() + defer pm.permsMu.RUnlock() + + // If the given uri string is one of our permissions, then it is not + // a regex. + if _, ok := pm.perms[uriRegex]; ok { + return nil, false + } + + // Construct the regex type from the given string. + r, err := regexp.Compile(uriRegex) + if err != nil { + return nil, false + } + + // Iterate over the list of permissions and collect all permissions that + // match the given regex. + var matches []string + for uri := range pm.perms { + if !r.MatchString(uri) { + continue + } + + matches = append(matches, uri) + } + + return matches, true +} + // 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 { +func (pm *Manager) ActivePermissions(readOnly bool) []bakery.Op { pm.permsMu.RLock() defer pm.permsMu.RUnlock() @@ -254,7 +289,7 @@ func (pm *PermissionsManager) ActivePermissions(readOnly bool) []bakery.Op { // 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 { +func (pm *Manager) GetLitPerms() map[string][]bakery.Op { mapSize := len(pm.fixedPerms[litPerms]) + len(pm.fixedPerms[faradayPerms]) + len(pm.fixedPerms[loopPerms]) + len(pm.fixedPerms[poolPerms]) @@ -276,7 +311,7 @@ func (pm *PermissionsManager) GetLitPerms() map[string][]bakery.Op { } // IsLndURI returns true if the given URI belongs to an RPC of lnd. -func (pm *PermissionsManager) IsLndURI(uri string) bool { +func (pm *Manager) IsLndURI(uri string) bool { var lndSubServerCall bool for _, subserverPermissions := range pm.lndSubServerPerms { _, found := subserverPermissions[uri] @@ -290,25 +325,25 @@ func (pm *PermissionsManager) IsLndURI(uri string) bool { } // IsLoopURI returns true if the given URI belongs to an RPC of loopd. -func (pm *PermissionsManager) IsLoopURI(uri string) bool { +func (pm *Manager) IsLoopURI(uri string) bool { _, ok := pm.fixedPerms[loopPerms][uri] return ok } // IsFaradayURI returns true if the given URI belongs to an RPC of faraday. -func (pm *PermissionsManager) IsFaradayURI(uri string) bool { +func (pm *Manager) IsFaradayURI(uri string) bool { _, ok := pm.fixedPerms[faradayPerms][uri] return ok } // IsPoolURI returns true if the given URI belongs to an RPC of poold. -func (pm *PermissionsManager) IsPoolURI(uri string) bool { +func (pm *Manager) IsPoolURI(uri string) bool { _, ok := pm.fixedPerms[poolPerms][uri] return ok } // IsLitURI returns true if the given URI belongs to an RPC of LiT. -func (pm *PermissionsManager) IsLitURI(uri string) bool { +func (pm *Manager) IsLitURI(uri string) bool { _, ok := pm.fixedPerms[litPerms][uri] return ok } diff --git a/perms/permissions_test.go b/perms/permissions_test.go new file mode 100644 index 000000000..f5428a0f2 --- /dev/null +++ b/perms/permissions_test.go @@ -0,0 +1,81 @@ +package perms + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// TestMatchRegexURI tests the behaviour of the MatchRegexURI method of the +// Manager. +func TestMatchRegexURI(t *testing.T) { + // Construct a new Manager with a predefined list of perms. + m := &Manager{ + perms: map[string][]bakery.Op{ + "/lnrpc.WalletUnlocker/GenSeed": {}, + "/lnrpc.WalletUnlocker/InitWallet": {}, + "/lnrpc.Lightning/SendCoins": {{ + Entity: "onchain", + Action: "write", + }}, + "/litrpc.Sessions/AddSession": {{ + Entity: "sessions", + Action: "write", + }}, + "/litrpc.Sessions/ListSessions": {{ + Entity: "sessions", + Action: "read", + }}, + "/litrpc.Sessions/RevokeSession": {{ + Entity: "sessions", + Action: "write", + }}, + }, + } + + // Assert that a full URI is not considered a wild card. + uris, isRegex := m.MatchRegexURI("/litrpc.Sessions/RevokeSession") + require.False(t, isRegex) + require.Empty(t, uris) + + // Assert that an invalid URI is also caught as such. + uris, isRegex = m.MatchRegexURI("***") + require.False(t, isRegex) + require.Nil(t, uris) + + // Assert that the function correctly matches on a valid wild card for + // litrpc URIs. + uris, isRegex = m.MatchRegexURI("/litrpc.Sessions/.*") + require.True(t, isRegex) + require.ElementsMatch(t, uris, []string{ + "/litrpc.Sessions/AddSession", + "/litrpc.Sessions/ListSessions", + "/litrpc.Sessions/RevokeSession", + }) + + // Assert that the function correctly matches on a valid wild card for + // lnd URIs. First we check that we can specify that only the + // "WalletUnlocker" methods should be included. + uris, isRegex = m.MatchRegexURI("/lnrpc.WalletUnlocker/.*") + require.True(t, isRegex) + require.ElementsMatch(t, uris, []string{ + "/lnrpc.WalletUnlocker/GenSeed", + "/lnrpc.WalletUnlocker/InitWallet", + }) + + // Now we check that we can include all the `lnrpc` methods. + uris, isRegex = m.MatchRegexURI("/lnrpc\\..*") + require.True(t, isRegex) + require.ElementsMatch(t, uris, []string{ + "/lnrpc.WalletUnlocker/GenSeed", + "/lnrpc.WalletUnlocker/InitWallet", + "/lnrpc.Lightning/SendCoins", + }) + + // Assert that the function does not return any URIs for a wild card + // URI that does not match on any of its perms. + uris, isRegex = m.MatchRegexURI("/poolrpc.Trader/.*") + require.True(t, isRegex) + require.Empty(t, uris) +} diff --git a/rpc_proxy.go b/rpc_proxy.go index 3a05f5e0c..d4bd45788 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -13,6 +13,7 @@ import ( "time" "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/macaroons" @@ -58,7 +59,7 @@ func (e *proxyErr) Unwrap() error { // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, superMacValidator session.SuperMacaroonValidator, - permsMgr *PermissionsManager, bufListener *bufconn.Listener) *rpcProxy { + permsMgr *perms.Manager, 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 @@ -146,7 +147,7 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, type rpcProxy struct { cfg *Config basicAuth string - permsMgr *PermissionsManager + permsMgr *perms.Manager macValidator macaroons.MacaroonValidator superMacValidator session.SuperMacaroonValidator diff --git a/session_rpcserver.go b/session_rpcserver.go index 55bb39222..0e5a79ae4 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/lightning-node-connect/mailbox" "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" @@ -41,7 +42,7 @@ type sessionRpcServerConfig struct { superMacBaker func(ctx context.Context, rootKeyID uint64, recipe *session.MacaroonRecipe) (string, error) firstConnectionDeadline time.Duration - permMgr *PermissionsManager + permMgr *perms.Manager } // newSessionRPCServer creates a new sessionRpcServer using the passed config. @@ -142,12 +143,38 @@ func (s *sessionRpcServer) AddSession(_ context.Context, } for _, op := range req.MacaroonCustomPermissions { - if op.Entity == macaroons.PermissionEntityCustomURI { - _, ok := s.cfg.permMgr.URIPermissions(op.Action) - if !ok { - return nil, fmt.Errorf("URI %s is "+ - "unknown to LiT", op.Action) + if op.Entity != macaroons.PermissionEntityCustomURI { + permissions = append(permissions, bakery.Op{ + Entity: op.Entity, + Action: op.Action, + }) + + continue + } + + // First check if this is a regex URI. + uris, isRegex := s.cfg.permMgr.MatchRegexURI(op.Action) + if isRegex { + // This is a regex URI, and so we add each of + // the matching URIs returned from the + // permissions' manager. + for _, uri := range uris { + permissions = append( + permissions, bakery.Op{ + Entity: op.Entity, + Action: uri, + }, + ) } + continue + } + + // This is not a wild card URI, so just check that the + // permissions' manager is aware of this URI. + _, ok := s.cfg.permMgr.URIPermissions(op.Action) + if !ok { + return nil, fmt.Errorf("URI %s is unknown to "+ + "LiT", op.Action) } permissions = append(permissions, bakery.Op{ diff --git a/terminal.go b/terminal.go index cb6befa32..ee693f88d 100644 --- a/terminal.go +++ b/terminal.go @@ -22,6 +22,7 @@ import ( "github.com/lightninglabs/faraday/frdrpc" "github.com/lightninglabs/faraday/frdrpcserver" "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/queue" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" "github.com/lightninglabs/lightning-terminal/session" @@ -136,7 +137,7 @@ type LightningTerminal struct { defaultImplCfg *lnd.ImplementationCfg - permsMgr *PermissionsManager + permsMgr *perms.Manager // lndInterceptorChain is a reference to lnd's interceptor chain that // guards all incoming calls. This is only set in integrated mode! @@ -204,8 +205,8 @@ func (g *LightningTerminal) Run() error { g.errQueue.Start() defer g.errQueue.Stop() - // Construct a new PermissionsManager. - g.permsMgr, err = NewPermissionsManager() + // Construct a new Manager. + g.permsMgr, err = perms.NewManager() if err != nil { return fmt.Errorf("could not create permissions manager") } @@ -589,7 +590,7 @@ func (g *LightningTerminal) startSubservers() error { DBPath: filepath.Join(g.cfg.LitDir, g.cfg.Network), MacaroonLocation: "litd", StatelessInit: !createDefaultMacaroons, - RequiredPerms: litPermissions, + RequiredPerms: perms.LitPermissions, LndClient: &g.lndClient.LndServices, EphemeralKey: lndclient.SharedKeyNUMS, KeyLocator: lndclient.SharedKeyLocator,