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")
|
|
|
|
|
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-03-23 12:04:32 +00:00
|
|
|
GetAll(ctx context.Context, userID uuid.UUID) (freeze *AccountFreezeEvent, warning *AccountFreezeEvent, 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 {
|
|
|
|
UserID uuid.UUID
|
|
|
|
Type AccountFreezeEventType
|
|
|
|
Limits *AccountFreezeEventLimits
|
|
|
|
CreatedAt time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2022-12-15 03:56:11 +00:00
|
|
|
// AccountFreezeEventType is used to indicate the account freeze event's type.
|
|
|
|
type AccountFreezeEventType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Freeze signifies that the user has been frozen.
|
|
|
|
Freeze AccountFreezeEventType = 0
|
|
|
|
// Warning signifies that the user has been warned that they may be frozen soon.
|
|
|
|
Warning AccountFreezeEventType = 1
|
|
|
|
)
|
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 {
|
|
|
|
case Freeze:
|
|
|
|
return "Freeze"
|
|
|
|
case Warning:
|
|
|
|
return "Warning"
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2022-12-15 07:11:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewAccountFreezeService creates a new account freeze service.
|
2023-04-19 01:02:47 +01:00
|
|
|
func NewAccountFreezeService(freezeEventsDB AccountFreezeEvents, usersDB Users, projectsDB Projects, tracker analytics.FreezeTracker) *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,
|
2022-12-15 07:11:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsUserFrozen returns whether the user specified by the given ID is frozen.
|
|
|
|
func (s *AccountFreezeService) IsUserFrozen(ctx context.Context, userID uuid.UUID) (_ bool, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
_, err = s.freezeEventsDB.Get(ctx, userID, Freeze)
|
|
|
|
switch {
|
|
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
|
|
return false, nil
|
|
|
|
case err != nil:
|
|
|
|
return false, ErrAccountFreeze.Wrap(err)
|
|
|
|
default:
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FreezeUser freezes the user specified by the given ID.
|
|
|
|
func (s *AccountFreezeService) FreezeUser(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)
|
|
|
|
}
|
|
|
|
|
2023-03-23 12:04:32 +00:00
|
|
|
freeze, warning, err := s.freezeEventsDB.GetAll(ctx, userID)
|
|
|
|
if err != nil {
|
|
|
|
return ErrAccountFreeze.Wrap(err)
|
|
|
|
}
|
|
|
|
if warning != nil {
|
|
|
|
err = s.freezeEventsDB.DeleteByUserIDAndEvent(ctx, userID, Warning)
|
|
|
|
if err != nil {
|
|
|
|
return ErrAccountFreeze.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if freeze == nil {
|
|
|
|
freeze = &AccountFreezeEvent{
|
2022-12-15 07:11:03 +00:00
|
|
|
UserID: userID,
|
|
|
|
Type: Freeze,
|
|
|
|
Limits: &AccountFreezeEventLimits{
|
|
|
|
User: UsageLimits{
|
|
|
|
Storage: user.ProjectStorageLimit,
|
|
|
|
Bandwidth: user.ProjectBandwidthLimit,
|
|
|
|
Segment: user.ProjectSegmentLimit,
|
|
|
|
},
|
|
|
|
Projects: make(map[uuid.UUID]UsageLimits),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
userLimits := UsageLimits{
|
|
|
|
Storage: user.ProjectStorageLimit,
|
|
|
|
Bandwidth: user.ProjectBandwidthLimit,
|
|
|
|
Segment: user.ProjectSegmentLimit,
|
|
|
|
}
|
|
|
|
// If user limits have been zeroed already, we should not override what is in the freeze table.
|
|
|
|
if userLimits != (UsageLimits{}) {
|
2023-03-23 12:04:32 +00:00
|
|
|
freeze.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-03-23 12:04:32 +00:00
|
|
|
freeze.Limits.Projects[p.ID] = projLimits
|
2022-12-15 07:11:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-23 12:04:32 +00:00
|
|
|
_, err = s.freezeEventsDB.Upsert(ctx, freeze)
|
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-04-19 01:02:47 +01:00
|
|
|
s.tracker.TrackAccountFrozen(userID, user.Email)
|
2022-12-15 07:11:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnfreezeUser reverses the freeze placed on the user specified by the given ID.
|
|
|
|
func (s *AccountFreezeService) UnfreezeUser(ctx context.Context, userID uuid.UUID) (err error) {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-12-15 07:11:03 +00:00
|
|
|
event, err := s.freezeEventsDB.Get(ctx, userID, Freeze)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return ErrAccountFreeze.New("user is not 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)
|
|
|
|
}
|
|
|
|
|
2023-03-10 11:19:07 +00:00
|
|
|
err = ErrAccountFreeze.Wrap(s.freezeEventsDB.DeleteAllByUserID(ctx, userID))
|
|
|
|
if err != nil {
|
|
|
|
return 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
|
|
|
|
|
|
|
// WarnUser adds a warning event to the freeze events table.
|
|
|
|
func (s *AccountFreezeService) WarnUser(ctx context.Context, userID uuid.UUID) (err error) {
|
|
|
|
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-02-13 17:32:39 +00:00
|
|
|
_, err = s.freezeEventsDB.Upsert(ctx, &AccountFreezeEvent{
|
|
|
|
UserID: userID,
|
|
|
|
Type: Warning,
|
|
|
|
})
|
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-03-23 12:04:32 +00:00
|
|
|
// UnWarnUser reverses the warning placed on the user specified by the given ID.
|
|
|
|
func (s *AccountFreezeService) UnWarnUser(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)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.freezeEventsDB.Get(ctx, userID, Warning)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return ErrAccountFreeze.New("user is not warned")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ErrAccountFreeze.Wrap(s.freezeEventsDB.DeleteByUserIDAndEvent(ctx, userID, Warning))
|
|
|
|
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-02-13 17:32:39 +00:00
|
|
|
// GetAll returns all events for a user.
|
|
|
|
func (s *AccountFreezeService) GetAll(ctx context.Context, userID uuid.UUID) (freeze *AccountFreezeEvent, warning *AccountFreezeEvent, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
freeze, warning, err = s.freezeEventsDB.GetAll(ctx, userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, ErrAccountFreeze.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return freeze, warning, nil
|
|
|
|
}
|
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-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
|
|
|
|
}
|