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:
parent
9e00d495c4
commit
9e3d54fec4
@ -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.
|
||||
|
||||
#### 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
|
||||
|
||||
Manages oauth clients known to the Satellite.
|
||||
|
@ -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}/mfa", server.disableUserMFA).Methods("DELETE")
|
||||
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/{id}", server.updateOAuthClient).Methods("PUT")
|
||||
fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.deleteOAuthClient).Methods("DELETE")
|
||||
|
@ -472,6 +472,36 @@ Blank fields will not be updated.`,
|
||||
func: async (email: string): 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: [
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
@ -159,10 +160,11 @@ func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
FullName string `json:"fullName"`
|
||||
Email string `json:"email"`
|
||||
ProjectLimit int `json:"projectLimit"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
FullName string `json:"fullName"`
|
||||
Email string `json:"email"`
|
||||
ProjectLimit int `json:"projectLimit"`
|
||||
Placement storj.PlacementConstraint `json:"placement"`
|
||||
}
|
||||
type Project struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
@ -181,6 +183,7 @@ func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) {
|
||||
FullName: user.FullName,
|
||||
Email: user.Email,
|
||||
ProjectLimit: user.ProjectLimit,
|
||||
Placement: user.DefaultPlacement,
|
||||
}
|
||||
for _, p := range projects {
|
||||
output.Projects = append(output.Projects, Project{
|
||||
@ -764,3 +767,81 @@ func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
@ -41,7 +42,7 @@ func TestUserGet(t *testing.T) {
|
||||
|
||||
link := "http://" + address.String() + "/api/users/" + project.Owner.Email
|
||||
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)
|
||||
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user