Skip to content

multi: support using a regex when specifying custom perms #450

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/litcli/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
},
}
Expand Down
3 changes: 2 additions & 1 deletion itest/litd_mode_integrated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
75 changes: 55 additions & 20 deletions subserver_permissions.go → perms/permissions.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package terminal
package perms

import (
"net"
"regexp"
"strings"
"sync"

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -163,7 +164,7 @@ func NewPermissionsManager() (*PermissionsManager, error) {
}
}

return &PermissionsManager{
return &Manager{
lndSubServerPerms: lndSubServerPerms,
fixedPerms: permissions,
perms: allPerms,
Expand All @@ -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()

Expand Down Expand Up @@ -202,18 +203,52 @@ 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()

ops, ok := pm.perms[uri]
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()

Expand Down Expand Up @@ -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])
Expand All @@ -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]
Expand All @@ -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
}
Expand Down
81 changes: 81 additions & 0 deletions perms/permissions_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
5 changes: 3 additions & 2 deletions rpc_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 33 additions & 6 deletions session_rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand Down
Loading