diff --git a/lntest/itest/lnd_macaroons_test.go b/lntest/itest/lnd_macaroons_test.go index d808af55d..08e19803c 100644 --- a/lntest/itest/lnd_macaroons_test.go +++ b/lntest/itest/lnd_macaroons_test.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "sort" "strconv" - "strings" "testing" "github.com/lightningnetwork/lnd/lnrpc" @@ -18,246 +17,332 @@ import ( "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 ) - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - // 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) - require.NoError(t.t, err) - defer conn.Close() - client := lnrpc.NewLightningClient(conn) - _, err = client.GetInfo(ctxt, infoReq) - if err == nil || !errContains(err, "expected 1 macaroon") { - t.Fatalf("expected to get an error when connecting without " + - "macaroons") + 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") + }, + }} + + 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) + }) } - - // Second test: Ensure that an invalid macaroon also triggers an error. - invalidMac, _ := macaroon.New( - []byte("dummy_root_key"), []byte("0"), "itest", - macaroon.LatestVersion, - ) - cleanup, client := macaroonClient(t.t, testNode, invalidMac) - defer cleanup() - _, err = client.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, - ) - require.NoError(t.t, err) - cleanup, client = macaroonClient(t.t, testNode, readonlyMac) - defer cleanup() - _, err = client.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), - ) - require.NoError(t.t, err) - cleanup, client = macaroonClient(t.t, testNode, timeoutMac) - defer cleanup() - _, err = client.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"), - ) - require.NoError(t.t, err) - cleanup, client = macaroonClient(t.t, testNode, invalidIpAddrMac) - defer cleanup() - _, err = client.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, - ) - require.NoError(t.t, err) - adminMac, err = macaroons.AddConstraints( - adminMac, macaroons.TimeoutConstraint(30), - macaroons.IPLockConstraint("127.0.0.1"), - ) - require.NoError(t.t, err) - cleanup, client = macaroonClient(t.t, testNode, adminMac) - defer cleanup() - res, err := client.NewAddress(ctxt, newAddrReq) - require.NoError(t.t, err, "get new address") - assert.Contains(t.t, res.Address, "bcrt1") } // 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 - ) - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() + var 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, - ) - require.NoError(t.t, err) - cleanup, client := macaroonClient(t.t, testNode, adminMac) - defer cleanup() - _, err = client.BakeMacaroon(ctxt, req) - if err == nil || !errContains(err, "permission list cannot be empty") { - t.Fatalf("expected an error, got %v", err) - } + 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) { - // 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", - }, + req := &lnrpc.BakeMacaroonRequest{} + _, err := adminClient.BakeMacaroon(ctxt, req) + require.Error(t, err) + assert.Contains( + t, err.Error(), "permission list cannot be "+ + "empty", + ) }, - } - _, err = client.BakeMacaroon(ctxt, req) - if err == nil || !errContains(err, "invalid permission action") { - 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. + name: "invalid permission list", + run: func(ctxt context.Context, t *testing.T, + adminClient lnrpc.LightningClient) { - // 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", - }, + 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", + ) }, - } - _, err = client.BakeMacaroon(ctxt, req) - if err == nil || !errContains(err, "invalid permission entity") { - 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. + name: "invalid permission entity", + run: func(ctxt context.Context, t *testing.T, + adminClient lnrpc.LightningClient) { - // 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", - }, + 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", + ) }, - } - _, err = client.BakeMacaroon(ctxt, req) - require.NoError(t.t, err) + }, { + // 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) { - listReq := &lnrpc.ListMacaroonIDsRequest{} - resp, err := client.ListMacaroonIDs(ctxt, listReq) - require.NoError(t.t, err) - if resp.RootKeyIds[0] != 0 { - t.Fatalf("expected ID to be 0, found: %v", resp.RootKeyIds) - } + req := &lnrpc.BakeMacaroonRequest{ + Permissions: []*lnrpc.MacaroonPermission{{ + Entity: "macaroon", + Action: "read", + }}, + } + _, err := adminClient.BakeMacaroon(ctxt, req) + require.NoError(t, err) - // 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", - }, + listReq := &lnrpc.ListMacaroonIDsRequest{} + resp, err := adminClient.ListMacaroonIDs(ctxt, listReq) + require.NoError(t, err) + require.Equal(t, resp.RootKeyIds[0], uint64(0)) }, - } - bakeResp, err := client.BakeMacaroon(ctxt, req) - require.NoError(t.t, err) + }, { + // 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) { - listReq = &lnrpc.ListMacaroonIDsRequest{} - resp, err = client.ListMacaroonIDs(ctxt, listReq) - require.NoError(t.t, err) + rootKeyID := uint64(4200) + req := &lnrpc.BakeMacaroonRequest{ + RootKeyId: rootKeyID, + Permissions: []*lnrpc.MacaroonPermission{{ + Entity: "macaroon", + Action: "read", + }}, + } + _, err := adminClient.BakeMacaroon(ctxt, req) + 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] - }) - 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], - ) - } + listReq := &lnrpc.ListMacaroonIDsRequest{} + resp, err := adminClient.ListMacaroonIDs(ctxt, listReq) + require.NoError(t, err) - // 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) - require.NoError(t.t, err) - cleanup, client = macaroonClient(t.t, testNode, newMac) - defer cleanup() + // 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) { - // BakeMacaroon requires a write permission, so this call should return an - // error. - _, err = client.BakeMacaroon(ctxt, req) - if err == nil || !errContains(err, "permission denied") { - t.Fatalf("expected an error, got %v", err) - } + 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) - // ListMacaroon requires a read permission, so this call should succeed. - listReq = &lnrpc.ListMacaroonIDsRequest{} - resp, err = client.ListMacaroonIDs(ctxt, listReq) - require.NoError(t.t, err) + newMac, err := readMacaroonFromHex(bakeResp.Macaroon) + require.NoError(t, err) + cleanup, readOnlyClient := macaroonClient( + t, testNode, newMac, + ) + defer cleanup() - // Current macaroon can only work on entity macaroon, so a GetInfo request - // will fail. - infoReq := &lnrpc.GetInfoRequest{} - _, err = client.GetInfo(ctxt, infoReq) - if err == nil || !errContains(err, "permission denied") { - t.Fatalf("expected error not returned, got %v", err) + // 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) + }) } }