Skip to content

Commit

Permalink
Middleware system with secure auth contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanfrey committed Jun 30, 2017
1 parent f6395c8 commit 98e6512
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 44 deletions.
22 changes: 22 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package basecoin

import "github.com/tendermint/go-wire/data"

type Permission struct {
App string // Which app authorized this?
Address data.Bytes // App-specific identifier
}

func NewPermission(app string, addr []byte) Permission {
return Permission{App: app, Address: addr}
}

// Context is an interface, so we can implement "secure" variants that
// rely on private fields to control the actions
type Context interface {
// context.Context
WithPermissions(perms ...Permission) Context
HasPermission(perm Permission) bool
IsParent(ctx Context) bool
Reset() Context
}
62 changes: 22 additions & 40 deletions handler.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package basecoin

import (
"bytes"

abci "github.com/tendermint/abci/types"
"github.com/tendermint/go-wire/data"

Expand All @@ -17,18 +15,36 @@ type Checker interface {
CheckTx(ctx Context, store types.KVStore, tx Tx) (Result, error)
}

type Deliver interface {
DeliverTx(ctx Context, store types.KVStore, tx Tx) (Result, error)
}

type CheckerMiddle interface {
CheckTx(ctx Context, store types.KVStore, tx Tx, next Checker) (Result, error)
}

// CheckerFunc (like http.HandlerFunc) is a shortcut for making wrapers
type CheckerFunc func(Context, types.KVStore, Tx) (Result, error)

func (c CheckerFunc) CheckTx(ctx Context, store types.KVStore, tx Tx) (Result, error) {
return c(ctx, store, tx)
}

var _ Checker = CheckerFunc(nil)

type Deliver interface {
DeliverTx(ctx Context, store types.KVStore, tx Tx) (Result, error)
}

type DeliverMiddle interface {
DeliverTx(ctx Context, store types.KVStore, tx Tx, next Deliver) (Result, error)
}

// DeliverFunc (like http.HandlerFunc) is a shortcut for making wrapers
type DeliverFunc func(Context, types.KVStore, Tx) (Result, error)

func (c DeliverFunc) DeliverTx(ctx Context, store types.KVStore, tx Tx) (Result, error) {
return c(ctx, store, tx)
}

var _ Deliver = DeliverFunc(nil)

// Handler is anything that processes a transaction
type Handler interface {
Checker
Expand All @@ -51,40 +67,6 @@ type Middleware interface {
Named
}

// TODO: handle this in some secure way, only certain apps can add permissions
type Permission struct {
App string // Which app authorized this?
Address []byte // App-specific identifier
}

// TODO: Context is a place-holder, soon we add some request data here from the
// higher-levels (like tell an app who signed).
// Trust me, we will need it like CallContext now...
type Context struct {
perms []Permission
}

// TOTALLY insecure. will redo later, but you get the point
func (c Context) AddPermissions(perms ...Permission) Context {
return Context{
perms: append(c.perms, perms...),
}
}

func (c Context) HasPermission(app string, addr []byte) bool {
for _, p := range c.perms {
if app == p.App && bytes.Equal(addr, p.Address) {
return true
}
}
return false
}

// New should give a fresh context, and know what info makes sense to carry over
func (c Context) New() Context {
return Context{}
}

// Result captures any non-error abci result
// to make sure people use error for error cases
type Result struct {
Expand Down
4 changes: 2 additions & 2 deletions handlers/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (h SimpleFeeHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx
return res, errors.InsufficientFees()
}

if !ctx.HasPermission(NameSigs, feeTx.Payer) {
if !ctx.HasPermission(SigPerm(feeTx.Payer)) {
return res, errors.Unauthorized()
}

Expand All @@ -67,7 +67,7 @@ func (h SimpleFeeHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, t
return res, errors.InsufficientFees()
}

if !ctx.HasPermission(NameSigs, feeTx.Payer) {
if !ctx.HasPermission(SigPerm(feeTx.Payer)) {
return res, errors.Unauthorized()
}

Expand Down
104 changes: 104 additions & 0 deletions handlers/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package handlers

import (
"bytes"
"math/rand"

"github.com/pkg/errors"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/types"
)

// store nonce as it's own type so no one can even try to fake it
type nonce int64

type secureContext struct {
id nonce
app string
perms []basecoin.Permission
}

func NewContext() basecoin.Context {
return secureContext{
id: nonce(rand.Int63()),
}
}

var _ basecoin.Context = secureContext{}

// WithPermissions will panic if they try to set permission without the proper app
func (c secureContext) WithPermissions(perms ...basecoin.Permission) basecoin.Context {
// the guard makes sure you only set permissions for the app you are inside
for _, p := range perms {
if p.App != c.app {
err := errors.Errorf("Cannot set permission for %s from %s", c.app, p.App)
panic(err)
}
}

return secureContext{
id: c.id,
perms: append(c.perms, perms...),
}
}

func (c secureContext) HasPermission(perm basecoin.Permission) bool {
for _, p := range c.perms {
if perm.App == p.App && bytes.Equal(perm.Address, p.Address) {
return true
}
}
return false
}

// IsParent ensures that this is derived from the given secureClient
func (c secureContext) IsParent(other basecoin.Context) bool {
so, ok := other.(secureContext)
if !ok {
return false
}
return c.id == so.id
}

// Reset should give a fresh context,
// but carry on knowledge that this is a child
func (c secureContext) Reset() basecoin.Context {
return secureContext{
id: c.id,
app: c.app,
}
}

// withApp is a private method that we can use to properly set the
// app controls in the middleware
func withApp(ctx basecoin.Context, app string) basecoin.Context {
sc, ok := ctx.(secureContext)
if !ok {
return ctx
}
return secureContext{
id: sc.id,
app: app,
perms: sc.perms,
}
}

func secureCheck(h basecoin.Checker, parent basecoin.Context) basecoin.Checker {
next := func(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) {
if !parent.IsParent(ctx) {
return res, errors.New("Passing in non-child Context")
}
return h.CheckTx(ctx, store, tx)
}
return basecoin.CheckerFunc(next)
}

func secureDeliver(h basecoin.Deliver, parent basecoin.Context) basecoin.Deliver {
next := func(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) {
if !parent.IsParent(ctx) {
return res, errors.New("Passing in non-child Context")
}
return h.DeliverTx(ctx, store, tx)
}
return basecoin.DeliverFunc(next)
}
73 changes: 73 additions & 0 deletions handlers/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package handlers

import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/types"
)

// middleware lets us wrap a whole stack up into one Handler
//
// heavily inspired by negroni's design
type middleware struct {
middleware basecoin.Middleware
next basecoin.Handler
}

var _ basecoin.Handler = &middleware{}

func (m *middleware) Name() string {
return m.middleware.Name()
}

// CheckTx always returns an empty success tx
func (m *middleware) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (basecoin.Result, error) {
// make sure we pass in proper context to child
next := secureCheck(m.next, ctx)
// set the permissions for this app
ctx = withApp(ctx, m.Name())
return m.middleware.CheckTx(ctx, store, tx, next)
}

// DeliverTx always returns an empty success tx
func (m *middleware) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) {
// make sure we pass in proper context to child
next := secureDeliver(m.next, ctx)
// set the permissions for this app
ctx = withApp(ctx, m.Name())
return m.middleware.DeliverTx(ctx, store, tx, next)
}

// Stack is the entire application stack
type Stack struct {
middles []basecoin.Middleware
handler basecoin.Handler
basecoin.Handler // the compiled version, which we expose
}

var _ basecoin.Handler = &Stack{}

// NewStack prepares a middleware stack, you must `.Use()` a Handler
// before you can execute it.
func NewStack(middlewares ...basecoin.Middleware) *Stack {
return &Stack{
middles: middlewares,
}
}

// Use sets the final handler for the stack and prepares it for use
func (s *Stack) Use(handler basecoin.Handler) *Stack {
if handler == nil {
panic("Cannot have a Stack without an end handler")
}
s.handler = handler
s.Handler = build(s.middles, s.handler)
return s
}

func build(mid []basecoin.Middleware, end basecoin.Handler) basecoin.Handler {
if len(mid) == 0 {
return end
}
next := build(mid[1:], end)
return &middleware{mid[0], next}
}
8 changes: 6 additions & 2 deletions handlers/sigs.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ func (_ SignedHandler) Name() string {

var _ basecoin.Middleware = SignedHandler{}

func SigPerm(addr []byte) basecoin.Permission {
return basecoin.Permission{App: NameSigs, Address: addr}
}

// Signed allows us to use txs.OneSig and txs.MultiSig (and others??)
type Signed interface {
basecoin.TxLayer
Expand Down Expand Up @@ -50,10 +54,10 @@ func (h SignedHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx b
func addSigners(ctx basecoin.Context, sigs []crypto.PubKey) basecoin.Context {
perms := make([]basecoin.Permission, len(sigs))
for i, s := range sigs {
perms[i] = basecoin.Permission{App: NameSigs, Address: s.Address()}
perms[i] = SigPerm(s.Address())
}
// add the signers to the context and continue
return ctx.AddPermissions(perms...)
return ctx.WithPermissions(perms...)
}

func getSigners(tx basecoin.Tx) ([]crypto.PubKey, basecoin.Tx, error) {
Expand Down
29 changes: 29 additions & 0 deletions handlers/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package handlers

import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/types"
)

const (
NameVoid = "void"
)

// voidHandler just used to return okay to everything
type voidHandler struct{}

var _ basecoin.Handler = voidHandler{}

func (_ voidHandler) Name() string {
return NameVoid
}

// CheckTx always returns an empty success tx
func (_ voidHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) {
return
}

// DeliverTx always returns an empty success tx
func (_ voidHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) {
return
}

0 comments on commit 98e6512

Please sign in to comment.