macaroons: add tests for service and constraints

This commit is contained in:
Oliver Gugger 2018-04-27 00:06:00 +03:00 committed by Oliver Gugger
parent 04f4dbe639
commit 8e4e2bd889
2 changed files with 218 additions and 85 deletions

@ -1,109 +1,108 @@
package macaroons package macaroons_test
import ( import (
"errors"
"fmt"
"testing" "testing"
"github.com/lightningnetwork/lnd/macaroons"
"gopkg.in/macaroon.v2"
"time" "time"
"strings"
"gopkg.in/macaroon-bakery.v1/bakery"
"gopkg.in/macaroon-bakery.v1/bakery/checkers"
macaroon "gopkg.in/macaroon.v1"
) )
type macError struct { var (
message string testRootKey = []byte("dummyRootKey")
} testId = []byte("dummyId")
testLocation = "lnd"
testVersion = macaroon.LatestVersion
expectedTimeCaveatSubstring = "time-before " + string(time.Now().Year())
)
func (err macError) Error() string { func createDummyMacaroon(t *testing.T) *macaroon.Macaroon {
return err.message dummyMacaroon, err := macaroon.New(testRootKey, testId,
} testLocation, testVersion)
func testConstraint(constraint Constraint, ok checkers.Checker,
failFn func() checkers.Checker) error {
macParams := bakery.NewServiceParams{}
svc, err := bakery.NewService(macParams)
if err != nil { if err != nil {
return errors.New("Failed to create a new service") t.Fatalf("Error creating initial macaroon: %v", err)
} }
mac, err := svc.NewMacaroon("", nil, nil) return dummyMacaroon
if err != nil {
return errors.New("Failed to create a new macaroon")
}
mac, err = AddConstraints(mac, constraint)
if err != nil {
return errors.New("Failed to add macaroon constraint")
}
okChecker := checkers.New(ok)
if err := svc.Check(macaroon.Slice{mac}, okChecker); err != nil {
msg := "Correct checker failed: %v"
return macError{fmt.Sprintf(msg, ok)}
}
fail := failFn()
failChecker := checkers.New(fail)
if err := svc.Check(macaroon.Slice{mac}, failChecker); err == nil {
msg := "Incorrect checker succeeded: %v"
return macError{fmt.Sprintf(msg, fail)}
}
return nil
} }
func TestAllowConstraint(t *testing.T) { // TestAddConstraints tests that constraints can be added to an existing
if err := testConstraint( // macaroon and therefore tighten its restrictions.
AllowConstraint("op1", "op2", "op4"), func TestAddConstraints(t *testing.T) {
AllowChecker("op1"), // We need a dummy macaroon to start with. Create one without
func() checkers.Checker { // a bakery, because we mock everything anyway.
return AllowChecker("op3") initialMac := createDummyMacaroon(t)
},
); err != nil { // Now add a constraint and make sure we have a cloned macaroon
t.Fatalf(err.Error()) // with the constraint applied instead of a mutated initial one.
newMac, err := macaroons.AddConstraints(initialMac,
macaroons.TimeoutConstraint(1))
if err != nil {
t.Fatalf("Error adding constraint: %v", err)
}
if &newMac == &initialMac {
t.Fatalf("Initial macaroon has been changed, something " +
"went wrong!")
}
// Finally, test that the constraint has been added.
if len(initialMac.Caveats()) == len(newMac.Caveats()) {
t.Fatalf("No caveat has been added to the macaroon when " +
"constraint was applied")
} }
} }
// TestTimeoutConstraint tests that a caveat for the lifetime of
// a macaroon is created.
func TestTimeoutConstraint(t *testing.T) { func TestTimeoutConstraint(t *testing.T) {
if err := testConstraint( // Get a configured version of the constraint function.
TimeoutConstraint(1), constraintFunc := macaroons.TimeoutConstraint(3)
TimeoutChecker(),
func() checkers.Checker { // Now we need a dummy macaroon that we can apply the constraint
time.Sleep(time.Second) // function to.
return TimeoutChecker() testMacaroon := createDummyMacaroon(t)
}, err := constraintFunc(testMacaroon)
); err != nil { if err != nil {
t.Fatalf(err.Error()) t.Fatalf("Error applying timeout constraint: %v", err)
}
// Finally, check that the created caveat has an
// acceptable value
if strings.HasPrefix(string(testMacaroon.Caveats()[0].Id),
expectedTimeCaveatSubstring) {
t.Fatalf("Added caveat '%s' does not meet the expectations!",
testMacaroon.Caveats()[0].Id)
} }
} }
func TestIPLockConstraint(t *testing.T) { // TestTimeoutConstraint tests that a caveat for the lifetime of
if err := testConstraint( // a macaroon is created.
IPLockConstraint("127.0.0.1"), func TestIpLockConstraint(t *testing.T) {
IPLockChecker("127.0.0.1"), // Get a configured version of the constraint function.
func() checkers.Checker { constraintFunc := macaroons.IPLockConstraint("127.0.0.1")
return IPLockChecker("0.0.0.0")
}, // Now we need a dummy macaroon that we can apply the constraint
); err != nil { // function to.
t.Fatalf(err.Error()) testMacaroon := createDummyMacaroon(t)
} err := constraintFunc(testMacaroon)
} if err != nil {
t.Fatalf("Error applying timeout constraint: %v", err)
func TestIPLockEmptyIP(t *testing.T) { }
if err := testConstraint(
IPLockConstraint(""), // Finally, check that the created caveat has an
IPLockChecker("127.0.0.1"), // acceptable value
func() checkers.Checker { if string(testMacaroon.Caveats()[0].Id) != "ipaddr 127.0.0.1" {
return IPLockChecker("0.0.0.0") t.Fatalf("Added caveat '%s' does not meet the expectations!",
}, testMacaroon.Caveats()[0].Id)
); err != nil {
if _, ok := err.(macError); !ok {
t.Fatalf("IPLock with an empty IP should always pass")
}
} }
} }
// TestIPLockBadIP tests that an IP constraint cannot be added if the
// provided string is not a valid IP address.
func TestIPLockBadIP(t *testing.T) { func TestIPLockBadIP(t *testing.T) {
if err := IPLockConstraint("127.0.0/800"); err == nil { constraintFunc := macaroons.IPLockConstraint("127.0.0/800");
t.Fatalf("IPLockConstraint with bad IP should fail") testMacaroon := createDummyMacaroon(t)
err := constraintFunc(testMacaroon)
if err == nil {
t.Fatalf("IPLockConstraint with bad IP should fail.")
} }
} }

