Merge pull request #4463 from guggero/macaroon-custom-permissions

Advanced macaroons 1/2: Custom URI permissions
This commit is contained in:
Oliver Gugger 2020-09-04 11:42:42 +02:00 committed by GitHub
commit b4bf4b2906
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1929 additions and 1232 deletions

View file

@ -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
}

View file

@ -303,6 +303,8 @@ func main() {
bakeMacaroonCommand,
listMacaroonIDsCommand,
deleteMacaroonIDCommand,
listPermissionsCommand,
printMacaroonCommand,
trackPaymentCommand,
versionCommand,
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
)

View file

@ -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;
}

View file

@ -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": {

View 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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

View file

@ -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