Skip to content

Commit ce2cf12

Browse files
authored
Merge pull request #450 from ellemouton/wildCardURIs
multi: support using a regex when specifying custom perms
2 parents 98513f7 + 15cd1bd commit ce2cf12

File tree

7 files changed

+184
-34
lines changed

7 files changed

+184
-34
lines changed

cmd/litcli/sessions.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ var addSessionCommand = cli.Command{
7373
"this flag will only be used if the 'type' " +
7474
"flag is set to 'custom'. This flag can be " +
7575
"specified multiple times if multiple URIs " +
76-
"should be included",
76+
"should be included. Note that a regex can " +
77+
"also be specified which will then result in " +
78+
"all URIs matching the regex to be included. " +
79+
"For example, '/lnrpc\\..*' will result in " +
80+
"all `lnrpc` permissions being included.",
7781
},
7882
},
7983
}

itest/litd_mode_integrated_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/lightninglabs/lightning-node-connect/mailbox"
2222
terminal "github.com/lightninglabs/lightning-terminal"
2323
"github.com/lightninglabs/lightning-terminal/litrpc"
24+
"github.com/lightninglabs/lightning-terminal/perms"
2425
"github.com/lightninglabs/lightning-terminal/session"
2526
"github.com/lightninglabs/loop/looprpc"
2627
"github.com/lightninglabs/pool/poolrpc"
@@ -945,7 +946,7 @@ func bakeSuperMacaroon(cfg *LitNodeConfig, readOnly bool) (string, error) {
945946
lndAdminCtx := macaroonContext(ctxt, lndAdminMacBytes)
946947
lndConn := lnrpc.NewLightningClient(rawConn)
947948

948-
permsMgr, err := terminal.NewPermissionsManager()
949+
permsMgr, err := perms.NewManager()
949950
if err != nil {
950951
return "", err
951952
}

subserver_permissions.go renamed to perms/permissions.go

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
package terminal
1+
package perms
22

33
import (
44
"net"
5+
"regexp"
56
"strings"
67
"sync"
78

@@ -30,9 +31,9 @@ import (
3031
)
3132

3233
var (
33-
// litPermissions is a map of all LiT RPC methods and their required
34+
// LitPermissions is a map of all LiT RPC methods and their required
3435
// macaroon permissions to access the session service.
35-
litPermissions = map[string][]bakery.Op{
36+
LitPermissions = map[string][]bakery.Op{
3637
"/litrpc.Sessions/AddSession": {{
3738
Entity: "sessions",
3839
Action: "write",
@@ -93,15 +94,15 @@ const (
9394
lndPerms subServerName = "lnd"
9495
)
9596

96-
// PermissionsManager manages the permission lists that Lit requires.
97-
type PermissionsManager struct {
97+
// Manager manages the permission lists that Lit requires.
98+
type Manager struct {
9899
// lndSubServerPerms is a map from LND subserver name to permissions
99100
// map. This is used once the manager receives a list of build tags
100101
// that LND has been compiled with so that the correct permissions can
101102
// be extracted based on subservers that LND has been compiled with.
102103
lndSubServerPerms map[string]map[string][]bakery.Op
103104

104-
// fixedPerms is constructed once on creation of the PermissionsManager.
105+
// fixedPerms is constructed once on creation of the Manager.
105106
// It contains all the permissions that will not change throughout the
106107
// lifetime of the manager. It maps sub-server name to uri to permission
107108
// operations.
@@ -117,14 +118,14 @@ type PermissionsManager struct {
117118
permsMu sync.RWMutex
118119
}
119120

120-
// NewPermissionsManager constructs a new PermissionsManager instance and
121-
// collects any of the fixed permissions.
122-
func NewPermissionsManager() (*PermissionsManager, error) {
121+
// NewManager constructs a new Manager instance and collects any of the fixed
122+
// permissions.
123+
func NewManager() (*Manager, error) {
123124
permissions := make(map[subServerName]map[string][]bakery.Op)
124125
permissions[faradayPerms] = faraday.RequiredPermissions
125126
permissions[loopPerms] = loop.RequiredPermissions
126127
permissions[poolPerms] = pool.RequiredPermissions
127-
permissions[litPerms] = litPermissions
128+
permissions[litPerms] = LitPermissions
128129
permissions[lndPerms] = lnd.MainRPCServerPermissions()
129130
for k, v := range whiteListedLNDMethods {
130131
permissions[lndPerms][k] = v
@@ -163,7 +164,7 @@ func NewPermissionsManager() (*PermissionsManager, error) {
163164
}
164165
}
165166

166-
return &PermissionsManager{
167+
return &Manager{
167168
lndSubServerPerms: lndSubServerPerms,
168169
fixedPerms: permissions,
169170
perms: allPerms,
@@ -174,7 +175,7 @@ func NewPermissionsManager() (*PermissionsManager, error) {
174175
// obtained. It then uses those build tags to decide which of the LND sub-server
175176
// permissions to add to the main permissions list. This method should only
176177
// be called once.
177-
func (pm *PermissionsManager) OnLNDBuildTags(lndBuildTags []string) {
178+
func (pm *Manager) OnLNDBuildTags(lndBuildTags []string) {
178179
pm.permsMu.Lock()
179180
defer pm.permsMu.Unlock()
180181

@@ -202,18 +203,52 @@ func (pm *PermissionsManager) OnLNDBuildTags(lndBuildTags []string) {
202203
// URIPermissions returns a list of permission operations for the given URI if
203204
// the uri is known to the manager. The second return parameter will be false
204205
// if the URI is unknown to the manager.
205-
func (pm *PermissionsManager) URIPermissions(uri string) ([]bakery.Op, bool) {
206+
func (pm *Manager) URIPermissions(uri string) ([]bakery.Op, bool) {
206207
pm.permsMu.RLock()
207208
defer pm.permsMu.RUnlock()
208209

209210
ops, ok := pm.perms[uri]
210211
return ops, ok
211212
}
212213

214+
// MatchRegexURI first checks that the given URI is in fact a regex. If it is,
215+
// then it is used to match on the perms that the manager has. The return values
216+
// are a list of URIs that match the regex and the boolean represents whether
217+
// the given uri is in fact a regex.
218+
func (pm *Manager) MatchRegexURI(uriRegex string) ([]string, bool) {
219+
pm.permsMu.RLock()
220+
defer pm.permsMu.RUnlock()
221+
222+
// If the given uri string is one of our permissions, then it is not
223+
// a regex.
224+
if _, ok := pm.perms[uriRegex]; ok {
225+
return nil, false
226+
}
227+
228+
// Construct the regex type from the given string.
229+
r, err := regexp.Compile(uriRegex)
230+
if err != nil {
231+
return nil, false
232+
}
233+
234+
// Iterate over the list of permissions and collect all permissions that
235+
// match the given regex.
236+
var matches []string
237+
for uri := range pm.perms {
238+
if !r.MatchString(uri) {
239+
continue
240+
}
241+
242+
matches = append(matches, uri)
243+
}
244+
245+
return matches, true
246+
}
247+
213248
// ActivePermissions returns all the available active permissions that the
214249
// manager is aware of. Optionally, readOnly can be set to true if only the
215250
// read-only permissions should be returned.
216-
func (pm *PermissionsManager) ActivePermissions(readOnly bool) []bakery.Op {
251+
func (pm *Manager) ActivePermissions(readOnly bool) []bakery.Op {
217252
pm.permsMu.RLock()
218253
defer pm.permsMu.RUnlock()
219254

@@ -254,7 +289,7 @@ func (pm *PermissionsManager) ActivePermissions(readOnly bool) []bakery.Op {
254289
// GetLitPerms returns a map of all permissions that the manager is aware of
255290
// _except_ for any LND permissions. In other words, this returns permissions
256291
// for which the external validator of Lit is responsible.
257-
func (pm *PermissionsManager) GetLitPerms() map[string][]bakery.Op {
292+
func (pm *Manager) GetLitPerms() map[string][]bakery.Op {
258293
mapSize := len(pm.fixedPerms[litPerms]) +
259294
len(pm.fixedPerms[faradayPerms]) +
260295
len(pm.fixedPerms[loopPerms]) + len(pm.fixedPerms[poolPerms])
@@ -276,7 +311,7 @@ func (pm *PermissionsManager) GetLitPerms() map[string][]bakery.Op {
276311
}
277312

278313
// IsLndURI returns true if the given URI belongs to an RPC of lnd.
279-
func (pm *PermissionsManager) IsLndURI(uri string) bool {
314+
func (pm *Manager) IsLndURI(uri string) bool {
280315
var lndSubServerCall bool
281316
for _, subserverPermissions := range pm.lndSubServerPerms {
282317
_, found := subserverPermissions[uri]
@@ -290,25 +325,25 @@ func (pm *PermissionsManager) IsLndURI(uri string) bool {
290325
}
291326

292327
// IsLoopURI returns true if the given URI belongs to an RPC of loopd.
293-
func (pm *PermissionsManager) IsLoopURI(uri string) bool {
328+
func (pm *Manager) IsLoopURI(uri string) bool {
294329
_, ok := pm.fixedPerms[loopPerms][uri]
295330
return ok
296331
}
297332

298333
// IsFaradayURI returns true if the given URI belongs to an RPC of faraday.
299-
func (pm *PermissionsManager) IsFaradayURI(uri string) bool {
334+
func (pm *Manager) IsFaradayURI(uri string) bool {
300335
_, ok := pm.fixedPerms[faradayPerms][uri]
301336
return ok
302337
}
303338

304339
// IsPoolURI returns true if the given URI belongs to an RPC of poold.
305-
func (pm *PermissionsManager) IsPoolURI(uri string) bool {
340+
func (pm *Manager) IsPoolURI(uri string) bool {
306341
_, ok := pm.fixedPerms[poolPerms][uri]
307342
return ok
308343
}
309344

310345
// IsLitURI returns true if the given URI belongs to an RPC of LiT.
311-
func (pm *PermissionsManager) IsLitURI(uri string) bool {
346+
func (pm *Manager) IsLitURI(uri string) bool {
312347
_, ok := pm.fixedPerms[litPerms][uri]
313348
return ok
314349
}

perms/permissions_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package perms
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"gopkg.in/macaroon-bakery.v2/bakery"
8+
)
9+
10+
// TestMatchRegexURI tests the behaviour of the MatchRegexURI method of the
11+
// Manager.
12+
func TestMatchRegexURI(t *testing.T) {
13+
// Construct a new Manager with a predefined list of perms.
14+
m := &Manager{
15+
perms: map[string][]bakery.Op{
16+
"/lnrpc.WalletUnlocker/GenSeed": {},
17+
"/lnrpc.WalletUnlocker/InitWallet": {},
18+
"/lnrpc.Lightning/SendCoins": {{
19+
Entity: "onchain",
20+
Action: "write",
21+
}},
22+
"/litrpc.Sessions/AddSession": {{
23+
Entity: "sessions",
24+
Action: "write",
25+
}},
26+
"/litrpc.Sessions/ListSessions": {{
27+
Entity: "sessions",
28+
Action: "read",
29+
}},
30+
"/litrpc.Sessions/RevokeSession": {{
31+
Entity: "sessions",
32+
Action: "write",
33+
}},
34+
},
35+
}
36+
37+
// Assert that a full URI is not considered a wild card.
38+
uris, isRegex := m.MatchRegexURI("/litrpc.Sessions/RevokeSession")
39+
require.False(t, isRegex)
40+
require.Empty(t, uris)
41+
42+
// Assert that an invalid URI is also caught as such.
43+
uris, isRegex = m.MatchRegexURI("***")
44+
require.False(t, isRegex)
45+
require.Nil(t, uris)
46+
47+
// Assert that the function correctly matches on a valid wild card for
48+
// litrpc URIs.
49+
uris, isRegex = m.MatchRegexURI("/litrpc.Sessions/.*")
50+
require.True(t, isRegex)
51+
require.ElementsMatch(t, uris, []string{
52+
"/litrpc.Sessions/AddSession",
53+
"/litrpc.Sessions/ListSessions",
54+
"/litrpc.Sessions/RevokeSession",
55+
})
56+
57+
// Assert that the function correctly matches on a valid wild card for
58+
// lnd URIs. First we check that we can specify that only the
59+
// "WalletUnlocker" methods should be included.
60+
uris, isRegex = m.MatchRegexURI("/lnrpc.WalletUnlocker/.*")
61+
require.True(t, isRegex)
62+
require.ElementsMatch(t, uris, []string{
63+
"/lnrpc.WalletUnlocker/GenSeed",
64+
"/lnrpc.WalletUnlocker/InitWallet",
65+
})
66+
67+
// Now we check that we can include all the `lnrpc` methods.
68+
uris, isRegex = m.MatchRegexURI("/lnrpc\\..*")
69+
require.True(t, isRegex)
70+
require.ElementsMatch(t, uris, []string{
71+
"/lnrpc.WalletUnlocker/GenSeed",
72+
"/lnrpc.WalletUnlocker/InitWallet",
73+
"/lnrpc.Lightning/SendCoins",
74+
})
75+
76+
// Assert that the function does not return any URIs for a wild card
77+
// URI that does not match on any of its perms.
78+
uris, isRegex = m.MatchRegexURI("/poolrpc.Trader/.*")
79+
require.True(t, isRegex)
80+
require.Empty(t, uris)
81+
}

rpc_proxy.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
"github.com/improbable-eng/grpc-web/go/grpcweb"
16+
"github.com/lightninglabs/lightning-terminal/perms"
1617
"github.com/lightninglabs/lightning-terminal/session"
1718
"github.com/lightningnetwork/lnd/lncfg"
1819
"github.com/lightningnetwork/lnd/macaroons"
@@ -58,7 +59,7 @@ func (e *proxyErr) Unwrap() error {
5859
// component.
5960
func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator,
6061
superMacValidator session.SuperMacaroonValidator,
61-
permsMgr *PermissionsManager, bufListener *bufconn.Listener) *rpcProxy {
62+
permsMgr *perms.Manager, bufListener *bufconn.Listener) *rpcProxy {
6263

6364
// The gRPC web calls are protected by HTTP basic auth which is defined
6465
// by base64(username:password). Because we only have a password, we
@@ -146,7 +147,7 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator,
146147
type rpcProxy struct {
147148
cfg *Config
148149
basicAuth string
149-
permsMgr *PermissionsManager
150+
permsMgr *perms.Manager
150151

151152
macValidator macaroons.MacaroonValidator
152153
superMacValidator session.SuperMacaroonValidator

session_rpcserver.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/btcsuite/btcd/btcec/v2"
1111
"github.com/lightninglabs/lightning-node-connect/mailbox"
1212
"github.com/lightninglabs/lightning-terminal/litrpc"
13+
"github.com/lightninglabs/lightning-terminal/perms"
1314
"github.com/lightninglabs/lightning-terminal/session"
1415
"github.com/lightningnetwork/lnd/macaroons"
1516
"google.golang.org/grpc"
@@ -41,7 +42,7 @@ type sessionRpcServerConfig struct {
4142
superMacBaker func(ctx context.Context, rootKeyID uint64,
4243
recipe *session.MacaroonRecipe) (string, error)
4344
firstConnectionDeadline time.Duration
44-
permMgr *PermissionsManager
45+
permMgr *perms.Manager
4546
}
4647

4748
// newSessionRPCServer creates a new sessionRpcServer using the passed config.
@@ -142,12 +143,38 @@ func (s *sessionRpcServer) AddSession(_ context.Context,
142143
}
143144

144145
for _, op := range req.MacaroonCustomPermissions {
145-
if op.Entity == macaroons.PermissionEntityCustomURI {
146-
_, ok := s.cfg.permMgr.URIPermissions(op.Action)
147-
if !ok {
148-
return nil, fmt.Errorf("URI %s is "+
149-
"unknown to LiT", op.Action)
146+
if op.Entity != macaroons.PermissionEntityCustomURI {
147+
permissions = append(permissions, bakery.Op{
148+
Entity: op.Entity,
149+
Action: op.Action,
150+
})
151+
152+
continue
153+
}
154+
155+
// First check if this is a regex URI.
156+
uris, isRegex := s.cfg.permMgr.MatchRegexURI(op.Action)
157+
if isRegex {
158+
// This is a regex URI, and so we add each of
159+
// the matching URIs returned from the
160+
// permissions' manager.
161+
for _, uri := range uris {
162+
permissions = append(
163+
permissions, bakery.Op{
164+
Entity: op.Entity,
165+
Action: uri,
166+
},
167+
)
150168
}
169+
continue
170+
}
171+
172+
// This is not a wild card URI, so just check that the
173+
// permissions' manager is aware of this URI.
174+
_, ok := s.cfg.permMgr.URIPermissions(op.Action)
175+
if !ok {
176+
return nil, fmt.Errorf("URI %s is unknown to "+
177+
"LiT", op.Action)
151178
}
152179

153180
permissions = append(permissions, bakery.Op{

0 commit comments

Comments
 (0)