diff --git a/satellite/accounting/projectlimitcache_test.go b/satellite/accounting/projectlimitcache_test.go index b38bdbde3..753a0b0b1 100644 --- a/satellite/accounting/projectlimitcache_test.go +++ b/satellite/accounting/projectlimitcache_test.go @@ -199,7 +199,8 @@ func TestProjectLimitCache(t *testing.T) { require.EqualValues(t, expectedSegmentLimit, *actualSegmentLimitFromDB) // rate and burst limit - require.NoError(t, projects.UpdateRateLimit(ctx, secondTestProject.ID, expectedRateLimit)) + rateLimit := expectedRateLimit + require.NoError(t, projects.UpdateRateLimit(ctx, secondTestProject.ID, &rateLimit)) require.NoError(t, projects.UpdateBurstLimit(ctx, secondTestProject.ID, expectedBurstLimit)) limits, err := projectLimitCache.GetLimits(ctx, secondTestProject.ID) diff --git a/satellite/admin/project.go b/satellite/admin/project.go index ac0c6e26e..8a4e97f19 100644 --- a/satellite/admin/project.go +++ b/satellite/admin/project.go @@ -248,7 +248,7 @@ func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) { return } - err = server.db.Console().Projects().UpdateRateLimit(ctx, project.ID, *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) diff --git a/satellite/console/accountfreezes.go b/satellite/console/accountfreezes.go index 146e48129..aeeb8840c 100644 --- a/satellite/console/accountfreezes.go +++ b/satellite/console/accountfreezes.go @@ -556,6 +556,11 @@ func (s *AccountFreezeService) LegalFreezeUser(ctx context.Context, userID uuid. return errs.New("User is already frozen due to ToS violation") } + var limits *AccountFreezeEventLimits + if freezes.BillingFreeze != nil { + limits = freezes.BillingFreeze.Limits + } + userLimits := UsageLimits{ Storage: user.ProjectStorageLimit, Bandwidth: user.ProjectBandwidthLimit, @@ -564,13 +569,16 @@ func (s *AccountFreezeService) LegalFreezeUser(ctx context.Context, userID uuid. legalFreeze := freezes.LegalFreeze if legalFreeze == nil { + if limits == nil { + limits = &AccountFreezeEventLimits{ + User: userLimits, + Projects: make(map[uuid.UUID]UsageLimits), + } + } legalFreeze = &AccountFreezeEvent{ UserID: userID, Type: LegalFreeze, - Limits: &AccountFreezeEventLimits{ - User: userLimits, - Projects: make(map[uuid.UUID]UsageLimits), - }, + Limits: limits, } } @@ -595,9 +603,15 @@ func (s *AccountFreezeService) LegalFreezeUser(ctx context.Context, userID uuid. projLimits.Segment = *p.SegmentLimit } // If project limits have been zeroed already, we should not override what is in the freeze table. - if projLimits != (UsageLimits{}) { - legalFreeze.Limits.Projects[p.ID] = projLimits + if projLimits == (UsageLimits{}) { + if freezes.BillingFreeze == nil { + continue + } + // if limits were zeroed in a billing freeze, we should use those + projLimits = freezes.BillingFreeze.Limits.Projects[p.ID] } + projLimits.RateLimit = p.RateLimit + legalFreeze.Limits.Projects[p.ID] = projLimits } _, err = tx.AccountFreezeEvents().Upsert(ctx, legalFreeze) @@ -615,6 +629,13 @@ func (s *AccountFreezeService) LegalFreezeUser(ctx context.Context, userID uuid. if err != nil { return err } + + // zero project's rate limit to prevent lists/deletes + zeroLimit := 0 + err = tx.Projects().UpdateRateLimit(ctx, proj.ID, &zeroLimit) + if err != nil { + return ErrAccountFreeze.Wrap(err) + } } if freezes.BillingWarning != nil { @@ -662,6 +683,12 @@ func (s *AccountFreezeService) LegalUnfreezeUser(ctx context.Context, userID uui if err != nil { return err } + + // remove rate limit + err = tx.Projects().UpdateRateLimit(ctx, id, limits.RateLimit) + if err != nil { + return ErrAccountFreeze.Wrap(err) + } } err = tx.Users().UpdateUserProjectLimits(ctx, userID, event.Limits.User) diff --git a/satellite/console/accountfreezes_test.go b/satellite/console/accountfreezes_test.go index 87c5b1145..3c8fd1f30 100644 --- a/satellite/console/accountfreezes_test.go +++ b/satellite/console/accountfreezes_test.go @@ -16,6 +16,7 @@ import ( "storj.io/storj/private/testplanet" "storj.io/storj/satellite" "storj.io/storj/satellite/console" + "storj.io/uplink" ) func getUserLimits(u *console.User) console.UsageLimits { @@ -31,6 +32,7 @@ func getProjectLimits(p *console.Project) console.UsageLimits { Storage: p.StorageLimit.Int64(), Bandwidth: p.BandwidthLimit.Int64(), Segment: *p.SegmentLimit, + RateLimit: p.RateLimit, } } @@ -261,18 +263,26 @@ func TestAccountLegalFreeze(t *testing.T) { require.NoError(t, usersDB.UpdateUserProjectLimits(ctx, user.ID, userLimits)) projLimits := randUsageLimits() + rateLimit := 20000 + projLimits.RateLimit = &rateLimit proj, err := sat.AddProject(ctx, user.ID, "") require.NoError(t, err) require.NoError(t, projectsDB.UpdateUsageLimits(ctx, proj.ID, projLimits)) + require.NoError(t, projectsDB.UpdateRateLimit(ctx, proj.ID, projLimits.RateLimit)) - checkLimits := func(testT *testing.T) { + checkLimits := func(t *testing.T) { user, err = usersDB.Get(ctx, user.ID) require.NoError(t, err) require.Zero(t, getUserLimits(user)) proj, err = projectsDB.Get(ctx, proj.ID) require.NoError(t, err) - require.Zero(t, getProjectLimits(proj)) + usageLimits := getProjectLimits(proj) + require.Zero(t, usageLimits.Segment) + require.Zero(t, usageLimits.Storage) + require.Zero(t, usageLimits.Bandwidth) + zeroLimit := 0 + require.Equal(t, &zeroLimit, usageLimits.RateLimit) } frozen, err := service.IsUserFrozen(ctx, user.ID, console.LegalFreeze) @@ -322,6 +332,16 @@ func TestAccountLegalFreeze(t *testing.T) { require.Nil(t, freezes.LegalFreeze.DaysTillEscalation) checkLimits(t) + + require.NoError(t, service.LegalUnfreezeUser(ctx, user.ID)) + + user, err = usersDB.Get(ctx, user.ID) + require.NoError(t, err) + require.Equal(t, userLimits, getUserLimits(user)) + + proj, err = projectsDB.Get(ctx, proj.ID) + require.NoError(t, err) + require.Equal(t, projLimits, getProjectLimits(proj)) }) } @@ -490,6 +510,9 @@ func TestFreezeEffects(t *testing.T) { Reconfigure: testplanet.Reconfigure{ Satellite: func(log *zap.Logger, index int, config *satellite.Config) { config.AccountFreeze.Enabled = true + // disable limit caching + config.ProjectLimit.CacheCapacity = 0 + config.Metainfo.RateLimiter.CacheCapacity = 0 }, }, }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { @@ -520,67 +543,86 @@ func TestFreezeEffects(t *testing.T) { shouldNotUploadAndDownload := func(testT *testing.T) { // Should not be able to upload because account is frozen. err = uplink1.Upload(ctx, sat, bucketName, path, expectedData) - require.Error(t, err) + require.Error(testT, err) // Should not be able to download because account is frozen. _, err = uplink1.Download(ctx, sat, bucketName, path) - require.Error(t, err) + require.Error(testT, err) // Should not be able to create bucket because account is frozen. err = uplink1.CreateBucket(ctx, sat, "anotherBucket") - require.Error(t, err) + require.Error(testT, err) } shouldListAndDelete := func(testT *testing.T) { // Should be able to list even if frozen. - objects, err := uplink1.ListObjects(ctx, sat, bucketName) - require.NoError(t, err) - require.Len(t, objects, 1) + _, err := uplink1.ListObjects(ctx, sat, bucketName) + require.NoError(testT, err) // Should be able to delete even if frozen. err = uplink1.DeleteObject(ctx, sat, bucketName, path) - require.NoError(t, err) + require.NoError(testT, err) + } + + shouldNotListAndDelete := func(testT *testing.T) { + // Should not be able to list. + _, err := uplink1.ListObjects(ctx, sat, bucketName) + require.Error(testT, err) + require.ErrorIs(testT, err, uplink.ErrTooManyRequests) + + // Should not be able to delete. + err = uplink1.DeleteObject(ctx, sat, bucketName, path) + require.Error(testT, err) + require.ErrorIs(testT, err, uplink.ErrTooManyRequests) } t.Run("BillingFreeze effect on project owner", func(t *testing.T) { shouldUploadAndDownload(t) + shouldListAndDelete(t) require.NoError(t, freezeService.BillingWarnUser(ctx, user1.ID)) - // Should be able to download because account is not frozen. + // Should be able to download and list because account is not frozen. shouldUploadAndDownload(t) + shouldListAndDelete(t) require.NoError(t, freezeService.BillingFreezeUser(ctx, user1.ID)) shouldNotUploadAndDownload(t) - shouldListAndDelete(t) require.NoError(t, freezeService.BillingUnfreezeUser(ctx, user1.ID)) + + shouldUploadAndDownload(t) }) t.Run("ViolationFreeze effect on project owner", func(t *testing.T) { shouldUploadAndDownload(t) + shouldListAndDelete(t) require.NoError(t, freezeService.ViolationFreezeUser(ctx, user1.ID)) shouldNotUploadAndDownload(t) - shouldListAndDelete(t) require.NoError(t, freezeService.ViolationUnfreezeUser(ctx, user1.ID)) + + shouldUploadAndDownload(t) }) t.Run("LegalFreeze effect on project owner", func(t *testing.T) { shouldUploadAndDownload(t) + shouldListAndDelete(t) require.NoError(t, freezeService.LegalFreezeUser(ctx, user1.ID)) shouldNotUploadAndDownload(t) - - shouldListAndDelete(t) + shouldNotListAndDelete(t) require.NoError(t, freezeService.LegalUnfreezeUser(ctx, user1.ID)) + + shouldListAndDelete(t) + shouldUploadAndDownload(t) }) }) } diff --git a/satellite/console/observerupgradeuser.go b/satellite/console/observerupgradeuser.go index 4615b5980..7e73f51f1 100644 --- a/satellite/console/observerupgradeuser.go +++ b/satellite/console/observerupgradeuser.go @@ -18,15 +18,17 @@ type UpgradeUserObserver struct { transactionsDB billing.TransactionsDB usageLimitsConfig UsageLimitsConfig userBalanceForUpgrade int64 + freezeService *AccountFreezeService } // NewUpgradeUserObserver creates new observer instance. -func NewUpgradeUserObserver(consoleDB DB, transactionsDB billing.TransactionsDB, usageLimitsConfig UsageLimitsConfig, userBalanceForUpgrade int64) *UpgradeUserObserver { +func NewUpgradeUserObserver(consoleDB DB, transactionsDB billing.TransactionsDB, usageLimitsConfig UsageLimitsConfig, userBalanceForUpgrade int64, freezeService *AccountFreezeService) *UpgradeUserObserver { return &UpgradeUserObserver{ consoleDB: consoleDB, transactionsDB: transactionsDB, usageLimitsConfig: usageLimitsConfig, userBalanceForUpgrade: userBalanceForUpgrade, + freezeService: freezeService, } } @@ -34,6 +36,16 @@ func NewUpgradeUserObserver(consoleDB DB, transactionsDB billing.TransactionsDB, func (o *UpgradeUserObserver) Process(ctx context.Context, transaction billing.Transaction) (err error) { defer mon.Task()(&ctx)(&err) + freezes, err := o.freezeService.GetAll(ctx, transaction.UserID) + if err != nil { + return err + } + + if freezes.LegalFreeze != nil || freezes.ViolationFreeze != nil { + // user can not exit these freezes by paying with tokens + return nil + } + user, err := o.consoleDB.Users().Get(ctx, transaction.UserID) if err != nil { return err diff --git a/satellite/console/projects.go b/satellite/console/projects.go index a21151e92..976a0d440 100644 --- a/satellite/console/projects.go +++ b/satellite/console/projects.go @@ -43,7 +43,7 @@ type Projects interface { ListByOwnerID(ctx context.Context, userID uuid.UUID, cursor ProjectsCursor) (ProjectsPage, error) // UpdateRateLimit is a method for updating projects rate limit. - UpdateRateLimit(ctx context.Context, id uuid.UUID, newLimit int) error + UpdateRateLimit(ctx context.Context, id uuid.UUID, newLimit *int) error // UpdateBurstLimit is a method for updating projects burst limit. UpdateBurstLimit(ctx context.Context, id uuid.UUID, newLimit int) error diff --git a/satellite/console/projects_test.go b/satellite/console/projects_test.go index dc05e14f4..d7dc33f6a 100644 --- a/satellite/console/projects_test.go +++ b/satellite/console/projects_test.go @@ -442,7 +442,7 @@ func TestRateLimit_ProjectRateLimitZero(t *testing.T) { require.Len(t, projects, 1) zeroRateLimit := 0 - err = satellite.DB.Console().Projects().UpdateRateLimit(ctx, projects[0].ID, zeroRateLimit) + err = satellite.DB.Console().Projects().UpdateRateLimit(ctx, projects[0].ID, &zeroRateLimit) require.NoError(t, err) var group errs2.Group diff --git a/satellite/console/projectusagelimits.go b/satellite/console/projectusagelimits.go index 06681efbf..ecfcc1d91 100644 --- a/satellite/console/projectusagelimits.go +++ b/satellite/console/projectusagelimits.go @@ -24,4 +24,5 @@ type UsageLimits struct { Storage int64 `json:"storage"` Bandwidth int64 `json:"bandwidth"` Segment int64 `json:"segment"` + RateLimit *int `json:"rateLimit"` } diff --git a/satellite/core.go b/satellite/core.go index a5da6f08e..10de99f39 100644 --- a/satellite/core.go +++ b/satellite/core.go @@ -525,15 +525,12 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, debug.Cycle("Payments Storjscan", peer.Payments.StorjscanChore.TransactionCycle), ) + freezeService := console.NewAccountFreezeService(peer.DB.Console(), peer.Analytics.Service, config.Console.AccountFreeze) choreObservers := billing.ChoreObservers{ - UpgradeUser: console.NewUpgradeUserObserver(peer.DB.Console(), peer.DB.Billing(), config.Console.UsageLimits, config.Console.UserBalanceForUpgrade), + UpgradeUser: console.NewUpgradeUserObserver(peer.DB.Console(), peer.DB.Billing(), config.Console.UsageLimits, config.Console.UserBalanceForUpgrade, freezeService), PayInvoices: console.NewInvoiceTokenPaymentObserver( peer.DB.Console(), peer.Payments.Accounts.Invoices(), - console.NewAccountFreezeService( - peer.DB.Console(), - peer.Analytics.Service, - config.Console.AccountFreeze, - ), + freezeService, ), } diff --git a/satellite/payments/billing/chore_test.go b/satellite/payments/billing/chore_test.go index c64426132..e322f39c7 100644 --- a/satellite/payments/billing/chore_test.go +++ b/satellite/payments/billing/chore_test.go @@ -76,7 +76,7 @@ func TestChore(t *testing.T) { assert.Equal(t, expected, actual, "unexpected balance for user %s (%q)", userID, names[userID]) } - runTest := func(ctx *testcontext.Context, t *testing.T, consoleDB console.DB, db billing.TransactionsDB, bonusRate int64, mikeTXs, joeTXs, robertTXs []billing.Transaction, mikeBalance, joeBalance, robertBalance currency.Amount, usageLimitsConfig console.UsageLimitsConfig, userBalanceForUpgrade int64) { + runTest := func(ctx *testcontext.Context, t *testing.T, consoleDB console.DB, db billing.TransactionsDB, bonusRate int64, mikeTXs, joeTXs, robertTXs []billing.Transaction, mikeBalance, joeBalance, robertBalance currency.Amount, usageLimitsConfig console.UsageLimitsConfig, userBalanceForUpgrade int64, freezeService *console.AccountFreezeService) { paymentTypes := []billing.PaymentType{ newFakePaymentType(billing.StorjScanSource, []billing.Transaction{mike1, joe1, joe2}, @@ -88,7 +88,7 @@ func TestChore(t *testing.T) { } choreObservers := billing.ChoreObservers{ - UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db, usageLimitsConfig, userBalanceForUpgrade), + UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db, usageLimitsConfig, userBalanceForUpgrade, freezeService), } chore := billing.NewChore(zaptest.NewLogger(t), paymentTypes, db, time.Hour, false, bonusRate, choreObservers) @@ -118,6 +118,8 @@ func TestChore(t *testing.T) { sat := planet.Satellites[0] db := sat.DB + freezeService := console.NewAccountFreezeService(db.Console(), sat.Core.Analytics.Service, sat.Config.Console.AccountFreeze) + runTest(ctx, t, db.Console(), db.Billing(), 0, []billing.Transaction{mike2, mike1}, []billing.Transaction{joe1, joe2}, @@ -127,6 +129,7 @@ func TestChore(t *testing.T) { currency.AmountFromBaseUnits(30000000, currency.USDollarsMicro), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, + freezeService, ) }) }) @@ -138,6 +141,8 @@ func TestChore(t *testing.T) { sat := planet.Satellites[0] db := sat.DB + freezeService := console.NewAccountFreezeService(db.Console(), sat.Core.Analytics.Service, sat.Config.Console.AccountFreeze) + runTest(ctx, t, db.Console(), db.Billing(), 10, []billing.Transaction{mike2, mike2Bonus, mike1, mike1Bonus}, []billing.Transaction{joe1, joe1Bonus, joe2}, @@ -147,6 +152,7 @@ func TestChore(t *testing.T) { currency.AmountFromBaseUnits(30000000, currency.USDollarsMicro), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, + freezeService, ) }) }) @@ -161,29 +167,48 @@ func TestChore_UpgradeUserObserver(t *testing.T) { usageLimitsConfig := sat.Config.Console.UsageLimits ts := makeTimestamp() + freezeService := console.NewAccountFreezeService(db.Console(), sat.Core.Analytics.Service, sat.Config.Console.AccountFreeze) + user, err := sat.AddUser(ctx, console.CreateUser{ FullName: "Test User", Email: "choreobserver@mail.test", }, 1) require.NoError(t, err) + user2, err := sat.AddUser(ctx, console.CreateUser{ + FullName: "Test User", + Email: "choreobserver2@mail.test", + }, 1) + require.NoError(t, err) + + user3, err := sat.AddUser(ctx, console.CreateUser{ + FullName: "Test User", + Email: "choreobserver3@mail.test", + }, 1) + require.NoError(t, err) + _, err = sat.AddProject(ctx, user.ID, "Test Project") require.NoError(t, err) choreObservers := billing.ChoreObservers{ - UpgradeUser: console.NewUpgradeUserObserver(db.Console(), db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade), + UpgradeUser: console.NewUpgradeUserObserver(db.Console(), db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, freezeService), } amount1 := int64(200) // $2 amount2 := int64(800) // $8 transaction1 := makeFakeTransaction(user.ID, billing.StorjScanSource, billing.TransactionTypeCredit, amount1, ts, `{"fake": "transaction1"}`) transaction2 := makeFakeTransaction(user.ID, billing.StorjScanSource, billing.TransactionTypeCredit, amount2, ts.Add(time.Second*2), `{"fake": "transaction2"}`) + transaction3 := makeFakeTransaction(user2.ID, billing.StorjScanSource, billing.TransactionTypeCredit, amount1+amount2, ts, `{"fake": "transaction3"}`) + transaction4 := makeFakeTransaction(user3.ID, billing.StorjScanSource, billing.TransactionTypeCredit, amount1+amount2, ts.Add(time.Second*2), `{"fake": "transaction4"}`) paymentTypes := []billing.PaymentType{ newFakePaymentType(billing.StorjScanSource, []billing.Transaction{transaction1}, []billing.Transaction{}, []billing.Transaction{transaction2}, []billing.Transaction{}, + []billing.Transaction{transaction3}, + []billing.Transaction{transaction4}, + []billing.Transaction{}, ), } @@ -241,6 +266,39 @@ func TestChore_UpgradeUserObserver(t *testing.T) { require.Equal(t, usageLimitsConfig.Segment.Paid, *p.SegmentLimit) } }) + + t.Run("no upgrade for legal/violation freeze", func(t *testing.T) { + require.NoError(t, freezeService.LegalFreezeUser(ctx, user2.ID)) + require.NoError(t, freezeService.ViolationFreezeUser(ctx, user3.ID)) + + chore.TransactionCycle.TriggerWait() + chore.TransactionCycle.Pause() + + expected := currency.AmountFromBaseUnits((amount1+amount2)*int64(10000), currency.USDollarsMicro) + + chore.TransactionCycle.TriggerWait() + chore.TransactionCycle.Pause() + + balance, err := db.Billing().GetBalance(ctx, user2.ID) + require.NoError(t, err) + require.True(t, expected.Equal(balance)) + + chore.TransactionCycle.TriggerWait() + + balance, err = db.Billing().GetBalance(ctx, user3.ID) + require.NoError(t, err) + require.True(t, expected.Equal(balance)) + + // users should not be upgraded though they have enough balance + // since they are in legal/violation freeze. + user, err = db.Console().Users().Get(ctx, user2.ID) + require.NoError(t, err) + require.False(t, user.PaidTier) + + user, err = db.Console().Users().Get(ctx, user3.ID) + require.NoError(t, err) + require.False(t, user.PaidTier) + }) }) } @@ -275,7 +333,7 @@ func TestChore_PayInvoiceObserver(t *testing.T) { freezeService := console.NewAccountFreezeService(consoleDB, sat.Core.Analytics.Service, sat.Config.Console.AccountFreeze) choreObservers := billing.ChoreObservers{ - UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade), + UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, freezeService), PayInvoices: console.NewInvoiceTokenPaymentObserver(consoleDB, sat.Core.Payments.Accounts.Invoices(), freezeService), } diff --git a/satellite/satellitedb/projects.go b/satellite/satellitedb/projects.go index 3c0b026e6..3b6dc2d52 100644 --- a/satellite/satellitedb/projects.go +++ b/satellite/satellitedb/projects.go @@ -265,17 +265,22 @@ func (projects *projects) Update(ctx context.Context, project *console.Project) } // UpdateRateLimit is a method for updating projects rate limit. -func (projects *projects) UpdateRateLimit(ctx context.Context, id uuid.UUID, newLimit int) (err error) { +func (projects *projects) UpdateRateLimit(ctx context.Context, id uuid.UUID, newLimit *int) (err error) { defer mon.Task()(&ctx)(&err) - if newLimit < 0 { + if newLimit != nil && *newLimit < 0 { return Error.New("limit can't be set to negative value") } + rateLimit := dbx.Project_RateLimit_Null() + if newLimit != nil { + rateLimit = dbx.Project_RateLimit(*newLimit) + } + _, err = projects.db.Update_Project_By_Id(ctx, dbx.Project_Id(id[:]), dbx.Project_Update_Fields{ - RateLimit: dbx.Project_RateLimit(newLimit), + RateLimit: rateLimit, }) return err