github.com/decred/dcrlnd@v0.7.6/macaroons/service_test.go (about)

     1  package macaroons_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"testing"
    10  
    11  	"github.com/decred/dcrlnd/kvdb"
    12  	"github.com/decred/dcrlnd/macaroons"
    13  	"github.com/stretchr/testify/require"
    14  	"google.golang.org/grpc/metadata"
    15  	"gopkg.in/macaroon-bakery.v2/bakery"
    16  	"gopkg.in/macaroon-bakery.v2/bakery/checkers"
    17  )
    18  
    19  var (
    20  	testOperation = bakery.Op{
    21  		Entity: "testEntity",
    22  		Action: "read",
    23  	}
    24  	testOperationURI = bakery.Op{
    25  		Entity: macaroons.PermissionEntityCustomURI,
    26  		Action: "SomeMethod",
    27  	}
    28  	defaultPw = []byte("hello")
    29  )
    30  
    31  // setupTestRootKeyStorage creates a dummy root key storage by
    32  // creating a temporary macaroons.db and initializing it with the
    33  // default password of 'hello'. Only the path to the temporary
    34  // DB file is returned, because the service will open the file
    35  // and read the store on its own.
    36  func setupTestRootKeyStorage(t *testing.T) (string, kvdb.Backend) {
    37  	tempDir, err := ioutil.TempDir("", "macaroonstore-")
    38  	if err != nil {
    39  		t.Fatalf("Error creating temp dir: %v", err)
    40  	}
    41  	db, err := kvdb.Create(
    42  		kvdb.BoltBackendName, path.Join(tempDir, "macaroons.db"), true,
    43  		kvdb.DefaultDBTimeout,
    44  	)
    45  	if err != nil {
    46  		t.Fatalf("Error opening store DB: %v", err)
    47  	}
    48  	store, err := macaroons.NewRootKeyStorage(db)
    49  	if err != nil {
    50  		db.Close()
    51  		t.Fatalf("Error creating root key store: %v", err)
    52  	}
    53  	defer store.Close()
    54  	err = store.CreateUnlock(&defaultPw)
    55  	if err != nil {
    56  		t.Fatalf("Error setting an encryption key: %v", err)
    57  	}
    58  	return tempDir, db
    59  }
    60  
    61  // TestNewService tests the creation of the macaroon service.
    62  func TestNewService(t *testing.T) {
    63  	// First, initialize a dummy DB file with a store that the service
    64  	// can read from. Make sure the file is removed in the end.
    65  	tempDir, db := setupTestRootKeyStorage(t)
    66  	defer os.RemoveAll(tempDir)
    67  
    68  	// Second, create the new service instance, unlock it and pass in a
    69  	// checker that we expect it to add to the bakery.
    70  	service, err := macaroons.NewService(
    71  		db, "dcrlnd", false, macaroons.IPLockChecker,
    72  	)
    73  	if err != nil {
    74  		t.Fatalf("Error creating new service: %v", err)
    75  	}
    76  	defer service.Close()
    77  	err = service.CreateUnlock(&defaultPw)
    78  	if err != nil {
    79  		t.Fatalf("Error unlocking root key storage: %v", err)
    80  	}
    81  
    82  	// Third, check if the created service can bake macaroons.
    83  	_, err = service.NewMacaroon(context.TODO(), nil, testOperation)
    84  	if err != macaroons.ErrMissingRootKeyID {
    85  		t.Fatalf("Received %v instead of ErrMissingRootKeyID", err)
    86  	}
    87  
    88  	macaroon, err := service.NewMacaroon(
    89  		context.TODO(), macaroons.DefaultRootKeyID, testOperation,
    90  	)
    91  	if err != nil {
    92  		t.Fatalf("Error creating macaroon from service: %v", err)
    93  	}
    94  	if macaroon.Namespace().String() != "std:" {
    95  		t.Fatalf("The created macaroon has an invalid namespace: %s",
    96  			macaroon.Namespace().String())
    97  	}
    98  
    99  	// Finally, check if the service has been initialized correctly and
   100  	// the checker has been added.
   101  	var checkerFound = false
   102  	checker := service.Checker.FirstPartyCaveatChecker.(*checkers.Checker)
   103  	for _, info := range checker.Info() {
   104  		if info.Name == "ipaddr" &&
   105  			info.Prefix == "" &&
   106  			info.Namespace == "std" {
   107  			checkerFound = true
   108  		}
   109  	}
   110  	if !checkerFound {
   111  		t.Fatalf("Checker '%s' not found in service.", "ipaddr")
   112  	}
   113  }
   114  
   115  // TestValidateMacaroon tests the validation of a macaroon that is in an
   116  // incoming context.
   117  func TestValidateMacaroon(t *testing.T) {
   118  	// First, initialize the service and unlock it.
   119  	tempDir, db := setupTestRootKeyStorage(t)
   120  	defer os.RemoveAll(tempDir)
   121  	service, err := macaroons.NewService(
   122  		db, "dcrlnd", false, macaroons.IPLockChecker,
   123  	)
   124  	if err != nil {
   125  		t.Fatalf("Error creating new service: %v", err)
   126  	}
   127  	defer service.Close()
   128  
   129  	err = service.CreateUnlock(&defaultPw)
   130  	if err != nil {
   131  		t.Fatalf("Error unlocking root key storage: %v", err)
   132  	}
   133  
   134  	// Then, create a new macaroon that we can serialize.
   135  	macaroon, err := service.NewMacaroon(
   136  		context.TODO(), macaroons.DefaultRootKeyID, testOperation,
   137  		testOperationURI,
   138  	)
   139  	if err != nil {
   140  		t.Fatalf("Error creating macaroon from service: %v", err)
   141  	}
   142  	macaroonBinary, err := macaroon.M().MarshalBinary()
   143  	if err != nil {
   144  		t.Fatalf("Error serializing macaroon: %v", err)
   145  	}
   146  
   147  	// Because the macaroons are always passed in a context, we need to
   148  	// mock one that has just the serialized macaroon as a value.
   149  	md := metadata.New(map[string]string{
   150  		"macaroon": hex.EncodeToString(macaroonBinary),
   151  	})
   152  	mockContext := metadata.NewIncomingContext(context.Background(), md)
   153  
   154  	// Finally, validate the macaroon against the required permissions.
   155  	err = service.ValidateMacaroon(
   156  		mockContext, []bakery.Op{testOperation}, "FooMethod",
   157  	)
   158  	if err != nil {
   159  		t.Fatalf("Error validating the macaroon: %v", err)
   160  	}
   161  
   162  	// If the macaroon has the method specific URI permission, the list of
   163  	// required entity/action pairs is irrelevant.
   164  	err = service.ValidateMacaroon(
   165  		mockContext, []bakery.Op{{Entity: "irrelevant"}}, "SomeMethod",
   166  	)
   167  	if err != nil {
   168  		t.Fatalf("Error validating the macaroon: %v", err)
   169  	}
   170  }
   171  
   172  // TestListMacaroonIDs checks that ListMacaroonIDs returns the expected result.
   173  func TestListMacaroonIDs(t *testing.T) {
   174  	// First, initialize a dummy DB file with a store that the service
   175  	// can read from. Make sure the file is removed in the end.
   176  	tempDir, db := setupTestRootKeyStorage(t)
   177  	defer os.RemoveAll(tempDir)
   178  
   179  	// Second, create the new service instance, unlock it and pass in a
   180  	// checker that we expect it to add to the bakery.
   181  	service, err := macaroons.NewService(
   182  		db, "dcrlnd", false, macaroons.IPLockChecker,
   183  	)
   184  	require.NoError(t, err, "Error creating new service")
   185  	defer service.Close()
   186  
   187  	err = service.CreateUnlock(&defaultPw)
   188  	require.NoError(t, err, "Error unlocking root key storage")
   189  
   190  	// Third, make 3 new macaroons with different root key IDs.
   191  	expectedIDs := [][]byte{{1}, {2}, {3}}
   192  	for _, v := range expectedIDs {
   193  		_, err := service.NewMacaroon(context.TODO(), v, testOperation)
   194  		require.NoError(t, err, "Error creating macaroon from service")
   195  	}
   196  
   197  	// Finally, check that calling List return the expected values.
   198  	ids, _ := service.ListMacaroonIDs(context.TODO())
   199  	require.Equal(t, expectedIDs, ids, "root key IDs mismatch")
   200  }
   201  
   202  // TestDeleteMacaroonID removes the specific root key ID.
   203  func TestDeleteMacaroonID(t *testing.T) {
   204  	ctxb := context.Background()
   205  
   206  	// First, initialize a dummy DB file with a store that the service
   207  	// can read from. Make sure the file is removed in the end.
   208  	tempDir, db := setupTestRootKeyStorage(t)
   209  	defer os.RemoveAll(tempDir)
   210  
   211  	// Second, create the new service instance, unlock it and pass in a
   212  	// checker that we expect it to add to the bakery.
   213  	service, err := macaroons.NewService(
   214  		db, "dcrlnd", false, macaroons.IPLockChecker,
   215  	)
   216  	require.NoError(t, err, "Error creating new service")
   217  	defer service.Close()
   218  
   219  	err = service.CreateUnlock(&defaultPw)
   220  	require.NoError(t, err, "Error unlocking root key storage")
   221  
   222  	// Third, checks that removing encryptedKeyID returns an error.
   223  	encryptedKeyID := []byte("enckey")
   224  	_, err = service.DeleteMacaroonID(ctxb, encryptedKeyID)
   225  	require.Equal(t, macaroons.ErrDeletionForbidden, err)
   226  
   227  	// Fourth, checks that removing DefaultKeyID returns an error.
   228  	_, err = service.DeleteMacaroonID(ctxb, macaroons.DefaultRootKeyID)
   229  	require.Equal(t, macaroons.ErrDeletionForbidden, err)
   230  
   231  	// Fifth, checks that removing empty key id returns an error.
   232  	_, err = service.DeleteMacaroonID(ctxb, []byte{})
   233  	require.Equal(t, macaroons.ErrMissingRootKeyID, err)
   234  
   235  	// Sixth, checks that removing a non-existed key id returns nil.
   236  	nonExistedID := []byte("test-non-existed")
   237  	deletedID, err := service.DeleteMacaroonID(ctxb, nonExistedID)
   238  	require.NoError(t, err, "deleting macaroon ID got an error")
   239  	require.Nil(t, deletedID, "deleting non-existed ID should return nil")
   240  
   241  	// Seventh, make 3 new macaroons with different root key IDs, and delete
   242  	// one.
   243  	expectedIDs := [][]byte{{1}, {2}, {3}}
   244  	for _, v := range expectedIDs {
   245  		_, err := service.NewMacaroon(ctxb, v, testOperation)
   246  		require.NoError(t, err, "Error creating macaroon from service")
   247  	}
   248  	deletedID, err = service.DeleteMacaroonID(ctxb, expectedIDs[0])
   249  	require.NoError(t, err, "deleting macaroon ID got an error")
   250  
   251  	// Finally, check that the ID is deleted.
   252  	require.Equal(t, expectedIDs[0], deletedID, "expected ID to be removed")
   253  	ids, _ := service.ListMacaroonIDs(ctxb)
   254  	require.Equal(t, expectedIDs[1:], ids, "root key IDs mismatch")
   255  }
   256  
   257  // TestCloneMacaroons tests that macaroons can be cloned correctly and that
   258  // modifications to the copy don't affect the original.
   259  func TestCloneMacaroons(t *testing.T) {
   260  	// Get a configured version of the constraint function.
   261  	constraintFunc := macaroons.TimeoutConstraint(3)
   262  
   263  	// Now we need a dummy macaroon that we can apply the constraint
   264  	// function to.
   265  	testMacaroon := createDummyMacaroon(t)
   266  	err := constraintFunc(testMacaroon)
   267  	require.NoError(t, err)
   268  
   269  	// Check that the caveat has an empty location.
   270  	require.Equal(
   271  		t, "", testMacaroon.Caveats()[0].Location,
   272  		"expected caveat location to be empty, found: %s",
   273  		testMacaroon.Caveats()[0].Location,
   274  	)
   275  
   276  	// Make a copy of the macaroon.
   277  	newMacCred, err := macaroons.NewMacaroonCredential(testMacaroon)
   278  	require.NoError(t, err)
   279  
   280  	newMac := newMacCred.Macaroon
   281  	require.Equal(
   282  		t, "", newMac.Caveats()[0].Location,
   283  		"expected new caveat location to be empty, found: %s",
   284  		newMac.Caveats()[0].Location,
   285  	)
   286  
   287  	// They should be deep equal as well.
   288  	testMacaroonBytes, err := testMacaroon.MarshalBinary()
   289  	require.NoError(t, err)
   290  	newMacBytes, err := newMac.MarshalBinary()
   291  	require.NoError(t, err)
   292  	require.Equal(t, testMacaroonBytes, newMacBytes)
   293  
   294  	// Modify the caveat location on the old macaroon.
   295  	testMacaroon.Caveats()[0].Location = "mars"
   296  
   297  	// The old macaroon's caveat location should be changed.
   298  	require.Equal(
   299  		t, "mars", testMacaroon.Caveats()[0].Location,
   300  		"expected caveat location to be empty, found: %s",
   301  		testMacaroon.Caveats()[0].Location,
   302  	)
   303  
   304  	// The new macaroon's caveat location should stay untouched.
   305  	require.Equal(
   306  		t, "", newMac.Caveats()[0].Location,
   307  		"expected new caveat location to be empty, found: %s",
   308  		newMac.Caveats()[0].Location,
   309  	)
   310  }