From 6195b8cd52ca29bf495aecc23f50a5737b58b726 Mon Sep 17 00:00:00 2001 From: Moby von Briesen Date: Tue, 5 Sep 2023 15:28:39 -0400 Subject: [PATCH] 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 --- satellite/admin/apikeys.go | 41 +++++++---- satellite/admin/bucket.go | 2 +- satellite/admin/common.go | 25 +++++++ satellite/admin/project.go | 124 +++++++++++++++----------------- satellite/admin/project_test.go | 50 +++++++++++++ 5 files changed, 163 insertions(+), 79 deletions(-) diff --git a/satellite/admin/apikeys.go b/satellite/admin/apikeys.go index 72feda81b..384dcad4c 100644 --- a/satellite/admin/apikeys.go +++ b/satellite/admin/apikeys.go @@ -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, diff --git a/satellite/admin/bucket.go b/satellite/admin/bucket.go index 7b1357c2e..82d716f01 100644 --- a/satellite/admin/bucket.go +++ b/satellite/admin/bucket.go @@ -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") } diff --git a/satellite/admin/common.go b/satellite/admin/common.go index 8158ece58..ceec0da1d 100644 --- a/satellite/admin/common.go +++ b/satellite/admin/common.go @@ -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 +} diff --git a/satellite/admin/project.go b/satellite/admin/project.go index ca5912469..0c7b8e8ab 100644 --- a/satellite/admin/project.go +++ b/satellite/admin/project.go @@ -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 + } +} diff --git a/satellite/admin/project_test.go b/satellite/admin/project_test.go index fa9e46d88..3fafa5b0e 100644 --- a/satellite/admin/project_test.go +++ b/satellite/admin/project_test.go @@ -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,