satellite/admin: support more options for passing project ID

This change does two things:
* allow using either public ID or private ID to do project-related
  requests in admin UI
* allow passing a UUID string not containing dashes (i.e. a pure hex
  string) in order to do project-related requests in admin UI

Change-Id: I4807a5d7252a48f4a09e3966c406645d55c856e2
This commit is contained in:
Moby von Briesen 2023-09-05 15:28:39 -04:00 committed by Storj Robot
parent 091c72319a
commit 6195b8cd52
5 changed files with 163 additions and 79 deletions

View File

@ -29,10 +29,15 @@ func (server *Server) addAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
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, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
@ -60,7 +65,7 @@ func (server *Server) addAPIKey(w http.ResponseWriter, r *http.Request) {
return
}
_, err = server.db.Console().APIKeys().GetByNameAndProjectID(ctx, input.Name, projectUUID)
_, 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)
@ -83,7 +88,7 @@ func (server *Server) addAPIKey(w http.ResponseWriter, r *http.Request) {
apikey := console.APIKeyInfo{
Name: input.Name,
ProjectID: projectUUID,
ProjectID: project.ID,
Secret: secret,
}
@ -248,10 +253,15 @@ func (server *Server) deleteAPIKeyByName(w http.ResponseWriter, r *http.Request)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
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, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
@ -262,7 +272,7 @@ func (server *Server) deleteAPIKeyByName(w http.ResponseWriter, r *http.Request)
return
}
info, err := server.db.Console().APIKeys().GetByNameAndProjectID(ctx, apikeyName, projectUUID)
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)
@ -293,10 +303,15 @@ func (server *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
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, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
@ -304,7 +319,7 @@ func (server *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
var apiKeys []console.APIKeyInfo
for i := uint(1); true; i++ {
page, err := server.db.Console().APIKeys().GetPagedByProjectID(
ctx, projectUUID, console.APIKeyCursor{
ctx, project.ID, console.APIKeyCursor{
Limit: apiKeysPerPage,
Page: i,
Order: console.KeyName,

View File

@ -21,7 +21,7 @@ func validateBucketPathParameters(vars map[string]string) (project uuid.NullUUID
return project, bucket, fmt.Errorf("project-uuid missing")
}
project.UUID, err = uuid.FromString(projectUUIDString)
project.UUID, err = uuidFromString(projectUUIDString)
if err != nil {
return project, bucket, fmt.Errorf("project-uuid is not a valid uuid")
}

View File

@ -4,10 +4,13 @@
package admin
import (
"encoding/hex"
"encoding/json"
"net/http"
"github.com/zeebo/errs"
"storj.io/common/uuid"
)
// Error is default error class for admin package.
@ -35,3 +38,25 @@ func sendJSONData(w http.ResponseWriter, statusCode int, data []byte) {
w.WriteHeader(statusCode)
_, _ = w.Write(data) // any error here entitles a client side disconnect or similar, which we do not care about.
}
// uuidFromString converts a hex string into a UUID type. It works regardless of whether the string version contains `-` characters.
func uuidFromString(uuidString string) (id uuid.UUID, err error) {
if len(uuidString) == len(uuid.UUID{}.String()) {
id, err = uuid.FromString(uuidString)
if err != nil {
return id, Error.Wrap(err)
}
} else {
// this case means that dashes may not have been included in the ID passed in
// to parse, decode from hex, and create UUID from bytes
b, err := hex.DecodeString(uuidString)
if err != nil {
return id, Error.Wrap(err)
}
id, err = uuid.FromBytes(b)
if err != nil {
return id, Error.Wrap(err)
}
}
return id, nil
}

View File

@ -38,14 +38,19 @@ func (server *Server) checkProjectUsage(w http.ResponseWriter, r *http.Request)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
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, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
if !server.checkUsage(ctx, w, projectUUID) {
if !server.checkUsage(ctx, w, project.ID) {
sendJSONData(w, http.StatusOK, []byte(`{"result":"no project usage exist"}`))
}
}
@ -61,20 +66,18 @@ func (server *Server) getProject(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
sendJSONError(w, "invalid form",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
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, "unable to fetch project details",
err.Error(), http.StatusInternalServerError)
@ -102,14 +105,7 @@ func (server *Server) getProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
@ -175,13 +171,6 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
var arguments struct {
Usage *memory.Size `schema:"usage"`
Bandwidth *memory.Size `schema:"bandwidth"`
@ -198,7 +187,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
}
decoder := schema.NewDecoder()
err = decoder.Decode(&arguments, r.Form)
err := decoder.Decode(&arguments, r.Form)
if err != nil {
sendJSONError(w, "invalid arguments",
err.Error(), http.StatusBadRequest)
@ -206,7 +195,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
}
// check if the project exists.
_, err = server.db.Console().Projects().Get(ctx, projectUUID)
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
@ -225,7 +214,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
err = server.db.ProjectAccounting().UpdateProjectUsageLimit(ctx, projectUUID, *arguments.Usage)
err = server.db.ProjectAccounting().UpdateProjectUsageLimit(ctx, project.ID, *arguments.Usage)
if err != nil {
sendJSONError(w, "failed to update usage",
err.Error(), http.StatusInternalServerError)
@ -240,7 +229,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
err = server.db.ProjectAccounting().UpdateProjectBandwidthLimit(ctx, projectUUID, *arguments.Bandwidth)
err = server.db.ProjectAccounting().UpdateProjectBandwidthLimit(ctx, project.ID, *arguments.Bandwidth)
if err != nil {
sendJSONError(w, "failed to update bandwidth",
err.Error(), http.StatusInternalServerError)
@ -255,7 +244,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
err = server.db.Console().Projects().UpdateRateLimit(ctx, projectUUID, *arguments.Rate)
err = server.db.Console().Projects().UpdateRateLimit(ctx, project.ID, *arguments.Rate)
if err != nil {
sendJSONError(w, "failed to update rate",
err.Error(), http.StatusInternalServerError)
@ -270,7 +259,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
err = server.db.Console().Projects().UpdateBurstLimit(ctx, projectUUID, *arguments.Burst)
err = server.db.Console().Projects().UpdateBurstLimit(ctx, project.ID, *arguments.Burst)
if err != nil {
sendJSONError(w, "failed to update burst",
err.Error(), http.StatusInternalServerError)
@ -285,7 +274,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
err = server.db.Console().Projects().UpdateBucketLimit(ctx, projectUUID, *arguments.Buckets)
err = server.db.Console().Projects().UpdateBucketLimit(ctx, project.ID, *arguments.Buckets)
if err != nil {
sendJSONError(w, "failed to update bucket limit",
err.Error(), http.StatusInternalServerError)
@ -300,7 +289,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
return
}
err = server.db.ProjectAccounting().UpdateProjectSegmentLimit(ctx, projectUUID, *arguments.Segments)
err = server.db.ProjectAccounting().UpdateProjectSegmentLimit(ctx, project.ID, *arguments.Segments)
if err != nil {
sendJSONError(w, "failed to update segments limit",
err.Error(), http.StatusInternalServerError)
@ -386,14 +375,7 @@ func (server *Server) renameProject(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
@ -454,14 +436,7 @@ func (server *Server) updateProjectsUserAgent(w http.ResponseWriter, r *http.Req
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusNotFound)
@ -530,9 +505,9 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
project, err := server.getProjectByAnyID(ctx, projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
sendJSONError(w, "error getting project",
err.Error(), http.StatusBadRequest)
return
}
@ -544,7 +519,7 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
}
options := buckets.ListOptions{Limit: 1, Direction: buckets.DirectionForward}
buckets, err := server.buckets.ListBuckets(ctx, projectUUID, options, macaroon.AllowedBuckets{All: true})
buckets, err := server.buckets.ListBuckets(ctx, project.ID, options, macaroon.AllowedBuckets{All: true})
if err != nil {
sendJSONError(w, "unable to list buckets",
err.Error(), http.StatusInternalServerError)
@ -556,7 +531,7 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
return
}
keys, err := server.db.Console().APIKeys().GetPagedByProjectID(ctx, projectUUID, console.APIKeyCursor{Limit: 1, Page: 1})
keys, err := server.db.Console().APIKeys().GetPagedByProjectID(ctx, project.ID, console.APIKeyCursor{Limit: 1, Page: 1})
if err != nil {
sendJSONError(w, "unable to list api-keys",
err.Error(), http.StatusInternalServerError)
@ -569,11 +544,11 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
}
// if usage exist, return error to client and exit
if server.checkUsage(ctx, w, projectUUID) {
if server.checkUsage(ctx, w, project.ID) {
return
}
err = server.db.Console().Projects().Delete(ctx, projectUUID)
err = server.db.Console().Projects().Delete(ctx, project.ID)
if err != nil {
sendJSONError(w, "unable to delete project",
err.Error(), http.StatusInternalServerError)
@ -723,19 +698,18 @@ func (server *Server) setGeofenceForProject(w http.ResponseWriter, r *http.Reque
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
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",
"", http.StatusInternalServerError)
return
}
project.DefaultPlacement = placement
@ -754,3 +728,23 @@ func bucketNames(buckets []buckets.Bucket) []string {
}
return xs
}
// getProjectByAnyID takes a string version of a project public or private ID. If a valid public or private UUID, the associated project will be returned.
func (server *Server) getProjectByAnyID(ctx context.Context, projectUUIDString string) (p *console.Project, err error) {
projectID, err := uuidFromString(projectUUIDString)
if err != nil {
return nil, Error.Wrap(err)
}
p, err = server.db.Console().Projects().GetByPublicID(ctx, projectID)
switch {
case errors.Is(err, sql.ErrNoRows):
// if failed to get by public ID, try using provided ID as a private ID
p, err = server.db.Console().Projects().Get(ctx, projectID)
return p, Error.Wrap(err)
case err != nil:
return nil, Error.Wrap(err)
default:
return p, nil
}
}

View File

@ -70,6 +70,56 @@ func TestProjectGet(t *testing.T) {
})
}
func TestProjectGetByAnyID(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(_ *zap.Logger, _ int, config *satellite.Config) {
config.Admin.Address = "127.0.0.1:0"
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
address := sat.Admin.Admin.Listener.Addr()
project, err := sat.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
require.NoError(t, err)
testGetProject := func(pid string) {
link := "http://" + address.String() + "/api/projects/" + pid
expected := fmt.Sprintf(
`{"id":"%s","publicId":"%s","name":"%s","description":"%s","userAgent":null,"ownerId":"%s","rateLimit":null,"burstLimit":null,"maxBuckets":null,"createdAt":"%s","memberCount":0,"storageLimit":"25.00 GB","bandwidthLimit":"25.00 GB","userSpecifiedStorageLimit":null,"userSpecifiedBandwidthLimit":null,"segmentLimit":10000,"defaultPlacement":0}`,
project.ID.String(),
project.PublicID.String(),
project.Name,
project.Description,
project.OwnerID.String(),
project.CreatedAt.Format(time.RFC3339Nano),
)
assertGet(ctx, t, link, expected, planet.Satellites[0].Config.Console.AuthToken)
}
// should work with either public or private ID
t.Run("Get by public ID", func(t *testing.T) {
testGetProject(project.PublicID.String())
})
t.Run("Get by private ID", func(t *testing.T) {
testGetProject(project.ID.String())
})
// should work even if provided UUID does not contain dashes
t.Run("Get by public ID no dashes", func(t *testing.T) {
publicIDNoDashes := strings.ReplaceAll(project.PublicID.String(), "-", "")
testGetProject(publicIDNoDashes)
})
t.Run("Get by private ID no dashes", func(t *testing.T) {
privateIDNoDashes := strings.ReplaceAll(project.ID.String(), "-", "")
testGetProject(privateIDNoDashes)
})
})
}
func TestProjectLimit(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,