storj/multinode/console/controllers/nodes.go
Clement Sam 7e5025cac0 {storagenode,multinode/nodes}: use multinodeauth.Secret instead of []byte for APISecret
When enconding structs into JSON, byte slices are marshalled as base64
encoded string using the base64.StdEncoding.Encode():
ea9c3fd42d/src/encoding/json/encode.go (L833-L861)

We, however, expect API Secrets to be encoded as base64URL, so when
an marshalled secret (with byte slice type) is added to the multinode
dashboard, it fails with `illegal base64 data at input byte XX`.

This change changes the type of APISecret field in the
multinode/nodes.Nodes struct to use multinodeauth.Secret type instead
of []byte.
multinodeauth.Secret is extended with custom MarshalJSON and
UnmarshalJSON methods which implement the json.Marshaler and
json.Unmarshaler interfaces, respectively.

Resolves https://github.com/storj/storj/issues/4949

Change-Id: Ib14b5f49ceaac109620c25d7ff83be865c698343
2022-08-23 11:04:04 +00:00

271 lines
7.3 KiB
Go

// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package controllers
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/common/storj"
"storj.io/storj/multinode/nodes"
"storj.io/storj/private/multinodeauth"
)
var (
// ErrNodes is an internal error type for nodes web api controller.
ErrNodes = errs.Class("nodes web api controller")
)
// Nodes is a web api controller.
type Nodes struct {
log *zap.Logger
service *nodes.Service
}
// NewNodes is a constructor for Nodes.
func NewNodes(log *zap.Logger, service *nodes.Service) *Nodes {
return &Nodes{
log: log,
service: service,
}
}
// Add handles node addition.
func (controller *Nodes) Add(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Add("Content-Type", "application/json")
var payload struct {
ID string `json:"id"`
APISecret string `json:"apiSecret"`
PublicAddress string `json:"publicAddress"`
}
if err = json.NewDecoder(r.Body).Decode(&payload); err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
id, err := storj.NodeIDFromString(payload.ID)
if err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
apiSecret, err := multinodeauth.SecretFromBase64(payload.APISecret)
if err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
if err = controller.service.Add(ctx, nodes.Node{ID: id, APISecret: apiSecret, PublicAddress: payload.PublicAddress}); err != nil {
switch {
case nodes.ErrNodeNotReachable.Has(err):
controller.serveError(w, http.StatusNotFound, ErrNodes.Wrap(err))
case nodes.ErrNodeAPIKeyInvalid.Has(err):
controller.serveError(w, http.StatusUnauthorized, ErrNodes.Wrap(err))
case nodes.Error.Has(err):
default:
controller.log.Error("could not add node", zap.Error(err))
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
}
return
}
}
// UpdateName is an endpoint to update node name.
func (controller *Nodes) UpdateName(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Add("Content-Type", "application/json")
segmentParams := mux.Vars(r)
idString, ok := segmentParams["id"]
if !ok {
controller.serveError(w, http.StatusBadRequest, ErrNodes.New("id segment parameter is missing"))
return
}
id, err := storj.NodeIDFromString(idString)
if err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
var payload struct {
Name string `json:"name"`
}
if err = json.NewDecoder(r.Body).Decode(&payload); err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
err = controller.service.UpdateName(ctx, id, payload.Name)
if err != nil {
// TODO: add more error checks in future, like not found if node is missing.
controller.log.Error("update node name internal error", zap.Error(err))
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
return
}
}
// Get handles retrieving node by id.
func (controller *Nodes) Get(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Add("Content-Type", "application/json")
vars := mux.Vars(r)
nodeID, err := storj.NodeIDFromString(vars["id"])
if err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
node, err := controller.service.Get(ctx, nodeID)
if err != nil {
controller.log.Error("get node not found error", zap.Error(err))
if nodes.ErrNoNode.Has(err) {
controller.serveError(w, http.StatusNotFound, ErrNodes.Wrap(err))
return
}
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
return
}
if err = json.NewEncoder(w).Encode(node); err != nil {
controller.log.Error("failed to write json response", zap.Error(err))
return
}
}
// Delete handles node removal.
func (controller *Nodes) Delete(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Add("Content-Type", "application/json")
segmentParams := mux.Vars(r)
idString, ok := segmentParams["id"]
if !ok {
controller.serveError(w, http.StatusBadRequest, ErrNodes.New("id segment parameter is missing"))
return
}
id, err := storj.NodeIDFromString(idString)
if err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
if err = controller.service.Remove(ctx, id); err != nil {
// TODO: add more error checks in future, like not found if node is missing.
controller.log.Error("delete node internal error", zap.Error(err))
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
return
}
}
// ListInfos handles node basic info list retrieval.
func (controller *Nodes) ListInfos(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Add("Content-Type", "application/json")
infos, err := controller.service.ListInfos(ctx)
if err != nil {
controller.log.Error("list node infos internal error", zap.Error(err))
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
return
}
if err = json.NewEncoder(w).Encode(infos); err != nil {
controller.log.Error("failed to write json response", zap.Error(err))
return
}
}
// ListInfosSatellite handles node satellite specific info list retrieval.
func (controller *Nodes) ListInfosSatellite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Add("Content-Type", "application/json")
vars := mux.Vars(r)
satelliteID, err := storj.NodeIDFromString(vars["satelliteID"])
if err != nil {
controller.serveError(w, http.StatusBadRequest, ErrNodes.Wrap(err))
return
}
infos, err := controller.service.ListInfosSatellite(ctx, satelliteID)
if err != nil {
controller.log.Error("list node satellite infos internal error", zap.Error(err))
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
return
}
if err = json.NewEncoder(w).Encode(infos); err != nil {
controller.log.Error("failed to write json response", zap.Error(err))
return
}
}
// TrustedSatellites handles retrieval of unique trusted satellites node urls list.
func (controller *Nodes) TrustedSatellites(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
nodeURLs, err := controller.service.TrustedSatellites(ctx)
if err != nil {
controller.log.Error("list node trusted satellites internal error", zap.Error(err))
controller.serveError(w, http.StatusInternalServerError, ErrNodes.Wrap(err))
return
}
if err = json.NewEncoder(w).Encode(nodeURLs); err != nil {
controller.log.Error("failed to write json response", zap.Error(err))
return
}
}
// serveError set http statuses and send json error.
func (controller *Nodes) serveError(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
var response struct {
Error string `json:"error"`
}
response.Error = err.Error()
err = json.NewEncoder(w).Encode(response)
if err != nil {
controller.log.Error("failed to write json error response", zap.Error(err))
}
}