itest: parallelize test steps

This commit is contained in:
Oliver Gugger 2020-09-04 09:22:44 +02:00
parent 9862ee7cd6
commit decd2d975c
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -7,7 +7,6 @@ import (
"encoding/hex" "encoding/hex"
"sort" "sort"
"strconv" "strconv"
"strings"
"testing" "testing"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
@ -18,246 +17,332 @@ import (
"gopkg.in/macaroon.v2" "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 // testMacaroonAuthentication makes sure that if macaroon authentication is
// enabled on the gRPC interface, no requests with missing or invalid // enabled on the gRPC interface, no requests with missing or invalid
// macaroons are allowed. Further, the specific access rights (read/write, // macaroons are allowed. Further, the specific access rights (read/write,
// entity based) and first-party caveats are tested as well. // entity based) and first-party caveats are tested as well.
func testMacaroonAuthentication(net *lntest.NetworkHarness, t *harnessTest) { func testMacaroonAuthentication(net *lntest.NetworkHarness, t *harnessTest) {
var ( var (
ctxb = context.Background()
infoReq = &lnrpc.GetInfoRequest{} infoReq = &lnrpc.GetInfoRequest{}
newAddrReq = &lnrpc.NewAddressRequest{ newAddrReq = &lnrpc.NewAddressRequest{
Type: AddrTypeWitnessPubkeyHash, Type: AddrTypeWitnessPubkeyHash,
} }
testNode = net.Alice 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 testCases := []struct {
// to connect to a node that has macaroon authentication enabled. name string
conn, err := testNode.ConnectRPC(false) run func(ctxt context.Context, t *testing.T)
require.NoError(t.t, err) }{{
defer conn.Close() // First test: Make sure we get an error if we use no macaroons
client := lnrpc.NewLightningClient(conn) // but try to connect to a node that has macaroon authentication
_, err = client.GetInfo(ctxt, infoReq) // enabled.
if err == nil || !errContains(err, "expected 1 macaroon") { name: "no macaroon",
t.Fatalf("expected to get an error when connecting without " + run: func(ctxt context.Context, t *testing.T) {
"macaroons") 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 // testBakeMacaroon checks that when creating macaroons, the permissions param
// in the request must be set correctly, and the baked macaroon has the intended // in the request must be set correctly, and the baked macaroon has the intended
// permissions. // permissions.
func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) { func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) {
var ( var testNode = net.Alice
ctxb = context.Background()
req = &lnrpc.BakeMacaroonRequest{}
testNode = net.Alice
)
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// First test: when the permission list is empty in the request, an error testCases := []struct {
// should be returned. name string
adminMac, err := testNode.ReadMacaroon( run func(ctxt context.Context, t *testing.T,
testNode.AdminMacPath(), defaultTimeout, adminClient lnrpc.LightningClient)
) }{{
require.NoError(t.t, err) // First test: when the permission list is empty in the request,
cleanup, client := macaroonClient(t.t, testNode, adminMac) // an error should be returned.
defer cleanup() name: "no permission list",
_, err = client.BakeMacaroon(ctxt, req) run: func(ctxt context.Context, t *testing.T,
if err == nil || !errContains(err, "permission list cannot be empty") { adminClient lnrpc.LightningClient) {
t.Fatalf("expected an error, got %v", err)
}
// Second test: when the action in the permission list is not valid, req := &lnrpc.BakeMacaroonRequest{}
// an error should be returned. _, err := adminClient.BakeMacaroon(ctxt, req)
req = &lnrpc.BakeMacaroonRequest{ require.Error(t, err)
Permissions: []*lnrpc.MacaroonPermission{ assert.Contains(
{ t, err.Error(), "permission list cannot be "+
Entity: "macaroon", "empty",
Action: "invalid123", )
},
}, },
} }, {
_, err = client.BakeMacaroon(ctxt, req) // Second test: when the action in the permission list is not
if err == nil || !errContains(err, "invalid permission action") { // valid, an error should be returned.
t.Fatalf("expected an error, got %v", err) 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, req := &lnrpc.BakeMacaroonRequest{
// an error should be returned. Permissions: []*lnrpc.MacaroonPermission{{
req = &lnrpc.BakeMacaroonRequest{ Entity: "macaroon",
Permissions: []*lnrpc.MacaroonPermission{ Action: "invalid123",
{ }},
Entity: "invalid123", }
Action: "read", _, err := adminClient.BakeMacaroon(ctxt, req)
}, require.Error(t, err)
assert.Contains(
t, err.Error(), "invalid permission action",
)
}, },
} }, {
_, err = client.BakeMacaroon(ctxt, req) // Third test: when the entity in the permission list is not
if err == nil || !errContains(err, "invalid permission entity") { // valid, an error should be returned.
t.Fatalf("expected an error, got %v", err) 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 req := &lnrpc.BakeMacaroonRequest{
// root key ID is used. Permissions: []*lnrpc.MacaroonPermission{{
req = &lnrpc.BakeMacaroonRequest{ Entity: "invalid123",
Permissions: []*lnrpc.MacaroonPermission{ Action: "read",
{ }},
Entity: "macaroon", }
Action: "read", _, err := adminClient.BakeMacaroon(ctxt, req)
}, require.Error(t, err)
assert.Contains(
t, err.Error(), "invalid permission entity",
)
}, },
} }, {
_, err = client.BakeMacaroon(ctxt, req) // Fourth test: check that when no root key ID is specified, the
require.NoError(t.t, err) // default root keyID is used.
name: "default root key ID",
run: func(ctxt context.Context, t *testing.T,
adminClient lnrpc.LightningClient) {
listReq := &lnrpc.ListMacaroonIDsRequest{} req := &lnrpc.BakeMacaroonRequest{
resp, err := client.ListMacaroonIDs(ctxt, listReq) Permissions: []*lnrpc.MacaroonPermission{{
require.NoError(t.t, err) Entity: "macaroon",
if resp.RootKeyIds[0] != 0 { Action: "read",
t.Fatalf("expected ID to be 0, found: %v", resp.RootKeyIds) }},
} }
_, err := adminClient.BakeMacaroon(ctxt, req)
require.NoError(t, err)
// Fifth test: create a macaroon use a non-default root key ID. listReq := &lnrpc.ListMacaroonIDsRequest{}
rootKeyID := uint64(4200) resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
req = &lnrpc.BakeMacaroonRequest{ require.NoError(t, err)
RootKeyId: rootKeyID, require.Equal(t, resp.RootKeyIds[0], uint64(0))
Permissions: []*lnrpc.MacaroonPermission{
{
Entity: "macaroon",
Action: "read",
},
}, },
} }, {
bakeResp, err := client.BakeMacaroon(ctxt, req) // Fifth test: create a macaroon use a non-default root key ID.
require.NoError(t.t, err) name: "custom root key ID",
run: func(ctxt context.Context, t *testing.T,
adminClient lnrpc.LightningClient) {
listReq = &lnrpc.ListMacaroonIDsRequest{} rootKeyID := uint64(4200)
resp, err = client.ListMacaroonIDs(ctxt, listReq) req := &lnrpc.BakeMacaroonRequest{
require.NoError(t.t, err) 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 listReq := &lnrpc.ListMacaroonIDsRequest{}
// the newly created ID. The returned response is sorted to guarantee the resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
// order so that we can compare them one by one. require.NoError(t, err)
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 // the ListMacaroonIDs should give a list of two IDs,
// should succeed in reading, and fail to write a macaroon. // the default ID 0, and the newly created ID. The
newMac, err := readMacaroonFromHex(bakeResp.Macaroon) // returned response is sorted to guarantee the order so
require.NoError(t.t, err) // that we can compare them one by one.
cleanup, client = macaroonClient(t.t, testNode, newMac) sort.Slice(resp.RootKeyIds, func(i, j int) bool {
defer cleanup() 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 rootKeyID := uint64(4200)
// error. req := &lnrpc.BakeMacaroonRequest{
_, err = client.BakeMacaroon(ctxt, req) RootKeyId: rootKeyID,
if err == nil || !errContains(err, "permission denied") { Permissions: []*lnrpc.MacaroonPermission{{
t.Fatalf("expected an error, got %v", err) Entity: "macaroon",
} Action: "read",
}},
}
bakeResp, err := adminClient.BakeMacaroon(ctxt, req)
require.NoError(t, err)
// ListMacaroon requires a read permission, so this call should succeed. newMac, err := readMacaroonFromHex(bakeResp.Macaroon)
listReq = &lnrpc.ListMacaroonIDsRequest{} require.NoError(t, err)
resp, err = client.ListMacaroonIDs(ctxt, listReq) cleanup, readOnlyClient := macaroonClient(
require.NoError(t.t, err) t, testNode, newMac,
)
defer cleanup()
// Current macaroon can only work on entity macaroon, so a GetInfo request // BakeMacaroon requires a write permission, so this
// will fail. // call should return an error.
infoReq := &lnrpc.GetInfoRequest{} _, err = readOnlyClient.BakeMacaroon(ctxt, req)
_, err = client.GetInfo(ctxt, infoReq) require.Error(t, err)
if err == nil || !errContains(err, "permission denied") { require.Contains(t, err.Error(), "permission denied")
t.Fatalf("expected error not returned, got %v", err)
// 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)
})
} }
} }