multi: add Lnd's registered subserver perms

In this commit, we let the PermissionsManager manage LND's subserver
permissions. Once Litd is connected to LND, it can get LND's build tags
and pass them to the PermissionsManager which will then adjust its list
of permissions accordingly.
This commit is contained in:
Elle Mouton 2022-08-31 10:47:22 +02:00
parent c2eb98db38
commit b3ad8114fe
No known key found for this signature in database
GPG key ID: D7D916376026F177
5 changed files with 247 additions and 15 deletions

View file

@ -26,6 +26,8 @@ import (
"github.com/lightninglabs/pool/poolrpc"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
"google.golang.org/grpc"
@ -81,17 +83,33 @@ var (
// gRPC request. One byte version and then 4 bytes content length.
emptyGrpcWebRequest = []byte{0, 0, 0, 0, 0}
lndRequestFn = func(ctx context.Context,
lnrpcRequestFn = func(ctx context.Context,
c grpc.ClientConnInterface) (proto.Message, error) {
lndConn := lnrpc.NewLightningClient(c)
return lndConn.GetInfo(
lnrpcConn := lnrpc.NewLightningClient(c)
return lnrpcConn.GetInfo(
ctx, &lnrpc.GetInfoRequest{},
)
}
lndMacaroonFn = func(cfg *LitNodeConfig) string {
return cfg.AdminMacPath
}
routerrpcRequestFn = func(ctx context.Context,
c grpc.ClientConnInterface) (proto.Message, error) {
routerrpcConn := routerrpc.NewRouterClient(c)
return routerrpcConn.GetMissionControlConfig(
ctx, &routerrpc.GetMissionControlConfigRequest{},
)
}
walletrpcRequestFn = func(ctx context.Context,
c grpc.ClientConnInterface) (proto.Message, error) {
walletrpcConn := walletrpc.NewWalletKitClient(c)
return walletrpcConn.ListUnspent(
ctx, &walletrpc.ListUnspentRequest{},
)
}
faradayRequestFn = func(ctx context.Context,
c grpc.ClientConnInterface) (proto.Message, error) {
@ -145,14 +163,32 @@ var (
allowedThroughLNC bool
grpcWebURI string
restWebURI string
restPOST bool
}{{
name: "lnrpc",
macaroonFn: lndMacaroonFn,
requestFn: lndRequestFn,
requestFn: lnrpcRequestFn,
successPattern: "\"identity_pubkey\":\"0",
allowedThroughLNC: true,
grpcWebURI: "/lnrpc.Lightning/GetInfo",
restWebURI: "/v1/getinfo",
}, {
name: "routerrpc",
macaroonFn: lndMacaroonFn,
requestFn: routerrpcRequestFn,
successPattern: "\"config\":{",
allowedThroughLNC: true,
grpcWebURI: "/routerrpc.Router/GetMissionControlConfig",
restWebURI: "/v2/router/mccfg",
}, {
name: "walletrpc",
macaroonFn: lndMacaroonFn,
requestFn: walletrpcRequestFn,
successPattern: "\"utxos\":[",
allowedThroughLNC: true,
grpcWebURI: "/walletrpc.WalletKit/ListUnspent",
restWebURI: "/v2/wallet/utxos",
restPOST: true,
}, {
name: "frdrpc",
macaroonFn: faradayMacaroonFn,
@ -322,6 +358,7 @@ func testModeIntegrated(net *NetworkHarness, t *harnessTest) {
endpoint.macaroonFn(cfg),
endpoint.restWebURI,
endpoint.successPattern,
endpoint.restPOST,
)
})
}
@ -529,7 +566,7 @@ func runGRPCWebAuthTest(t *testing.T, hostPort, uiPassword, grpcWebURI string) {
// runRESTAuthTest tests authentication of the given REST interface.
func runRESTAuthTest(t *testing.T, hostPort, uiPassword, macaroonPath, restURI,
successPattern string) {
successPattern string, usePOST bool) {
basicAuth := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", uiPassword, uiPassword)),
@ -539,13 +576,19 @@ func runRESTAuthTest(t *testing.T, hostPort, uiPassword, macaroonPath, restURI,
}
url := fmt.Sprintf("https://%s%s", hostPort, restURI)
method := "GET"
if usePOST {
method = "POST"
}
// First test a REST call without authorization, which should fail.
body, responseHeader, err := callURL(url, "GET", nil, nil, false)
body, responseHeader, err := callURL(url, method, nil, nil, false)
require.NoError(t, err)
require.Equal(
require.Equalf(
t, "application/grpc",
responseHeader.Get("grpc-metadata-content-type"),
"response headers: %v, body: %v", responseHeader, body,
)
require.Equal(
t, "application/json",
@ -558,7 +601,7 @@ func runRESTAuthTest(t *testing.T, hostPort, uiPassword, macaroonPath, restURI,
// Now add the UI password which should make the request succeed.
body, responseHeader, err = callURL(
url, "GET", nil, basicAuthHeader, false,
url, method, nil, basicAuthHeader, false,
)
require.NoError(t, err)
require.Contains(t, body, successPattern)
@ -573,7 +616,7 @@ func runRESTAuthTest(t *testing.T, hostPort, uiPassword, macaroonPath, restURI,
},
}
body, responseHeader, err = callURL(
url, "GET", nil, macaroonHeader, false,
url, method, nil, macaroonHeader, false,
)
require.NoError(t, err)
require.Contains(t, body, successPattern)

View file

@ -125,6 +125,7 @@ func testModeRemote(net *NetworkHarness, t *harnessTest) {
endpoint.macaroonFn(cfg),
endpoint.restWebURI,
endpoint.successPattern,
endpoint.restPOST,
)
})
}

View file

@ -11,7 +11,7 @@ windows-386 \
windows-amd64 \
windows-arm
LND_RELEASE_TAGS = litd autopilotrpc signrpc walletrpc chainrpc invoicesrpc watchtowerrpc
LND_RELEASE_TAGS = litd autopilotrpc signrpc walletrpc chainrpc invoicesrpc watchtowerrpc neutrinorpc peersrpc
# By default we will build all systems. But with the 'sys' tag, a specific
# system can be specified. This is useful to release for a subset of

View file

@ -1,10 +1,31 @@
package terminal
import (
"net"
"strings"
"sync"
faraday "github.com/lightninglabs/faraday/frdrpcserver/perms"
loop "github.com/lightninglabs/loop/loopd/perms"
pool "github.com/lightninglabs/pool/perms"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"github.com/lightningnetwork/lnd/lnrpc/devrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
"github.com/lightningnetwork/lnd/lnrpc/peersrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/sweep"
"gopkg.in/macaroon-bakery.v2/bakery"
)
@ -39,6 +60,26 @@ var (
"/lnrpc.State/SubscribeState": {},
"/lnrpc.State/GetState": {},
}
// lndSubServerNameToTag is a map from the name of an LND subserver to
// the name of the LND tag that corresponds to the subserver. This map
// only contains the subserver-to-tag pairs for the pairs where the
// names differ.
lndSubServerNameToTag = map[string]string{
"WalletKitRPC": "walletrpc",
"DevRPC": "dev",
"NeutrinoKitRPC": "neutrinorpc",
"VersionRPC": "verrpc",
"WatchtowerClientRPC": "wtclientrpc",
}
// lndAutoCompiledSubServers is a map of the LND subservers that are
// automatically compiled with it and therefore don't need a build tag.
lndAutoCompiledSubServers = map[string]bool{
"VersionRPC": true,
"RouterRPC": true,
"WatchtowerClientRPC": true,
}
)
// subServerName is a name used to identify a particular Lit sub-server.
@ -54,6 +95,12 @@ const (
// PermissionsManager manages the permission lists that Lit requires.
type PermissionsManager 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.
// It contains all the permissions that will not change throughout the
// lifetime of the manager. It maps sub-server name to uri to permission
@ -61,8 +108,13 @@ type PermissionsManager struct {
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
// are available for use. This map will start out not including any of
// lnd's sub-server permissions. Only when the LND build tags are
// obtained and OnLNDBuildTags is called will this map include the
// available LND sub-server permissions. This map must only be accessed
// once the permsMu mutex is held.
perms map[string][]bakery.Op
permsMu sync.RWMutex
}
// NewPermissionsManager constructs a new PermissionsManager instance and
@ -78,6 +130,32 @@ func NewPermissionsManager() (*PermissionsManager, error) {
permissions[lndPerms][k] = v
}
// Collect all LND sub-server permissions along with the name of the
// sub-server that each permission is associated with.
lndSubServerPerms := make(map[string]map[string][]bakery.Op)
ss := lnrpc.RegisteredSubServers()
for _, subServer := range ss {
_, perms, err := subServer.NewGrpcHandler().CreateSubServer(
&mockConfig{},
)
if err != nil {
return nil, err
}
name := subServer.SubServerName
lndSubServerPerms[name] = make(map[string][]bakery.Op)
for key, value := range perms {
lndSubServerPerms[name][key] = value
// If this sub-server is one that we know is
// automatically compiled in LND then we add it to our
// map of active permissions.
if lndAutoCompiledSubServers[name] {
permissions[lndPerms][key] = value
}
}
}
allPerms := make(map[string][]bakery.Op)
for _, perms := range permissions {
for k, v := range perms {
@ -86,15 +164,48 @@ func NewPermissionsManager() (*PermissionsManager, error) {
}
return &PermissionsManager{
fixedPerms: permissions,
perms: allPerms,
lndSubServerPerms: lndSubServerPerms,
fixedPerms: permissions,
perms: allPerms,
}, nil
}
// OnLNDBuildTags should be called once a list of LND build tags has been
// 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) {
pm.permsMu.Lock()
defer pm.permsMu.Unlock()
tagLookup := make(map[string]bool)
for _, t := range lndBuildTags {
tagLookup[strings.ToLower(t)] = true
}
for subServerName, perms := range pm.lndSubServerPerms {
name := subServerName
if tagName, ok := lndSubServerNameToTag[name]; ok {
name = tagName
}
if !tagLookup[strings.ToLower(name)] {
continue
}
for key, value := range perms {
pm.perms[key] = value
}
}
}
// 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) {
pm.permsMu.RLock()
defer pm.permsMu.RUnlock()
ops, ok := pm.perms[uri]
return ops, ok
}
@ -103,6 +214,9 @@ func (pm *PermissionsManager) URIPermissions(uri string) ([]bakery.Op, bool) {
// 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 {
pm.permsMu.RLock()
defer pm.permsMu.RUnlock()
// De-dup the permissions and optionally apply the read-only filter.
dedupMap := make(map[string]map[string]bool)
for _, methodPerms := range pm.perms {
@ -163,8 +277,16 @@ 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 {
var lndSubServerCall bool
for _, subserverPermissions := range pm.lndSubServerPerms {
_, found := subserverPermissions[uri]
if found {
lndSubServerCall = true
break
}
}
_, lndCall := pm.fixedPerms[lndPerms][uri]
return lndCall
return lndCall || lndSubServerCall
}
// IsLoopURI returns true if the given URI belongs to an RPC of loopd.
@ -190,3 +312,65 @@ func (pm *PermissionsManager) IsLitURI(uri string) bool {
_, ok := pm.fixedPerms[litPerms][uri]
return ok
}
// mockConfig implements lnrpc.SubServerConfigDispatcher. It provides the
// functionality required so that the lnrpc.GrpcHandler.CreateSubServer
// function can be called without panicking.
type mockConfig struct{}
var _ lnrpc.SubServerConfigDispatcher = (*mockConfig)(nil)
// FetchConfig is a mock implementation of lnrpc.SubServerConfigDispatcher. It
// is used as a parameter to lnrpc.GrpcHandler.CreateSubServer and allows the
// function to be called without panicking. This is useful because
// CreateSubServer can be used to extract the permissions required by each
// registered subserver.
//
// TODO(elle): remove this once the sub-server permission lists in LND have been
// exported
func (t *mockConfig) FetchConfig(subServerName string) (interface{}, bool) {
switch subServerName {
case "InvoicesRPC":
return &invoicesrpc.Config{}, true
case "WatchtowerClientRPC":
return &wtclientrpc.Config{
Resolver: func(_, _ string) (*net.TCPAddr, error) {
return nil, nil
},
}, true
case "AutopilotRPC":
return &autopilotrpc.Config{
Manager: &autopilot.Manager{},
}, true
case "ChainRPC":
return &chainrpc.Config{
ChainNotifier: &chainreg.NoChainBackend{},
}, true
case "DevRPC":
return &devrpc.Config{}, true
case "NeutrinoKitRPC":
return &neutrinorpc.Config{}, true
case "PeersRPC":
return &peersrpc.Config{}, true
case "RouterRPC":
return &routerrpc.Config{
Router: &routing.ChannelRouter{},
}, true
case "SignRPC":
return &signrpc.Config{
Signer: &mock.DummySigner{},
}, true
case "WalletKitRPC":
return &walletrpc.Config{
FeeEstimator: &chainreg.NoChainBackend{},
Wallet: &mock.WalletController{},
KeyRing: &mock.SecretKeyRing{},
Sweeper: &sweep.UtxoSweeper{},
Chain: &mock.ChainIO{},
}, true
case "WatchtowerRPC":
return &watchtowerrpc.Config{}, true
default:
return nil, false
}
}

View file

@ -494,6 +494,10 @@ func (g *LightningTerminal) startSubservers() error {
return err
}
// Pass LND's build tags to the permission manager so that it can
// filter the available permissions accordingly.
g.permsMgr.OnLNDBuildTags(g.lndClient.Version.BuildTags)
// In the integrated mode, we received an admin macaroon once lnd was
// ready. We can now bake a "super macaroon" that contains all
// permissions of all daemons that we can use for any internal calls.