storj/satellite/console/consoleweb/consoleapi/apikeys.go
Wilfred Asomani fe9f69a757 satellite/{consoleweb,consoleapi}: add cross user api tests
This change adds tests to ensure critical endpoints are not able to be
called by users for other users. It asserts that if cases like that
do happen, a 401 response will be sent.

Issue: https://github.com/storj/storj-private/issues/407

Change-Id: I70097a80f691a7d0fcb0bc5dbce8291144177720
2023-08-24 20:16:20 +00:00

314 lines
7.9 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi
import (
"context"
"encoding/json"
"io"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/common/uuid"
"storj.io/storj/private/web"
"storj.io/storj/satellite/console"
)
var (
// ErrAPIKeysAPI - console api keys api error type.
ErrAPIKeysAPI = errs.Class("console api keys")
)
// APIKeys is an api controller that exposes all api keys related functionality.
type APIKeys struct {
log *zap.Logger
service *console.Service
}
// NewAPIKeys is a constructor for api api keys controller.
func NewAPIKeys(log *zap.Logger, service *console.Service) *APIKeys {
return &APIKeys{
log: log,
service: service,
}
}
// CreateAPIKey creates new API key for given project.
func (keys *APIKeys) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var ok bool
var idParam string
if idParam, ok = mux.Vars(r)["projectID"]; !ok {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("missing projectID route param"))
return
}
projectID, err := uuid.FromString(idParam)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
name := string(bodyBytes)
info, key, err := keys.service.CreateAPIKey(ctx, projectID, name)
if err != nil {
if console.ErrUnauthorized.Has(err) || console.ErrNoMembership.Has(err) {
keys.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}
keys.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
response := console.CreateAPIKeyResponse{
Key: key.Serialize(),
KeyInfo: info,
}
err = json.NewEncoder(w).Encode(response)
if err != nil {
keys.log.Error("failed to write json create api key response", zap.Error(ErrAPIKeysAPI.Wrap(err)))
}
}
// GetProjectAPIKeys returns paged API keys by project ID.
func (keys *APIKeys) GetProjectAPIKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
query := r.URL.Query()
projectIDParam := query.Get("projectID")
if projectIDParam == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("parameter 'projectID' can't be empty"))
return
}
projectID, err := uuid.FromString(projectIDParam)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
limitParam := query.Get("limit")
if limitParam == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("parameter 'limit' can't be empty"))
return
}
limit, err := strconv.ParseUint(limitParam, 10, 32)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
pageParam := query.Get("page")
if pageParam == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("parameter 'page' can't be empty"))
return
}
page, err := strconv.ParseUint(pageParam, 10, 32)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
orderParam := query.Get("order")
if orderParam == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("parameter 'order' can't be empty"))
return
}
order, err := strconv.ParseUint(orderParam, 10, 32)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
orderDirectionParam := query.Get("orderDirection")
if orderDirectionParam == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("parameter 'orderDirection' can't be empty"))
return
}
orderDirection, err := strconv.ParseUint(orderDirectionParam, 10, 32)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
searchString := query.Get("search")
cursor := console.APIKeyCursor{
Search: searchString,
Limit: uint(limit),
Page: uint(page),
Order: console.APIKeyOrder(order),
OrderDirection: console.OrderDirection(orderDirection),
}
apiKeys, err := keys.service.GetAPIKeys(ctx, projectID, cursor)
if err != nil {
if console.ErrUnauthorized.Has(err) {
keys.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}
keys.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
err = json.NewEncoder(w).Encode(apiKeys)
if err != nil {
keys.log.Error("failed to write json all api keys response", zap.Error(ErrAPIKeysAPI.Wrap(err)))
}
}
// GetAllAPIKeyNames returns all API key names by project ID.
func (keys *APIKeys) GetAllAPIKeyNames(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
projectIDString := r.URL.Query().Get("projectID")
if projectIDString == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("Project ID was not provided."))
return
}
projectID, err := uuid.FromString(projectIDString)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
apiKeyNames, err := keys.service.GetAllAPIKeyNamesByProjectID(ctx, projectID)
if err != nil {
if console.ErrUnauthorized.Has(err) {
keys.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}
keys.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
err = json.NewEncoder(w).Encode(apiKeyNames)
if err != nil {
keys.log.Error("failed to write json all api key names response", zap.Error(ErrAPIKeysAPI.Wrap(err)))
}
}
// DeleteByIDs deletes API keys by given IDs.
func (keys *APIKeys) DeleteByIDs(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var data struct {
IDs []string `json:"ids"`
}
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
var keyIDs []uuid.UUID
for _, id := range data.IDs {
keyID, err := uuid.FromString(id)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
keyIDs = append(keyIDs, keyID)
}
err = keys.service.DeleteAPIKeys(ctx, keyIDs)
if err != nil {
if console.ErrUnauthorized.Has(err) {
keys.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}
keys.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
}
// DeleteByNameAndProjectID deletes specific API key by it's name and project ID.
// ID here may be project.publicID or project.ID.
func (keys *APIKeys) DeleteByNameAndProjectID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
name := r.URL.Query().Get("name")
projectIDString := r.URL.Query().Get("projectID")
publicIDString := r.URL.Query().Get("publicID")
if name == "" {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
var projectID uuid.UUID
if projectIDString != "" {
projectID, err = uuid.FromString(projectIDString)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
} else if publicIDString != "" {
projectID, err = uuid.FromString(publicIDString)
if err != nil {
keys.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
} else {
keys.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("Project ID was not provided."))
return
}
err = keys.service.DeleteAPIKeyByNameAndProjectID(ctx, name, projectID)
if err != nil {
if console.ErrUnauthorized.Has(err) {
keys.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}
if console.ErrNoAPIKey.Has(err) {
keys.serveJSONError(ctx, w, http.StatusNoContent, err)
return
}
keys.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
}
// serveJSONError writes JSON error to response output stream.
func (keys *APIKeys) serveJSONError(ctx context.Context, w http.ResponseWriter, status int, err error) {
web.ServeJSONError(ctx, keys.log, w, status, err)
}