multi: upgrade macaroons to v2, replace per-method auth with interceptors
This commit reworks the macaroon authentication framework to use the v2 macaroon format and bakery API. It also replaces the code in each RPC method which calls the macaroon verifier with interceptors which call the macaroon verifier instead. In addition, the operation permissions are reworked to fit the new format of "allow" commands (specifically, entity/operation permissions instead of method permissions).
This commit is contained in:
1 parent
20098e8cb3
commit
21c29c33d7
11 files changed
+427
-514
No files matched your search
+11
-29
@@ -3,15 +3,13 @@ package macaroons
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
|
||||
"gopkg.in/macaroon-bakery.v1/bakery"
|
||||
"gopkg.in/macaroon-bakery.v1/bakery/checkers"
|
||||
macaroon "gopkg.in/macaroon.v1"
|
||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
macaroon "gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
// MacaroonCredential wraps a macaroon to implement the
|
||||
@@ -54,13 +52,11 @@ func NewMacaroonCredential(m *macaroon.Macaroon) MacaroonCredential {
|
||||
// bakery service, context, and uri. Within the passed context.Context, we
|
||||
// expect a macaroon to be encoded as request metadata using the key
|
||||
// "macaroon".
|
||||
func ValidateMacaroon(ctx context.Context, method string,
|
||||
svc *bakery.Service) error {
|
||||
func ValidateMacaroon(ctx context.Context, requiredPermissions []bakery.Op,
|
||||
svc *bakery.Bakery) error {
|
||||
|
||||
// Get macaroon bytes from context and unmarshal into macaroon.
|
||||
//
|
||||
// TODO(aakselrod): use FromIncomingContext after grpc update in glide.
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to get metadata from context")
|
||||
}
|
||||
@@ -69,16 +65,6 @@ func ValidateMacaroon(ctx context.Context, method string,
|
||||
len(md["macaroon"]))
|
||||
}
|
||||
|
||||
// Get peer info and extract IP address from it for macaroon check
|
||||
pr, ok := peer.FromContext(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to get peer info from context")
|
||||
}
|
||||
peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse peer address")
|
||||
}
|
||||
|
||||
// With the macaroon obtained, we'll now decode the hex-string
|
||||
// encoding, then unmarshal it from binary into its concrete struct
|
||||
// representation.
|
||||
@@ -93,12 +79,8 @@ func ValidateMacaroon(ctx context.Context, method string,
|
||||
}
|
||||
|
||||
// Check the method being called against the permitted operation and
|
||||
// the expiration time and return the result.
|
||||
//
|
||||
// TODO(aakselrod): Add more checks as required.
|
||||
return svc.Check(macaroon.Slice{mac}, checkers.New(
|
||||
AllowChecker(method),
|
||||
TimeoutChecker(),
|
||||
IPLockChecker(peerAddr),
|
||||
))
|
||||
// the expiration time and IP address and return the result.
|
||||
authChecker := svc.Checker.Auth(macaroon.Slice{mac})
|
||||
_, err = authChecker.Allow(ctx, requiredPermissions...)
|
||||
return err
|
||||
}
|
||||
+36
-38
@@ -5,14 +5,22 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"gopkg.in/macaroon-bakery.v1/bakery/checkers"
|
||||
macaroon "gopkg.in/macaroon.v1"
|
||||
"google.golang.org/grpc/peer"
|
||||
|
||||
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
|
||||
macaroon "gopkg.in/macaroon.v2"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Constraint type adds a layer of indirection over macaroon caveats and
|
||||
// checkers.
|
||||
// Constraint type adds a layer of indirection over macaroon caveats.
|
||||
type Constraint func(*macaroon.Macaroon) error
|
||||
|
||||
// Checker type adds a layer of indirection over macaroon checkers. A Checker
|
||||
// returns the name of the checker and the checker function; these are used to
|
||||
// register the function with the bakery service's compound checker.
|
||||
type Checker func() (string, checkers.Func)
|
||||
|
||||
// AddConstraints returns new derived macaroon by applying every passed
|
||||
// constraint and tightening its restrictions.
|
||||
func AddConstraints(mac *macaroon.Macaroon, cs ...Constraint) (*macaroon.Macaroon, error) {
|
||||
@@ -27,21 +35,7 @@ func AddConstraints(mac *macaroon.Macaroon, cs ...Constraint) (*macaroon.Macaroo
|
||||
|
||||
// Each *Constraint function is a functional option, which takes a pointer
|
||||
// to the macaroon and adds another restriction to it. For each *Constraint,
|
||||
// the corresponding *Checker is provided.
|
||||
|
||||
// AllowConstraint restricts allowed operations set to the ones
|
||||
// passed to it.
|
||||
func AllowConstraint(ops ...string) func(*macaroon.Macaroon) error {
|
||||
return func(mac *macaroon.Macaroon) error {
|
||||
caveat := checkers.AllowCaveat(ops...)
|
||||
return mac.AddFirstPartyCaveat(caveat.Condition)
|
||||
}
|
||||
}
|
||||
|
||||
// AllowChecker wraps default checkers.OperationChecker.
|
||||
func AllowChecker(method string) checkers.Checker {
|
||||
return checkers.OperationChecker(method)
|
||||
}
|
||||
// the corresponding *Checker is provided if not provided by default.
|
||||
|
||||
// TimeoutConstraint restricts the lifetime of the macaroon
|
||||
// to the amount of seconds given.
|
||||
@@ -50,15 +44,10 @@ func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error {
|
||||
macaroonTimeout := time.Duration(seconds)
|
||||
requestTimeout := time.Now().Add(time.Second * macaroonTimeout)
|
||||
caveat := checkers.TimeBeforeCaveat(requestTimeout)
|
||||
return mac.AddFirstPartyCaveat(caveat.Condition)
|
||||
return mac.AddFirstPartyCaveat([]byte(caveat.Condition))
|
||||
}
|
||||
}
|
||||
|
||||
// TimeoutChecker wraps default checkers.TimeBefore checker.
|
||||
func TimeoutChecker() checkers.Checker {
|
||||
return checkers.TimeBefore
|
||||
}
|
||||
|
||||
// IPLockConstraint locks macaroon to a specific IP address.
|
||||
// If address is an empty string, this constraint does nothing to
|
||||
// accommodate default value's desired behavior.
|
||||
@@ -69,24 +58,33 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
|
||||
if macaroonIPAddr == nil {
|
||||
return fmt.Errorf("incorrect macaroon IP-lock address")
|
||||
}
|
||||
caveat := checkers.ClientIPAddrCaveat(macaroonIPAddr)
|
||||
return mac.AddFirstPartyCaveat(caveat.Condition)
|
||||
caveat := checkers.Condition("ipaddr",
|
||||
macaroonIPAddr.String())
|
||||
return mac.AddFirstPartyCaveat([]byte(caveat))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IPLockChecker accepts client IP from the validation context and compares it
|
||||
// with IP locked in the macaroon.
|
||||
func IPLockChecker(clientIP string) checkers.Checker {
|
||||
return checkers.CheckerFunc{
|
||||
Condition_: checkers.CondClientIPAddr,
|
||||
Check_: func(_, cav string) error {
|
||||
if !net.ParseIP(cav).Equal(net.ParseIP(clientIP)) {
|
||||
msg := "macaroon locked to different IP address"
|
||||
return fmt.Errorf(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// with IP locked in the macaroon. It is of the `Checker` type.
|
||||
func IPLockChecker() (string, checkers.Func) {
|
||||
return "ipaddr", func(ctx context.Context, cond, arg string) error {
|
||||
// Get peer info and extract IP address from it for macaroon
|
||||
// check.
|
||||
pr, ok := peer.FromContext(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to get peer info from context")
|
||||
}
|
||||
peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse peer address")
|
||||
}
|
||||
|
||||
if !net.ParseIP(arg).Equal(net.ParseIP(peerAddr)) {
|
||||
msg := "macaroon locked to different IP address"
|
||||
return fmt.Errorf(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+92
-10
@@ -1,9 +1,15 @@
|
||||
package macaroons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"gopkg.in/macaroon-bakery.v1/bakery"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
@@ -15,8 +21,13 @@ var (
|
||||
)
|
||||
|
||||
// NewService returns a service backed by the macaroon Bolt DB stored in the
|
||||
// passed directory.
|
||||
func NewService(dir string) (*bakery.Service, error) {
|
||||
// passed directory. The `checks` argument can be any of the `Checker` type
|
||||
// functions defined in this package, or a custom checker if desired. This
|
||||
// constructor prevents double-registration of checkers to prevent panics, so
|
||||
// listing the same checker more than once is not harmful. Default checkers,
|
||||
// such as those for `allow`, `time-before`, `declared`, and `error` caveats
|
||||
// are registered automatically and don't need to be added.
|
||||
func NewService(dir string, checks ...Checker) (*bakery.Bakery, error) {
|
||||
// Open the database that we'll use to store the primary macaroon key,
|
||||
// and all generated macaroons+caveats.
|
||||
macaroonDB, err := bolt.Open(path.Join(dir, dbFilename), 0600,
|
||||
@@ -29,19 +40,90 @@ func NewService(dir string) (*bakery.Service, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
macaroonStore, err := NewStorage(macaroonDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macaroonParams := bakery.NewServiceParams{
|
||||
macaroonParams := bakery.BakeryParams{
|
||||
Location: "lnd",
|
||||
Store: macaroonStore,
|
||||
RootKeyStore: rootKeyStore,
|
||||
// No third-party caveat support for now.
|
||||
// TODO(aakselrod): Add third-party caveat support.
|
||||
Locator: nil,
|
||||
Key: nil,
|
||||
}
|
||||
return bakery.NewService(macaroonParams)
|
||||
|
||||
svc := bakery.New(macaroonParams)
|
||||
|
||||
// Register all custom caveat checkers with the bakery's checker.
|
||||
// TODO(aakselrod): Add more checks as required.
|
||||
checker := svc.Checker.FirstPartyCaveatChecker.(*checkers.Checker)
|
||||
for _, check := range checks {
|
||||
cond, fun := check()
|
||||
if !isRegistered(checker, cond) {
|
||||
checker.Register(cond, "std", fun)
|
||||
}
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// isRegistered checks to see if the required checker has already been
|
||||
// registered in order to avoid a panic caused by double registration.
|
||||
func isRegistered(c *checkers.Checker, name string) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, info := range c.Info() {
|
||||
if info.Name == name && info.Prefix == "std" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// UnaryServerInterceptor is a GRPC interceptor that checks whether the
|
||||
// request is authorized by the included macaroons.
|
||||
func UnaryServerInterceptor(svc *bakery.Bakery,
|
||||
permissionMap map[string][]bakery.Op) grpc.UnaryServerInterceptor {
|
||||
|
||||
return func(ctx context.Context, req interface{},
|
||||
info *grpc.UnaryServerInfo,
|
||||
handler grpc.UnaryHandler) (interface{}, error) {
|
||||
|
||||
if _, ok := permissionMap[info.FullMethod]; !ok {
|
||||
return nil, fmt.Errorf("%s: unknown permissions "+
|
||||
"required for method", info.FullMethod)
|
||||
}
|
||||
|
||||
err := ValidateMacaroon(ctx, permissionMap[info.FullMethod],
|
||||
svc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return handler(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
// StreamServerInterceptor is a GRPC interceptor that checks whether the
|
||||
// request is authorized by the included macaroons.
|
||||
func StreamServerInterceptor(svc *bakery.Bakery,
|
||||
permissionMap map[string][]bakery.Op) grpc.StreamServerInterceptor {
|
||||
|
||||
return func(srv interface{}, ss grpc.ServerStream,
|
||||
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
|
||||
if _, ok := permissionMap[info.FullMethod]; !ok {
|
||||
return fmt.Errorf("%s: unknown permissions required "+
|
||||
"for method", info.FullMethod)
|
||||
}
|
||||
|
||||
err := ValidateMacaroon(ss.Context(),
|
||||
permissionMap[info.FullMethod], svc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler(srv, ss)
|
||||
}
|
||||
}
|
||||
+10
-66
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
@@ -21,7 +23,7 @@ var (
|
||||
// just 0, to emulate the memory storage that comes with bakery.
|
||||
//
|
||||
// TODO(aakselrod): Add support for key rotation.
|
||||
defaultRootKeyID = "0"
|
||||
defaultRootKeyID = []byte("0")
|
||||
|
||||
// macaroonBucketName is the name of the macaroon store bucket.
|
||||
macaroonBucketName = []byte("macaroons")
|
||||
@@ -49,13 +51,13 @@ func NewRootKeyStorage(db *bolt.DB) (*RootKeyStorage, error) {
|
||||
}
|
||||
|
||||
// Get implements the Get method for the bakery.RootKeyStorage interface.
|
||||
func (r *RootKeyStorage) Get(id string) ([]byte, error) {
|
||||
func (r *RootKeyStorage) Get(_ context.Context, id []byte) ([]byte, error) {
|
||||
var rootKey []byte
|
||||
err := r.View(func(tx *bolt.Tx) error {
|
||||
dbKey := tx.Bucket(rootKeyBucketName).Get([]byte(id))
|
||||
dbKey := tx.Bucket(rootKeyBucketName).Get(id)
|
||||
if len(dbKey) == 0 {
|
||||
return fmt.Errorf("root key with id %s doesn't exist",
|
||||
id)
|
||||
string(id))
|
||||
}
|
||||
|
||||
rootKey = make([]byte, len(dbKey))
|
||||
@@ -72,12 +74,12 @@ func (r *RootKeyStorage) Get(id string) ([]byte, error) {
|
||||
// RootKey implements the RootKey method for the bakery.RootKeyStorage
|
||||
// interface.
|
||||
// TODO(aakselrod): Add support for key rotation.
|
||||
func (r *RootKeyStorage) RootKey() ([]byte, string, error) {
|
||||
func (r *RootKeyStorage) RootKey(_ context.Context) ([]byte, []byte, error) {
|
||||
var rootKey []byte
|
||||
id := defaultRootKeyID
|
||||
err := r.Update(func(tx *bolt.Tx) error {
|
||||
ns := tx.Bucket(rootKeyBucketName)
|
||||
rootKey = ns.Get([]byte(id))
|
||||
rootKey = ns.Get(id)
|
||||
|
||||
// If there's no root key stored in the bucket yet, create one.
|
||||
if len(rootKey) != 0 {
|
||||
@@ -89,69 +91,11 @@ func (r *RootKeyStorage) RootKey() ([]byte, string, error) {
|
||||
if _, err := io.ReadFull(rand.Reader, rootKey[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return ns.Put([]byte(id), rootKey)
|
||||
return ns.Put(id, rootKey)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return rootKey, id, nil
|
||||
}
|
||||
|
||||
// Storage implements the bakery.Storage interface.
|
||||
type Storage struct {
|
||||
*bolt.DB
|
||||
}
|
||||
|
||||
// NewStorage creates a Storage instance.
|
||||
//
|
||||
// TODO(aakselrod): Add support for encryption of data with passphrase.
|
||||
func NewStorage(db *bolt.DB) (*Storage, error) {
|
||||
// If the store's bucket doesn't exist, create it.
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists(macaroonBucketName)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the DB wrapped in a Storage object.
|
||||
return &Storage{db}, nil
|
||||
}
|
||||
|
||||
// Put implements the Put method for the bakery.Storage interface.
|
||||
func (s *Storage) Put(location string, item string) error {
|
||||
return s.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(macaroonBucketName).Put([]byte(location),
|
||||
[]byte(item))
|
||||
})
|
||||
}
|
||||
|
||||
// Get implements the Get method for the bakery.Storage interface.
|
||||
func (s *Storage) Get(location string) (string, error) {
|
||||
var item []byte
|
||||
err := s.View(func(tx *bolt.Tx) error {
|
||||
itemBytes := tx.Bucket(macaroonBucketName).Get([]byte(location))
|
||||
if len(itemBytes) == 0 {
|
||||
return fmt.Errorf("couldn't get item for location %s",
|
||||
location)
|
||||
}
|
||||
|
||||
item = make([]byte, len(itemBytes))
|
||||
copy(item, itemBytes)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(item), nil
|
||||
}
|
||||
|
||||
// Del implements the Del method for the bakery.Storage interface.
|
||||
func (s *Storage) Del(location string) error {
|
||||
return s.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(macaroonBucketName).Delete([]byte(location))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user