diff --git a/context.go b/context.go index b82aa042d..d67fa0d9a 100644 --- a/context.go +++ b/context.go @@ -1,6 +1,8 @@ package basecoin import ( + "bytes" + wire "github.com/tendermint/go-wire" "github.com/tendermint/go-wire/data" "github.com/tendermint/tmlibs/log" @@ -21,10 +23,18 @@ func NewActor(app string, addr []byte) Actor { return Actor{App: app, Address: addr} } +// Bytes makes a binary coding, useful for turning this into a key in the store func (a Actor) Bytes() []byte { return wire.BinaryBytes(a) } +// Equals checks if two actors are the same +func (a Actor) Equals(b Actor) bool { + return a.ChainID == b.ChainID && + a.App == b.App && + bytes.Equal(a.Address, b.Address) +} + // Context is an interface, so we can implement "secure" variants that // rely on private fields to control the actions type Context interface { diff --git a/modules/roles/error.go b/modules/roles/error.go index 52041a252..1fb8333a2 100644 --- a/modules/roles/error.go +++ b/modules/roles/error.go @@ -15,8 +15,11 @@ var ( errInsufficientSigs = fmt.Errorf("Not enough signatures") errNoMembers = fmt.Errorf("No members specified") errTooManyMembers = fmt.Errorf("Too many members specified") + errNotEnoughMembers = fmt.Errorf("Not enough members specified") ) +// TODO: codegen? +// ex: err-gen NoRole,"No such role",CodeType_Unauthorized func ErrNoRole() errors.TMError { return errors.WithCode(errNoRole, abci.CodeType_Unauthorized) } @@ -58,3 +61,10 @@ func ErrTooManyMembers() errors.TMError { func IsTooManyMembersErr(err error) bool { return errors.IsSameError(errTooManyMembers, err) } + +func ErrNotEnoughMembers() errors.TMError { + return errors.WithCode(errNotEnoughMembers, abci.CodeType_Unauthorized) +} +func IsNotEnoughMembersErr(err error) bool { + return errors.IsSameError(errNotEnoughMembers, err) +} diff --git a/modules/roles/store.go b/modules/roles/store.go index fe83a7416..61ca16e61 100644 --- a/modules/roles/store.go +++ b/modules/roles/store.go @@ -15,6 +15,37 @@ type Role struct { Signers []basecoin.Actor `json:"signers"` } +func NewRole(min uint32, signers []basecoin.Actor) Role { + return Role{ + MinSigs: min, + Signers: signers, + } +} + +// IsSigner checks if the given Actor is allowed to sign this role +func (r Role) IsSigner(a basecoin.Actor) bool { + for _, s := range r.Signers { + if a.Equals(s) { + return true + } + } + return false +} + +// IsAuthorized checks if the context has permission to assume the role +func (r Role) IsAuthorized(ctx basecoin.Context) bool { + needed := r.MinSigs + for _, s := range r.Signers { + if ctx.HasPermission(s) { + needed-- + if needed <= 0 { + return true + } + } + } + return false +} + // MakeKey creates the lookup key for a role func MakeKey(role []byte) []byte { prefix := []byte(NameRole + "/") @@ -34,6 +65,7 @@ func loadRole(store state.KVStore, key []byte) (role Role, err error) { return role, nil } +// we only have create here, no update, since we don't allow update yet func createRole(store state.KVStore, key []byte, role Role) error { if _, err := loadRole(store, key); !IsNoRoleErr(err) { return ErrRoleExists() diff --git a/modules/roles/store_test.go b/modules/roles/store_test.go new file mode 100644 index 000000000..a38592cf6 --- /dev/null +++ b/modules/roles/store_test.go @@ -0,0 +1,60 @@ +package roles_test + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/modules/roles" + "github.com/tendermint/basecoin/stack" +) + +func TestRole(t *testing.T) { + assert := assert.New(t) + + // prepare some actors... + a := basecoin.Actor{App: "foo", Address: []byte("bar")} + b := basecoin.Actor{ChainID: "eth", App: "foo", Address: []byte("bar")} + c := basecoin.Actor{App: "foo", Address: []byte("baz")} + d := basecoin.Actor{App: "si-ly", Address: []byte("bar")} + e := basecoin.Actor{App: "si-ly", Address: []byte("big")} + f := basecoin.Actor{App: "sig", Address: []byte{1}} + g := basecoin.Actor{App: "sig", Address: []byte{2, 3, 4}} + + cases := []struct { + sigs uint32 + allowed []basecoin.Actor + signers []basecoin.Actor + valid bool + }{ + // make sure simple compare is correct + {1, []basecoin.Actor{a}, []basecoin.Actor{a}, true}, + {1, []basecoin.Actor{a}, []basecoin.Actor{b}, false}, + {1, []basecoin.Actor{a}, []basecoin.Actor{c}, false}, + {1, []basecoin.Actor{a}, []basecoin.Actor{d}, false}, + // make sure multi-sig counts to 1 + {1, []basecoin.Actor{a, b, c}, []basecoin.Actor{d, e, a, f}, true}, + {1, []basecoin.Actor{a, b, c}, []basecoin.Actor{a, b, c, d}, true}, + {1, []basecoin.Actor{a, b, c}, []basecoin.Actor{d, e, f}, false}, + // make sure multi-sig counts higher + {2, []basecoin.Actor{b, e, g}, []basecoin.Actor{g, c, a, d, b}, true}, + {2, []basecoin.Actor{b, e, g}, []basecoin.Actor{c, a, d, b}, false}, + {3, []basecoin.Actor{a, b, c}, []basecoin.Actor{g}, false}, + } + + for idx, tc := range cases { + i := strconv.Itoa(idx) + // make sure IsSigner works + role := roles.NewRole(tc.sigs, tc.allowed) + for _, a := range tc.allowed { + assert.True(role.IsSigner(a), i) + } + // make sure IsAuthorized works + ctx := stack.MockContext("chain-id").WithPermissions(tc.signers...) + allowed := role.IsAuthorized(ctx) + assert.Equal(tc.valid, allowed, i) + } + +} diff --git a/modules/roles/tx.go b/modules/roles/tx.go index 270c43027..c4f47d1e8 100644 --- a/modules/roles/tx.go +++ b/modules/roles/tx.go @@ -7,6 +7,10 @@ import ( "github.com/tendermint/basecoin/errors" ) +const ( + MaxMembers = 10 +) + // AssumeRoleTx is a layered tx that can wrap your normal tx to give it // the authority to use a given role. type AssumeRoleTx struct { @@ -61,6 +65,12 @@ func (tx CreateRoleTx) ValidateBasic() error { if len(tx.Signers) == 0 { return ErrNoMembers() } + if len(tx.Signers) < int(tx.MinSigs) { + return ErrNotEnoughMembers() + } + if len(tx.Signers) > MaxMembers { + return ErrTooManyMembers() + } return nil } diff --git a/stack/mock.go b/stack/mock.go index 2f6ef9a5d..b64f4e12f 100644 --- a/stack/mock.go +++ b/stack/mock.go @@ -1,7 +1,6 @@ package stack import ( - "bytes" "math/rand" "github.com/tendermint/tmlibs/log" @@ -52,7 +51,7 @@ func (c naiveContext) WithPermissions(perms ...basecoin.Actor) basecoin.Context func (c naiveContext) HasPermission(perm basecoin.Actor) bool { for _, p := range c.perms { - if perm.App == p.App && bytes.Equal(perm.Address, p.Address) { + if p.Equals(perm) { return true } }