134
macaroons/service_test.go Normal file

@ -0,0 +1,134 @@
package macaroons_test
import (
"testing"
"path"
"os"
"context"
"io/ioutil"
"encoding/hex"
"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/macaroons"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
"gopkg.in/macaroon-bakery.v2/bakery"
"google.golang.org/grpc/metadata"
)
var (
testOperation = bakery.Op{
Entity: "testEntity",
Action: "read",
}
defaultPw = []byte("hello")
)
// setupTestRootKeyStorage creates a dummy root key storage by
// creating a temporary macaroons.db and initializing it with the
// default password of 'hello'. Only the path to the temporary
// DB file is returned, because the service will open the file
// and read the store on its own.
func setupTestRootKeyStorage(t *testing.T) string {
tempDir, err := ioutil.TempDir("", "macaroonstore-")
if err != nil {
t.Fatalf("Error creating temp dir: %v", err)
}
db, err := bolt.Open(path.Join(tempDir, "macaroons.db"), 0600,
bolt.DefaultOptions)
if err != nil {
t.Fatalf("Error opening store DB: %v", err)
}
store, err := macaroons.NewRootKeyStorage(db)
if err != nil {
db.Close()
t.Fatalf("Error creating root key store: %v", err)
}
defer store.Close()
err = store.CreateUnlock(&defaultPw)
return tempDir
}
// TestNewService tests the creation of the macaroon service.
func TestNewService(t *testing.T) {
// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
tempDir := setupTestRootKeyStorage(t)
defer os.RemoveAll(tempDir)
// Second, create the new service instance, unlock it and pass in a
// checker that we expect it to add to the bakery.
service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker)
defer service.Close()
if err != nil {
t.Fatalf("Error creating new service: %v", err)
}
err = service.CreateUnlock(&defaultPw)
if err != nil {
t.Fatalf("Error unlocking root key storage: %v", err)
}
// Third, check if the created service can bake macaroons.
macaroon, err := service.Oven.NewMacaroon(nil, bakery.LatestVersion,
nil, testOperation)
if err != nil {
t.Fatalf("Error creating macaroon from service: %v", err)
}
if macaroon.Namespace().String() != "std:" {
t.Fatalf("The created macaroon has an invalid namespace: %s",
macaroon.Namespace().String())
}
// Finally, check if the service has been initialized correctly and
// the checker has been added.
var checkerFound = false
checker := service.Checker.FirstPartyCaveatChecker.(*checkers.Checker)
for _, info := range checker.Info() {
if info.Name == "ipaddr" &&
info.Prefix == "" &&
info.Namespace == "std" {
checkerFound = true
}
}
if !checkerFound {
t.Fatalf("Checker '%s' not found in service.", "ipaddr")
}
}
// TestValidateMacaroon tests the validation of a macaroon that is in an
// incoming context.
func TestValidateMacaroon(t *testing.T) {
// First, initialize the service and unlock it
tempDir := setupTestRootKeyStorage(t)
defer os.RemoveAll(tempDir)
service, err := macaroons.NewService(tempDir, macaroons.IPLockChecker)
defer service.Close()
if err != nil {
t.Fatalf("Error creating new service: %v", err)
}
err = service.CreateUnlock(&defaultPw)
if err != nil {
t.Fatalf("Error unlocking root key storage: %v", err)
}
// Then, create a new macaroon that we can serialize.
macaroon, err := service.Oven.NewMacaroon(nil, bakery.LatestVersion,
nil, testOperation)
if err != nil {
t.Fatalf("Error creating macaroon from service: %v", err)
}
macaroonBinary, err := macaroon.M().MarshalBinary()
if err != nil {
t.Fatalf("Error serializing macaroon: %v", err)
}
// Because the macaroons are always passed in a context, we need to
// mock one that has just the serialized macaroon as a value.
md := metadata.New(map[string]string{"macaroon": hex.EncodeToString(macaroonBinary)})
mockContext := metadata.NewIncomingContext(context.Background(), md)
// Finally, validate the macaroon against the required permissions.
err = service.ValidateMacaroon(mockContext, []bakery.Op{testOperation})
if err != nil {
t.Fatalf("Error validating the macaroon: %v", err)
}
}