satellite/admin: add endpoints to legal freeze/unfreeze users
This change adds two new admin endpoints to freeze users for legal review and to remove them from that state Issue: storj/storj-private#492 Change-Id: I6c8e3ffcb80375e81e78bc6ecc785c1047328cf7
This commit is contained in:
parent
2b4f347c33
commit
33fb21c8e0
@ -23,6 +23,8 @@ Requires setting `Authorization` header for requests.
|
||||
* [DELETE /api/users/{user-email}/billing-freeze](#delete-apiusersuser-emailbilling-freeze)
|
||||
* [PUT /api/users/{user-email}/violation-freeze](#put-apiusersuser-emailviolation-freeze)
|
||||
* [DELETE /api/users/{user-email}/violation-freeze](#delete-apiusersuser-emailviolation-freeze)
|
||||
* [PUT /api/users/{user-email}/legal-freeze](#put-apiusersuser-emaillegal-freeze)
|
||||
* [DELETE /api/users/{user-email}/legal-freeze](#delete-apiusersuser-emaillegal-freeze)
|
||||
* [DELETE /api/users/{user-email}/billing-warning](#delete-apiusersuser-emailbilling-warning)
|
||||
* [GET /api/users/pending-deletion](#get-apiuserspending-deletion)
|
||||
* [PATCH /api/users/{user-email}/geofence](#patch-apiusersuser-emailgeofence)
|
||||
@ -196,6 +198,17 @@ User status is also set to Pending Deletion. The user cannot exit this state aut
|
||||
Removes the violation freeze on a user account so uploads and downloads may resume.
|
||||
User status is set back to Active. This is the only way to exit the violation frozen state.
|
||||
|
||||
#### PUT /api/users/{user-email}/legal-freeze
|
||||
|
||||
Freezes a user account for legal review so no uploads or downloads may occur
|
||||
User status is also set to Legal hold. The user cannot exit this state automatically.
|
||||
|
||||
#### DELETE /api/users/{user-email}/legal-freeze
|
||||
|
||||
Removes the legal freeze on a user account so uploads and downloads may resume.
|
||||
User status is set back to Active. This is the only way to exit the legal frozen state.
|
||||
|
||||
|
||||
#### DELETE /api/users/{user-email}/billing-warning
|
||||
|
||||
Removes the billing warning status from a user's account.
|
||||
|
@ -177,6 +177,8 @@ func NewServer(
|
||||
limitUpdateAPI.HandleFunc("/users/{useremail}/billing-warning", server.billingUnWarnUser).Methods("DELETE")
|
||||
limitUpdateAPI.HandleFunc("/users/{useremail}/violation-freeze", server.violationFreezeUser).Methods("PUT")
|
||||
limitUpdateAPI.HandleFunc("/users/{useremail}/violation-freeze", server.violationUnfreezeUser).Methods("DELETE")
|
||||
limitUpdateAPI.HandleFunc("/users/{useremail}/legal-freeze", server.legalFreezeUser).Methods("PUT")
|
||||
limitUpdateAPI.HandleFunc("/users/{useremail}/legal-freeze", server.legalUnfreezeUser).Methods("DELETE")
|
||||
limitUpdateAPI.HandleFunc("/users/pending-deletion", server.usersPendingDeletion).Methods("GET")
|
||||
limitUpdateAPI.HandleFunc("/projects/{project}/limit", server.getProjectLimit).Methods("GET")
|
||||
limitUpdateAPI.HandleFunc("/projects/{project}/limit", server.putProjectLimit).Methods("PUT", "POST")
|
||||
|
@ -492,6 +492,22 @@ Blank fields will not be updated.`,
|
||||
return this.fetch('DELETE', `users/${email}/violation-freeze`) as Promise<null>;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'legal freeze user',
|
||||
desc: 'freeze a user for legal review, set limits to zero and status to legal hold',
|
||||
params: [['email', new InputText('email', true)]],
|
||||
func: async (email: string): Promise<null> => {
|
||||
return this.fetch('PUT', `users/${email}/legal-freeze`) as Promise<null>;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'legal unfreeze user',
|
||||
desc: "remove a user's legal freeze, reinstating their limits and status (Active)",
|
||||
params: [['email', new InputText('email', true)]],
|
||||
func: async (email: string): Promise<null> => {
|
||||
return this.fetch('DELETE', `users/${email}/legal-freeze`) as Promise<null>;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'unwarn user',
|
||||
desc: "Remove a user's warning status",
|
||||
|
@ -720,8 +720,12 @@ func (server *Server) billingUnfreezeUser(w http.ResponseWriter, r *http.Request
|
||||
|
||||
err = server.freezeAccounts.BillingUnfreezeUser(ctx, u.ID)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errs.Is(err, console.ErrNoFreezeStatus) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
sendJSONError(w, "failed to billing unfreeze user",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
err.Error(), status)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -749,8 +753,12 @@ func (server *Server) billingUnWarnUser(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if err = server.freezeAccounts.BillingUnWarnUser(ctx, u.ID); err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errs.Is(err, console.ErrNoFreezeStatus) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
sendJSONError(w, "failed to billing unwarn user",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
err.Error(), status)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -821,10 +829,90 @@ func (server *Server) violationUnfreezeUser(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
err = server.freezeAccounts.ViolationUnfreezeUser(ctx, u.ID)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errs.Is(err, console.ErrNoFreezeStatus) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
sendJSONError(w, "failed to violation unfreeze user",
|
||||
err.Error(), status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) legalFreezeUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userEmail, ok := vars["useremail"]
|
||||
if !ok {
|
||||
sendJSONError(w, "user-email missing", "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := server.db.Console().Users().GetByEmail(ctx, userEmail)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail),
|
||||
"", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
sendJSONError(w, "failed to get user details",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = server.freezeAccounts.LegalFreezeUser(ctx, u.ID)
|
||||
if err != nil {
|
||||
sendJSONError(w, "failed to legal freeze user",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
invoices, err := server.payments.Invoices().List(ctx, u.ID)
|
||||
if err != nil {
|
||||
server.log.Error("failed to get invoices for legal frozen user", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, invoice := range invoices {
|
||||
if invoice.Status == payments.InvoiceStatusOpen {
|
||||
server.analytics.TrackLegalHoldUnpaidInvoice(invoice.ID, u.ID, u.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) legalUnfreezeUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userEmail, ok := vars["useremail"]
|
||||
if !ok {
|
||||
sendJSONError(w, "user-email missing", "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := server.db.Console().Users().GetByEmail(ctx, userEmail)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail),
|
||||
"", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
sendJSONError(w, "failed to get user details",
|
||||
err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = server.freezeAccounts.LegalUnfreezeUser(ctx, u.ID)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errs.Is(err, console.ErrNoFreezeStatus) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
sendJSONError(w, "failed to legal unfreeze user",
|
||||
err.Error(), status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -424,8 +424,8 @@ func TestBillingFreezeUnfreezeUser(t *testing.T) {
|
||||
require.Equal(t, projectPreFreeze.StorageLimit, unfrozenProject.StorageLimit)
|
||||
require.Equal(t, projectPreFreeze.BandwidthLimit, unfrozenProject.BandwidthLimit)
|
||||
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusInternalServerError, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), "not frozen")
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusNotFound, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), console.ErrNoFreezeStatus.Error())
|
||||
})
|
||||
}
|
||||
|
||||
@ -486,8 +486,70 @@ func TestViolationFreezeUnfreezeUser(t *testing.T) {
|
||||
require.Equal(t, projectPreFreeze.StorageLimit, unfrozenProject.StorageLimit)
|
||||
require.Equal(t, projectPreFreeze.BandwidthLimit, unfrozenProject.BandwidthLimit)
|
||||
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusInternalServerError, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), "not violation frozen")
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusNotFound, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), console.ErrNoFreezeStatus.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLegalFreezeUnfreezeUser(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
StorageNodeCount: 0,
|
||||
UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(_ *zap.Logger, _ 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()
|
||||
userPreFreeze, err := planet.Satellites[0].DB.Console().Users().Get(ctx, planet.Uplinks[0].Projects[0].Owner.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, console.Active, userPreFreeze.Status)
|
||||
require.NotZero(t, userPreFreeze.ProjectStorageLimit)
|
||||
require.NotZero(t, userPreFreeze.ProjectBandwidthLimit)
|
||||
|
||||
projectPreFreeze, err := planet.Satellites[0].DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, projectPreFreeze.BandwidthLimit)
|
||||
require.NotZero(t, projectPreFreeze.StorageLimit)
|
||||
|
||||
// freeze can be run multiple times. Test that doing so does not affect Unfreeze result.
|
||||
link := fmt.Sprintf("http://"+address.String()+"/api/users/%s/legal-freeze", userPreFreeze.Email)
|
||||
body := assertReq(ctx, t, link, http.MethodPut, "", http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Len(t, body, 0)
|
||||
|
||||
body = assertReq(ctx, t, link, http.MethodPut, "", http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Len(t, body, 0)
|
||||
|
||||
userPostFreeze, err := planet.Satellites[0].DB.Console().Users().Get(ctx, userPreFreeze.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, console.LegalHold, userPostFreeze.Status)
|
||||
require.Zero(t, userPostFreeze.ProjectStorageLimit)
|
||||
require.Zero(t, userPostFreeze.ProjectBandwidthLimit)
|
||||
|
||||
projectPostFreeze, err := planet.Satellites[0].DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, projectPostFreeze.BandwidthLimit.Int64())
|
||||
require.Zero(t, projectPostFreeze.StorageLimit.Int64())
|
||||
|
||||
link = fmt.Sprintf("http://"+address.String()+"/api/users/%s/legal-freeze", userPreFreeze.Email)
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Len(t, body, 0)
|
||||
|
||||
unfrozenUser, err := planet.Satellites[0].DB.Console().Users().Get(ctx, userPreFreeze.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, console.Active, unfrozenUser.Status)
|
||||
require.Equal(t, userPreFreeze.ProjectStorageLimit, unfrozenUser.ProjectStorageLimit)
|
||||
require.Equal(t, userPreFreeze.ProjectBandwidthLimit, unfrozenUser.ProjectBandwidthLimit)
|
||||
|
||||
unfrozenProject, err := planet.Satellites[0].DB.Console().Projects().Get(ctx, projectPreFreeze.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, projectPreFreeze.StorageLimit, unfrozenProject.StorageLimit)
|
||||
require.Equal(t, projectPreFreeze.BandwidthLimit, unfrozenProject.BandwidthLimit)
|
||||
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusNotFound, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), console.ErrNoFreezeStatus.Error())
|
||||
})
|
||||
}
|
||||
|
||||
@ -521,8 +583,8 @@ func TestBillingWarnUnwarnUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, freezes.BillingWarning)
|
||||
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusInternalServerError, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), "user is not warned")
|
||||
body = assertReq(ctx, t, link, http.MethodDelete, "", http.StatusNotFound, "", planet.Satellites[0].Config.Console.AuthToken)
|
||||
require.Contains(t, string(body), console.ErrNoFreezeStatus.Error())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,9 @@ import (
|
||||
// ErrAccountFreeze is the class for errors that occur during operation of the account freeze service.
|
||||
var ErrAccountFreeze = errs.Class("account freeze service")
|
||||
|
||||
// ErrNoFreezeStatus is the error for when a user doesn't have a particular freeze status.
|
||||
var ErrNoFreezeStatus = errs.New("this freeze event does not exist for this user")
|
||||
|
||||
// AccountFreezeEvents exposes methods to manage the account freeze events table in database.
|
||||
//
|
||||
// architecture: Database
|
||||
@ -264,7 +267,7 @@ func (s *AccountFreezeService) BillingUnfreezeUser(ctx context.Context, userID u
|
||||
|
||||
event, err := tx.AccountFreezeEvents().Get(ctx, userID, BillingFreeze)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errs.New("user is not frozen due to nonpayment of invoices")
|
||||
return ErrNoFreezeStatus
|
||||
}
|
||||
|
||||
if event.Limits == nil {
|
||||
@ -358,7 +361,7 @@ func (s *AccountFreezeService) BillingUnWarnUser(ctx context.Context, userID uui
|
||||
|
||||
_, err = tx.AccountFreezeEvents().Get(ctx, userID, BillingWarning)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrAccountFreeze.New("user is not warned")
|
||||
return ErrAccountFreeze.Wrap(errs.Combine(err, ErrNoFreezeStatus))
|
||||
}
|
||||
|
||||
err = ErrAccountFreeze.Wrap(tx.AccountFreezeEvents().DeleteByUserIDAndEvent(ctx, userID, BillingWarning))
|
||||
@ -497,7 +500,7 @@ func (s *AccountFreezeService) ViolationUnfreezeUser(ctx context.Context, userID
|
||||
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
||||
event, err := tx.AccountFreezeEvents().Get(ctx, userID, ViolationFreeze)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errs.New("user is not violation frozen")
|
||||
return ErrNoFreezeStatus
|
||||
}
|
||||
|
||||
if event.Limits == nil {
|
||||
@ -647,7 +650,7 @@ func (s *AccountFreezeService) LegalUnfreezeUser(ctx context.Context, userID uui
|
||||
|
||||
event, err := tx.AccountFreezeEvents().Get(ctx, userID, LegalFreeze)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errs.New("user is not legal-frozen")
|
||||
return ErrNoFreezeStatus
|
||||
}
|
||||
|
||||
if event.Limits == nil {
|
||||
|
Loading…
Reference in New Issue
Block a user