satellite/admin: add user deletion

Change-Id: Ideea978698183e25f5e0d73128c4f93c2caa1577
This commit is contained in:
stefanbenten 2020-07-06 23:31:40 +02:00 committed by Stefan Benten
parent 0a32ba0e6b
commit 133fda4bef
9 changed files with 232 additions and 7 deletions

View File

@ -22,6 +22,8 @@ import (
"storj.io/storj/private/version/checker"
"storj.io/storj/satellite/admin"
"storj.io/storj/satellite/metainfo"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
// Admin is the satellite core process that runs chores
@ -46,6 +48,12 @@ type Admin struct {
Service *checker.Service
}
Payments struct {
Accounts payments.Accounts
Service *stripecoinpayments.Service
Stripe stripecoinpayments.StripeClient
}
Admin struct {
Listener net.Listener
Server *admin.Server
@ -104,6 +112,42 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB,
})
}
{ // setup payments
pc := config.Payments
var stripeClient stripecoinpayments.StripeClient
var err error
switch pc.Provider {
default:
stripeClient = stripecoinpayments.NewStripeMock()
case "stripecoinpayments":
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
}
peer.Payments.Service, err = stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"),
stripeClient,
pc.StripeCoinPayments,
peer.DB.StripeCoinPayments(),
peer.DB.Console().Projects(),
peer.DB.ProjectAccounting(),
pc.StorageTBPrice,
pc.EgressTBPrice,
pc.ObjectPrice,
pc.BonusRate,
pc.CouponValue,
pc.CouponDuration,
pc.CouponProjectLimit,
pc.MinCoinPayment)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.Stripe = stripeClient
peer.Payments.Accounts = peer.Payments.Service.Accounts()
}
{ // setup debug
var err error
peer.Admin.Listener, err = net.Listen("tcp", config.Admin.Address)
@ -114,7 +158,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB,
adminConfig := config.Admin
adminConfig.AuthorizationToken = config.Console.AuthToken
peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, adminConfig)
peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, peer.Payments.Accounts.Invoices(), adminConfig)
peer.Servers.Add(lifecycle.Item{
Name: "admin",
Run: peer.Admin.Server.Run,

View File

@ -80,6 +80,10 @@ A successful response body:
}
```
## DELETE /api/user/{user-email}
Deletes the user.
## POST /api/coupon
Adds a coupon for specific user.

View File

@ -19,6 +19,7 @@ import (
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/metainfo"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
@ -49,18 +50,22 @@ type Server struct {
server http.Server
mux *mux.Router
db DB
db DB
invoices payments.Invoices
}
// NewServer returns a new debug.Server.
func NewServer(log *zap.Logger, listener net.Listener, db DB, config Config) *Server {
func NewServer(log *zap.Logger, listener net.Listener, db DB, invoices payments.Invoices, config Config) *Server {
server := &Server{
log: log,
log: log,
listener: listener,
mux: mux.NewRouter(),
db: db,
invoices: invoices,
}
server.db = db
server.listener = listener
server.mux = mux.NewRouter()
server.server.Handler = &protectedServer{
allowedAuthorization: config.AuthorizationToken,
next: server.mux,
@ -70,6 +75,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, config Config) *Se
server.mux.HandleFunc("/api/user", server.addUser).Methods("POST")
server.mux.HandleFunc("/api/user/{useremail}", server.updateUser).Methods("PUT")
server.mux.HandleFunc("/api/user/{useremail}", server.userInfo).Methods("GET")
server.mux.HandleFunc("/api/user/{useremail}", server.deleteUser).Methods("DELETE")
server.mux.HandleFunc("/api/coupon", server.addCoupon).Methods("POST")
server.mux.HandleFunc("/api/coupon/{couponid}", server.couponInfo).Methods("GET")
server.mux.HandleFunc("/api/coupon/{couponid}", server.deleteCoupon).Methods("DELETE")

View File

@ -231,3 +231,90 @@ func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) {
return
}
}
func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
userEmail, ok := vars["useremail"]
if !ok {
http.Error(w, "user-email missing", http.StatusBadRequest)
return
}
user, err := server.db.Console().Users().GetByEmail(ctx, userEmail)
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, fmt.Sprintf("user with email %q not found", userEmail), http.StatusNotFound)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("failed to get user %q: %v", userEmail, err), http.StatusInternalServerError)
return
}
// Ensure user has no own projects any longer
projects, err := server.db.Console().Projects().GetByUserID(ctx, user.ID)
if err != nil {
http.Error(w, fmt.Sprintf("unable to list buckets: %v", err), http.StatusInternalServerError)
return
}
if len(projects) > 0 {
http.Error(w, fmt.Sprintf("some projects still exist: %v", projects), http.StatusConflict)
return
}
// Delete memberships in foreign projects
members, err := server.db.Console().ProjectMembers().GetByMemberID(ctx, user.ID)
if err != nil {
http.Error(w, fmt.Sprintf("unable to search for user project memberships: %v", err), http.StatusInternalServerError)
return
}
if len(members) > 0 {
for _, project := range members {
err := server.db.Console().ProjectMembers().Delete(ctx, user.ID, project.ProjectID)
if err != nil {
http.Error(w, fmt.Sprintf("unable to delete user project membership: %v", err), http.StatusInternalServerError)
return
}
}
}
// ensure no unpaid invoices exist.
invoices, err := server.invoices.List(ctx, user.ID)
if err != nil {
http.Error(w, fmt.Sprintf("unable to list user invoices: %v", err), http.StatusInternalServerError)
return
}
if len(invoices) > 0 {
for _, invoice := range invoices {
if invoice.Status != "paid" {
http.Error(w, "user has unpaid/pending invoices", http.StatusConflict)
return
}
}
}
hasItems, err := server.invoices.CheckPendingItems(ctx, user.ID)
if err != nil {
http.Error(w, fmt.Sprintf("unable to list pending invoice items: %v", err), http.StatusInternalServerError)
return
}
if hasItems {
http.Error(w, "user has pending invoice items", http.StatusConflict)
return
}
userInfo := &console.User{
ID: user.ID,
FullName: "",
ShortName: "",
Email: fmt.Sprintf("deactivated+%s@storj.io", user.ID.String()),
Status: console.Deleted,
}
err = server.db.Console().Users().Update(ctx, userInfo)
if err != nil {
http.Error(w, fmt.Sprintf("unable to delete user: %v", err), http.StatusInternalServerError)
return
}
}

View File

@ -141,3 +141,49 @@ func TestUpdateUser(t *testing.T) {
require.Equal(t, user.Status, updatedUser.Status)
})
}
func TestDeleteUser(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
StorageNodeCount: 0,
UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index 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()
user, err := planet.Satellites[0].DB.Console().Users().GetByEmail(ctx, planet.Uplinks[0].Projects[0].Owner.Email)
require.NoError(t, err)
// Deleting the user should fail, as project exists
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/user/%s", user.Email), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusConflict, response.StatusCode)
responseBody, err := ioutil.ReadAll(response.Body)
require.NoError(t, err)
require.NoError(t, response.Body.Close())
require.Greater(t, len(responseBody), 0)
err = planet.Satellites[0].DB.Console().Projects().Delete(ctx, planet.Uplinks[0].Projects[0].ID)
require.NoError(t, err)
// Deleting the user should pass, as no project exists for given user
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/user/%s", user.Email), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
response, err = http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode)
responseBody, err = ioutil.ReadAll(response.Body)
require.NoError(t, err)
require.NoError(t, response.Body.Close())
require.Equal(t, len(responseBody), 0)
})
}

View File

@ -16,6 +16,8 @@ import (
type Invoices interface {
// List returns a list of invoices for a given payment account.
List(ctx context.Context, userID uuid.UUID) ([]Invoice, error)
// CheckPendingItems returns if pending invoice items for a given payment account exist.
CheckPendingItems(ctx context.Context, userID uuid.UUID) (existingItems bool, err error)
}
// Invoice holds all public information about invoice.

View File

@ -9,6 +9,7 @@ import (
"github.com/stripe/stripe-go/client"
"github.com/stripe/stripe-go/customerbalancetransaction"
"github.com/stripe/stripe-go/invoice"
"github.com/stripe/stripe-go/invoiceitem"
"github.com/stripe/stripe-go/paymentmethod"
"go.uber.org/zap"
)
@ -48,6 +49,7 @@ type StripeInvoices interface {
// StripeInvoiceItems Stripe InvoiceItems interface.
type StripeInvoiceItems interface {
New(params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error)
List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter
}
// StripeCharges Stripe Charges interface.

View File

@ -62,3 +62,32 @@ func (invoices *invoices) List(ctx context.Context, userID uuid.UUID) (invoicesL
return invoicesList, nil
}
// CheckPendingItems returns if pending invoice items for a given payment account exist.
func (invoices *invoices) CheckPendingItems(ctx context.Context, userID uuid.UUID) (existingItems bool, err error) {
defer mon.Task()(&ctx, userID)(&err)
customerID, err := invoices.service.db.Customers().GetCustomerID(ctx, userID)
if err != nil {
return false, Error.Wrap(err)
}
params := &stripe.InvoiceItemListParams{
Customer: &customerID,
Pending: stripe.Bool(true),
}
itemIterator := invoices.service.stripeClient.InvoiceItems().List(params)
for itemIterator.Next() {
item := itemIterator.InvoiceItem()
if item != nil {
return true, nil
}
}
if err = itemIterator.Err(); err != nil {
return false, Error.Wrap(err)
}
return false, nil
}

View File

@ -12,6 +12,7 @@ import (
"github.com/stripe/stripe-go/customerbalancetransaction"
"github.com/stripe/stripe-go/form"
"github.com/stripe/stripe-go/invoice"
"github.com/stripe/stripe-go/invoiceitem"
"github.com/stripe/stripe-go/paymentmethod"
"storj.io/common/uuid"
@ -177,6 +178,10 @@ func (m *mockInvoiceItems) New(params *stripe.InvoiceItemParams) (*stripe.Invoic
return nil, nil
}
func (m *mockInvoiceItems) List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter {
return &invoiceitem.Iter{Iter: &stripe.Iter{}}
}
type mockCustomerBalanceTransactions struct {
transactions map[string][]*stripe.CustomerBalanceTransaction
}