From 8e1bfee6ca185c0f7ded2e2b3985d6839a62a52b Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Thu, 4 Oct 2018 20:27:43 +0800 Subject: [PATCH] Add swagger-ui for key management --- PENDING.md | 2 +- client/keys/add.go | 209 ++++++++++++++++++++--------- client/keys/delete.go | 8 +- client/keys/list.go | 57 ++++---- client/keys/root.go | 9 +- client/keys/show.go | 72 +++++----- client/keys/update.go | 8 +- client/keys/utils.go | 25 ++++ client/lcd/root.go | 2 +- client/lcd/swagger-ui/swagger.yaml | 208 +++++++++++++++++++++++++++- x/auth/client/rest/query.go | 3 +- 11 files changed, 450 insertions(+), 153 deletions(-) diff --git a/PENDING.md b/PENDING.md index ca77f2ad45..18eddd7dc9 100644 --- a/PENDING.md +++ b/PENDING.md @@ -77,7 +77,7 @@ FEATURES * [gaia-lite] [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add support for `generate_only=true` query argument to generate offline unsigned transactions * [gaia-lite] [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) Add /sign endpoint to sign transactions generated with `generate_only=true`. * [gaia-lite] [\#1954](https://github.com/cosmos/cosmos-sdk/issues/1954) Add /broadcast endpoint to broadcast transactions signed by the /sign endpoint. - * [gaia-lite] [\#2113](https://github.com/cosmos/cosmos-sdk/issues/2113) Rename `/accounts/{address}/send` to `/bank/accounts/{address}/transfers` + * [gaia-lite] [\#2113](https://github.com/cosmos/cosmos-sdk/issues/2113) Rename `/accounts/{address}/send` to `/bank/accounts/{address}/transfers`, rename `/accounts/{address}` to `/auth/accounts/{address}` * Gaia CLI (`gaiacli`) * [cli] Cmds to query staking pool and params diff --git a/client/keys/add.go b/client/keys/add.go index be2825a55d..fa1b87eef2 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -144,11 +144,16 @@ func printCreate(info keys.Info, seed string) { if !viper.GetBool(flagNoBackup) { out.Seed = seed } - json, err := MarshalJSON(out) + var jsonString []byte + if viper.GetBool(client.FlagIndentResponse) { + jsonString, err = cdc.MarshalJSONIndent(out, "", " ") + } else { + jsonString, err = cdc.MarshalJSON(out) + } if err != nil { panic(err) // really shouldn't happen... } - fmt.Println(string(json)) + fmt.Println(string(jsonString)) default: panic(fmt.Sprintf("I can't speak: %s", output)) } @@ -165,77 +170,75 @@ type NewKeyBody struct { } // add new key REST handler -func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) { - var kb keys.Keybase - var m NewKeyBody +func AddNewKeyRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var kb keys.Keybase + var m NewKeyBody - kb, err := GetKeyBase() - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - - body, err := ioutil.ReadAll(r.Body) - err = json.Unmarshal(body, &m) - - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) - return - } - if m.Name == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You have to specify a name for the locally stored account.")) - return - } - if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You have to specify a password for the locally stored account.")) - return - } - - // check if already exists - infos, err := kb.List() - for _, i := range infos { - if i.GetName() == m.Name { - w.WriteHeader(http.StatusConflict) - w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", m.Name))) + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) return } - } - // create account - seed := m.Seed - if seed == "" { - seed = getSeed(keys.Secp256k1) - } - info, err := kb.CreateKey(m.Name, seed, m.Password) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = json.Unmarshal(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + if m.Name == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a name for the locally stored account.")) + return + } + if m.Password == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a password for the locally stored account.")) + return + } - keyOutput, err := Bech32KeyOutput(info) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return + // check if already exists + infos, err := kb.List() + for _, i := range infos { + if i.GetName() == m.Name { + w.WriteHeader(http.StatusConflict) + w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", m.Name))) + return + } + } + + // create account + seed := m.Seed + if seed == "" { + seed = getSeed(keys.Secp256k1) + } + info, err := kb.CreateKey(m.Name, seed, m.Password) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput.Seed = seed + + PostProcessResponse(w, cdc, keyOutput, indent) } - - keyOutput.Seed = seed - - bz, err := json.Marshal(keyOutput) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - - w.Write(bz) } - // function to just a new seed to display in the UI before actually persisting it in the keybase func getSeed(algo keys.SigningAlgo) string { kb := client.MockKeyBase() @@ -258,3 +261,77 @@ func SeedRequestHandler(w http.ResponseWriter, r *http.Request) { seed := getSeed(algo) w.Write([]byte(seed)) } + +// RecoverKeyBody is recover key request REST body +type RecoverKeyBody struct { + Password string `json:"password"` + Seed string `json:"seed"` +} + +func RecoverRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + var m RecoverKeyBody + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + if name == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a name for the locally stored account.")) + return + } + if m.Password == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify a password for the locally stored account.")) + return + } + if m.Seed == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("You have to specify seed for key recover.")) + return + } + + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + // check if already exists + infos, err := kb.List() + for _, i := range infos { + if i.GetName() == name { + w.WriteHeader(http.StatusConflict) + w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", name))) + return + } + } + + info, err := kb.CreateKey(name, m.Seed, m.Password) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + PostProcessResponse(w, cdc, keyOutput, indent) + } +} \ No newline at end of file diff --git a/client/keys/delete.go b/client/keys/delete.go index 944feb4b19..23fc41ffd6 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -68,14 +68,14 @@ func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&m) if err != nil { - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } kb, err = GetKeyBase() if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } @@ -83,10 +83,10 @@ func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { // TODO handle error if key is not available or pass is wrong err = kb.Delete(name, m.Password) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) } diff --git a/client/keys/list.go b/client/keys/list.go index 22f163f1d8..f232fccffb 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -1,7 +1,6 @@ package keys import ( - "encoding/json" "net/http" "github.com/spf13/cobra" @@ -35,35 +34,31 @@ func runListCmd(cmd *cobra.Command, args []string) error { // REST // query key list REST handler -func QueryKeysRequestHandler(w http.ResponseWriter, r *http.Request) { - kb, err := GetKeyBase() - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return +func QueryKeysRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + kb, err := GetKeyBase() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + infos, err := kb.List() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + // an empty list will be JSONized as null, but we want to keep the empty list + if len(infos) == 0 { + PostProcessResponse(w, cdc, "[]", indent) + return + } + keysOutput, err := Bech32KeysOutput(infos) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + PostProcessResponse(w, cdc, keysOutput, indent) } - infos, err := kb.List() - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - // an empty list will be JSONized as null, but we want to keep the empty list - if len(infos) == 0 { - w.Write([]byte("[]")) - return - } - keysOutput, err := Bech32KeysOutput(infos) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - output, err := json.MarshalIndent(keysOutput, "", " ") - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - w.Write(output) } diff --git a/client/keys/root.go b/client/keys/root.go index f2a27dc0c8..a7a7d2e6fc 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -30,11 +30,12 @@ func Commands() *cobra.Command { } // resgister REST routes -func RegisterRoutes(r *mux.Router) { - r.HandleFunc("/keys", QueryKeysRequestHandler).Methods("GET") - r.HandleFunc("/keys", AddNewKeyRequestHandler).Methods("POST") +func RegisterRoutes(r *mux.Router, indent bool) { + r.HandleFunc("/keys", QueryKeysRequestHandler(indent)).Methods("GET") + r.HandleFunc("/keys", AddNewKeyRequestHandler(indent)).Methods("POST") r.HandleFunc("/keys/seed", SeedRequestHandler).Methods("GET") - r.HandleFunc("/keys/{name}", GetKeyRequestHandler).Methods("GET") + r.HandleFunc("/keys/{name}/recover", RecoverRequestHandler(indent)).Methods("POST") + r.HandleFunc("/keys/{name}", GetKeyRequestHandler(indent)).Methods("GET") r.HandleFunc("/keys/{name}", UpdateKeyRequestHandler).Methods("PUT") r.HandleFunc("/keys/{name}", DeleteKeyRequestHandler).Methods("DELETE") } diff --git a/client/keys/show.go b/client/keys/show.go index 82c6f9883d..b567daf124 100644 --- a/client/keys/show.go +++ b/client/keys/show.go @@ -1,7 +1,6 @@ package keys import ( - "encoding/json" "fmt" "net/http" @@ -91,44 +90,39 @@ func getBechKeyOut(bechPrefix string) (bechKeyOutFn, error) { // REST // get key REST handler -func GetKeyRequestHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - bechPrefix := r.URL.Query().Get(FlagBechPrefix) +func GetKeyRequestHandler(indent bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + bechPrefix := r.URL.Query().Get(FlagBechPrefix) - if bechPrefix == "" { - bechPrefix = "acc" + if bechPrefix == "" { + bechPrefix = "acc" + } + + bechKeyOut, err := getBechKeyOut(bechPrefix) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + info, err := GetKeyInfo(name) + // TODO: check for the error if key actually does not exist, instead of + // assuming this as the reason + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(err.Error())) + return + } + + keyOutput, err := bechKeyOut(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + PostProcessResponse(w, cdc, keyOutput, indent) } - - bechKeyOut, err := getBechKeyOut(bechPrefix) - if err != nil { - w.WriteHeader(400) - w.Write([]byte(err.Error())) - return - } - - info, err := GetKeyInfo(name) - // TODO: check for the error if key actually does not exist, instead of - // assuming this as the reason - if err != nil { - w.WriteHeader(404) - w.Write([]byte(err.Error())) - return - } - - keyOutput, err := bechKeyOut(info) - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - - output, err := json.MarshalIndent(keyOutput, "", " ") - if err != nil { - w.WriteHeader(500) - w.Write([]byte(err.Error())) - return - } - - w.Write(output) } diff --git a/client/keys/update.go b/client/keys/update.go index 78a81bf0e6..18a18be588 100644 --- a/client/keys/update.go +++ b/client/keys/update.go @@ -69,14 +69,14 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&m) if err != nil { - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } kb, err = GetKeyBase() if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } @@ -86,10 +86,10 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { // TODO check if account exists and if password is correct err = kb.Update(name, m.OldPassword, getNewpass) if err != nil { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(err.Error())) return } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) } diff --git a/client/keys/utils.go b/client/keys/utils.go index 95fc83ed65..a4fa867eb0 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -13,6 +13,8 @@ import ( "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" + "net/http" + "github.com/cosmos/cosmos-sdk/codec" ) // KeyDBName is the directory under root where we store the keys @@ -231,3 +233,26 @@ func printPubKey(info keys.Info, bechKeyOut bechKeyOutFn) { fmt.Println(ko.PubKey) } + +// PostProcessResponse performs post process for rest response +func PostProcessResponse(w http.ResponseWriter, cdc *codec.Codec, response interface{}, indent bool) { + var output []byte + switch response.(type) { + default: + var err error + if indent { + output, err = cdc.MarshalJSONIndent(response, "", " ") + } else { + output, err = cdc.MarshalJSON(response) + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + case []byte: + output = response.([]byte) + } + w.Header().Set("Content-Type", "application/json") + w.Write(output) +} diff --git a/client/lcd/root.go b/client/lcd/root.go index ef690ed555..475186ed0a 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -145,7 +145,7 @@ func createHandler(cdc *codec.Codec) *mux.Router { r.HandleFunc("/version", CLIVersionRequestHandler).Methods("GET") r.HandleFunc("/node_version", NodeVersionRequestHandler(cliCtx)).Methods("GET") - keys.RegisterRoutes(r) + keys.RegisterRoutes(r, cliCtx.Indent) rpc.RegisterRoutes(cliCtx, r) tx.RegisterRoutes(cliCtx, r, cdc) auth.RegisterRoutes(cliCtx, r, cdc, "acc") diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index 9c42463949..b7fb70ff33 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -346,6 +346,184 @@ paths: $ref: "#/definitions/BroadcastTxCommitResult" 400: description: The Tx was malformated + /keys: + get: + summary: List of accounts stored locally + tags: + - ICS1 + produces: + - application/json + responses: + 200: + description: Array of accounts + schema: + type: array + items: + $ref: '#/definitions/Account' + post: + summary: Create a new account locally + tags: + - ICS1 + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: account + description: The account to create + schema: + type: object + required: + - name + - password + - seed + properties: + name: + type: string + password: + type: string + seed: + type: string + responses: + 200: + description: Returns account information of the created key + schema: + $ref: "#/definitions/Account" + /keys/seed: + get: + summary: Create a new seed to create a new account with + tags: + - ICS1 + responses: + 200: + description: 16 word Seed + schema: + type: string + /keys/{name}/recover: + post: + summary: Recover a account from a seed + tags: + - ICS1 + consumes: + - application/json + produces: + - application/json + parameters: + - in: path + name: name + description: Account name + required: true + type: string + - in: body + name: pwdAndSeed + description: Provide password and seed to recover a key + schema: + type: object + required: + - password + - seed + properties: + password: + type: string + seed: + type: string + responses: + 200: + description: Returns account information of the recovered key + schema: + $ref: "#/definitions/Account" + /keys/{name}: + parameters: + - in: path + name: name + description: Account name + required: true + type: string + get: + summary: Get a certain locally stored account + tags: + - ICS1 + produces: + - application/json + responses: + 200: + description: Locally stored account + schema: + $ref: "#/definitions/Account" + 404: + description: Account is not available + put: + summary: Update the password for this account in the KMS + tags: + - ICS1 + consumes: + - application/json + parameters: + - in: body + name: account + description: The new and old password + schema: + type: object + required: + - new_password + - old_password + properties: + new_password: + type: string + old_password: + type: string + responses: + 200: + description: Updated password + 401: + description: Password is wrong + 404: + description: Account is not available + delete: + summary: Remove an account + tags: + - ICS1 + consumes: + - application/json + parameters: + - in: body + name: account + description: The password of the account to remove from the KMS + schema: + type: object + required: + - password + properties: + password: + type: string + responses: + 200: + description: Removed account + 401: + description: Password is wrong + 404: + description: Account is not available + /auth/accounts/{address}: + get: + summary: Get the account information on blockchain + tags: + - ICS1 + produces: + - application/json + parameters: + - in: path + name: address + description: Account address + required: true + type: string + responses: + 200: + description: Account information on the blockchain + schema: + $ref: "#/definitions/AccountQueryResponse" + 404: + description: Account is not available definitions: CheckTxResult: @@ -561,7 +739,35 @@ definitions: address: $ref: "#/definitions/Address" pub_key: - $ref: "#/definitions/PubKey" + type: string + example: "cosmospub1addwnpepqfgv3pakxazq2fgs8tmmhmzsrs94fptl7kyztyxprjpf0mkus3h7cxqe70s" + type: + type: string + example: local + seed: + type: string + AccountInfo: + type: object + properties: + account_number: + type: string + address: + type: string + coins: + type: array + items: + $ref: "#/definitions/Coin" + public_key: + type: string + sequence: + type: string + AccountQueryResponse: + type: object + properties: + type: + type: string + value: + $ref: "#/definitions/AccountInfo" BlockID: type: object properties: diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index ccd3a4bf79..5d47eb6921 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -17,7 +17,7 @@ import ( // register REST routes func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) { r.HandleFunc( - "/accounts/{address}", + "/auth/accounts/{address}", QueryAccountRequestHandlerFn(storeName, cdc, authcmd.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") r.HandleFunc( @@ -36,7 +36,6 @@ func QueryAccountRequestHandlerFn( decoder auth.AccountDecoder, cliCtx context.CLIContext, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") vars := mux.Vars(r) bech32addr := vars["address"]