diff --git a/satellite/admin/project.go b/satellite/admin/project.go index fc907976e..c8581bd16 100644 --- a/satellite/admin/project.go +++ b/satellite/admin/project.go @@ -20,6 +20,7 @@ import ( "storj.io/common/macaroon" "storj.io/common/memory" + "storj.io/common/storj" "storj.io/common/uuid" "storj.io/storj/satellite/buckets" "storj.io/storj/satellite/console" @@ -697,6 +698,55 @@ func (server *Server) checkUsage(ctx context.Context, w http.ResponseWriter, pro return server.checkInvoicing(ctx, w, projectID) } +func (server *Server) createGeofenceForProject(w http.ResponseWriter, r *http.Request) { + placement, err := parsePlacementConstraint(r.URL.Query().Get("region")) + if err != nil { + sendJSONError(w, err.Error(), "available: EU, EEA, US, DE, NR", http.StatusBadRequest) + return + } + + server.setGeofenceForProject(w, r, placement) +} + +func (server *Server) deleteGeofenceForProject(w http.ResponseWriter, r *http.Request) { + server.setGeofenceForProject(w, r, storj.EveryCountry) +} + +func (server *Server) setGeofenceForProject(w http.ResponseWriter, r *http.Request, placement storj.PlacementConstraint) { + ctx := r.Context() + + vars := mux.Vars(r) + projectUUIDString, ok := vars["project"] + if !ok { + sendJSONError(w, "project-uuid missing", + "", http.StatusBadRequest) + return + } + + projectUUID, err := uuid.FromString(projectUUIDString) + if err != nil { + sendJSONError(w, "invalid project-uuid", + err.Error(), http.StatusBadRequest) + return + } + + project, err := server.db.Console().Projects().Get(ctx, projectUUID) + if errors.Is(err, sql.ErrNoRows) { + sendJSONError(w, "project with specified uuid does not exist", + "", http.StatusNotFound) + return + } + + project.DefaultPlacement = placement + + err = server.db.Console().Projects().Update(ctx, project) + if err != nil { + sendJSONError(w, "unable to set geofence for project", + err.Error(), http.StatusInternalServerError) + return + } +} + func bucketNames(buckets []buckets.Bucket) []string { var xs []string for _, b := range buckets { diff --git a/satellite/admin/project_testplanet_test.go b/satellite/admin/project_testplanet_test.go new file mode 100644 index 000000000..c3a683bd0 --- /dev/null +++ b/satellite/admin/project_testplanet_test.go @@ -0,0 +1,99 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +package admin_test + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "storj.io/common/storj" + "storj.io/common/testcontext" + "storj.io/common/uuid" + "storj.io/storj/private/testplanet" + "storj.io/storj/satellite" +) + +func TestAdminProjectGeofenceAPI(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) { + uplink := planet.Uplinks[0] + sat := planet.Satellites[0] + address := sat.Admin.Admin.Listener.Addr() + project, err := sat.DB.Console().Projects().Get(ctx, uplink.Projects[0].ID) + require.NoError(t, err) + + // update project set default placement to EEA + project.DefaultPlacement = storj.EEA + require.NoError(t, sat.DB.Console().Projects().Update(ctx, project)) + + testCases := []struct { + name string + project uuid.UUID + // expectations + status int + body string + }{ + { + name: "project does not exist", + project: uuid.NullUUID{}.UUID, + status: http.StatusNotFound, + body: `{"error":"project with specified uuid does not exist","detail":""}`, + }, + { + name: "validated", + project: project.ID, + status: http.StatusOK, + body: "", + }, + } + + for _, testCase := range testCases { + baseURL := fmt.Sprintf("http://%s/api/projects/%s", address, testCase.project) + t.Log(baseURL) + baseGeofenceURL := fmt.Sprintf("http://%s/api/projects/%s/geofence", address, testCase.project) + t.Log(baseGeofenceURL) + + t.Run(testCase.name, func(t *testing.T) { + assertReq(ctx, t, baseGeofenceURL+"?region=EU", "POST", "", testCase.status, testCase.body, sat.Config.Console.AuthToken) + + if testCase.status == http.StatusOK { + + t.Run("Set", func(t *testing.T) { + project, err := sat.DB.Console().Projects().Get(ctx, testCase.project) + require.NoError(t, err) + require.Equal(t, storj.EU, project.DefaultPlacement) + + expected, err := json.Marshal(project) + require.NoError(t, err, "failed to json encode expected bucket") + + assertGet(ctx, t, baseURL, string(expected), sat.Config.Console.AuthToken) + }) + t.Run("Delete", func(t *testing.T) { + assertReq(ctx, t, baseGeofenceURL, "DELETE", "", testCase.status, testCase.body, sat.Config.Console.AuthToken) + + project, err := sat.DB.Console().Projects().Get(ctx, testCase.project) + require.NoError(t, err) + + expected, err := json.Marshal(project) + require.NoError(t, err) + assertGet(ctx, t, baseURL, string(expected), sat.Config.Console.AuthToken) + }) + } + }) + } + }) +} diff --git a/satellite/admin/server.go b/satellite/admin/server.go index 5b5683c8c..d11fb9bd0 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -138,6 +138,8 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.deleteGeofenceForBucket).Methods("DELETE") fullAccessAPI.HandleFunc("/projects/{project}/usage", server.checkProjectUsage).Methods("GET") fullAccessAPI.HandleFunc("/projects/{project}/useragent", server.updateProjectsUserAgent).Methods("PATCH") + fullAccessAPI.HandleFunc("/projects/{project}/geofence", server.createGeofenceForProject).Methods("POST") + fullAccessAPI.HandleFunc("/projects/{project}/geofence", server.deleteGeofenceForProject).Methods("DELETE") fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.getAPIKey).Methods("GET") fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.deleteAPIKey).Methods("DELETE") fullAccessAPI.HandleFunc("/restkeys/{useremail}", server.addRESTKey).Methods("POST")