From 5da2544e6271d8b3bfeb73fff5ef279b2955959e Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 6 Jan 2023 16:40:03 -0500 Subject: [PATCH] satellite/console: support public id as generated api parameter On generated console api endpoints allow either the project ID or the public ID to be used as the ID parameter. github issue: https://github.com/storj/storj/issues/5412 Change-Id: Ic9901ed273931a50ae12f20142a3c4938dfcc8c0 --- satellite/console/service.go | 95 ++++++++++++++++++------- satellite/console/service_test.go | 113 ++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 25 deletions(-) diff --git a/satellite/console/service.go b/satellite/console/service.go index 20e611b9b..bb916042e 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -6,6 +6,7 @@ package console import ( "context" "database/sql" + "errors" "fmt" "math" "net/http" @@ -1612,7 +1613,7 @@ func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err e return Error.Wrap(err) } - _, err = s.isProjectOwner(ctx, user.ID, projectID) + _, _, err = s.isProjectOwner(ctx, user.ID, projectID) if err != nil { return Error.Wrap(err) } @@ -1643,14 +1644,20 @@ func (s *Service) GenDeleteProject(ctx context.Context, projectID uuid.UUID) (ht } } - _, err = s.isProjectOwner(ctx, user.ID, projectID) + _, p, err := s.isProjectOwner(ctx, user.ID, projectID) if err != nil { + status := http.StatusInternalServerError + if ErrUnauthorized.Has(err) { + status = http.StatusUnauthorized + } return api.HTTPError{ - Status: http.StatusUnauthorized, + Status: status, Err: Error.Wrap(err), } } + projectID = p.ID + err = s.checkProjectCanBeDeleted(ctx, user, projectID) if err != nil { return api.HTTPError{ @@ -1766,7 +1773,6 @@ func (s *Service) GenUpdateProject(ctx context.Context, projectID uuid.UUID, pro Err: Error.Wrap(err), } } - err = ValidateNameAndDescription(projectInfo.Name, projectInfo.Description) if err != nil { return nil, api.HTTPError{ @@ -1915,10 +1921,13 @@ func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, return Error.Wrap(err) } - if _, err = s.isProjectMember(ctx, user.ID, projectID); err != nil { + var isMember isProjectMember + if isMember, err = s.isProjectMember(ctx, user.ID, projectID); err != nil { return Error.Wrap(err) } + projectID = isMember.project.ID + var userIDs []uuid.UUID var userErr errs.Group @@ -1930,7 +1939,7 @@ func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, continue } - isOwner, err := s.isProjectOwner(ctx, user.ID, projectID) + isOwner, _, err := s.isProjectOwner(ctx, user.ID, projectID) if isOwner { return ErrValidation.New(projectOwnerDeletionForbiddenErrMsg, user.Email) } @@ -2045,7 +2054,7 @@ func (s *Service) GenCreateAPIKey(ctx context.Context, requestInfo CreateAPIKeyR } } - projectID, err := uuid.FromString(requestInfo.ProjectID) + reqProjectID, err := uuid.FromString(requestInfo.ProjectID) if err != nil { return nil, api.HTTPError{ Status: http.StatusBadRequest, @@ -2053,7 +2062,7 @@ func (s *Service) GenCreateAPIKey(ctx context.Context, requestInfo CreateAPIKeyR } } - _, err = s.isProjectMember(ctx, user.ID, projectID) + isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID) if err != nil { return nil, api.HTTPError{ Status: http.StatusUnauthorized, @@ -2061,6 +2070,8 @@ func (s *Service) GenCreateAPIKey(ctx context.Context, requestInfo CreateAPIKeyR } } + projectID := isMember.project.ID + _, err = s.store.APIKeys().GetByNameAndProjectID(ctx, requestInfo.Name, projectID) if err == nil { return nil, api.HTTPError{ @@ -2101,6 +2112,9 @@ func (s *Service) GenCreateAPIKey(ctx context.Context, requestInfo CreateAPIKeyR } } + // in case the project ID from the request is the public ID, replace projectID with reqProjectID + info.ProjectID = reqProjectID + return &CreateAPIKeyResponse{ Key: key.Serialize(), KeyInfo: info, @@ -2279,19 +2293,21 @@ func (s *Service) DeleteAPIKeyByNameAndProjectID(ctx context.Context, name strin } // GetAPIKeys returns paged api key list for given Project. -func (s *Service) GetAPIKeys(ctx context.Context, projectID uuid.UUID, cursor APIKeyCursor) (page *APIKeyPage, err error) { +func (s *Service) GetAPIKeys(ctx context.Context, reqProjectID uuid.UUID, cursor APIKeyCursor) (page *APIKeyPage, err error) { defer mon.Task()(&ctx)(&err) - user, err := s.getUserAndAuditLog(ctx, "get api keys", zap.String("projectID", projectID.String())) + user, err := s.getUserAndAuditLog(ctx, "get api keys", zap.String("projectID", reqProjectID.String())) if err != nil { return nil, Error.Wrap(err) } - _, err = s.isProjectMember(ctx, user.ID, projectID) + isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID) if err != nil { return nil, ErrUnauthorized.Wrap(err) } + projectID := isMember.project.ID + if cursor.Limit > maxLimit { cursor.Limit = maxLimit } @@ -2301,7 +2317,14 @@ func (s *Service) GetAPIKeys(ctx context.Context, projectID uuid.UUID, cursor AP return nil, Error.Wrap(err) } - return + // if project ID from request is public ID, replace api key's project IDs with public ID + if projectID != reqProjectID { + for i := range page.APIKeys { + page.APIKeys[i].ProjectID = reqProjectID + } + } + + return page, err } // CreateRESTKey creates a satellite rest key. @@ -2438,11 +2461,11 @@ func (s *Service) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID } // GenGetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period for generated api. -func (s *Service) GenGetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since, before time.Time) (rollups []accounting.BucketUsageRollup, httpError api.HTTPError) { +func (s *Service) GenGetBucketUsageRollups(ctx context.Context, reqProjectID uuid.UUID, since, before time.Time) (rollups []accounting.BucketUsageRollup, httpError api.HTTPError) { var err error defer mon.Task()(&ctx)(&err) - user, err := s.getUserAndAuditLog(ctx, "get bucket usage rollups", zap.String("projectID", projectID.String())) + user, err := s.getUserAndAuditLog(ctx, "get bucket usage rollups", zap.String("projectID", reqProjectID.String())) if err != nil { return nil, api.HTTPError{ Status: http.StatusUnauthorized, @@ -2450,7 +2473,7 @@ func (s *Service) GenGetBucketUsageRollups(ctx context.Context, projectID uuid.U } } - _, err = s.isProjectMember(ctx, user.ID, projectID) + isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID) if err != nil { return nil, api.HTTPError{ Status: http.StatusUnauthorized, @@ -2458,6 +2481,8 @@ func (s *Service) GenGetBucketUsageRollups(ctx context.Context, projectID uuid.U } } + projectID := isMember.project.ID + rollups, err = s.projectAccounting.GetBucketUsageRollups(ctx, projectID, since, before) if err != nil { return nil, api.HTTPError{ @@ -2466,15 +2491,22 @@ func (s *Service) GenGetBucketUsageRollups(ctx context.Context, projectID uuid.U } } - return + // if project ID from request is public ID, replace rollup's project ID with public ID + if reqProjectID != projectID { + for i := range rollups { + rollups[i].ProjectID = reqProjectID + } + } + + return rollups, httpError } // GenGetSingleBucketUsageRollup retrieves usage rollup for single bucket of particular project for a given period for generated api. -func (s *Service) GenGetSingleBucketUsageRollup(ctx context.Context, projectID uuid.UUID, bucket string, since, before time.Time) (rollup *accounting.BucketUsageRollup, httpError api.HTTPError) { +func (s *Service) GenGetSingleBucketUsageRollup(ctx context.Context, reqProjectID uuid.UUID, bucket string, since, before time.Time) (rollup *accounting.BucketUsageRollup, httpError api.HTTPError) { var err error defer mon.Task()(&ctx)(&err) - user, err := s.getUserAndAuditLog(ctx, "get single bucket usage rollup", zap.String("projectID", projectID.String())) + user, err := s.getUserAndAuditLog(ctx, "get single bucket usage rollup", zap.String("projectID", reqProjectID.String())) if err != nil { return nil, api.HTTPError{ Status: http.StatusUnauthorized, @@ -2482,7 +2514,7 @@ func (s *Service) GenGetSingleBucketUsageRollup(ctx context.Context, projectID u } } - _, err = s.isProjectMember(ctx, user.ID, projectID) + isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID) if err != nil { return nil, api.HTTPError{ Status: http.StatusUnauthorized, @@ -2490,6 +2522,8 @@ func (s *Service) GenGetSingleBucketUsageRollup(ctx context.Context, projectID u } } + projectID := isMember.project.ID + rollup, err = s.projectAccounting.GetSingleBucketUsageRollup(ctx, projectID, bucket, since, before) if err != nil { return nil, api.HTTPError{ @@ -2498,7 +2532,10 @@ func (s *Service) GenGetSingleBucketUsageRollup(ctx context.Context, projectID u } } - return + // make sure to replace rollup project ID with reqProjectID in case it is the public ID + rollup.ProjectID = reqProjectID + + return rollup, httpError } // GetDailyProjectUsage returns daily usage by project ID. @@ -2791,18 +2828,26 @@ type isProjectMember struct { } // isProjectOwner checks if the user is an owner of a project. -func (s *Service) isProjectOwner(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (isOwner bool, err error) { +func (s *Service) isProjectOwner(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (isOwner bool, project *Project, err error) { defer mon.Task()(&ctx)(&err) - project, err := s.store.Projects().Get(ctx, projectID) + + project, err = s.store.Projects().GetByPublicID(ctx, projectID) if err != nil { - return false, err + if errors.Is(err, sql.ErrNoRows) { + project, err = s.store.Projects().Get(ctx, projectID) + if err != nil { + return false, nil, Error.Wrap(err) + } + } else { + return false, nil, Error.Wrap(err) + } } if project.OwnerID != userID { - return false, ErrUnauthorized.New(unauthorizedErrMsg) + return false, nil, ErrUnauthorized.New(unauthorizedErrMsg) } - return true, nil + return true, project, nil } // isProjectMember checks if the user is a member of given project. diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index 337d6e5c0..f2698d5de 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -1188,3 +1188,116 @@ func TestPaymentsWalletPayments(t *testing.T) { require.Equal(t, expected, walletPayments.Payments) }) } + +func TestServiceGenMethods(t *testing.T) { + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 2, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + sat := planet.Satellites[0] + s := sat.API.Console.Service + u0 := planet.Uplinks[0] + u1 := planet.Uplinks[1] + user0Ctx, err := sat.UserContext(ctx, u0.Projects[0].Owner.ID) + require.NoError(t, err) + user1Ctx, err := sat.UserContext(ctx, u1.Projects[0].Owner.ID) + require.NoError(t, err) + + p0ID := u0.Projects[0].ID + p, err := s.GetProject(user1Ctx, u1.Projects[0].ID) + require.NoError(t, err) + p1PublicID := p.PublicID + + for _, tt := range []struct { + name string + ID uuid.UUID + ctx context.Context + uplink *testplanet.Uplink + }{ + {"projectID", p0ID, user0Ctx, u0}, + {"publicID", p1PublicID, user1Ctx, u1}, + } { + + t.Run("GenUpdateProject with "+tt.name, func(t *testing.T) { + updatedName := "name " + tt.name + updatedDescription := "desc " + tt.name + updatedStorageLimit := memory.Size(100) + updatedBandwidthLimit := memory.Size(100) + + info := console.ProjectInfo{ + Name: updatedName, + Description: updatedDescription, + StorageLimit: updatedStorageLimit, + BandwidthLimit: updatedBandwidthLimit, + } + updatedProject, err := s.GenUpdateProject(tt.ctx, tt.ID, info) + require.NoError(t, err.Err) + if tt.name == "projectID" { + require.Equal(t, tt.ID, updatedProject.ID) + } else { + require.Equal(t, tt.ID, updatedProject.PublicID) + } + require.Equal(t, info.Name, updatedProject.Name) + require.Equal(t, info.Description, updatedProject.Description) + }) + t.Run("GenCreateAPIKey with "+tt.name, func(t *testing.T) { + request := console.CreateAPIKeyRequest{ + ProjectID: tt.ID.String(), + Name: tt.name + " Key", + } + apiKey, err := s.GenCreateAPIKey(tt.ctx, request) + require.NoError(t, err.Err) + require.Equal(t, tt.ID, apiKey.KeyInfo.ProjectID) + require.Equal(t, request.Name, apiKey.KeyInfo.Name) + }) + t.Run("GenGetAPIKeys with "+tt.name, func(t *testing.T) { + apiKeys, err := s.GenGetAPIKeys(tt.ctx, tt.ID, "", 10, 1, 0, 0) + require.NoError(t, err.Err) + require.NotEmpty(t, apiKeys) + for _, key := range apiKeys.APIKeys { + require.Equal(t, tt.ID, key.ProjectID) + } + }) + + bucket := "testbucket" + require.NoError(t, tt.uplink.CreateBucket(tt.ctx, sat, bucket)) + require.NoError(t, tt.uplink.Upload(tt.ctx, sat, bucket, "helloworld.txt", []byte("hello world"))) + sat.Accounting.Tally.Loop.TriggerWait() + + t.Run("GenGetSingleBucketUsageRollup with "+tt.name, func(t *testing.T) { + rollup, err := s.GenGetSingleBucketUsageRollup(tt.ctx, tt.ID, bucket, time.Now().Add(-24*time.Hour), time.Now()) + require.NoError(t, err.Err) + require.NotNil(t, rollup) + require.Equal(t, tt.ID, rollup.ProjectID) + }) + t.Run("GenGetBucketUsageRollups with "+tt.name, func(t *testing.T) { + rollups, err := s.GenGetBucketUsageRollups(tt.ctx, tt.ID, time.Now().Add(-24*time.Hour), time.Now()) + require.NoError(t, err.Err) + require.NotEmpty(t, rollups) + for _, r := range rollups { + require.Equal(t, tt.ID, r.ProjectID) + } + }) + + // create empty project for easy deletion + p, err := s.CreateProject(tt.ctx, console.ProjectInfo{ + Name: "foo", + Description: "bar", + }) + require.NoError(t, err) + + t.Run("GenDeleteProject with "+tt.name, func(t *testing.T) { + var id uuid.UUID + if tt.name == "projectID" { + id = p.ID + } else { + id = p.PublicID + } + httpErr := s.GenDeleteProject(tt.ctx, id) + require.NoError(t, httpErr.Err) + p, err := s.GetProject(ctx, id) + require.Error(t, err) + require.Nil(t, p) + }) + } + }) +}