satellite/admin: extend API to allow setting and deleting account level geofence

Issue: https://github.com/storj/storj-private/issues/357
Change-Id: I04589e18214e7090ccd686fd531066d942afa6ed
This commit is contained in:
Clement Sam 2023-07-27 12:54:48 +00:00
parent 9e00d495c4
commit 9e3d54fec4
5 changed files with 182 additions and 5 deletions

View File

@ -179,6 +179,22 @@ Freezes a user account so no uploads or downloads may occur.
Unfreezes a user account so uploads and downloads may resume. Unfreezes a user account so uploads and downloads may resume.
#### PATCH /api/users/{user-email}/geofence
Sets the account level geofence for the user.
Example request:
```json
{
"region": "US"
}
```
#### DELETE /api/users/{user-email}/geofence
Removes the account level geofence for the user.
### OAuth Client Management ### OAuth Client Management
Manages oauth clients known to the Satellite. Manages oauth clients known to the Satellite.

View File

@ -123,6 +123,8 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S
fullAccessAPI.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE") fullAccessAPI.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE")
fullAccessAPI.HandleFunc("/users/{useremail}/mfa", server.disableUserMFA).Methods("DELETE") fullAccessAPI.HandleFunc("/users/{useremail}/mfa", server.disableUserMFA).Methods("DELETE")
fullAccessAPI.HandleFunc("/users/{useremail}/useragent", server.updateUsersUserAgent).Methods("PATCH") fullAccessAPI.HandleFunc("/users/{useremail}/useragent", server.updateUsersUserAgent).Methods("PATCH")
fullAccessAPI.HandleFunc("/users/{useremail}/geofence", server.createGeofenceForAccount).Methods("PATCH")
fullAccessAPI.HandleFunc("/users/{useremail}/geofence", server.deleteGeofenceForAccount).Methods("DELETE")
fullAccessAPI.HandleFunc("/oauth/clients", server.createOAuthClient).Methods("POST") fullAccessAPI.HandleFunc("/oauth/clients", server.createOAuthClient).Methods("POST")
fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.updateOAuthClient).Methods("PUT") fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.updateOAuthClient).Methods("PUT")
fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.deleteOAuthClient).Methods("DELETE") fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.deleteOAuthClient).Methods("DELETE")

View File

