Skip to content

Commit

Permalink
satellite/admin: harden project deletion requirements
Browse files Browse the repository at this point in the history
Change-Id: Ia7ea469f87469b16e464dc22af24b98a6ef1873d
  • Loading branch information
stefanbenten authored and ihaid committed Jul 14, 2020
1 parent 8abb907 commit eb0f6a0
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 22 deletions.
46 changes: 46 additions & 0 deletions satellite/admin/project.go
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"time"

"github.com/gorilla/mux"
"github.com/gorilla/schema"
Expand All @@ -17,6 +18,7 @@ import (
"storj.io/common/storj"
"storj.io/common/uuid"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments/stripecoinpayments"
)

func (server *Server) getProjectLimit(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -259,6 +261,50 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
return
}

// do not delete projects that have usage for the current month.
year, month, _ := time.Now().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)

currentUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectUUID, firstOfMonth, time.Now())
if err != nil {
http.Error(w, fmt.Sprintf("unable to list project usage: %v", err), http.StatusInternalServerError)
return
}
if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.ObjectCount > 0 {
http.Error(w, "usage for current month exists", http.StatusConflict)
return
}

// if usage of last month exist, make sure to look for billing records
lastMonthUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
if err != nil {
http.Error(w, "error getting project totals", http.StatusInternalServerError)
return
}

if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.ObjectCount > 0 {
err := server.db.StripeCoinPayments().ProjectRecords().Check(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour))
switch err {
case stripecoinpayments.ErrProjectRecordExists:
record, err := server.db.StripeCoinPayments().ProjectRecords().Get(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour))
if err != nil {
http.Error(w, fmt.Sprintf("unable to get project records: %v", err), http.StatusInternalServerError)
return
}
// state = 0 means unapplied and not invoiced yet.
if record.State == 0 {
http.Error(w, "unapplied project invoice record exist", http.StatusConflict)
return
}
case nil:
http.Error(w, "usage for last month exist, but is not billed yet", http.StatusConflict)
return
default:
http.Error(w, fmt.Sprintf("unable to get project records: %v", err), http.StatusInternalServerError)
return
}
}

err = server.db.Console().Projects().Delete(ctx, projectUUID)
if err != nil {
http.Error(w, fmt.Sprintf("unable to delete project: %v", err), http.StatusInternalServerError)
Expand Down
167 changes: 167 additions & 0 deletions satellite/admin/project_test.go
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
"go.uber.org/zap"
Expand All @@ -21,6 +22,7 @@ import (
"storj.io/common/uuid"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console"
)

Expand Down Expand Up @@ -193,6 +195,171 @@ func TestDeleteProject(t *testing.T) {
})
}

func TestDeleteProjectWithUsageCurrentMonth(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.Admin.Address = "127.0.0.1:0"
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
projectID := planet.Uplinks[0].Projects[0].ID

apiKeys, err := planet.Satellites[0].DB.Console().APIKeys().GetPagedByProjectID(ctx, projectID, console.APIKeyCursor{
Page: 1,
Limit: 2,
Search: "",
})
require.NoError(t, err)
require.Len(t, apiKeys.APIKeys, 1)

err = planet.Satellites[0].DB.Console().APIKeys().Delete(ctx, apiKeys.APIKeys[0].ID)
require.NoError(t, err)

accTime := time.Now().UTC().AddDate(0,0,-1)
tally := accounting.BucketStorageTally{
BucketName: "test",
ProjectID: projectID,
IntervalStart: accTime,
ObjectCount: 1,
InlineSegmentCount: 1,
RemoteSegmentCount: 1,
InlineBytes: 10,
RemoteBytes: 640000,
MetadataSize: 2,
}
err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally)
require.NoError(t, err)
tally = accounting.BucketStorageTally{
BucketName: "test",
ProjectID: projectID,
IntervalStart: accTime.AddDate(0,0,1),
ObjectCount: 1,
InlineSegmentCount: 1,
RemoteSegmentCount: 1,
InlineBytes: 10,
RemoteBytes: 640000,
MetadataSize: 2,
}
err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally)
require.NoError(t, err)

inline, remote, err := planet.Satellites[0].DB.ProjectAccounting().GetStorageTotals(ctx, projectID)
require.NoError(t, err)
require.Equal(t, int64(10), inline)
require.Equal(t, int64(640000), remote)

