2022-12-15 03:56:11 +00:00
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package console
import (
"context"
2022-12-15 07:11:03 +00:00
"database/sql"
"errors"
2022-12-15 03:56:11 +00:00
"time"
2022-12-15 07:11:03 +00:00
"github.com/zeebo/errs"
2022-12-15 03:56:11 +00:00
"storj.io/common/uuid"
2023-03-10 11:19:07 +00:00
"storj.io/storj/satellite/analytics"
2022-12-15 03:56:11 +00:00
)
2022-12-15 07:11:03 +00:00
// ErrAccountFreeze is the class for errors that occur during operation of the account freeze service.
var ErrAccountFreeze = errs . Class ( "account freeze service" )
2023-09-28 15:55:24 +01:00
// ErrFreezeUserStatusUpdate is error returned if updating the user status as part of violation (un)freeze
// fails.
var ErrFreezeUserStatusUpdate = errs . New ( "user status update failed" )
2022-12-15 03:56:11 +00:00
// AccountFreezeEvents exposes methods to manage the account freeze events table in database.
//
// architecture: Database
type AccountFreezeEvents interface {
2022-12-15 07:11:03 +00:00
// Upsert is a method for updating an account freeze event if it exists and inserting it otherwise.
Upsert ( ctx context . Context , event * AccountFreezeEvent ) ( * AccountFreezeEvent , error )
2022-12-15 03:56:11 +00:00
// Get is a method for querying account freeze event from the database by user ID and event type.
Get ( ctx context . Context , userID uuid . UUID , eventType AccountFreezeEventType ) ( * AccountFreezeEvent , error )
2023-07-25 15:08:57 +01:00
// GetAllEvents is a method for querying all account freeze events from the database.
GetAllEvents ( ctx context . Context , cursor FreezeEventsCursor ) ( events * FreezeEventsPage , err error )
2023-02-13 17:32:39 +00:00
// GetAll is a method for querying all account freeze events from the database by user ID.
2023-09-28 15:55:24 +01:00
GetAll ( ctx context . Context , userID uuid . UUID ) ( freezes * UserFreezeEvents , err error )
2022-12-15 03:56:11 +00:00
// DeleteAllByUserID is a method for deleting all account freeze events from the database by user ID.
DeleteAllByUserID ( ctx context . Context , userID uuid . UUID ) error
2023-03-23 12:04:32 +00:00
// DeleteByUserIDAndEvent is a method for deleting all account `eventType` events from the database by user ID.
DeleteByUserIDAndEvent ( ctx context . Context , userID uuid . UUID , eventType AccountFreezeEventType ) error
2022-12-15 03:56:11 +00:00
}
// AccountFreezeEvent represents an event related to account freezing.
type AccountFreezeEvent struct {
2023-10-18 19:53:54 +01:00
UserID uuid . UUID
Type AccountFreezeEventType
Limits * AccountFreezeEventLimits
DaysTillEscalation * int
CreatedAt time . Time
2022-12-15 03:56:11 +00:00
}
// AccountFreezeEventLimits represents the usage limits for a user's account and projects before they were frozen.
type AccountFreezeEventLimits struct {
User UsageLimits ` json:"user" `
Projects map [ uuid . UUID ] UsageLimits ` json:"projects" `
}
2023-07-25 15:08:57 +01:00
// FreezeEventsCursor holds info for freeze events
// cursor pagination.
type FreezeEventsCursor struct {
Limit int
// StartingAfter is the last user ID of the previous page.
// The next page will start after this user ID.
StartingAfter * uuid . UUID
}
// FreezeEventsPage returns paginated freeze events.
type FreezeEventsPage struct {
Events [ ] AccountFreezeEvent
// Next indicates whether there are more events to retrieve.
Next bool
}
2023-09-28 15:55:24 +01:00
// UserFreezeEvents holds the freeze events for a user.
type UserFreezeEvents struct {
BillingFreeze , BillingWarning , ViolationFreeze * AccountFreezeEvent
}
2022-12-15 03:56:11 +00:00
// AccountFreezeEventType is used to indicate the account freeze event's type.
type AccountFreezeEventType int
const (
2023-09-28 12:12:09 +01:00
// BillingFreeze signifies that the user has been frozen due to nonpayment of invoices.
BillingFreeze AccountFreezeEventType = 0
// BillingWarning signifies that the user has been warned that they may be frozen soon
// due to nonpayment of invoices.
BillingWarning AccountFreezeEventType = 1
// ViolationFreeze signifies that the user has been frozen due to ToS violation.
ViolationFreeze AccountFreezeEventType = 2
2022-12-15 03:56:11 +00:00
)
2022-12-15 07:11:03 +00:00
2023-08-15 17:58:50 +01:00
// String returns a string representation of this event.
func ( et AccountFreezeEventType ) String ( ) string {
switch et {
2023-09-28 12:12:09 +01:00
case BillingFreeze :
return "Billing Freeze"
case BillingWarning :
return "Billing Warning"
case ViolationFreeze :
return "Violation Freeze"
2023-08-15 17:58:50 +01:00
default :
return ""
}
}
2023-10-19 16:57:01 +01:00
// AccountFreezeConfig contains configurable values for account freeze service.
type AccountFreezeConfig struct {
BillingWarnGracePeriod time . Duration ` help:"How long to wait between a billing warning event and billing freezing an account." default:"360h" `
BillingFreezeGracePeriod time . Duration ` help:"How long to wait between a billing freeze event and setting pending deletion account status." default:"1440h" `
}
2022-12-15 07:11:03 +00:00
// AccountFreezeService encapsulates operations concerning account freezes.
type AccountFreezeService struct {
freezeEventsDB AccountFreezeEvents
usersDB Users
projectsDB Projects
2023-04-19 01:02:47 +01:00
tracker analytics . FreezeTracker
2023-10-19 16:57:01 +01:00
config AccountFreezeConfig
2022-12-15 07:11:03 +00:00
}
// NewAccountFreezeService creates a new account freeze service.
2023-10-19 16:57:01 +01:00
func NewAccountFreezeService ( freezeEventsDB AccountFreezeEvents , usersDB Users , projectsDB Projects , tracker analytics . FreezeTracker , config AccountFreezeConfig ) * AccountFreezeService {
2022-12-15 07:11:03 +00:00
return & AccountFreezeService {
freezeEventsDB : freezeEventsDB ,
usersDB : usersDB ,
projectsDB : projectsDB ,
2023-04-19 01:02:47 +01:00
tracker : tracker ,
2023-10-19 16:57:01 +01:00
config : config ,
2022-12-15 07:11:03 +00:00
}
}
2023-09-28 12:12:09 +01:00
// IsUserBillingFrozen returns whether the user specified by the given ID is frozen
// due to nonpayment of invoices.
func ( s * AccountFreezeService ) IsUserBillingFrozen ( ctx context . Context , userID uuid . UUID ) ( _ bool , err error ) {
2022-12-15 07:11:03 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2023-09-28 12:12:09 +01:00
_ , err = s . freezeEventsDB . Get ( ctx , userID , BillingFreeze )
2022-12-15 07:11:03 +00:00
switch {
case errors . Is ( err , sql . ErrNoRows ) :
return false , nil
case err != nil :
return false , ErrAccountFreeze . Wrap ( err )
default :
return true , nil
}
}
2023-09-28 15:55:24 +01:00
// IsUserViolationFrozen returns whether the user specified by the given ID is frozen.
func ( s * AccountFreezeService ) IsUserViolationFrozen ( ctx context . Context , userID uuid . UUID ) ( _ bool , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
_ , err = s . freezeEventsDB . Get ( ctx , userID , ViolationFreeze )
switch {
case errors . Is ( err , sql . ErrNoRows ) :
return false , nil
case err != nil :
return false , ErrAccountFreeze . Wrap ( err )
default :
return true , nil
}
}
2023-09-28 12:12:09 +01:00
// BillingFreezeUser freezes the user specified by the given ID due to nonpayment of invoices.
func ( s * AccountFreezeService ) BillingFreezeUser ( ctx context . Context , userID uuid . UUID ) ( err error ) {
2022-12-15 07:11:03 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
user , err := s . usersDB . Get ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-09-28 15:55:24 +01:00
freezes , err := s . freezeEventsDB . GetAll ( ctx , userID )
2023-03-23 12:04:32 +00:00
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-09-28 15:55:24 +01:00
if freezes . ViolationFreeze != nil {
return ErrAccountFreeze . New ( "User is already frozen due to ToS violation" )
2023-03-23 12:04:32 +00:00
}
2023-09-28 15:55:24 +01:00
userLimits := UsageLimits {
Storage : user . ProjectStorageLimit ,
Bandwidth : user . ProjectBandwidthLimit ,
Segment : user . ProjectSegmentLimit ,
}
2023-10-19 16:57:01 +01:00
daysTillEscalation := int ( s . config . BillingFreezeGracePeriod . Hours ( ) / 24 )
2023-09-28 15:55:24 +01:00
billingFreeze := freezes . BillingFreeze
if billingFreeze == nil {
billingFreeze = & AccountFreezeEvent {
2023-10-19 16:57:01 +01:00
UserID : userID ,
Type : BillingFreeze ,
DaysTillEscalation : & daysTillEscalation ,
2022-12-15 07:11:03 +00:00
Limits : & AccountFreezeEventLimits {
2023-09-28 15:55:24 +01:00
User : userLimits ,
2022-12-15 07:11:03 +00:00
Projects : make ( map [ uuid . UUID ] UsageLimits ) ,
} ,
}
}
// If user limits have been zeroed already, we should not override what is in the freeze table.
if userLimits != ( UsageLimits { } ) {
2023-09-28 15:55:24 +01:00
billingFreeze . Limits . User = userLimits
2022-12-15 07:11:03 +00:00
}
projects , err := s . projectsDB . GetOwn ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
for _ , p := range projects {
projLimits := UsageLimits { }
if p . StorageLimit != nil {
projLimits . Storage = p . StorageLimit . Int64 ( )
}
if p . BandwidthLimit != nil {
projLimits . Bandwidth = p . BandwidthLimit . Int64 ( )
}
if p . SegmentLimit != nil {
projLimits . Segment = * p . SegmentLimit
}
// If project limits have been zeroed already, we should not override what is in the freeze table.
if projLimits != ( UsageLimits { } ) {
2023-09-28 15:55:24 +01:00
billingFreeze . Limits . Projects [ p . ID ] = projLimits
2022-12-15 07:11:03 +00:00
}
}
2023-09-28 15:55:24 +01:00
_ , err = s . freezeEventsDB . Upsert ( ctx , billingFreeze )
2022-12-15 07:11:03 +00:00
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
err = s . usersDB . UpdateUserProjectLimits ( ctx , userID , UsageLimits { } )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
for _ , proj := range projects {
err := s . projectsDB . UpdateUsageLimits ( ctx , proj . ID , UsageLimits { } )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
2023-09-28 15:55:24 +01:00
if freezes . BillingWarning != nil {
err = s . freezeEventsDB . DeleteByUserIDAndEvent ( ctx , userID , BillingWarning )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
2023-04-19 01:02:47 +01:00
s . tracker . TrackAccountFrozen ( userID , user . Email )
2022-12-15 07:11:03 +00:00
return nil
}
2023-09-28 12:12:09 +01:00
// BillingUnfreezeUser reverses the billing freeze placed on the user specified by the given ID.
func ( s * AccountFreezeService ) BillingUnfreezeUser ( ctx context . Context , userID uuid . UUID ) ( err error ) {
2022-12-15 07:11:03 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2023-03-10 11:19:07 +00:00
user , err := s . usersDB . Get ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-09-28 12:12:09 +01:00
event , err := s . freezeEventsDB . Get ( ctx , userID , BillingFreeze )
2022-12-15 07:11:03 +00:00
if errors . Is ( err , sql . ErrNoRows ) {
2023-09-28 12:12:09 +01:00
return ErrAccountFreeze . New ( "user is not frozen due to nonpayment of invoices" )
2022-12-15 07:11:03 +00:00
}
if event . Limits == nil {
return ErrAccountFreeze . New ( "freeze event limits are nil" )
}
for id , limits := range event . Limits . Projects {
err := s . projectsDB . UpdateUsageLimits ( ctx , id , limits )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
err = s . usersDB . UpdateUserProjectLimits ( ctx , userID , event . Limits . User )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-09-28 15:55:24 +01:00
err = ErrAccountFreeze . Wrap ( s . freezeEventsDB . DeleteByUserIDAndEvent ( ctx , userID , BillingFreeze ) )
2023-03-10 11:19:07 +00:00
if err != nil {
return err
}
2023-10-04 10:48:21 +01:00
if user . Status == PendingDeletion {
status := Active
err = s . usersDB . Update ( ctx , userID , UpdateUserRequest {
Status : & status ,
} )
if err != nil {
return ErrAccountFreeze . Wrap ( errs . Combine ( ErrFreezeUserStatusUpdate , err ) )
}
}
2023-04-19 01:02:47 +01:00
s . tracker . TrackAccountUnfrozen ( userID , user . Email )
2023-03-10 11:19:07 +00:00
return nil
2022-12-15 07:11:03 +00:00
}
2023-02-13 17:32:39 +00:00
2023-09-28 12:12:09 +01:00
// BillingWarnUser adds a billing warning event to the freeze events table.
func ( s * AccountFreezeService ) BillingWarnUser ( ctx context . Context , userID uuid . UUID ) ( err error ) {
2023-02-13 17:32:39 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2023-03-10 11:19:07 +00:00
user , err := s . usersDB . Get ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-09-28 15:55:24 +01:00
freezes , err := s . freezeEventsDB . GetAll ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
if freezes . ViolationFreeze != nil || freezes . BillingFreeze != nil {
return ErrAccountFreeze . New ( "User is already frozen" )
}
if freezes . BillingWarning != nil {
return nil
}
2023-10-19 16:57:01 +01:00
daysTillEscalation := int ( s . config . BillingWarnGracePeriod . Hours ( ) / 24 )
2023-02-13 17:32:39 +00:00
_ , err = s . freezeEventsDB . Upsert ( ctx , & AccountFreezeEvent {
2023-10-19 16:57:01 +01:00
UserID : userID ,
Type : BillingWarning ,
DaysTillEscalation : & daysTillEscalation ,
2023-02-13 17:32:39 +00:00
} )
2023-03-10 11:19:07 +00:00
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-02-13 17:32:39 +00:00
2023-04-19 01:02:47 +01:00
s . tracker . TrackAccountFreezeWarning ( userID , user . Email )
2023-03-10 11:19:07 +00:00
return nil
2023-02-13 17:32:39 +00:00
}
2023-09-28 12:12:09 +01:00
// BillingUnWarnUser reverses the warning placed on the user specified by the given ID.
func ( s * AccountFreezeService ) BillingUnWarnUser ( ctx context . Context , userID uuid . UUID ) ( err error ) {
2023-03-23 12:04:32 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
user , err := s . usersDB . Get ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
2023-09-28 12:12:09 +01:00
_ , err = s . freezeEventsDB . Get ( ctx , userID , BillingWarning )
2023-03-23 12:04:32 +00:00
if errors . Is ( err , sql . ErrNoRows ) {
return ErrAccountFreeze . New ( "user is not warned" )
}
2023-09-28 12:12:09 +01:00
err = ErrAccountFreeze . Wrap ( s . freezeEventsDB . DeleteByUserIDAndEvent ( ctx , userID , BillingWarning ) )
2023-03-23 12:04:32 +00:00
if err != nil {
return err
}
2023-04-19 01:02:47 +01:00
s . tracker . TrackAccountUnwarned ( userID , user . Email )
2023-03-23 12:04:32 +00:00
return nil
}
2023-09-28 15:55:24 +01:00
// ViolationFreezeUser freezes the user specified by the given ID due to ToS violation.
func ( s * AccountFreezeService ) ViolationFreezeUser ( ctx context . Context , userID uuid . UUID ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
user , err := s . usersDB . Get ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
freezes , err := s . freezeEventsDB . GetAll ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
var limits * AccountFreezeEventLimits
if freezes . BillingFreeze != nil {
limits = freezes . BillingFreeze . Limits
}
userLimits := UsageLimits {
Storage : user . ProjectStorageLimit ,
Bandwidth : user . ProjectBandwidthLimit ,
Segment : user . ProjectSegmentLimit ,
}
violationFreeze := freezes . ViolationFreeze
if violationFreeze == nil {
if limits == nil {
limits = & AccountFreezeEventLimits {
User : userLimits ,
Projects : make ( map [ uuid . UUID ] UsageLimits ) ,
}
}
violationFreeze = & AccountFreezeEvent {
UserID : userID ,
Type : ViolationFreeze ,
Limits : limits ,
}
}
// If user limits have been zeroed already, we should not override what is in the freeze table.
if userLimits != ( UsageLimits { } ) {
violationFreeze . Limits . User = userLimits
}
projects , err := s . projectsDB . GetOwn ( ctx , userID )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
for _ , p := range projects {
projLimits := UsageLimits { }
if p . StorageLimit != nil {
projLimits . Storage = p . StorageLimit . Int64 ( )
}
if p . BandwidthLimit != nil {
projLimits . Bandwidth = p . BandwidthLimit . Int64 ( )
}
if p . SegmentLimit != nil {
projLimits . Segment = * p . SegmentLimit
}
// If project limits have been zeroed already, we should not override what is in the freeze table.
if projLimits != ( UsageLimits { } ) {
violationFreeze . Limits . Projects [ p . ID ] = projLimits
}
}
_ , err = s . freezeEventsDB . Upsert ( ctx , violationFreeze )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
err = s . usersDB . UpdateUserProjectLimits ( ctx , userID , UsageLimits { } )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
for _ , proj := range projects {
err := s . projectsDB . UpdateUsageLimits ( ctx , proj . ID , UsageLimits { } )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
status := PendingDeletion
err = s . usersDB . Update ( ctx , userID , UpdateUserRequest {
Status : & status ,
} )
if err != nil {
return ErrAccountFreeze . Wrap ( errs . Combine ( ErrFreezeUserStatusUpdate , err ) )
}
if freezes . BillingWarning != nil {
err = s . freezeEventsDB . DeleteByUserIDAndEvent ( ctx , userID , BillingWarning )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
if freezes . BillingFreeze != nil {
err = s . freezeEventsDB . DeleteByUserIDAndEvent ( ctx , userID , BillingFreeze )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
return nil
}
// ViolationUnfreezeUser reverses the violation freeze placed on the user specified by the given ID.
func ( s * AccountFreezeService ) ViolationUnfreezeUser ( ctx context . Context , userID uuid . UUID ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
event , err := s . freezeEventsDB . Get ( ctx , userID , ViolationFreeze )
if errors . Is ( err , sql . ErrNoRows ) {
return ErrAccountFreeze . New ( "user is not violation frozen" )
}
if event . Limits == nil {
return ErrAccountFreeze . New ( "freeze event limits are nil" )
}
for id , limits := range event . Limits . Projects {
err := s . projectsDB . UpdateUsageLimits ( ctx , id , limits )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
}
err = s . usersDB . UpdateUserProjectLimits ( ctx , userID , event . Limits . User )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
err = s . freezeEventsDB . DeleteByUserIDAndEvent ( ctx , userID , ViolationFreeze )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
status := Active
err = s . usersDB . Update ( ctx , userID , UpdateUserRequest {
Status : & status ,
} )
if err != nil {
return ErrAccountFreeze . Wrap ( errs . Combine ( ErrFreezeUserStatusUpdate , err ) )
}
return nil
}
2023-02-13 17:32:39 +00:00
// GetAll returns all events for a user.
2023-09-28 15:55:24 +01:00
func ( s * AccountFreezeService ) GetAll ( ctx context . Context , userID uuid . UUID ) ( freezes * UserFreezeEvents , err error ) {
2023-02-13 17:32:39 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2023-09-28 15:55:24 +01:00
freezes , err = s . freezeEventsDB . GetAll ( ctx , userID )
2023-02-13 17:32:39 +00:00
if err != nil {
2023-09-28 15:55:24 +01:00
return nil , ErrAccountFreeze . Wrap ( err )
2023-02-13 17:32:39 +00:00
}
2023-09-28 15:55:24 +01:00
return freezes , nil
2023-02-13 17:32:39 +00:00
}
2023-04-19 01:02:47 +01:00
2023-07-25 15:08:57 +01:00
// GetAllEvents returns all events.
func ( s * AccountFreezeService ) GetAllEvents ( ctx context . Context , cursor FreezeEventsCursor ) ( events * FreezeEventsPage , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
events , err = s . freezeEventsDB . GetAllEvents ( ctx , cursor )
if err != nil {
return nil , ErrAccountFreeze . Wrap ( err )
}
return events , nil
}
2023-10-19 16:57:01 +01:00
// EscalateBillingFreeze deactivates escalation for this freeze event and sets the user status to pending deletion.
func ( s * AccountFreezeService ) EscalateBillingFreeze ( ctx context . Context , userID uuid . UUID , event AccountFreezeEvent ) error {
event . DaysTillEscalation = nil
_ , err := s . freezeEventsDB . Upsert ( ctx , & event )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
status := PendingDeletion
err = s . usersDB . Update ( ctx , userID , UpdateUserRequest {
Status : & status ,
} )
if err != nil {
return ErrAccountFreeze . Wrap ( err )
}
return nil
}
2023-04-19 01:02:47 +01:00
// TestChangeFreezeTracker changes the freeze tracker service for tests.
func ( s * AccountFreezeService ) TestChangeFreezeTracker ( t analytics . FreezeTracker ) {
s . tracker = t
}