From 1b229946310824c9719c0a1f0608c78432a410b5 Mon Sep 17 00:00:00 2001 From: Lizzy Thomson Date: Thu, 2 Feb 2023 15:58:17 -0700 Subject: [PATCH] satellite/admin: update user's limits and project limits from admin api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create an endpoint in the Admin Api to be able to update a user’s limits for all existing and new projects. Also added a GET endpoint to return user's limits. Fixes: https://github.com/storj/storj/issues/5395 Change-Id: I2c093dc08ebf79a4318391e63a37da4d2b403547 --- satellite/admin/README.md | 10 +++ satellite/admin/server.go | 2 + satellite/admin/ui/src/lib/api.ts | 31 ++++++++ satellite/admin/user.go | 128 ++++++++++++++++++++++++++++++ satellite/admin/user_test.go | 27 +++++++ 5 files changed, 198 insertions(+) diff --git a/satellite/admin/README.md b/satellite/admin/README.md index c4e7892ad..e3f950636 100644 --- a/satellite/admin/README.md +++ b/satellite/admin/README.md @@ -15,7 +15,9 @@ Requires setting `Authorization` header for requests. * [POST /api/users](#post-apiusers) * [PUT /api/users/{user-email}](#put-apiusersuser-email) * [GET /api/users/{user-email}](#get-apiusersuser-email) + * [GET /api/users/{user-email}/limits](#get-apiusersuser-emaillimits) * [DELETE /api/users/{user-email}](#delete-apiusersuser-email) + * [PUT /api/users/{user-email}/limits](#put-apiusersuser-emaillimits) * [DELETE /api/users/{user-email}/mfa](#delete-apiusersuser-emailmfa) * [PUT /api/users/{user-email}/freeze](#put-apiusersuser-emailfreeze) * [DELETE /api/users/{user-email}/freeze](#delete-apiusersuser-emailfreeze) @@ -152,10 +154,18 @@ A successful response body: } ``` +#### GET /api/users/{user-email}/limits + +This endpoint returns information about users limits. + #### DELETE /api/users/{user-email} Deletes the user. +#### PUT /api/users/{user-email}/limits + +Updates the limits of the user and user's existing project(s) limits found by its email. + #### DELETE /api/users/{user-email}/mfa Disables the user's mfa. diff --git a/satellite/admin/server.go b/satellite/admin/server.go index debb59337..69e52a456 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -97,7 +97,9 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S api.HandleFunc("/users", server.addUser).Methods("POST") api.HandleFunc("/users/{useremail}", server.updateUser).Methods("PUT") api.HandleFunc("/users/{useremail}", server.userInfo).Methods("GET") + api.HandleFunc("/users/{useremail}/limits", server.userLimits).Methods("GET") api.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE") + api.HandleFunc("/users/{useremail}/limits", server.updateLimits).Methods("PUT") 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") diff --git a/satellite/admin/ui/src/lib/api.ts b/satellite/admin/ui/src/lib/api.ts index 9f1163561..deef1029e 100644 --- a/satellite/admin/ui/src/lib/api.ts +++ b/satellite/admin/ui/src/lib/api.ts @@ -321,6 +321,14 @@ export class Admin { return this.fetch('GET', `users/${email}`); } }, + { + name: 'get user limits', + desc: 'Get the current limits for a user', + params: [['email', new InputText('email', true)]], + func: async (email: string): Promise> => { + return this.fetch('GET', `users/${email}/limits`); + } + }, { name: 'update', desc: `Update the information of a user's account. @@ -370,6 +378,29 @@ Blank fields will not be updated.`, }) as Promise; } }, + { + name: 'update user and project limits', + desc: `Update limits for all of user's existing and future projects. + Blank fields will not be updated.`, + params: [ + ["current user's email", new InputText('email', true)], + ['project storage limit (in bytes)', new InputText('number', false)], + ['project bandwidth limit (in bytes)', new InputText('number', false)], + ['project segment limit (max number)', new InputText('number', false)] + ], + func: async ( + currentEmail: string, + projectStorageLimit?: number, + projectBandwidthLimit?: number, + projectSegmentLimit?: number + ): Promise => { + return this.fetch('PUT', `users/${currentEmail}/limits`, null, { + projectStorageLimit, + projectBandwidthLimit, + projectSegmentLimit + }) as Promise; + } + }, { name: 'disable MFA', desc: "Disable user's mulifactor authentication", diff --git a/satellite/admin/user.go b/satellite/admin/user.go index f78b77bcc..8cc7e8aad 100644 --- a/satellite/admin/user.go +++ b/satellite/admin/user.go @@ -197,6 +197,50 @@ func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) { sendJSONData(w, http.StatusOK, data) } +func (server *Server) userLimits(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 + } + + user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) + if errors.Is(err, sql.ErrNoRows) { + sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), + "", http.StatusNotFound) + return + } + if err != nil { + sendJSONError(w, "failed to get user", + err.Error(), http.StatusInternalServerError) + return + } + user.PasswordHash = nil + + var limits struct { + Storage int64 `json:"storage"` + Bandwidth int64 `json:"bandwidth"` + Segment int64 `json:"maxSegments"` + } + + limits.Storage = user.ProjectStorageLimit + limits.Bandwidth = user.ProjectBandwidthLimit + limits.Segment = user.ProjectSegmentLimit + + data, err := json.Marshal(limits) + if err != nil { + sendJSONError(w, "json encoding failed", + err.Error(), http.StatusInternalServerError) + return + } + + sendJSONData(w, http.StatusOK, data) +} + func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -287,6 +331,90 @@ func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) { } } +// updateLimits updates user limits and all project limits for that user (future and existing). +func (server *Server) updateLimits(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 + } + + user, err := server.db.Console().Users().GetByEmail(ctx, userEmail) + if errors.Is(err, sql.ErrNoRows) { + sendJSONError(w, fmt.Sprintf("user with email %q does not exist", userEmail), + "", http.StatusNotFound) + return + } + if err != nil { + sendJSONError(w, "failed to get user", + err.Error(), http.StatusInternalServerError) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + sendJSONError(w, "failed to read body", + err.Error(), http.StatusInternalServerError) + return + } + + type User struct { + console.User + } + + var input User + + err = json.Unmarshal(body, &input) + if err != nil { + sendJSONError(w, "failed to unmarshal request", + err.Error(), http.StatusBadRequest) + return + } + + updateRequest := console.UpdateUserRequest{} + + if input.ProjectStorageLimit > 0 { + updateRequest.ProjectStorageLimit = &input.ProjectStorageLimit + } + if input.ProjectBandwidthLimit > 0 { + updateRequest.ProjectBandwidthLimit = &input.ProjectBandwidthLimit + } + if input.ProjectSegmentLimit > 0 { + updateRequest.ProjectSegmentLimit = &input.ProjectSegmentLimit + } + + userLimits := console.UsageLimits{ + Storage: *updateRequest.ProjectStorageLimit, + Bandwidth: *updateRequest.ProjectBandwidthLimit, + Segment: *updateRequest.ProjectSegmentLimit, + } + + err = server.db.Console().Users().UpdateUserProjectLimits(ctx, user.ID, userLimits) + if err != nil { + sendJSONError(w, "failed to update user limits", + err.Error(), http.StatusInternalServerError) + } + + userProjects, err := server.db.Console().Projects().GetOwn(ctx, user.ID) + if err != nil { + sendJSONError(w, "failed to get user's projects", + err.Error(), http.StatusInternalServerError) + } + + for _, p := range userProjects { + err = server.db.Console().Projects().UpdateUsageLimits(ctx, p.ID, userLimits) + if err != nil { + sendJSONError(w, "failed to update project limits", + err.Error(), http.StatusInternalServerError) + } + } + +} + func (server *Server) disableUserMFA(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/satellite/admin/user_test.go b/satellite/admin/user_test.go index eb3207e42..f6f647215 100644 --- a/satellite/admin/user_test.go +++ b/satellite/admin/user_test.go @@ -203,6 +203,33 @@ func TestUserUpdate(t *testing.T) { require.Equal(t, newUsageLimit, updatedUserStatusAndUsageLimits.ProjectStorageLimit) require.Equal(t, newUsageLimit, updatedUserStatusAndUsageLimits.ProjectBandwidthLimit) require.Equal(t, newUsageLimit, updatedUserStatusAndUsageLimits.ProjectSegmentLimit) + + // Update user limits and project limits (current and existing projects for a user). + link = "http://" + address.String() + "/api/users/alice+2@mail.test/limits" + newStorageLimit := int64(15000) + newBandwidthLimit := int64(25000) + newSegmentLimit := int64(35000) + body2 := fmt.Sprintf(`{"projectStorageLimit":%d, "projectBandwidthLimit":%d, "projectSegmentLimit":%d}`, newStorageLimit, newBandwidthLimit, newSegmentLimit) + responseBody = assertReq(ctx, t, link, http.MethodPut, body2, http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken) + require.Len(t, responseBody, 0) + + // Get user limits returns new updated limits + link2 := "http://" + address.String() + "/api/users/alice+2@mail.test/limits" + expectedBody := `{` + + fmt.Sprintf(`"storage":%d,"bandwidth":%d,"maxSegments":%d}`, newStorageLimit, newBandwidthLimit, newSegmentLimit) + assertReq(ctx, t, link2, http.MethodGet, "", http.StatusOK, expectedBody, planet.Satellites[0].Config.Console.AuthToken) + + userUpdatedLimits, err := planet.Satellites[0].DB.Console().Users().Get(ctx, user.ID) + require.NoError(t, err) + require.Equal(t, newStorageLimit, userUpdatedLimits.ProjectStorageLimit) + require.Equal(t, newBandwidthLimit, userUpdatedLimits.ProjectBandwidthLimit) + require.Equal(t, newSegmentLimit, userUpdatedLimits.ProjectSegmentLimit) + + projectUpdatedLimits, err := planet.Satellites[0].DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID) + require.NoError(t, err) + require.Equal(t, newStorageLimit, projectUpdatedLimits.StorageLimit.Int64()) + require.Equal(t, newBandwidthLimit, projectUpdatedLimits.BandwidthLimit.Int64()) + require.Equal(t, newSegmentLimit, *projectUpdatedLimits.SegmentLimit) }) t.Run("Not found", func(t *testing.T) {