@ -472,6 +472,36 @@ Blank fields will not be updated.`,
func: async (email: string): Promise<null> => { func: async (email: string): Promise<null> => {
return this.fetch('DELETE', `users/${email}/warning`) as Promise<null>; return this.fetch('DELETE', `users/${email}/warning`) as Promise<null>;
} }
},
{
name: 'set geofencing',
desc: 'Set account level geofence for a user',
params: [
['email', new InputText('email', true)],
[
'Region',
new Select(false, true, [
{ text: 'European Union', value: 'EU' },
{ text: 'European Economic Area', value: 'EEA' },
{ text: 'United States', value: 'US' },
{ text: 'Germany', value: 'DE' },
{ text: 'No Russia and/or other sanctioned country', value: 'NR' }
])
]
],
func: async (email: string, region: string): Promise<null> => {
return this.fetch('PATCH', `users/${email}/geofence`, null, {
region
}) as Promise<null>;
}
},
{
name: 'delete geofencing',
desc: 'Delete account level geofence for a user',
params: [['email', new InputText('email', true)]],
func: async (email: string): Promise<null> => {
return this.fetch('DELETE', `users/${email}/geofence`) as Promise<null>;
}
} }
], ],
rest_api_keys: [ rest_api_keys: [

View File

@ -19,6 +19,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"storj.io/common/memory" "storj.io/common/memory"
"storj.io/common/storj"
"storj.io/common/uuid" "storj.io/common/uuid"
"storj.io/storj/satellite/console" "storj.io/storj/satellite/console"
) )
@ -159,10 +160,11 @@ func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) {
} }
type User struct { type User struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
Email string `json:"email"` Email string `json:"email"`
ProjectLimit int `json:"projectLimit"` ProjectLimit int `json:"projectLimit"`
Placement storj.PlacementConstraint `json:"placement"`
} }
type Project struct { type Project struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
@ -181,6 +183,7 @@ func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) {
FullName: user.FullName, FullName: user.FullName,
Email: user.Email, Email: user.Email,
ProjectLimit: user.ProjectLimit, ProjectLimit: user.ProjectLimit,
Placement: user.DefaultPlacement,
} }
for _, p := range projects { for _, p := range projects {
output.Projects = append(output.Projects, Project{ output.Projects = append(output.Projects, Project{
@ -764,3 +767,81 @@ func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
err.Error(), http.StatusInternalServerError) err.Error(), http.StatusInternalServerError)
} }
} }
func (server *Server) createGeofenceForAccount(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
sendJSONError(w, "failed to read body",
err.Error(), http.StatusInternalServerError)
return
}
var input struct {
Region string `json:"region"`
}
err = json.Unmarshal(body, &input)
if err != nil {
sendJSONError(w, "failed to unmarshal request",
err.Error(), http.StatusBadRequest)
return
}
if input.Region == "" {
sendJSONError(w, "region was not provided",
"", http.StatusBadRequest)
return
}
placement, err := parsePlacementConstraint(input.Region)
if err != nil {
sendJSONError(w, err.Error(), "available: EU, EEA, US, DE, NR", http.StatusBadRequest)
return
}
server.setGeofenceForUser(w, r, placement)
}
func (server *Server) deleteGeofenceForAccount(w http.ResponseWriter, r *http.Request) {
server.setGeofenceForUser(w, r, storj.EveryCountry)
}
func (server *Server) setGeofenceForUser(w http.ResponseWriter, r *http.Request, placement storj.PlacementConstraint) {
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 details",
err.Error(), http.StatusInternalServerError)
return
}
if user.DefaultPlacement == placement {
sendJSONError(w, "new placement is equal to user's current placement",
"", http.StatusBadRequest)
return
}
err = server.db.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Email: &user.Email,
DefaultPlacement: placement,
})
if err != nil {
sendJSONError(w, "unable to set geofence for user",
err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -15,6 +15,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"storj.io/common/memory" "storj.io/common/memory"
"storj.io/common/storj"
"storj.io/common/testcontext" "storj.io/common/testcontext"
"storj.io/storj/private/testplanet" "storj.io/storj/private/testplanet"
"storj.io/storj/satellite" "storj.io/storj/satellite"
@ -41,7 +42,7 @@ func TestUserGet(t *testing.T) {
link := "http://" + address.String() + "/api/users/" + project.Owner.Email link := "http://" + address.String() + "/api/users/" + project.Owner.Email
expectedBody := `{` + expectedBody := `{` +
fmt.Sprintf(`"user":{"id":"%s","fullName":"User uplink0_0","email":"%s","projectLimit":%d},`, project.Owner.ID, project.Owner.Email, projLimit) + fmt.Sprintf(`"user":{"id":"%s","fullName":"User uplink0_0","email":"%s","projectLimit":%d,"placement":%d},`, project.Owner.ID, project.Owner.Email, projLimit, storj.EveryCountry) +
fmt.Sprintf(`"projects":[{"id":"%s","name":"uplink0_0","description":"","ownerId":"%s"}]}`, project.ID, project.Owner.ID) fmt.Sprintf(`"projects":[{"id":"%s","name":"uplink0_0","description":"","ownerId":"%s"}]}`, project.ID, project.Owner.ID)
assertReq(ctx, t, link, http.MethodGet, "", http.StatusOK, expectedBody, planet.Satellites[0].Config.Console.AuthToken) assertReq(ctx, t, link, http.MethodGet, "", http.StatusOK, expectedBody, planet.Satellites[0].Config.Console.AuthToken)
@ -497,3 +498,50 @@ func TestUserDelete(t *testing.T) {
require.Contains(t, string(body), "does not exist") require.Contains(t, string(body), "does not exist")
}) })
} }
func TestSetUsersGeofence(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) {
db := planet.Satellites[0].DB
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
project := planet.Uplinks[0].Projects[0]
newPlacement := storj.EU
newPlacementStr := "EU"
link := fmt.Sprintf("http://"+address.String()+"/api/users/%s/geofence", project.Owner.Email)
t.Run("OK", func(t *testing.T) {
body := fmt.Sprintf(`{"region":"%s"}`, newPlacementStr)
assertReq(ctx, t, link, http.MethodPatch, body, http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken)
updatedUser, err := db.Console().Users().Get(ctx, project.Owner.ID)
require.NoError(t, err)
require.Equal(t, newPlacement, updatedUser.DefaultPlacement)
// DELETE
assertReq(ctx, t, link, http.MethodDelete, "", http.StatusOK, "", planet.Satellites[0].Config.Console.AuthToken)
updatedUser, err = db.Console().Users().Get(ctx, project.Owner.ID)
require.NoError(t, err)
require.Equal(t, storj.EveryCountry, updatedUser.DefaultPlacement)
})
t.Run("Same Placement", func(t *testing.T) {
err := db.Console().Users().Update(ctx, project.Owner.ID, console.UpdateUserRequest{
Email: &project.Owner.Email,
DefaultPlacement: newPlacement,
})
require.NoError(t, err)
body := fmt.Sprintf(`{"region":"%s"}`, newPlacementStr)
responseBody := assertReq(ctx, t, link, http.MethodPatch, body, http.StatusBadRequest, "", planet.Satellites[0].Config.Console.AuthToken)
require.Contains(t, string(responseBody), "new placement is equal to user's current placement")
})
})
}