satellite/admin: add endpoint to freeze/unfreeze user

Allow the admin to manually freeze/unfreeze users.

github issue: https://github.com/storj/storj/issues/5397

Change-Id: I402ad1bf2e13effb0a5a8ff35bb128d1fcf18448
This commit is contained in:
Cameron 2022-12-16 10:17:27 -05:00 committed by Storj Robot
parent d23e25ce0f
commit 01932bda42
5 changed files with 154 additions and 10 deletions

View File

@ -22,6 +22,7 @@ import (
"storj.io/storj/private/version/checker"
"storj.io/storj/satellite/admin"
"storj.io/storj/satellite/buckets"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/restkeys"
"storj.io/storj/satellite/metabase"
"storj.io/storj/satellite/payments"
@ -69,6 +70,10 @@ type Admin struct {
REST struct {
Keys *restkeys.Service
}
FreezeAccounts struct {
Service *console.AccountFreezeService
}
}
// NewAdmin creates a new satellite admin peer.
@ -174,6 +179,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
peer.Payments.Stripe = stripeClient
peer.Payments.Accounts = peer.Payments.Service.Accounts()
peer.FreezeAccounts.Service = console.NewAccountFreezeService(db.Console().AccountFreezeEvents(), db.Console().Users(), db.Console().Projects())
}
{ // setup admin endpoint
@ -186,7 +192,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
adminConfig := config.Admin
adminConfig.AuthorizationToken = config.Console.AuthToken
peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, peer.Buckets.Service, peer.REST.Keys, peer.Payments.Accounts, config.Console, adminConfig)
peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, peer.Buckets.Service, peer.REST.Keys, peer.FreezeAccounts.Service, peer.Payments.Accounts, config.Console, adminConfig)
peer.Servers.Add(lifecycle.Item{
Name: "admin",
Run: peer.Admin.Server.Run,

View File

@ -57,10 +57,11 @@ type Server struct {
listener net.Listener
server http.Server
db DB
payments payments.Accounts
buckets *buckets.Service
restKeys *restkeys.Service
db DB
payments payments.Accounts
buckets *buckets.Service
restKeys *restkeys.Service
freezeAccounts *console.AccountFreezeService
nowFn func() time.Time
@ -69,16 +70,17 @@ type Server struct {
}
// NewServer returns a new administration Server.
func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.Service, restKeys *restkeys.Service, accounts payments.Accounts, console consoleweb.Config, config Config) *Server {
func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.Service, restKeys *restkeys.Service, freezeAccounts *console.AccountFreezeService, accounts payments.Accounts, console consoleweb.Config, config Config) *Server {
server := &Server{
log: log,
listener: listener,
db: db,
payments: accounts,
buckets: buckets,
restKeys: restKeys,
db: db,
payments: accounts,
buckets: buckets,
restKeys: restKeys,
freezeAccounts: freezeAccounts,
nowFn: time.Now,
@ -97,6 +99,8 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
api.HandleFunc("/users/{useremail}", server.userInfo).Methods("GET")
api.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE")
api.HandleFunc("/users/{useremail}/mfa", server.disableUserMFA).Methods("DELETE")
api.HandleFunc("/users/{useremail}/freeze", server.freezeUser).Methods("PUT")
api.HandleFunc("/users/{useremail}/freeze", server.unfreezeUser).Methods("DELETE")
api.HandleFunc("/oauth/clients", server.createOAuthClient).Methods("POST")
api.HandleFunc("/oauth/clients/{id}", server.updateOAuthClient).Methods("PUT")
api.HandleFunc("/oauth/clients/{id}", server.deleteOAuthClient).Methods("DELETE")

View File

@ -379,6 +379,22 @@ Blank fields will not be updated.`,
func: async (email: string): Promise<null> => {
return this.fetch('DELETE', `users/${email}/mfa`) as Promise<null>;
}
},
{
name: 'freeze user',
desc: "insert user into account_freeze_events and set user's limits to zero",
params: [['email', new InputText('email', true)]],
func: async (email: string): Promise<null> => {
return this.fetch('PUT', `users/${email}/freeze`) as Promise<null>;
}
},
{
name: 'unfreeze user',
desc: "remove user from account_freeze_events and reset user's limits to what is stored in account_freeze_events",
params: [['email', new InputText('email', true)]],
func: async (email: string): Promise<null> => {
return this.fetch('DELETE', `users/${email}/freeze`) as Promise<null>;
}
}
],
rest_api_keys: [

View File

@ -326,6 +326,64 @@ func (server *Server) disableUserMFA(w http.ResponseWriter, r *http.Request) {
}
}
func (server *Server) freezeUser(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.FreezeUser(ctx, u.ID)
if err != nil {
sendJSONError(w, "failed to freeze user",
err.Error(), http.StatusInternalServerError)
}
}
func (server *Server) unfreezeUser(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
}
if err = server.freezeAccounts.UnfreezeUser(ctx, u.ID); err != nil {
sendJSONError(w, "failed to unfreeze user",
err.Error(), http.StatusInternalServerError)
return
}
}
func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View File

@ -260,6 +260,66 @@ func TestDisableMFA(t *testing.T) {
})
}
func TestFreezeUnfreezeUser(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.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/freeze", userPreFreeze.Email)
body := assertReq(ctx, t, link, http.MethodPut, "", http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken)
require.Len(t, body, 0)
link = fmt.Sprintf("http://"+address.String()+"/api/users/%s/freeze", userPreFreeze.Email)
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.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/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, 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.StatusInternalServerError, "", planet.Satellites[0].Config.Console.AuthToken)
require.Contains(t, string(body), "not frozen")
})
}
func TestUserDelete(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,