storj/satellite/admin/apikeys.go
Ivan Fraixedes e1c12674c5 satellite/admin: Fix API key delete by name
We couldn't delete API keys by name whose name contained slashes because
Gorilla Mux router interpreted the as path separator and didn't resolve
to the right endpoint.

To fix the issue the name is sent as a query parameter rather than as a
path parameter.

Change-Id: Ica67d6b9f047d7c33a5350457afc822cb8d4c7a1
2023-12-04 20:03:30 +00:00

356 lines
8.2 KiB
Go

// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package admin
import (
"database/sql"
"encoding/json"
"errors"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
"storj.io/common/macaroon"
"storj.io/common/uuid"
"storj.io/storj/satellite/console"
)
func (server *Server) addAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
sendJSONError(w, "failed to read body",
err.Error(), http.StatusInternalServerError)
return
}
var input struct {
Name string `json:"name"`
}
err = json.Unmarshal(body, &input)
if err != nil {
sendJSONError(w, "failed to unmarshal request",
err.Error(), http.StatusBadRequest)
return
}
if input.Name == "" {
sendJSONError(w, "Name is not set",
"", http.StatusBadRequest)
return
}
_, err = server.db.Console().APIKeys().GetByNameAndProjectID(ctx, input.Name, project.ID)
if err == nil {
sendJSONError(w, "api-key with given name already exists",
"", http.StatusConflict)
return
}
secret, err := macaroon.NewSecret()
if err != nil {
sendJSONError(w, "could not create macaroon secret",
err.Error(), http.StatusInternalServerError)
return
}
key, err := macaroon.NewAPIKey(secret)
if err != nil {
sendJSONError(w, "could not create api-key",
err.Error(), http.StatusInternalServerError)
return
}
apikey := console.APIKeyInfo{
Name: input.Name,
ProjectID: project.ID,
Secret: secret,
}
_, err = server.db.Console().APIKeys().Create(ctx, key.Head(), apikey)
if err != nil {
sendJSONError(w, "unable to add api-key to database",
err.Error(), http.StatusInternalServerError)
return
}
var output struct {
APIKey string `json:"apikey"`
}
output.APIKey = key.Serialize()
data, err := json.Marshal(output)
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) getAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
apikeyString, ok := vars["apikey"]
if !ok {
sendJSONError(w, "apikey missing",
"", http.StatusBadRequest)
return
}
apikey, err := macaroon.ParseAPIKey(apikeyString)
if err != nil {
sendJSONError(w, "invalid apikey format",
err.Error(), http.StatusBadRequest)
return
}
apiKeyInfo, err := server.db.Console().APIKeys().GetByHead(ctx, apikey.Head())
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "API key does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "could not get apikey id",
err.Error(), http.StatusInternalServerError)
return
}
project, err := server.db.Console().Projects().Get(ctx, apiKeyInfo.ProjectID)
if err != nil {
sendJSONError(w, "unable to fetch project details",
err.Error(), http.StatusInternalServerError)
return
}
user, err := server.db.Console().Users().Get(ctx, project.OwnerID)
if err != nil {
sendJSONError(w, "unable to fetch user details",
err.Error(), http.StatusInternalServerError)
return
}
type apiKeyData struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
type projectData struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}
type ownerData struct {
ID uuid.UUID `json:"id"`
FullName string `json:"fullName"`
Email string `json:"email"`
PaidTier bool `json:"paidTier"`
}
data, err := json.Marshal(struct {
APIKey apiKeyData `json:"api_key"`
Project projectData `json:"project"`
Owner ownerData `json:"owner"`
}{
APIKey: apiKeyData{
ID: apiKeyInfo.ID,
Name: apiKeyInfo.Name,
CreatedAt: apiKeyInfo.CreatedAt.UTC(),
},
Project: projectData{
ID: project.ID,
Name: project.Name,
},
Owner: ownerData{
ID: user.ID,
FullName: user.FullName,
Email: user.Email,
PaidTier: user.PaidTier,
},
})
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
apikeyString, ok := vars["apikey"]
if !ok {
sendJSONError(w, "apikey missing",
"", http.StatusBadRequest)
return
}
apikey, err := macaroon.ParseAPIKey(apikeyString)
if err != nil {
sendJSONError(w, "invalid apikey format",
err.Error(), http.StatusBadRequest)
return
}
info, err := server.db.Console().APIKeys().GetByHead(ctx, apikey.Head())
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "API key does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "could not get apikey id",
err.Error(), http.StatusInternalServerError)
return
}
err = server.db.Console().APIKeys().Delete(ctx, info.ID)
if err != nil {
sendJSONError(w, "unable to delete apikey",
err.Error(), http.StatusInternalServerError)
return
}
}
func (server *Server) deleteAPIKeyByName(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
apikeyName := r.URL.Query().Get("name")
if apikeyName == "" {
sendJSONError(w, "apikey name missing",
"", http.StatusBadRequest)
return
}
info, err := server.db.Console().APIKeys().GetByNameAndProjectID(ctx, apikeyName, project.ID)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "API key with specified name does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "could not get apikey id",
err.Error(), http.StatusInternalServerError)
return
}
err = server.db.Console().APIKeys().Delete(ctx, info.ID)
if err != nil {
sendJSONError(w, "unable to delete apikey",
err.Error(), http.StatusInternalServerError)
return
}
}
func (server *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
return
}
if err != nil {
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
const apiKeysPerPage = 50 // This is the current maximum API Keys per page.
var apiKeys []console.APIKeyInfo
for i := uint(1); true; i++ {
page, err := server.db.Console().APIKeys().GetPagedByProjectID(
ctx, project.ID, console.APIKeyCursor{
Limit: apiKeysPerPage,
Page: i,
Order: console.KeyName,
OrderDirection: console.Ascending,
},
)
if err != nil {
sendJSONError(w, "failed retrieving a cursor page of API Keys list",
err.Error(), http.StatusInternalServerError,
)
return
}
apiKeys = append(apiKeys, page.APIKeys...)
if len(page.APIKeys) < apiKeysPerPage {
break
}
}
var data []byte
if len(apiKeys) == 0 {
data = []byte("[]")
} else {
data, err = json.Marshal(apiKeys)
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
}
sendJSONData(w, http.StatusOK, data)
}