// credentials.
package accesstoken
-// TODO(tessr): merge this package into chain/net/http/authn
-
import (
"context"
"crypto/rand"
+ "encoding/json"
"fmt"
"regexp"
+ "strings"
"time"
+ dbm "github.com/tendermint/tmlibs/db"
+
"github.com/bytom/crypto/sha3pool"
"github.com/bytom/errors"
)
-const (
- tokenSize = 32
- defaultLimit = 100
-)
+const tokenSize = 32
var (
// ErrBadID is returned when Create is called on an invalid id string.
ErrDuplicateID = errors.New("duplicate access token ID")
// ErrBadType is returned when Create is called with a bad type.
ErrBadType = errors.New("type must be client or network")
+ // ErrNoMatchID is returned when Delete is called on nonexisting ID.
+ ErrNoMatchID = errors.New("nonexisting access token ID")
// validIDRegexp checks that all characters are alphumeric, _ or -.
// It also must have a length of at least 1.
validIDRegexp = regexp.MustCompile(`^[\w-]+$`)
)
+// Token describe the access token.
type Token struct {
ID string `json:"id"`
Token string `json:"token,omitempty"`
- Type string `json:"type,omitempty"` // deprecated in 1.2
+ Type string `json:"type,omitempty"`
Created time.Time `json:"created_at"`
- sortID string
}
+// CredentialStore store user access credential.
type CredentialStore struct {
- // DB pg.DB
+ DB dbm.DB
+}
+
+// NewStore creates and returns a new Store object.
+func NewStore(db dbm.DB) *CredentialStore {
+ return &CredentialStore{
+ DB: db,
+ }
}
// Create generates a new access token with the given ID.
-func (cs *Token) Create(ctx context.Context, id, typ string) (*Token, error) {
+func (cs *CredentialStore) Create(ctx context.Context, id, typ string) (*string, error) {
if !validIDRegexp.MatchString(id) {
return nil, errors.WithDetailf(ErrBadID, "invalid id %q", id)
}
-
+ k, err := json.Marshal(id)
+ if v := cs.DB.Get(k); v != nil {
+ return nil, errors.WithDetailf(ErrDuplicateID, "id %q already in use", id)
+ }
var secret [tokenSize]byte
- _, err := rand.Read(secret[:])
- if err != nil {
+ v, err := rand.Read(secret[:])
+ if err != nil || v != tokenSize {
return nil, err
}
- var hashedSecret [32]byte
+
+ var hashedSecret [tokenSize]byte
sha3pool.Sum256(hashedSecret[:], secret[:])
+ created := time.Now()
- /* const q = `
- INSERT INTO access_tokens (id, type, hashed_secret)
- VALUES($1, $2, $3)
- RETURNING created, sort_id
- `
- */var (
- created time.Time
- sortID string
- // maybeType = sql.NullString{String: typ, Valid: typ != ""}
- )
- /* err = cs.DB.QueryRowContext(ctx, q, id, maybeType, hashedSecret[:]).Scan(&created, &sortID)
- if pg.IsUniqueViolation(err) {
- return nil, errors.WithDetailf(ErrDuplicateID, "id %q already in use", id)
- }
- if err != nil {
- return nil, errors.Wrap(err)
- }
- */
- return &Token{
+ token := &Token{
ID: id,
- Token: fmt.Sprintf("%s:%x", id, secret),
+ Token: fmt.Sprintf("%s:%x", id, hashedSecret),
Type: typ,
Created: created,
- sortID: sortID,
- }, nil
+ }
+
+ key, err := json.Marshal(id)
+ if err != nil {
+ return nil, err
+ }
+ value, err := json.Marshal(token)
+ if err != nil {
+ return nil, err
+ }
+ cs.DB.Set(key, value)
+ hexsec := fmt.Sprintf("%s:%x", id, secret)
+ return &hexsec, nil
}
// Check returns whether or not an id-secret pair is a valid access token.
-func (cs *Token) Check(ctx context.Context, id string, secret []byte) (bool, error) {
- var (
- toHash [tokenSize]byte
- hashed [32]byte
- )
+func (cs *CredentialStore) Check(ctx context.Context, id string, secret []byte) (bool, error) {
+ if !validIDRegexp.MatchString(id) {
+ return false, errors.WithDetailf(ErrBadID, "invalid id %q", id)
+ }
+
+ var toHash [tokenSize]byte
+ var hashed [tokenSize]byte
copy(toHash[:], secret)
sha3pool.Sum256(hashed[:], toHash[:])
+ inToken := fmt.Sprintf("%s:%x", id, hashed[:])
- /*const q = `SELECT EXISTS(SELECT 1 FROM access_tokens WHERE id=$1 AND hashed_secret=$2)`
- var valid bool
- err := cs.DB.QueryRowContext(ctx, q, id, hashed[:]).Scan(&valid)
+ var value []byte
+ token := &Token{}
+ k, err := json.Marshal(id)
if err != nil {
return false, err
}
- */
- // return valid, nil
- return true, nil
-}
-// Exists returns whether an id is part of a valid access token. It does not validate a secret.
-func (cs *Token) Exists(ctx context.Context, id string) bool {
- /* const q = `SELECT EXISTS(SELECT 1 FROM access_tokens WHERE id=$1)`
- var valid bool
- err := cs.DB.QueryRowContext(ctx, q, id).Scan(&valid)
- if err != nil {
- return false
- }
- return valid
- */
- return true
+ if value = cs.DB.Get(k); value == nil {
+ return false, errors.WithDetailf(ErrNoMatchID, "check id %q nonexisting", id)
+ }
+ if err := json.Unmarshal(value, token); err != nil {
+ return false, err
+ }
+
+ if strings.Compare(token.Token, inToken) == 0 {
+ return true, nil
+ }
+
+ return false, nil
}
// List lists all access tokens.
-func (cs *Token) List(ctx context.Context, typ, after string, limit int) ([]*Token, string, error) {
- if limit == 0 {
- limit = defaultLimit
- }
- /* const q = `
- SELECT id, type, sort_id, created FROM access_tokens
- WHERE ($1='' OR type=$1::access_token_type) AND ($2='' OR sort_id<$2)
- ORDER BY sort_id DESC
- LIMIT $3
- `
- var tokens []*Token
- // err := pg.ForQueryRows(ctx, cs.DB, q, typ, after, limit, func(id string, maybeType sql.NullString, sortID string, created time.Time) {
- t := Token{
- ID: id,
- Created: created,
- Type: maybeType.String,
- sortID: sortID,
- }
- tokens = append(tokens, &t)
- })
- if err != nil {
- return nil, "", errors.Wrap(err)
+func (cs *CredentialStore) List(ctx context.Context) ([]*Token, error) {
+ tokens := make([]*Token, 0)
+ iter := cs.DB.Iterator()
+ defer iter.Release()
+
+ for iter.Next() {
+ token := &Token{}
+ if err := json.Unmarshal(iter.Value(), token); err != nil {
+ return nil, err
}
- */
- var tokens []*Token
- var next string
- if len(tokens) > 0 {
- next = tokens[len(tokens)-1].sortID
+ tokens = append(tokens, token)
}
-
- return tokens, next, nil
+ return tokens, nil
}
// Delete deletes an access token by id.
-func (cs *Token) Delete(ctx context.Context, id string) error {
- /* const q = `DELETE FROM access_tokens WHERE id=$1`
- res, err := cs.DB.ExecContext(ctx, q, id)
- if err != nil {
- return errors.Wrap(err)
- }
- */
- /* deleted, err := res.RowsAffected()
- if err != nil {
- return errors.Wrap(err)
- }
+func (cs *CredentialStore) Delete(ctx context.Context, id string) error {
+ if !validIDRegexp.MatchString(id) {
+ return errors.WithDetailf(ErrBadID, "invalid id %q", id)
+ }
+ k, err := json.Marshal(id)
+ if err != nil {
+ return err
+ }
+ cs.DB.Delete(k)
- if deleted == 0 {
- return errors.WithDetailf(pg.ErrUserInputNotFound, "acccess token id %s", id)
- }
- */return nil
+ return nil
}
--- /dev/null
+package accesstoken
+
+import (
+ "context"
+ "encoding/hex"
+ "strings"
+ "testing"
+
+ dbm "github.com/tendermint/tmlibs/db"
+
+ "github.com/bytom/errors"
+)
+
+func TestCreate(t *testing.T) {
+ testDB := dbm.NewDB("testdb1", "leveldb", ".data")
+ cs := NewStore(testDB)
+ ctx := context.Background()
+
+ cases := []struct {
+ id, typ string
+ want error
+ }{
+ {"a", "client", nil},
+ {"b", "network", nil},
+ {"", "client", ErrBadID},
+ {"bad:id", "client", ErrBadID},
+ {"a", "network", ErrDuplicateID}, // this aborts the transaction, so no tests can follow
+ }
+
+ for _, c := range cases {
+ _, err := cs.Create(ctx, c.id, c.typ)
+ if errors.Root(err) != c.want {
+ t.Errorf("Create(%s, %s) error = %s want %s", c.id, c.typ, err, c.want)
+ }
+ }
+}
+
+func TestList(t *testing.T) {
+ ctx := context.Background()
+ testDB := dbm.NewDB("testdb2", "leveldb", ".data")
+ cs := NewStore(testDB)
+
+ mustCreateToken(ctx, t, cs, "ab", "test")
+ mustCreateToken(ctx, t, cs, "bc", "test")
+ mustCreateToken(ctx, t, cs, "cd", "test")
+
+ cases := struct {
+ want []string
+ }{
+ want: []string{"ab", "bc", "cd"},
+ }
+
+ got, err := cs.List(ctx)
+ if err != nil {
+ t.Errorf("List errored: get list error")
+ }
+ for i, v := range got {
+ if m := strings.Compare(v.ID, cases.want[i]); m != 0 {
+ t.Errorf("List errored: %s %s", v.ID, cases.want[i])
+ }
+ continue
+ }
+}
+
+func TestCheck(t *testing.T) {
+ ctx := context.Background()
+ testDB := dbm.NewDB("testdb3", "leveldb", ".data")
+ cs := NewStore(testDB)
+
+ token := mustCreateToken(ctx, t, cs, "x", "client")
+ tokenParts := strings.Split(*token, ":")
+ tokenID := tokenParts[0]
+ tokenSecret, err := hex.DecodeString(tokenParts[1])
+ if err != nil {
+ t.Fatal("bad token secret")
+ }
+
+ valid, err := cs.Check(ctx, tokenID, tokenSecret)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !valid {
+ t.Fatal("expected token and secret to be valid")
+ }
+
+ valid, err = cs.Check(ctx, "x", []byte("badsecret"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if valid {
+ t.Fatal("expected bad secret to not be valid")
+ }
+}
+
+func TestDelete(t *testing.T) {
+ ctx := context.Background()
+ testDB := dbm.NewDB("testdb4", "leveldb", ".data")
+ cs := NewStore(testDB)
+
+ token := mustCreateToken(ctx, t, cs, "Y", "client")
+ tokenParts := strings.Split(*token, ":")
+ tokenID := tokenParts[0]
+
+ err := cs.Delete(ctx, tokenID)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func mustCreateToken(ctx context.Context, t *testing.T, cs *CredentialStore, id, typ string) *string {
+ token, err := cs.Create(ctx, id, typ)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return token
+}
import (
"context"
- "encoding/json"
+ "encoding/hex"
- "github.com/bytom/blockchain/accesstoken"
"github.com/bytom/errors"
- // "github.com/bytom/net/http/authz"
- "github.com/bytom/net/http/httpjson"
)
-/*
-const (
- defGenericPageSize = 100
-)
-*/
var errCurrentToken = errors.New("token cannot delete itself")
-func (bcr *BlockchainReactor) createAccessToken(ctx context.Context, x struct{ ID, Type string }) (*accesstoken.Token, error) {
- token, err := bcr.accesstoken.Create(ctx, x.ID, x.Type)
+func (a *BlockchainReactor) createAccessToken(ctx context.Context, x struct{ ID, Type string }) interface{} {
+ token, err := a.accessTokens.Create(ctx, x.ID, x.Type)
if err != nil {
- return nil, errors.Wrap(err)
+ return err.Error()
}
+ return token
+}
- if x.Type == "" {
- return token, nil
- }
-
- data := map[string]interface{}{
- "id": token.ID,
- }
- _, err = json.Marshal(data)
- // guardData, err := json.Marshal(data)
+func (a *BlockchainReactor) listAccessTokens(ctx context.Context) interface{} {
+ tokens, err := a.accessTokens.List(ctx)
if err != nil {
- return nil, errors.Wrap(err)
+ return err.Error()
}
- /*
- var grant *authz.Grant
-
- // Type is deprecated; however, for backward compatibility, using the
- // Type field will create a grant associated with this new token.
- switch x.Type {
- case "client":
- grant = &authz.Grant{
- GuardType: "access_token",
- GuardData: guardData,
- Policy: "client-readwrite",
- }
- case "network":
- grant = &authz.Grant{
- GuardType: "access_token",
- GuardData: guardData,
- Policy: "crosscore",
- }
- default:
- // We've already returned if x.Type wasn't specified, so this must be a bad type.
- return nil, accesstoken.ErrBadType
- }
- err = a.sdb.Exec(ctx, a.grants.Save(ctx, grant))
- if err != nil {
- return nil, errors.Wrap(err)
- }
- */
- token.Type = x.Type // deprecated
-
- return token, nil
+ return tokens
}
-func (bcr *BlockchainReactor) listAccessTokens(ctx context.Context, x requestQuery) (*page, error) {
- limit := x.PageSize
- if limit == 0 {
- limit = 100
- }
-
- tokens, next, err := bcr.accesstoken.List(ctx, x.Type, x.After, limit)
- if err != nil {
- return nil, err
+func (a *BlockchainReactor) deleteAccessToken(ctx context.Context, x struct{ ID, Token string }) interface{} {
+ //TODO Add delete permission verify.
+ if err := a.accessTokens.Delete(ctx, x.ID); err != nil {
+ return err.Error()
}
-
-
- outQuery := x
- outQuery.After = next
-
- return &page{
- Items: httpjson.Array(tokens),
- LastPage: len(tokens) < limit,
- Next: outQuery,
- }, nil
+ return "SUCCESS!"
}
-func (bcr *BlockchainReactor) deleteAccessToken(ctx context.Context, x struct{ ID string }) error {
- currentID, _, _ := httpjson.Request(ctx).BasicAuth()
- if currentID == x.ID {
- return errCurrentToken
+func (a *BlockchainReactor) checkAccessToken(ctx context.Context, x struct{ ID, Secret string }) interface{} {
+ secret, err := hex.DecodeString(x.Secret)
+ if err != nil {
+ return err.Error()
}
- err := bcr.accesstoken.Delete(ctx, x.ID)
+ result, err := a.accessTokens.Check(ctx, x.ID, secret)
if err != nil {
- return err
+ return err.Error()
}
-
-
- /* err = a.sdb.Exec(ctx, a.deleteGrantsByAccessToken(x.ID))
- if err != nil {
- // well, technically we did delete the access token, so don't return the error
- // TODO(tessr): make this whole operation atomic, such that we either delete
- // both the access token and its grants, or we return a failure.
- log.Printkv(ctx, log.KeyError, err, "at", "revoking grants for access token", "token", x.ID)
- }
- */return nil
+ return result
}
wallet *account.Wallet
accounts *account.Manager
assets *asset.Registry
- accesstoken *accesstoken.Token
+ accessTokens *accesstoken.CredentialStore
txFeedTracker *txfeed.Tracker
blockKeeper *blockKeeper
txPool *protocol.TxPool
m.Handle("/info", jsonHandler(bcr.info))
m.Handle("/submit-transaction", jsonHandler(bcr.submit))
m.Handle("/create-access-token", jsonHandler(bcr.createAccessToken))
- m.Handle("/list-access-tokens", jsonHandler(bcr.listAccessTokens))
+ m.Handle("/list-access-token", jsonHandler(bcr.listAccessTokens))
m.Handle("/delete-access-token", jsonHandler(bcr.deleteAccessToken))
+ m.Handle("/check-access-token", jsonHandler(bcr.checkAccessToken))
+
//hsm api
m.Handle("/create-key", jsonHandler(bcr.pseudohsmCreateKey))
m.Handle("/list-keys", jsonHandler(bcr.pseudohsmListKeys))
LastPage bool `json:"last_page"`
}
-func NewBlockchainReactor(chain *protocol.Chain, txPool *protocol.TxPool, accounts *account.Manager, assets *asset.Registry, sw *p2p.Switch, hsm *pseudohsm.HSM, wallet *account.Wallet, txfeeds *txfeed.Tracker) *BlockchainReactor {
+func NewBlockchainReactor(chain *protocol.Chain, txPool *protocol.TxPool, accounts *account.Manager, assets *asset.Registry, sw *p2p.Switch, hsm *pseudohsm.HSM, wallet *account.Wallet, txfeeds *txfeed.Tracker, accessTokens *accesstoken.CredentialStore) *BlockchainReactor {
mining := cpuminer.NewCPUMiner(chain, accounts, txPool)
bcR := &BlockchainReactor{
chain: chain,
sw: sw,
hsm: hsm,
txFeedTracker: txfeeds,
+ accessTokens: accessTokens,
}
bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR)
return bcR
"create-access-token": {createAccessToken},
"list-access-token": {listAccessTokens},
"delete-access-token": {deleteAccessToken},
+ "check-access-token": {checkAccessToken},
"create-key": {createKey},
"list-keys": {listKeys},
"delete-key": {deleteKey},
}
func createAccessToken(client *rpc.Client, args []string) {
- if len(args) != 0 {
- fatalln("error:createAccessToken not use args")
+ if len(args) != 1 {
+ fatalln("error:createAccessToken use args id")
}
type Token struct {
- ID string `json:"id"`
- Token string `json:"token,omitempty"`
- Type string `json:"type,omitempty"` // deprecated in 1.2
- Created time.Time `json:"created_at"`
- sortID string
+ ID string `json:"id"`
+ Type string `json:"type"`
}
var token Token
- token.ID = "Alice"
- token.Token = "token"
+ token.ID = args[0]
- client.Call(context.Background(), "/create-access-token", &[]Token{token}, nil)
+ var response interface{}
+
+ client.Call(context.Background(), "/create-access-token", &token, &response)
+ fmt.Println(response)
}
func listAccessTokens(client *rpc.Client, args []string) {
if len(args) != 0 {
fatalln("error:listAccessTokens not use args")
}
+ var response interface{}
+ client.Call(context.Background(), "/list-access-token", nil, &response)
+ fmt.Println(response)
+}
+
+func deleteAccessToken(client *rpc.Client, args []string) {
+ if len(args) != 1 {
+ fatalln("error:deleteAccessToken use args id")
+ }
type Token struct {
- ID string `json:"id"`
- Token string `json:"token,omitempty"`
- Type string `json:"type,omitempty"` // deprecated in 1.2
- Created time.Time `json:"created_at"`
- sortID string
+ ID string `json:"id"`
+ Secert string `json:"secert,omitempty"`
}
var token Token
- token.ID = "Alice"
- token.Token = "token"
-
- client.Call(context.Background(), "/list-access-token", &[]Token{token}, nil)
+ token.ID = args[0]
+ var response interface{}
+ client.Call(context.Background(), "/delete-access-token", &token, &response)
+ fmt.Println(response)
}
-func deleteAccessToken(client *rpc.Client, args []string) {
- if len(args) != 0 {
- fatalln("error:deleteAccessToken not use args")
+
+func checkAccessToken(client *rpc.Client, args []string) {
+ if len(args) != 1 {
+ fatalln("error:deleteAccessToken use args token")
}
type Token struct {
- ID string `json:"id"`
- Token string `json:"token,omitempty"`
- Type string `json:"type,omitempty"` // deprecated in 1.2
- Created time.Time `json:"created_at"`
- sortID string
+ ID string `json:"id"`
+ Secret string `json:"secret,omitempty"`
}
var token Token
- token.ID = "Alice"
- token.Token = "token"
-
- client.Call(context.Background(), "/delete-access-token", &[]Token{token}, nil)
+ inputs := strings.Split(args[0], ":")
+ token.ID = inputs[0]
+ token.Secret = inputs[1]
+ var response interface{}
+ client.Call(context.Background(), "/check-access-token", &token, &response)
+ fmt.Println(response)
}
func createKey(client *rpc.Client, args []string) {
dbm "github.com/tendermint/tmlibs/db"
bc "github.com/bytom/blockchain"
+ "github.com/bytom/blockchain/accesstoken"
"github.com/bytom/blockchain/account"
"github.com/bytom/blockchain/asset"
"github.com/bytom/blockchain/pseudohsm"
txDB := dbm.NewDB("txdb", config.DBBackend, config.DBDir())
store := txdb.NewStore(txDB)
+ tokenDB := dbm.NewDB("accesstoken", config.DBBackend, config.DBDir())
+ accessTokens := accesstoken.NewStore(tokenDB)
+
privKey := crypto.GenPrivKeyEd25519()
// Make event switch
if err != nil {
cmn.Exit(cmn.Fmt("initialize HSM failed: %v", err))
}
-
- bcReactor := bc.NewBlockchainReactor(chain, txPool, accounts, assets, sw, hsm, wallet, txFeed)
+ bcReactor := bc.NewBlockchainReactor(chain, txPool, accounts, assets, sw, hsm, wallet, txFeed, accessTokens)
sw.AddReactor("BLOCKCHAIN", bcReactor)
// go tool pprof http://profileHose/debug/pprof/heap
go func() {
http.ListenAndServe(profileHost, nil)
- } ()
+ }()
}
node := &Node{