satellite/{db,console,billing}: fix legal/violation freeze

This change fixes the behavior of legal freeze; it now does not allow
lists and deletes for users in that freeze. It also fixes an issue
where users who have been legal/violation frozen will have their limits
upgraded, partially exiting freeze status when they pay with STORJ
tokens.

Issue: storj/storj-private/#517
storj/storj-private/#515

Change-Id: I6fa2b6353159984883c60a2888617da1ee51ce0a
This commit is contained in:
Wilfred Asomani 2023-12-01 12:25:13 +00:00 committed by Storj Robot
parent e1c12674c5
commit 0590eadc17
11 changed files with 181 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,4 +24,5 @@ type UsageLimits struct {
Storage int64 `json:"storage"`
Bandwidth int64 `json:"bandwidth"`
Segment int64 `json:"segment"`
RateLimit *int `json:"rateLimit"`
}

View File

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

View File

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

View File

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