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
This commit is contained in:
Cameron 2023-01-06 16:40:03 -05:00 committed by Storj Robot
parent ed910b6087
commit 5da2544e62
2 changed files with 183 additions and 25 deletions

View File

@ -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.

View File

@ -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)
})
}
})
}