mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 06:35:07 +01:00
Merge pull request #4463 from guggero/macaroon-custom-permissions
Advanced macaroons 1/2: Custom URI permissions
This commit is contained in:
commit
b4bf4b2906
20 changed files with 1929 additions and 1232 deletions
|
@ -10,9 +10,11 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
|
@ -34,6 +36,19 @@ var bakeMacaroonCommand = cli.Command{
|
|||
colon. Multiple operations can be added as arguments, for example:
|
||||
|
||||
lncli bakemacaroon info:read invoices:write foo:bar
|
||||
|
||||
For even more fine-grained permission control, it is also possible to
|
||||
specify single RPC method URIs that are allowed to be accessed by a
|
||||
macaroon. This can be achieved by specifying "uri:<methodURI>" pairs,
|
||||
for example:
|
||||
|
||||
lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion
|
||||
|
||||
The macaroon created by this command would only be allowed to use the
|
||||
"lncli getinfo" and "lncli version" commands.
|
||||
|
||||
To get a list of all available URIs and permissions, use the
|
||||
"lncli listpermissions" command.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
|
@ -269,3 +284,125 @@ func deleteMacaroonID(ctx *cli.Context) error {
|
|||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var listPermissionsCommand = cli.Command{
|
||||
Name: "listpermissions",
|
||||
Category: "Macaroons",
|
||||
Usage: "Lists all RPC method URIs and the macaroon permissions they " +
|
||||
"require to be invoked.",
|
||||
Action: actionDecorator(listPermissions),
|
||||
}
|
||||
|
||||
func listPermissions(ctx *cli.Context) error {
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
request := &lnrpc.ListPermissionsRequest{}
|
||||
response, err := client.ListPermissions(context.Background(), request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type macaroonContent struct {
|
||||
Version uint16 `json:"version"`
|
||||
Location string `json:"location"`
|
||||
RootKeyID string `json:"root_key_id"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Caveats []string `json:"caveats"`
|
||||
}
|
||||
|
||||
var printMacaroonCommand = cli.Command{
|
||||
Name: "printmacaroon",
|
||||
Category: "Macaroons",
|
||||
Usage: "Print the content of a macaroon in a human readable format.",
|
||||
ArgsUsage: "[macaroon_content_hex]",
|
||||
Description: `
|
||||
Decode a macaroon and show its content in a more human readable format.
|
||||
The macaroon can either be passed as a hex encoded positional parameter
|
||||
or loaded from a file.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "macaroon_file",
|
||||
Usage: "load the macaroon from a file instead of the " +
|
||||
"command line directly",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(printMacaroon),
|
||||
}
|
||||
|
||||
func printMacaroon(ctx *cli.Context) error {
|
||||
// Show command help if no arguments or flags are set.
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "printmacaroon")
|
||||
}
|
||||
|
||||
var (
|
||||
macBytes []byte
|
||||
err error
|
||||
args = ctx.Args()
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("macaroon_file"):
|
||||
macPath := cleanAndExpandPath(ctx.String("macaroon_file"))
|
||||
|
||||
// Load the specified macaroon file.
|
||||
macBytes, err = ioutil.ReadFile(macPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read macaroon path %v: %v",
|
||||
macPath, err)
|
||||
}
|
||||
|
||||
case args.Present():
|
||||
macBytes, err = hex.DecodeString(args.First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to hex decode macaroon: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("macaroon parameter missing")
|
||||
}
|
||||
|
||||
// Decode the macaroon and its protobuf encoded internal identifier.
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return fmt.Errorf("unable to decode macaroon: %v", err)
|
||||
}
|
||||
rawID := mac.Id()
|
||||
if rawID[0] != byte(bakery.LatestVersion) {
|
||||
return fmt.Errorf("invalid macaroon version: %x", rawID)
|
||||
}
|
||||
decodedID := &lnrpc.MacaroonId{}
|
||||
idProto := rawID[1:]
|
||||
err = proto.Unmarshal(idProto, decodedID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode macaroon version: %v", err)
|
||||
}
|
||||
|
||||
// Prepare everything to be printed in a more human readable format.
|
||||
content := &macaroonContent{
|
||||
Version: uint16(mac.Version()),
|
||||
Location: mac.Location(),
|
||||
RootKeyID: string(decodedID.StorageId),
|
||||
Permissions: nil,
|
||||
Caveats: nil,
|
||||
}
|
||||
|
||||
for _, caveat := range mac.Caveats() {
|
||||
content.Caveats = append(content.Caveats, string(caveat.Id))
|
||||
}
|
||||
for _, op := range decodedID.Ops {
|
||||
permission := fmt.Sprintf("%s:%s", op.Entity, op.Actions[0])
|
||||
content.Permissions = append(content.Permissions, permission)
|
||||
}
|
||||
|
||||
printJSON(content)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -303,6 +303,8 @@ func main() {
|
|||
bakeMacaroonCommand,
|
||||
listMacaroonIDsCommand,
|
||||
deleteMacaroonIDCommand,
|
||||
listPermissionsCommand,
|
||||
printMacaroonCommand,
|
||||
trackPaymentCommand,
|
||||
versionCommand,
|
||||
}
|
||||
|
|
|
@ -137,6 +137,8 @@ http:
|
|||
get: "/v1/macaroon/ids"
|
||||
- selector: lnrpc.Lightning.DeleteMacaroonID
|
||||
delete: "/v1/macaroon/{root_key_id}"
|
||||
- selector: lnrpc.Lightning.ListPermissions
|
||||
get: "/v1/macaroon/permissions"
|
||||
|
||||
# walletunlocker.proto
|
||||
- selector: lnrpc.WalletUnlocker.GenSeed
|
||||
|
|
1754
lnrpc/rpc.pb.go
1754
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load diff
|
@ -1985,6 +1985,24 @@ func local_request_Lightning_DeleteMacaroonID_0(ctx context.Context, marshaler r
|
|||
|
||||
}
|
||||
|
||||
func request_Lightning_ListPermissions_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ListPermissionsRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := client.ListPermissions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Lightning_ListPermissions_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ListPermissionsRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := server.ListPermissions(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterLightningHandlerServer registers the http handlers for service Lightning to "mux".
|
||||
// UnaryRPC :call LightningServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
|
@ -2986,6 +3004,26 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_ListPermissions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Lightning_ListPermissions_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_ListPermissions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -4127,6 +4165,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_ListPermissions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Lightning_ListPermissions_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_ListPermissions_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -4240,6 +4298,8 @@ var (
|
|||
pattern_Lightning_ListMacaroonIDs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "macaroon", "ids"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_Lightning_DeleteMacaroonID_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "macaroon", "root_key_id"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_Lightning_ListPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "macaroon", "permissions"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -4352,4 +4412,6 @@ var (
|
|||
forward_Lightning_ListMacaroonIDs_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_DeleteMacaroonID_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_ListPermissions_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
|
|
@ -506,6 +506,13 @@ service Lightning {
|
|||
*/
|
||||
rpc DeleteMacaroonID (DeleteMacaroonIDRequest)
|
||||
returns (DeleteMacaroonIDResponse);
|
||||
|
||||
/* lncli: `listpermissions`
|
||||
ListPermissions lists all RPC method URIs and their required macaroon
|
||||
permissions to access them.
|
||||
*/
|
||||
rpc ListPermissions (ListPermissionsRequest)
|
||||
returns (ListPermissionsResponse);
|
||||
}
|
||||
|
||||
message Utxo {
|
||||
|
@ -3375,6 +3382,21 @@ message DeleteMacaroonIDResponse {
|
|||
bool deleted = 1;
|
||||
}
|
||||
|
||||
message MacaroonPermissionList {
|
||||
// A list of macaroon permissions.
|
||||
repeated MacaroonPermission permissions = 1;
|
||||
}
|
||||
|
||||
message ListPermissionsRequest {
|
||||
}
|
||||
message ListPermissionsResponse {
|
||||
/*
|
||||
A map between all RPC method URIs and their required macaroon permissions to
|
||||
access them.
|
||||
*/
|
||||
map<string, MacaroonPermissionList> method_permissions = 1;
|
||||
}
|
||||
|
||||
message Failure {
|
||||
enum FailureCode {
|
||||
/*
|
||||
|
@ -3537,3 +3559,14 @@ message ChannelUpdate {
|
|||
*/
|
||||
bytes extra_opaque_data = 12;
|
||||
}
|
||||
|
||||
message MacaroonId {
|
||||
bytes nonce = 1;
|
||||
bytes storageId = 2;
|
||||
repeated Op ops = 3;
|
||||
}
|
||||
|
||||
message Op {
|
||||
string entity = 1;
|
||||
repeated string actions = 2;
|
||||
}
|
||||
|
|
|
@ -1456,6 +1456,29 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/v1/macaroon/permissions": {
|
||||
"get": {
|
||||
"summary": "lncli: `listpermissions`\nListPermissions lists all RPC method URIs and their required macaroon\npermissions to access them.",
|
||||
"operationId": "ListPermissions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lnrpcListPermissionsResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/runtimeError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Lightning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/macaroon/{root_key_id}": {
|
||||
"delete": {
|
||||
"summary": "lncli: `deletemacaroonid`\nDeleteMacaroonID deletes the specified macaroon ID and invalidates all\nmacaroons derived from that ID.",
|
||||
|
@ -4220,6 +4243,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lnrpcListPermissionsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method_permissions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/lnrpcMacaroonPermissionList"
|
||||
},
|
||||
"description": "A map between all RPC method URIs and their required macaroon permissions to\naccess them."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcListUnspentResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -4260,6 +4295,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lnrpcMacaroonPermissionList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/lnrpcMacaroonPermission"
|
||||
},
|
||||
"description": "A list of macaroon permissions."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcMultiChanBackup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
516
lntest/itest/lnd_macaroons_test.go
Normal file
516
lntest/itest/lnd_macaroons_test.go
Normal file
|
@ -0,0 +1,516 @@
|
|||
// +build rpctest
|
||||
|
||||
package itest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
// testMacaroonAuthentication makes sure that if macaroon authentication is
|
||||
// enabled on the gRPC interface, no requests with missing or invalid
|
||||
// macaroons are allowed. Further, the specific access rights (read/write,
|
||||
// entity based) and first-party caveats are tested as well.
|
||||
func testMacaroonAuthentication(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
var (
|
||||
infoReq = &lnrpc.GetInfoRequest{}
|
||||
newAddrReq = &lnrpc.NewAddressRequest{
|
||||
Type: AddrTypeWitnessPubkeyHash,
|
||||
}
|
||||
testNode = net.Alice
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(ctxt context.Context, t *testing.T)
|
||||
}{{
|
||||
// First test: Make sure we get an error if we use no macaroons
|
||||
// but try to connect to a node that has macaroon authentication
|
||||
// enabled.
|
||||
name: "no macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
conn, err := testNode.ConnectRPC(false)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = conn.Close() }()
|
||||
client := lnrpc.NewLightningClient(conn)
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "expected 1 macaroon")
|
||||
},
|
||||
}, {
|
||||
// Second test: Ensure that an invalid macaroon also triggers an
|
||||
// error.
|
||||
name: "invalid macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
invalidMac, _ := macaroon.New(
|
||||
[]byte("dummy_root_key"), []byte("0"), "itest",
|
||||
macaroon.LatestVersion,
|
||||
)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, invalidMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err := client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "cannot get macaroon")
|
||||
},
|
||||
}, {
|
||||
// Third test: Try to access a write method with read-only
|
||||
// macaroon.
|
||||
name: "read only macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.ReadMacPath(), defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, readonlyMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err = client.NewAddress(ctxt, newAddrReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}, {
|
||||
// Fourth test: Check first-party caveat with timeout that
|
||||
// expired 30 seconds ago.
|
||||
name: "expired macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.ReadMacPath(), defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
timeoutMac, err := macaroons.AddConstraints(
|
||||
readonlyMac, macaroons.TimeoutConstraint(-30),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, timeoutMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "macaroon has expired")
|
||||
},
|
||||
}, {
|
||||
// Fifth test: Check first-party caveat with invalid IP address.
|
||||
name: "invalid IP macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.ReadMacPath(), defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
invalidIpAddrMac, err := macaroons.AddConstraints(
|
||||
readonlyMac, macaroons.IPLockConstraint(
|
||||
"1.1.1.1",
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, invalidIpAddrMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "different IP address")
|
||||
},
|
||||
}, {
|
||||
// Sixth test: Make sure that if we do everything correct and
|
||||
// send the admin macaroon with first-party caveats that we can
|
||||
// satisfy, we get a correct answer.
|
||||
name: "correct macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.AdminMacPath(), defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
adminMac, err = macaroons.AddConstraints(
|
||||
adminMac, macaroons.TimeoutConstraint(30),
|
||||
macaroons.IPLockConstraint("127.0.0.1"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(t, testNode, adminMac)
|
||||
defer cleanup()
|
||||
res, err := client.NewAddress(ctxt, newAddrReq)
|
||||
require.NoError(t, err, "get new address")
|
||||
assert.Contains(t, res.Address, "bcrt1")
|
||||
},
|
||||
}, {
|
||||
// Seventh test: Bake a macaroon that can only access exactly
|
||||
// two RPCs and make sure it works as expected.
|
||||
name: "custom URI permissions",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
entity := macaroons.PermissionEntityCustomURI
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: entity,
|
||||
Action: "/lnrpc.Lightning/GetInfo",
|
||||
}, {
|
||||
Entity: entity,
|
||||
Action: "/lnrpc.Lightning/List" +
|
||||
"Permissions",
|
||||
}},
|
||||
}
|
||||
bakeRes, err := testNode.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a connection that uses the custom macaroon.
|
||||
customMacBytes, err := hex.DecodeString(
|
||||
bakeRes.Macaroon,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
customMac := &macaroon.Macaroon{}
|
||||
err = customMac.UnmarshalBinary(customMacBytes)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, customMac,
|
||||
)
|
||||
defer cleanup()
|
||||
|
||||
// Call GetInfo which should succeed.
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call ListPermissions which should also succeed.
|
||||
permReq := &lnrpc.ListPermissionsRequest{}
|
||||
permRes, err := client.ListPermissions(ctxt, permReq)
|
||||
require.NoError(t, err)
|
||||
require.Greater(
|
||||
t, len(permRes.MethodPermissions), 10,
|
||||
"permissions",
|
||||
)
|
||||
|
||||
// Try NewAddress which should be denied.
|
||||
_, err = client.NewAddress(ctxt, newAddrReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctxt, cancel := context.WithTimeout(
|
||||
context.Background(), defaultTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
tc.run(ctxt, t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testBakeMacaroon checks that when creating macaroons, the permissions param
|
||||
// in the request must be set correctly, and the baked macaroon has the intended
|
||||
// permissions.
|
||||
func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
var testNode = net.Alice
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient)
|
||||
}{{
|
||||
// First test: when the permission list is empty in the request,
|
||||
// an error should be returned.
|
||||
name: "no permission list",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(
|
||||
t, err.Error(), "permission list cannot be "+
|
||||
"empty",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
// Second test: when the action in the permission list is not
|
||||
// valid, an error should be returned.
|
||||
name: "invalid permission list",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "invalid123",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(
|
||||
t, err.Error(), "invalid permission action",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
// Third test: when the entity in the permission list is not
|
||||
// valid, an error should be returned.
|
||||
name: "invalid permission entity",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "invalid123",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(
|
||||
t, err.Error(), "invalid permission entity",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
// Fourth test: check that when no root key ID is specified, the
|
||||
// default root keyID is used.
|
||||
name: "default root key ID",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.RootKeyIds[0], uint64(0))
|
||||
},
|
||||
}, {
|
||||
// Fifth test: create a macaroon use a non-default root key ID.
|
||||
name: "custom root key ID",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
rootKeyID := uint64(4200)
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the ListMacaroonIDs should give a list of two IDs,
|
||||
// the default ID 0, and the newly created ID. The
|
||||
// returned response is sorted to guarantee the order so
|
||||
// that we can compare them one by one.
|
||||
sort.Slice(resp.RootKeyIds, func(i, j int) bool {
|
||||
return resp.RootKeyIds[i] < resp.RootKeyIds[j]
|
||||
})
|
||||
require.Equal(t, resp.RootKeyIds[0], uint64(0))
|
||||
require.Equal(t, resp.RootKeyIds[1], rootKeyID)
|
||||
},
|
||||
}, {
|
||||
// Sixth test: check the baked macaroon has the intended
|
||||
// permissions. It should succeed in reading, and fail to write
|
||||
// a macaroon.
|
||||
name: "custom macaroon permissions",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
rootKeyID := uint64(4200)
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
bakeResp, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
newMac, err := readMacaroonFromHex(bakeResp.Macaroon)
|
||||
require.NoError(t, err)
|
||||
cleanup, readOnlyClient := macaroonClient(
|
||||
t, testNode, newMac,
|
||||
)
|
||||
defer cleanup()
|
||||
|
||||
// BakeMacaroon requires a write permission, so this
|
||||
// call should return an error.
|
||||
_, err = readOnlyClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
|
||||
// ListMacaroon requires a read permission, so this call
|
||||
// should succeed.
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
_, err = readOnlyClient.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Current macaroon can only work on entity macaroon, so
|
||||
// a GetInfo request will fail.
|
||||
infoReq := &lnrpc.GetInfoRequest{}
|
||||
_, err = readOnlyClient.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctxt, cancel := context.WithTimeout(
|
||||
context.Background(), defaultTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.AdminMacPath(), defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(t, testNode, adminMac)
|
||||
defer cleanup()
|
||||
|
||||
tc.run(ctxt, t, client)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testDeleteMacaroonID checks that when deleting a macaroon ID, it removes the
|
||||
// specified ID and invalidates all macaroons derived from the key with that ID.
|
||||
// Also, it checks deleting the reserved marcaroon ID, DefaultRootKeyID or is
|
||||
// forbidden.
|
||||
func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
var (
|
||||
ctxb = context.Background()
|
||||
testNode = net.Alice
|
||||
)
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Use admin macaroon to create a connection.
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.AdminMacPath(), defaultTimeout,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
cleanup, client := macaroonClient(t.t, testNode, adminMac)
|
||||
defer cleanup()
|
||||
|
||||
// Record the number of macaroon IDs before creation.
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err := client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t.t, err)
|
||||
numMacIDs := len(listResp.RootKeyIds)
|
||||
|
||||
// Create macaroons for testing.
|
||||
rootKeyIDs := []uint64{1, 2, 3}
|
||||
macList := make([]string, 0, len(rootKeyIDs))
|
||||
for _, id := range rootKeyIDs {
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: id,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
resp, err := client.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t.t, err)
|
||||
macList = append(macList, resp.Macaroon)
|
||||
}
|
||||
|
||||
// Check that the creation is successful.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err = client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// The number of macaroon IDs should be increased by len(rootKeyIDs).
|
||||
require.Equal(t.t, numMacIDs+len(rootKeyIDs), len(listResp.RootKeyIds))
|
||||
|
||||
// First test: check deleting the DefaultRootKeyID returns an error.
|
||||
defaultID, _ := strconv.ParseUint(
|
||||
string(macaroons.DefaultRootKeyID), 10, 64,
|
||||
)
|
||||
req := &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: defaultID,
|
||||
}
|
||||
_, err = client.DeleteMacaroonID(ctxt, req)
|
||||
require.Error(t.t, err)
|
||||
require.Contains(
|
||||
t.t, err.Error(), macaroons.ErrDeletionForbidden.Error(),
|
||||
)
|
||||
|
||||
// Second test: check deleting the customized ID returns success.
|
||||
req = &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: rootKeyIDs[0],
|
||||
}
|
||||
resp, err := client.DeleteMacaroonID(ctxt, req)
|
||||
require.NoError(t.t, err)
|
||||
require.True(t.t, resp.Deleted)
|
||||
|
||||
// Check that the deletion is successful.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err = client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// The number of macaroon IDs should be decreased by 1.
|
||||
require.Equal(t.t, numMacIDs+len(rootKeyIDs)-1, len(listResp.RootKeyIds))
|
||||
|
||||
// Check that the deleted macaroon can no longer access macaroon:read.
|
||||
deletedMac, err := readMacaroonFromHex(macList[0])
|
||||
require.NoError(t.t, err)
|
||||
cleanup, client = macaroonClient(t.t, testNode, deletedMac)
|
||||
defer cleanup()
|
||||
|
||||
// Because the macaroon is deleted, it will be treated as an invalid one.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
_, err = client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.Error(t.t, err)
|
||||
require.Contains(t.t, err.Error(), "cannot get macaroon")
|
||||
}
|
||||
|
||||
// readMacaroonFromHex loads a macaroon from a hex string.
|
||||
func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) {
|
||||
macBytes, err := hex.DecodeString(macHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
func macaroonClient(t *testing.T, testNode *lntest.HarnessNode,
|
||||
mac *macaroon.Macaroon) (func(), lnrpc.LightningClient) {
|
||||
|
||||
conn, err := testNode.ConnectRPCWithMacaroon(mac)
|
||||
require.NoError(t, err, "connect to alice")
|
||||
|
||||
cleanup := func() {
|
||||
err := conn.Close()
|
||||
require.NoError(t, err, "close")
|
||||
}
|
||||
return cleanup, lnrpc.NewLightningClient(conn)
|
||||
}
|
|
@ -208,7 +208,7 @@
|
|||
<time> [ERR] RPCS: WS: error closing upgraded conn: write tcp4 <ip>-><ip>: write: connection reset by peer
|
||||
<time> [ERR] NTFN: chain notifier shutting down
|
||||
<time> [ERR] NTFN: Failed getting UTXO: get utxo request cancelled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission action. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission entity. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: permission list cannot be empty. specify at least one action/entity pair. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission action. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission entity. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: permission list cannot be empty. specify at least one action/entity pair. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/DeleteMacaroonID]: the specified ID cannot be deleted
|
||||
|
|
|
@ -1,476 +0,0 @@
|
|||
// +build rpctest
|
||||
|
||||
package itest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
// errContains is a helper function that returns true if a string is contained
|
||||
// in the message of an error.
|
||||
func errContains(err error, str string) bool {
|
||||
return strings.Contains(err.Error(), str)
|
||||
}
|
||||
|
||||
// testMacaroonAuthentication makes sure that if macaroon authentication is
|
||||
// enabled on the gRPC interface, no requests with missing or invalid
|
||||
// macaroons are allowed. Further, the specific access rights (read/write,
|
||||
// entity based) and first-party caveats are tested as well.
|
||||
func testMacaroonAuthentication(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
var (
|
||||
ctxb = context.Background()
|
||||
infoReq = &lnrpc.GetInfoRequest{}
|
||||
newAddrReq = &lnrpc.NewAddressRequest{
|
||||
Type: AddrTypeWitnessPubkeyHash,
|
||||
}
|
||||
testNode = net.Alice
|
||||
)
|
||||
|
||||
// First test: Make sure we get an error if we use no macaroons but try
|
||||
// to connect to a node that has macaroon authentication enabled.
|
||||
conn, err := testNode.ConnectRPC(false)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
noMacConnection := lnrpc.NewLightningClient(conn)
|
||||
_, err = noMacConnection.GetInfo(ctxt, infoReq)
|
||||
if err == nil || !errContains(err, "expected 1 macaroon") {
|
||||
t.Fatalf("expected to get an error when connecting without " +
|
||||
"macaroons")
|
||||
}
|
||||
|
||||
// Second test: Ensure that an invalid macaroon also triggers an error.
|
||||
invalidMac, _ := macaroon.New(
|
||||
[]byte("dummy_root_key"), []byte("0"), "itest",
|
||||
macaroon.LatestVersion,
|
||||
)
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(invalidMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
invalidMacConnection := lnrpc.NewLightningClient(conn)
|
||||
_, err = invalidMacConnection.GetInfo(ctxt, infoReq)
|
||||
if err == nil || !errContains(err, "cannot get macaroon") {
|
||||
t.Fatalf("expected to get an error when connecting with an " +
|
||||
"invalid macaroon")
|
||||
}
|
||||
|
||||
// Third test: Try to access a write method with read-only macaroon.
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.ReadMacPath(), defaultTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read readonly.macaroon from node: %v", err)
|
||||
}
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(readonlyMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
readonlyMacConnection := lnrpc.NewLightningClient(conn)
|
||||
_, err = readonlyMacConnection.NewAddress(ctxt, newAddrReq)
|
||||
if err == nil || !errContains(err, "permission denied") {
|
||||
t.Fatalf("expected to get an error when connecting to " +
|
||||
"write method with read-only macaroon")
|
||||
}
|
||||
|
||||
// Fourth test: Check first-party caveat with timeout that expired
|
||||
// 30 seconds ago.
|
||||
timeoutMac, err := macaroons.AddConstraints(
|
||||
readonlyMac, macaroons.TimeoutConstraint(-30),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add constraint to readonly macaroon: %v",
|
||||
err)
|
||||
}
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(timeoutMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
timeoutMacConnection := lnrpc.NewLightningClient(conn)
|
||||
_, err = timeoutMacConnection.GetInfo(ctxt, infoReq)
|
||||
if err == nil || !errContains(err, "macaroon has expired") {
|
||||
t.Fatalf("expected to get an error when connecting with an " +
|
||||
"invalid macaroon")
|
||||
}
|
||||
|
||||
// Fifth test: Check first-party caveat with invalid IP address.
|
||||
invalidIpAddrMac, err := macaroons.AddConstraints(
|
||||
readonlyMac, macaroons.IPLockConstraint("1.1.1.1"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add constraint to readonly macaroon: %v",
|
||||
err)
|
||||
}
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(invalidIpAddrMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
invalidIpAddrMacConnection := lnrpc.NewLightningClient(conn)
|
||||
_, err = invalidIpAddrMacConnection.GetInfo(ctxt, infoReq)
|
||||
if err == nil || !errContains(err, "different IP address") {
|
||||
t.Fatalf("expected to get an error when connecting with an " +
|
||||
"invalid macaroon")
|
||||
}
|
||||
|
||||
// Sixth test: Make sure that if we do everything correct and send
|
||||
// the admin macaroon with first-party caveats that we can satisfy,
|
||||
// we get a correct answer.
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.AdminMacPath(), defaultTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read admin.macaroon from node: %v", err)
|
||||
}
|
||||
adminMac, err = macaroons.AddConstraints(
|
||||
adminMac, macaroons.TimeoutConstraint(30),
|
||||
macaroons.IPLockConstraint("127.0.0.1"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add constraints to admin macaroon: %v", err)
|
||||
}
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(adminMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
adminMacConnection := lnrpc.NewLightningClient(conn)
|
||||
res, err := adminMacConnection.NewAddress(ctxt, newAddrReq)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get new address with valid macaroon: %v",
|
||||
err)
|
||||
}
|
||||
if !strings.HasPrefix(res.Address, "bcrt1") {
|
||||
t.Fatalf("returned address was not a regtest address")
|
||||
}
|
||||
}
|
||||
|
||||
// testBakeMacaroon checks that when creating macaroons, the permissions param
|
||||
// in the request must be set correctly, and the baked macaroon has the intended
|
||||
// permissions.
|
||||
func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
var (
|
||||
ctxb = context.Background()
|
||||
req = &lnrpc.BakeMacaroonRequest{}
|
||||
testNode = net.Alice
|
||||
)
|
||||
|
||||
// First test: when the permission list is empty in the request, an error
|
||||
// should be returned.
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.AdminMacPath(), defaultTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read admin.macaroon from node: %v", err)
|
||||
}
|
||||
conn, err := testNode.ConnectRPCWithMacaroon(adminMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
adminMacConnection := lnrpc.NewLightningClient(conn)
|
||||
_, err = adminMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err == nil || !errContains(err, "permission list cannot be empty") {
|
||||
t.Fatalf("expected an error, got %v", err)
|
||||
}
|
||||
|
||||
// Second test: when the action in the permission list is not valid,
|
||||
// an error should be returned.
|
||||
req = &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{
|
||||
{
|
||||
Entity: "macaroon",
|
||||
Action: "invalid123",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = adminMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err == nil || !errContains(err, "invalid permission action") {
|
||||
t.Fatalf("expected an error, got %v", err)
|
||||
}
|
||||
|
||||
// Third test: when the entity in the permission list is not valid,
|
||||
// an error should be returned.
|
||||
req = &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{
|
||||
{
|
||||
Entity: "invalid123",
|
||||
Action: "read",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = adminMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err == nil || !errContains(err, "invalid permission entity") {
|
||||
t.Fatalf("expected an error, got %v", err)
|
||||
}
|
||||
|
||||
// Fourth test: check that when no root key ID is specified, the default
|
||||
// root key ID is used.
|
||||
req = &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{
|
||||
{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = adminMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err := adminMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if resp.RootKeyIds[0] != 0 {
|
||||
t.Fatalf("expected ID to be 0, found: %v", resp.RootKeyIds)
|
||||
}
|
||||
|
||||
// Fifth test: create a macaroon use a non-default root key ID.
|
||||
rootKeyID := uint64(4200)
|
||||
req = &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{
|
||||
{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
},
|
||||
},
|
||||
}
|
||||
bakeResp, err := adminMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err = adminMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// the ListMacaroonIDs should give a list of two IDs, the default ID 0, and
|
||||
// the newly created ID. The returned response is sorted to guarantee the
|
||||
// order so that we can compare them one by one.
|
||||
sort.Slice(resp.RootKeyIds, func(i, j int) bool {
|
||||
return resp.RootKeyIds[i] < resp.RootKeyIds[j]
|
||||
})
|
||||
if resp.RootKeyIds[0] != 0 {
|
||||
t.Fatalf("expected ID to be %v, found: %v", 0, resp.RootKeyIds[0])
|
||||
}
|
||||
if resp.RootKeyIds[1] != rootKeyID {
|
||||
t.Fatalf(
|
||||
"expected ID to be %v, found: %v",
|
||||
rootKeyID, resp.RootKeyIds[1],
|
||||
)
|
||||
}
|
||||
|
||||
// Sixth test: check the baked macaroon has the intended permissions. It
|
||||
// should succeed in reading, and fail to write a macaroon.
|
||||
newMac, err := readMacaroonFromHex(bakeResp.Macaroon)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load macaroon from bytes, error: %v", err)
|
||||
}
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(newMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
newMacConnection := lnrpc.NewLightningClient(conn)
|
||||
|
||||
// BakeMacaroon requires a write permission, so this call should return an
|
||||
// error.
|
||||
_, err = newMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err == nil || !errContains(err, "permission denied") {
|
||||
t.Fatalf("expected an error, got %v", err)
|
||||
}
|
||||
|
||||
// ListMacaroon requires a read permission, so this call should succeed.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err = newMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Current macaroon can only work on entity macaroon, so a GetInfo request
|
||||
// will fail.
|
||||
infoReq := &lnrpc.GetInfoRequest{}
|
||||
_, err = newMacConnection.GetInfo(ctxt, infoReq)
|
||||
if err == nil || !errContains(err, "permission denied") {
|
||||
t.Fatalf("expected error not returned, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// testDeleteMacaroonID checks that when deleting a macaroon ID, it removes the
|
||||
// specified ID and invalidates all macaroons derived from the key with that ID.
|
||||
// Also, it checks deleting the reserved marcaroon ID, DefaultRootKeyID or is
|
||||
// forbidden.
|
||||
func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
var (
|
||||
ctxb = context.Background()
|
||||
testNode = net.Alice
|
||||
)
|
||||
|
||||
// Use admin macaroon to create a connection.
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.AdminMacPath(), defaultTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read admin.macaroon from node: %v", err)
|
||||
}
|
||||
conn, err := testNode.ConnectRPCWithMacaroon(adminMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
adminMacConnection := lnrpc.NewLightningClient(conn)
|
||||
|
||||
// Record the number of macaroon IDs before creation.
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err := adminMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
numMacIDs := len(listResp.RootKeyIds)
|
||||
|
||||
// Create macaroons for testing.
|
||||
rootKeyIDs := []uint64{1, 2, 3}
|
||||
macList := []string{}
|
||||
for _, id := range rootKeyIDs {
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: id,
|
||||
Permissions: []*lnrpc.MacaroonPermission{
|
||||
{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := adminMacConnection.BakeMacaroon(ctxt, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
macList = append(macList, resp.Macaroon)
|
||||
}
|
||||
|
||||
// Check that the creation is successful.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err = adminMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// The number of macaroon IDs should be increased by len(rootKeyIDs)
|
||||
if len(listResp.RootKeyIds) != numMacIDs+len(rootKeyIDs) {
|
||||
t.Fatalf(
|
||||
"expected to have %v ids, found: %v",
|
||||
numMacIDs+len(rootKeyIDs), len(listResp.RootKeyIds),
|
||||
)
|
||||
}
|
||||
|
||||
// First test: check deleting the DefaultRootKeyID returns an error.
|
||||
defaultID, _ := strconv.ParseUint(
|
||||
string(macaroons.DefaultRootKeyID), 10, 64,
|
||||
)
|
||||
req := &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: defaultID,
|
||||
}
|
||||
_, err = adminMacConnection.DeleteMacaroonID(ctxt, req)
|
||||
if err == nil || !errContains(err, macaroons.ErrDeletionForbidden.Error()) {
|
||||
t.Fatalf("expected an error, got %v", err)
|
||||
}
|
||||
|
||||
// Second test: check deleting the customized ID returns success.
|
||||
req = &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: rootKeyIDs[0],
|
||||
}
|
||||
resp, err := adminMacConnection.DeleteMacaroonID(ctxt, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if resp.Deleted != true {
|
||||
t.Fatalf("expected the ID to be deleted")
|
||||
}
|
||||
|
||||
// Check that the deletion is successful.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err = adminMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
// The number of macaroon IDs should be decreased by 1.
|
||||
if len(listResp.RootKeyIds) != numMacIDs+len(rootKeyIDs)-1 {
|
||||
t.Fatalf(
|
||||
"expected to have %v ids, found: %v",
|
||||
numMacIDs+len(rootKeyIDs)-1, len(listResp.RootKeyIds),
|
||||
)
|
||||
}
|
||||
|
||||
// Check that the deleted macaroon can no longer access macaroon:read.
|
||||
deletedMac, err := readMacaroonFromHex(macList[0])
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load macaroon from bytes, error: %v", err)
|
||||
}
|
||||
conn, err = testNode.ConnectRPCWithMacaroon(deletedMac)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect to alice: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
deletedMacConnection := lnrpc.NewLightningClient(conn)
|
||||
|
||||
// Because the macaroon is deleted, it will be treated as an invalid one.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
_, err = deletedMacConnection.ListMacaroonIDs(ctxt, listReq)
|
||||
if err == nil || !errContains(err, "cannot get macaroon") {
|
||||
t.Fatalf("expected error not returned, got %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// readMacaroonFromHex loads a macaroon from a hex string.
|
||||
func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) {
|
||||
macBytes, err := hex.DecodeString(macHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mac, nil
|
||||
}
|
|
@ -100,8 +100,18 @@ key `0` would be created with the following command:
|
|||
|
||||
`lncli bakemacaroon peers:read peers:write`
|
||||
|
||||
A full and up-to-date list of available entity/action pairs can be found by
|
||||
looking at the `rpcserver.go` in the root folder of the project.
|
||||
For even more fine-grained permission control, it is also possible to specify
|
||||
single RPC method URIs that are allowed to be accessed by a macaroon. This can
|
||||
be achieved by passing `uri:<methodURI>` pairs to `bakemacaroon`, for example:
|
||||
|
||||
`lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion`
|
||||
|
||||
The macaroon created by this call would only be allowed to call the `GetInfo` and
|
||||
`GetVersion` methods instead of all methods that have similar permissions (like
|
||||
`info:read` for example).
|
||||
|
||||
A full list of available entity/action pairs and RPC method URIs can be queried
|
||||
by using the `lncli listpermissions` command.
|
||||
|
||||
### Upgrading from v0.8.0-beta or earlier
|
||||
|
||||
|
|
|
@ -27,6 +27,15 @@ var (
|
|||
// ErrDeletionForbidden is used when attempting to delete the
|
||||
// DefaultRootKeyID or the encryptedKeyID.
|
||||
ErrDeletionForbidden = fmt.Errorf("the specified ID cannot be deleted")
|
||||
|
||||
// PermissionEntityCustomURI is a special entity name for a permission
|
||||
// that does not describe an entity:action pair but instead specifies a
|
||||
// specific URI that needs to be granted access to. This can be used for
|
||||
// more fine-grained permissions where a macaroon only grants access to
|
||||
// certain methods instead of a whole list of methods that define the
|
||||
// same entity:action pairs. For example: uri:/lnrpc.Lightning/GetInfo
|
||||
// only gives access to the GetInfo call.
|
||||
PermissionEntityCustomURI = "uri"
|
||||
)
|
||||
|
||||
// Service encapsulates bakery.Bakery and adds a Close() method that zeroes the
|
||||
|
@ -118,12 +127,15 @@ func (svc *Service) UnaryServerInterceptor(
|
|||
info *grpc.UnaryServerInfo,
|
||||
handler grpc.UnaryHandler) (interface{}, error) {
|
||||
|
||||
if _, ok := permissionMap[info.FullMethod]; !ok {
|
||||
uriPermissions, ok := permissionMap[info.FullMethod]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: unknown permissions "+
|
||||
"required for method", info.FullMethod)
|
||||
}
|
||||
|
||||
err := svc.ValidateMacaroon(ctx, permissionMap[info.FullMethod])
|
||||
err := svc.ValidateMacaroon(
|
||||
ctx, uriPermissions, info.FullMethod,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -140,13 +152,14 @@ func (svc *Service) StreamServerInterceptor(
|
|||
return func(srv interface{}, ss grpc.ServerStream,
|
||||
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
|
||||
if _, ok := permissionMap[info.FullMethod]; !ok {
|
||||
uriPermissions, ok := permissionMap[info.FullMethod]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s: unknown permissions required "+
|
||||
"for method", info.FullMethod)
|
||||
}
|
||||
|
||||
err := svc.ValidateMacaroon(
|
||||
ss.Context(), permissionMap[info.FullMethod],
|
||||
ss.Context(), uriPermissions, info.FullMethod,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -161,7 +174,7 @@ func (svc *Service) StreamServerInterceptor(
|
|||
// expect a macaroon to be encoded as request metadata using the key
|
||||
// "macaroon".
|
||||
func (svc *Service) ValidateMacaroon(ctx context.Context,
|
||||
requiredPermissions []bakery.Op) error {
|
||||
requiredPermissions []bakery.Op, fullMethod string) error {
|
||||
|
||||
// Get macaroon bytes from context and unmarshal into macaroon.
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
@ -190,6 +203,20 @@ func (svc *Service) ValidateMacaroon(ctx context.Context,
|
|||
// the expiration time and IP address and return the result.
|
||||
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
|
||||
_, err = authChecker.Allow(ctx, requiredPermissions...)
|
||||
|
||||
// If the macaroon contains broad permissions and checks out, we're
|
||||
// done.
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// To also allow the special permission of "uri:<FullMethod>" to be a
|
||||
// valid permission, we need to check it manually in case there is no
|
||||
// broader scope permission defined.
|
||||
_, err = authChecker.Allow(ctx, bakery.Op{
|
||||
Entity: PermissionEntityCustomURI,
|
||||
Action: fullMethod,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@ var (
|
|||
Entity: "testEntity",
|
||||
Action: "read",
|
||||
}
|
||||
testOperationURI = bakery.Op{
|
||||
Entity: macaroons.PermissionEntityCustomURI,
|
||||
Action: "SomeMethod",
|
||||
}
|
||||
defaultPw = []byte("hello")
|
||||
)
|
||||
|
||||
|
@ -125,6 +129,7 @@ func TestValidateMacaroon(t *testing.T) {
|
|||
// Then, create a new macaroon that we can serialize.
|
||||
macaroon, err := service.NewMacaroon(
|
||||
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
|
||||
testOperationURI,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating macaroon from service: %v", err)
|
||||
|
@ -142,7 +147,18 @@ func TestValidateMacaroon(t *testing.T) {
|
|||
mockContext := metadata.NewIncomingContext(context.Background(), md)
|
||||
|
||||
// Finally, validate the macaroon against the required permissions.
|
||||
err = service.ValidateMacaroon(mockContext, []bakery.Op{testOperation})
|
||||
err = service.ValidateMacaroon(
|
||||
mockContext, []bakery.Op{testOperation}, "FooMethod",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Error validating the macaroon: %v", err)
|
||||
}
|
||||
|
||||
// If the macaroon has the method specific URI permission, the list of
|
||||
// required entity/action pairs is irrelevant.
|
||||
err = service.ValidateMacaroon(
|
||||
mockContext, []bakery.Op{{Entity: "irrelevant"}}, "SomeMethod",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Error validating the macaroon: %v", err)
|
||||
}
|
||||
|
|
57
rpcserver.go
57
rpcserver.go
|
@ -193,6 +193,7 @@ var (
|
|||
validEntities = []string{
|
||||
"onchain", "offchain", "address", "message",
|
||||
"peers", "info", "invoices", "signer", "macaroon",
|
||||
macaroons.PermissionEntityCustomURI,
|
||||
}
|
||||
|
||||
// If the --no-macaroons flag is used to start lnd, the macaroon service
|
||||
|
@ -452,6 +453,10 @@ func mainRPCServerPermissions() map[string][]bakery.Op {
|
|||
Entity: "macaroon",
|
||||
Action: "write",
|
||||
}},
|
||||
"/lnrpc.Lightning/ListPermissions": {{
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/SubscribePeerEvents": {{
|
||||
Entity: "peers",
|
||||
Action: "read",
|
||||
|
@ -525,6 +530,10 @@ type rpcServer struct {
|
|||
|
||||
// selfNode is our own pubkey.
|
||||
selfNode route.Vertex
|
||||
|
||||
// allPermissions is a map of all registered gRPC URIs (including
|
||||
// internal and external subservers) to the permissions they require.
|
||||
allPermissions map[string][]bakery.Op
|
||||
}
|
||||
|
||||
// A compile time check to ensure that rpcServer fully implements the
|
||||
|
@ -732,6 +741,7 @@ func newRPCServer(cfg *Config, s *server, macService *macaroons.Service,
|
|||
quit: make(chan struct{}, 1),
|
||||
macService: macService,
|
||||
selfNode: selfNode.PubKeyBytes,
|
||||
allPermissions: permissions,
|
||||
}
|
||||
lnrpc.RegisterLightningServer(grpcServer, rootRPCServer)
|
||||
|
||||
|
@ -6452,15 +6462,27 @@ func (r *rpcServer) BakeMacaroon(ctx context.Context,
|
|||
// the bakery.
|
||||
requestedPermissions := make([]bakery.Op, len(req.Permissions))
|
||||
for idx, op := range req.Permissions {
|
||||
if !stringInSlice(op.Action, validActions) {
|
||||
return nil, fmt.Errorf("invalid permission action. %s",
|
||||
helpMsg)
|
||||
}
|
||||
if !stringInSlice(op.Entity, validEntities) {
|
||||
return nil, fmt.Errorf("invalid permission entity. %s",
|
||||
helpMsg)
|
||||
}
|
||||
|
||||
// Either we have the special entity "uri" which specifies a
|
||||
// full gRPC URI or we have one of the pre-defined actions.
|
||||
if op.Entity == macaroons.PermissionEntityCustomURI {
|
||||
_, ok := r.allPermissions[op.Action]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid permission " +
|
||||
"action, must be an existing URI in " +
|
||||
"the format /package.Service/" +
|
||||
"MethodName")
|
||||
}
|
||||
} else if !stringInSlice(op.Action, validActions) {
|
||||
return nil, fmt.Errorf("invalid permission action. %s",
|
||||
helpMsg)
|
||||
|
||||
}
|
||||
|
||||
requestedPermissions[idx] = bakery.Op{
|
||||
Entity: op.Entity,
|
||||
Action: op.Action,
|
||||
|
@ -6554,6 +6576,33 @@ func (r *rpcServer) DeleteMacaroonID(ctx context.Context,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// ListPermissions lists all RPC method URIs and their required macaroon
|
||||
// permissions to access them.
|
||||
func (r *rpcServer) ListPermissions(_ context.Context,
|
||||
_ *lnrpc.ListPermissionsRequest) (*lnrpc.ListPermissionsResponse,
|
||||
error) {
|
||||
|
||||
rpcsLog.Debugf("[listpermissions]")
|
||||
|
||||
permissionMap := make(map[string]*lnrpc.MacaroonPermissionList)
|
||||
for uri, perms := range r.allPermissions {
|
||||
rpcPerms := make([]*lnrpc.MacaroonPermission, len(perms))
|
||||
for idx, perm := range perms {
|
||||
rpcPerms[idx] = &lnrpc.MacaroonPermission{
|
||||
Entity: perm.Entity,
|
||||
Action: perm.Action,
|
||||
}
|
||||
}
|
||||
permissionMap[uri] = &lnrpc.MacaroonPermissionList{
|
||||
Permissions: rpcPerms,
|
||||
}
|
||||
}
|
||||
|
||||
return &lnrpc.ListPermissionsResponse{
|
||||
MethodPermissions: permissionMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FundingStateStep is an advanced funding related call that allows the caller
|
||||
// to either execute some preparatory steps for a funding workflow, or manually
|
||||
// progress a funding workflow. The primary way a funding flow is identified is
|
||||
|
|
Loading…
Add table
Reference in a new issue