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.
|
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.
|
||||||
|
@ -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")
|
||||||
|
@ -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: [
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user