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:
parent
091c72319a
commit
6195b8cd52
@ -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,
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user