diff --git a/stack/context.go b/stack/context.go index e688a5a08..9e3c247e8 100644 --- a/stack/context.go +++ b/stack/context.go @@ -14,6 +14,7 @@ type nonce int64 type secureContext struct { app string + ibc bool // this exposes the log.Logger and all other methods we don't override naiveContext } @@ -31,24 +32,35 @@ var _ basecoin.Context = secureContext{} func (c secureContext) WithPermissions(perms ...basecoin.Actor) basecoin.Context { // the guard makes sure you only set permissions for the app you are inside for _, p := range perms { - // TODO: also check chainID, limit only certain middleware can set IBC? - if p.App != c.app { - err := errors.Errorf("Cannot set permission for %s from %s", c.app, p.App) + if !c.validPermisison(p) { + err := errors.Errorf("Cannot set permission for %s/%s on (app=%s, ibc=%b)", + p.ChainID, p.App, c.app, c.ibc) panic(err) } } return secureContext{ app: c.app, + ibc: c.ibc, naiveContext: c.naiveContext.WithPermissions(perms...).(naiveContext), } } +func (c secureContext) validPermisison(p basecoin.Actor) bool { + // if app is set, then it must match + if c.app != "" && c.app != p.App { + return false + } + // if ibc, chain must be set, otherwise it must not + return c.ibc == (p.ChainID != "") +} + // Reset should clear out all permissions, // but carry on knowledge that this is a child func (c secureContext) Reset() basecoin.Context { return secureContext{ app: c.app, + ibc: c.ibc, naiveContext: c.naiveContext.Reset().(naiveContext), } } @@ -71,6 +83,20 @@ func withApp(ctx basecoin.Context, app string) basecoin.Context { } return secureContext{ app: app, + ibc: false, + naiveContext: sc.naiveContext, + } +} + +// withIBC is a private method so we can securely allow IBC permissioning +func withIBC(ctx basecoin.Context) basecoin.Context { + sc, ok := ctx.(secureContext) + if !ok { + return ctx + } + return secureContext{ + app: "", + ibc: true, naiveContext: sc.naiveContext, } } diff --git a/stack/middleware.go b/stack/middleware.go index e3400ff58..c5f6a9b71 100644 --- a/stack/middleware.go +++ b/stack/middleware.go @@ -12,6 +12,8 @@ import ( // heavily inspired by negroni's design type middleware struct { middleware Middleware + space string + allowIBC bool next basecoin.Handler } @@ -21,13 +23,20 @@ func (m *middleware) Name() string { return m.middleware.Name() } +func (m *middleware) wrapCtx(ctx basecoin.Context) basecoin.Context { + if m.allowIBC { + return withIBC(ctx) + } + return withApp(ctx, m.space) +} + // CheckTx always returns an empty success tx func (m *middleware) CheckTx(ctx basecoin.Context, store state.SimpleDB, 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()) - store = stateSpace(store, m.Name()) + ctx = m.wrapCtx(ctx) + store = stateSpace(store, m.space) return m.middleware.CheckTx(ctx, store, tx, next) } @@ -37,22 +46,48 @@ func (m *middleware) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx ba // 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()) - store = stateSpace(store, m.Name()) + ctx = m.wrapCtx(ctx) + store = stateSpace(store, m.space) return m.middleware.DeliverTx(ctx, store, tx, next) } func (m *middleware) SetOption(l log.Logger, store state.SimpleDB, module, key, value string) (string, error) { // set the namespace for the app - store = stateSpace(store, m.Name()) + store = stateSpace(store, m.space) return m.middleware.SetOption(l, store, module, key, value, m.next) } +// builder is used to associate info with the middleware, so we can build +// it properly +type builder struct { + middleware Middleware + stateSpace string + allowIBC bool +} + +func prep(m Middleware, ibc bool) builder { + return builder{ + middleware: m, + stateSpace: m.Name(), + allowIBC: ibc, + } +} + +// wrap sets up the middleware with the proper options +func (b builder) wrap(next basecoin.Handler) basecoin.Handler { + return &middleware{ + middleware: b.middleware, + space: b.stateSpace, + allowIBC: b.allowIBC, + next: next, + } +} + // Stack is the entire application stack type Stack struct { - middles []Middleware + middles []builder handler basecoin.Handler basecoin.Handler // the compiled version, which we expose } @@ -62,9 +97,26 @@ var _ basecoin.Handler = &Stack{} // New prepares a middleware stack, you must `.Use()` a Handler // before you can execute it. func New(middlewares ...Middleware) *Stack { - return &Stack{ - middles: middlewares, + stack := new(Stack) + return stack.Apps(middlewares...) +} + +// Apps adds the following Middlewares as typical application +// middleware to the stack (limit permission to one app) +func (s *Stack) Apps(middlewares ...Middleware) *Stack { + // TODO: some wrapper... + for _, m := range middlewares { + s.middles = append(s.middles, prep(m, false)) } + return s +} + +// IBC add the following middleware with permission to add cross-chain +// permissions +func (s *Stack) IBC(m Middleware) *Stack { + // TODO: some wrapper... + s.middles = append(s.middles, prep(m, true)) + return s } // Use sets the final handler for the stack and prepares it for use @@ -77,10 +129,17 @@ func (s *Stack) Use(handler basecoin.Handler) *Stack { return s } -func build(mid []Middleware, end basecoin.Handler) basecoin.Handler { +// Dispatch is like Use, but a convenience method to construct a +// dispatcher with a set of modules to route. +func (s *Stack) Dispatch(routes ...Dispatchable) *Stack { + d := NewDispatcher(routes...) + return s.Use(d) +} + +func build(mid []builder, end basecoin.Handler) basecoin.Handler { if len(mid) == 0 { return end } next := build(mid[1:], end) - return &middleware{mid[0], next} + return mid[0].wrap(next) } diff --git a/stack/middleware_test.go b/stack/middleware_test.go index 1b4ed8df3..bb33a9a62 100644 --- a/stack/middleware_test.go +++ b/stack/middleware_test.go @@ -31,25 +31,46 @@ func TestPermissionSandbox(t *testing.T) { // test cases to make sure permissioning is solid grantee := basecoin.Actor{App: NameGrant, Address: []byte{1}} grantee2 := basecoin.Actor{App: NameGrant, Address: []byte{2}} - signer := basecoin.Actor{App: nameSigner, Address: []byte{1}} + // ibc and grantee are the same, just different chains + ibc := basecoin.Actor{ChainID: "other", App: NameGrant, Address: []byte{1}} + ibc2 := basecoin.Actor{ChainID: "other", App: nameSigner, Address: []byte{21}} + signer := basecoin.Actor{App: nameSigner, Address: []byte{21}} cases := []struct { + asIBC bool grant basecoin.Actor require basecoin.Actor expectedRes data.Bytes expected func(error) bool }{ - {grantee, grantee, rawBytes, nil}, - {grantee, grantee2, nil, errors.IsUnauthorizedErr}, - {grantee, signer, nil, errors.IsUnauthorizedErr}, - {signer, signer, nil, errors.IsInternalErr}, + // grant as normal app middleware + {false, grantee, grantee, rawBytes, nil}, + {false, grantee, grantee2, nil, errors.IsUnauthorizedErr}, + {false, grantee2, grantee2, rawBytes, nil}, + {false, ibc, grantee, nil, errors.IsInternalErr}, + {false, grantee, ibc, nil, errors.IsUnauthorizedErr}, + {false, grantee, signer, nil, errors.IsUnauthorizedErr}, + {false, signer, signer, nil, errors.IsInternalErr}, + + // grant as ibc middleware + {true, ibc, ibc, rawBytes, nil}, // ibc can set permissions + {true, ibc2, ibc2, rawBytes, nil}, // for any app + // the must match, both app and chain + {true, ibc, ibc2, nil, errors.IsUnauthorizedErr}, + {true, ibc, grantee, nil, errors.IsUnauthorizedErr}, + // cannot set local apps from ibc middleware + {true, grantee, grantee, nil, errors.IsInternalErr}, } for i, tc := range cases { - app := New( - Recovery{}, // we need this so panics turn to errors - GrantMiddleware{Auth: tc.grant}, - CheckMiddleware{Required: tc.require}, - ).Use(EchoHandler{}) + app := New(Recovery{}) + if tc.asIBC { + app = app.IBC(GrantMiddleware{Auth: tc.grant}) + } else { + app = app.Apps(GrantMiddleware{Auth: tc.grant}) + } + app = app. + Apps(CheckMiddleware{Required: tc.require}). + Use(EchoHandler{}) res, err := app.CheckTx(ctx, store, raw) checkPerm(t, i, tc.expectedRes, tc.expected, res, err)