bw, err := planet.Satellites[0].DB.ProjectAccounting().GetAllocatedBandwidthTotal(ctx, projectID, accTime.AddDate(0,0,-1))
require.NoError(t, err)
require.EqualValues(t, 0, bw)

usage, err := planet.Satellites[0].DB.ProjectAccounting().GetProjectTotal(ctx, projectID, accTime.AddDate(0,0,-1), accTime.AddDate(0,0,2))
require.NoError(t, err)
require.NotEqual(t, 0, usage.Egress)
require.NotEqual(t, float64(0), usage.Storage)


req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/project/%s", projectID), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")

response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
responseBody, err := ioutil.ReadAll(response.Body)
require.NoError(t, err)
require.Equal(t, "usage for current month exists\n", string(responseBody))
require.NoError(t, response.Body.Close())
require.Equal(t, http.StatusConflict, response.StatusCode)
})
}

func TestDeleteProjectWithUsagePreviousMonth(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.Admin.Address = "127.0.0.1:0"
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
projectID := planet.Uplinks[0].Projects[0].ID

apiKeys, err := planet.Satellites[0].DB.Console().APIKeys().GetPagedByProjectID(ctx, projectID, console.APIKeyCursor{
Page: 1,
Limit: 2,
Search: "",
})
require.NoError(t, err)
require.Len(t, apiKeys.APIKeys, 1)

err = planet.Satellites[0].DB.Console().APIKeys().Delete(ctx, apiKeys.APIKeys[0].ID)
require.NoError(t, err)

//ToDo: Improve updating of DB entries
accTime := time.Now().UTC().AddDate(0,-1,0)
tally := accounting.BucketStorageTally{
BucketName: "test",
ProjectID: projectID,
IntervalStart: accTime,
ObjectCount: 1,
InlineSegmentCount: 1,
RemoteSegmentCount: 1,
InlineBytes: 10,
RemoteBytes: 640000,
MetadataSize: 2,
}
err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally)
require.NoError(t, err)
tally = accounting.BucketStorageTally{
BucketName: "test",
ProjectID: projectID,
IntervalStart: accTime.AddDate(0,0,1),
ObjectCount: 1,
InlineSegmentCount: 1,
RemoteSegmentCount: 1,
InlineBytes: 10,
RemoteBytes: 640000,
MetadataSize: 2,
}
err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally)
require.NoError(t, err)

inline, remote, err := planet.Satellites[0].DB.ProjectAccounting().GetStorageTotals(ctx, projectID)
require.NoError(t, err)
require.Equal(t, int64(10), inline)
require.Equal(t, int64(640000), remote)

bw, err := planet.Satellites[0].DB.ProjectAccounting().GetAllocatedBandwidthTotal(ctx, projectID, accTime.AddDate(0,0,-1))
require.NoError(t, err)
require.EqualValues(t, 0, bw)

usage, err := planet.Satellites[0].DB.ProjectAccounting().GetProjectTotal(ctx, projectID, accTime.AddDate(0,0,-1), accTime.AddDate(0,0,2))
require.NoError(t, err)
require.NotEqual(t, 0, usage.Egress)
require.NotEqual(t, float64(0), usage.Storage)


req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/project/%s", projectID), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")

response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
responseBody, err := ioutil.ReadAll(response.Body)
require.NoError(t, err)
require.Equal(t, "usage for last month exist, but is not billed yet\n", string(responseBody))
require.NoError(t, response.Body.Close())
require.Equal(t, http.StatusConflict, response.StatusCode)
})
}

func assertGet(t *testing.T, link string, expected string) {
t.Helper()

Expand Down
1 change: 1 addition & 0 deletions satellite/payments/stripecoinpayments/projectrecords.go
Expand Up @@ -47,6 +47,7 @@ type ProjectRecord struct {
Objects float64
PeriodStart time.Time
PeriodEnd time.Time
State int
}

// ProjectRecordsPage holds project records and
Expand Down
1 change: 1 addition & 0 deletions satellite/satellitedb/invoiceprojectrecords.go
Expand Up @@ -208,5 +208,6 @@ func fromDBXInvoiceProjectRecord(dbxRecord *dbx.StripecoinpaymentsInvoiceProject
Objects: float64(dbxRecord.Objects),
PeriodStart: dbxRecord.PeriodStart,
PeriodEnd: dbxRecord.PeriodEnd,
State: dbxRecord.State,
}, nil
}

0 comments on commit eb0f6a0

Please sign in to comment.