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/perms/permissions.go b/perms/permissions.go index f4fec297b..51d2fa12f 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -2,6 +2,7 @@ package perms import ( "net" + "regexp" "strings" "sync" @@ -210,6 +211,40 @@ func (pm *Manager) 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. 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/session_rpcserver.go b/session_rpcserver.go index a52b9461f..0e5a79ae4 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -143,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{