Merge pull request #4463 from guggero/macaroon-custom-permissions
Advanced macaroons 1/2: Custom URI permissions
This commit is contained in:
20 files changed
+1929
-1232
No files matched your search
@@ -10,9 +10,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||||
"gopkg.in/macaroon.v2"
|
"gopkg.in/macaroon.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +36,19 @@ var bakeMacaroonCommand = cli.Command{
|
|||||||
colon. Multiple operations can be added as arguments, for example:
|
colon. Multiple operations can be added as arguments, for example:
|
||||||
|
|
||||||
lncli bakemacaroon info:read invoices:write foo:bar
|
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{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
@@ -269,3 +284,125 @@ func deleteMacaroonID(ctx *cli.Context) error {
|
|||||||
printRespJSON(resp)
|
printRespJSON(resp)
|
||||||
return nil
|
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,
|
bakeMacaroonCommand,
|
||||||
listMacaroonIDsCommand,
|
listMacaroonIDsCommand,
|
||||||
deleteMacaroonIDCommand,
|
deleteMacaroonIDCommand,
|
||||||
|
listPermissionsCommand,
|
||||||
|
printMacaroonCommand,
|
||||||
trackPaymentCommand,
|
trackPaymentCommand,
|
||||||
versionCommand,
|
versionCommand,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ http:
|
|||||||
get: "/v1/macaroon/ids"
|
get: "/v1/macaroon/ids"
|
||||||
- selector: lnrpc.Lightning.DeleteMacaroonID
|
- selector: lnrpc.Lightning.DeleteMacaroonID
|
||||||
delete: "/v1/macaroon/{root_key_id}"
|
delete: "/v1/macaroon/{root_key_id}"
|
||||||
|
- selector: lnrpc.Lightning.ListPermissions
|
||||||
|
get: "/v1/macaroon/permissions"
|
||||||
|
|
||||||
# walletunlocker.proto
|
# walletunlocker.proto
|
||||||
- selector: lnrpc.WalletUnlocker.GenSeed
|
- selector: lnrpc.WalletUnlocker.GenSeed
|
||||||
|
|||||||
+1013
-741
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".
|
// RegisterLightningHandlerServer registers the http handlers for service Lightning to "mux".
|
||||||
// UnaryRPC :call LightningServer directly.
|
// UnaryRPC :call LightningServer directly.
|
||||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
// 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
|
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
|
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_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_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 (
|
var (
|
||||||
@@ -4352,4 +4412,6 @@ var (
|
|||||||
forward_Lightning_ListMacaroonIDs_0 = runtime.ForwardResponseMessage
|
forward_Lightning_ListMacaroonIDs_0 = runtime.ForwardResponseMessage
|
||||||
|
|
||||||
forward_Lightning_DeleteMacaroonID_0 = runtime.ForwardResponseMessage
|
forward_Lightning_DeleteMacaroonID_0 = runtime.ForwardResponseMessage
|
||||||
|
|
||||||
|
forward_Lightning_ListPermissions_0 = runtime.ForwardResponseMessage
|
||||||
)
|
)
|
||||||
@@ -506,6 +506,13 @@ service Lightning {
|
|||||||
*/
|
*/
|
||||||
rpc DeleteMacaroonID (DeleteMacaroonIDRequest)
|
rpc DeleteMacaroonID (DeleteMacaroonIDRequest)
|
||||||
returns (DeleteMacaroonIDResponse);
|
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 {
|
message Utxo {
|
||||||
@@ -3375,6 +3382,21 @@ message DeleteMacaroonIDResponse {
|
|||||||
bool deleted = 1;
|
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 {
|
message Failure {
|
||||||
enum FailureCode {
|
enum FailureCode {
|
||||||
/*
|
/*
|
||||||
@@ -3537,3 +3559,14 @@ message ChannelUpdate {
|
|||||||
*/
|
*/
|
||||||
bytes extra_opaque_data = 12;
|
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}": {
|
"/v1/macaroon/{root_key_id}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"summary": "lncli: `deletemacaroonid`\nDeleteMacaroonID deletes the specified macaroon ID and invalidates all\nmacaroons derived from that ID.",
|
"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": {
|
"lnrpcListUnspentResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4260,6 +4295,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lnrpcMacaroonPermissionList": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"permissions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/lnrpcMacaroonPermission"
|
||||||
|
},
|
||||||
|
"description": "A list of macaroon permissions."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"lnrpcMultiChanBackup": {
|
"lnrpcMultiChanBackup": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -208,7 +208,7 @@
|
|||||||
<time> [ERR] RPCS: WS: error closing upgraded conn: write tcp4 <ip>-><ip>: write: connection reset by peer
|
<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: chain notifier shutting down
|
||||||
<time> [ERR] NTFN: Failed getting UTXO: get utxo request cancelled
|
<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 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]
|
<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]
|
<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
|
<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
|
|
||||||
}
|
|
||||||
+12
-2
@@ -100,8 +100,18 @@ key `0` would be created with the following command:
|
|||||||
|
|
||||||
`lncli bakemacaroon peers:read peers:write`
|
`lncli bakemacaroon peers:read peers:write`
|
||||||
|
|
||||||
A full and up-to-date list of available entity/action pairs can be found by
|
For even more fine-grained permission control, it is also possible to specify
|
||||||
looking at the `rpcserver.go` in the root folder of the project.
|
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
|
### Upgrading from v0.8.0-beta or earlier
|
||||||
|
|
||||||
|
|||||||
+32
-5
@@ -27,6 +27,15 @@ var (
|
|||||||
// ErrDeletionForbidden is used when attempting to delete the
|
// ErrDeletionForbidden is used when attempting to delete the
|
||||||
// DefaultRootKeyID or the encryptedKeyID.
|
// DefaultRootKeyID or the encryptedKeyID.
|
||||||
ErrDeletionForbidden = fmt.Errorf("the specified ID cannot be deleted")
|
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
|
// Service encapsulates bakery.Bakery and adds a Close() method that zeroes the
|
||||||
@@ -118,12 +127,15 @@ func (svc *Service) UnaryServerInterceptor(
|
|||||||
info *grpc.UnaryServerInfo,
|
info *grpc.UnaryServerInfo,
|
||||||
handler grpc.UnaryHandler) (interface{}, error) {
|
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 "+
|
return nil, fmt.Errorf("%s: unknown permissions "+
|
||||||
"required for method", info.FullMethod)
|
"required for method", info.FullMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := svc.ValidateMacaroon(ctx, permissionMap[info.FullMethod])
|
err := svc.ValidateMacaroon(
|
||||||
|
ctx, uriPermissions, info.FullMethod,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -140,13 +152,14 @@ func (svc *Service) StreamServerInterceptor(
|
|||||||
return func(srv interface{}, ss grpc.ServerStream,
|
return func(srv interface{}, ss grpc.ServerStream,
|
||||||
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
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 "+
|
return fmt.Errorf("%s: unknown permissions required "+
|
||||||
"for method", info.FullMethod)
|
"for method", info.FullMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := svc.ValidateMacaroon(
|
err := svc.ValidateMacaroon(
|
||||||
ss.Context(), permissionMap[info.FullMethod],
|
ss.Context(), uriPermissions, info.FullMethod,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -161,7 +174,7 @@ func (svc *Service) StreamServerInterceptor(
|
|||||||
// expect a macaroon to be encoded as request metadata using the key
|
// expect a macaroon to be encoded as request metadata using the key
|
||||||
// "macaroon".
|
// "macaroon".
|
||||||
func (svc *Service) ValidateMacaroon(ctx context.Context,
|
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.
|
// Get macaroon bytes from context and unmarshal into macaroon.
|
||||||
md, ok := metadata.FromIncomingContext(ctx)
|
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.
|
// the expiration time and IP address and return the result.
|
||||||
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
|
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
|
||||||
_, err = authChecker.Allow(ctx, requiredPermissions...)
|
_, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ var (
|
|||||||
Entity: "testEntity",
|
Entity: "testEntity",
|
||||||
Action: "read",
|
Action: "read",
|
||||||
}
|
}
|
||||||
|
testOperationURI = bakery.Op{
|
||||||
|
Entity: macaroons.PermissionEntityCustomURI,
|
||||||
|
Action: "SomeMethod",
|
||||||
|
}
|
||||||
defaultPw = []byte("hello")
|
defaultPw = []byte("hello")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,6 +129,7 @@ func TestValidateMacaroon(t *testing.T) {
|
|||||||
// Then, create a new macaroon that we can serialize.
|
// Then, create a new macaroon that we can serialize.
|
||||||
macaroon, err := service.NewMacaroon(
|
macaroon, err := service.NewMacaroon(
|
||||||
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
|
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
|
||||||
|
testOperationURI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error creating macaroon from service: %v", err)
|
t.Fatalf("Error creating macaroon from service: %v", err)
|
||||||
@@ -142,7 +147,18 @@ func TestValidateMacaroon(t *testing.T) {
|
|||||||
mockContext := metadata.NewIncomingContext(context.Background(), md)
|
mockContext := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
|
||||||
// Finally, validate the macaroon against the required permissions.
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("Error validating the macaroon: %v", err)
|
t.Fatalf("Error validating the macaroon: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -193,6 +193,7 @@ var (
|
|||||||
validEntities = []string{
|
validEntities = []string{
|
||||||
"onchain", "offchain", "address", "message",
|
"onchain", "offchain", "address", "message",
|
||||||
"peers", "info", "invoices", "signer", "macaroon",
|
"peers", "info", "invoices", "signer", "macaroon",
|
||||||
|
macaroons.PermissionEntityCustomURI,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the --no-macaroons flag is used to start lnd, the macaroon service
|
// 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",
|
Entity: "macaroon",
|
||||||
Action: "write",
|
Action: "write",
|
||||||
}},
|
}},
|
||||||
|
"/lnrpc.Lightning/ListPermissions": {{
|
||||||
|
Entity: "info",
|
||||||
|
Action: "read",
|
||||||
|
}},
|
||||||
"/lnrpc.Lightning/SubscribePeerEvents": {{
|
"/lnrpc.Lightning/SubscribePeerEvents": {{
|
||||||
Entity: "peers",
|
Entity: "peers",
|
||||||
Action: "read",
|
Action: "read",
|
||||||
@@ -525,6 +530,10 @@ type rpcServer struct {
|
|||||||
|
|
||||||
// selfNode is our own pubkey.
|
// selfNode is our own pubkey.
|
||||||
selfNode route.Vertex
|
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
|
// 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),
|
quit: make(chan struct{}, 1),
|
||||||
macService: macService,
|
macService: macService,
|
||||||
selfNode: selfNode.PubKeyBytes,
|
selfNode: selfNode.PubKeyBytes,
|
||||||
|
allPermissions: permissions,
|
||||||
}
|
}
|
||||||
lnrpc.RegisterLightningServer(grpcServer, rootRPCServer)
|
lnrpc.RegisterLightningServer(grpcServer, rootRPCServer)
|
||||||
|
|
||||||
@@ -6452,15 +6462,27 @@ func (r *rpcServer) BakeMacaroon(ctx context.Context,
|
|||||||
// the bakery.
|
// the bakery.
|
||||||
requestedPermissions := make([]bakery.Op, len(req.Permissions))
|
requestedPermissions := make([]bakery.Op, len(req.Permissions))
|
||||||
for idx, op := range 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) {
|
if !stringInSlice(op.Entity, validEntities) {
|
||||||
return nil, fmt.Errorf("invalid permission entity. %s",
|
return nil, fmt.Errorf("invalid permission entity. %s",
|
||||||
helpMsg)
|
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{
|
requestedPermissions[idx] = bakery.Op{
|
||||||
Entity: op.Entity,
|
Entity: op.Entity,
|
||||||
Action: op.Action,
|
Action: op.Action,
|
||||||
@@ -6554,6 +6576,33 @@ func (r *rpcServer) DeleteMacaroonID(ctx context.Context,
|
|||||||
}, nil
|
}, 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
|
// FundingStateStep is an advanced funding related call that allows the caller
|
||||||
// to either execute some preparatory steps for a funding workflow, or manually
|
// 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
|
// progress a funding workflow. The primary way a funding flow is identified is
|
||||||
|
|||||||
Reference in new issue
Block a user