satellite/admin: update user's limits and project limits from admin api

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
This commit is contained in:
Lizzy Thomson 2023-02-02 15:58:17 -07:00 committed by Storj Robot
parent 109da3c798
commit 1b22994631
5 changed files with 198 additions and 0 deletions

View File

@ -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.

View File

@ -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")

View File

@ -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<Record<string, unknown>> => {
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<null>;
}
},
{
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<null> => {
return this.fetch('PUT', `users/${currentEmail}/limits`, null, {
projectStorageLimit,
projectBandwidthLimit,
projectSegmentLimit
}) as Promise<null>;
}
},
{
name: 'disable MFA',
desc: "Disable user's mulifactor authentication",

View File

@ -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()

View File

@ -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) {