diff --git a/go.mod b/go.mod index f3f963bb1..4a21cdc30 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/zeebo/blake3 v0.2.3 github.com/zeebo/clingy v0.0.0-20230602044025-906be850f10d github.com/zeebo/errs v1.3.0 + github.com/zeebo/errs/v2 v2.0.3 github.com/zeebo/ini v0.0.0-20210514163846-cc8fbd8d9599 github.com/zyedidia/generic v1.2.1 go.etcd.io/bbolt v1.3.5 @@ -124,7 +125,6 @@ require ( github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect github.com/zeebo/admission/v3 v3.0.3 // indirect - github.com/zeebo/errs/v2 v2.0.3 // indirect github.com/zeebo/float16 v0.1.0 // indirect github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 // indirect github.com/zeebo/mwc v0.0.4 // indirect diff --git a/satellite/admin/project.go b/satellite/admin/project.go index 7db31f8b8..fc907976e 100644 --- a/satellite/admin/project.go +++ b/satellite/admin/project.go @@ -4,6 +4,7 @@ package admin import ( + "bytes" "context" "database/sql" "encoding/json" @@ -15,6 +16,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/schema" + "github.com/zeebo/errs/v2" "storj.io/common/macaroon" "storj.io/common/memory" @@ -440,6 +442,82 @@ func (server *Server) renameProject(w http.ResponseWriter, r *http.Request) { } } +func (server *Server) updateProjectsUserAgent(w http.ResponseWriter, r *http.Request) { + 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 + } + if err != nil { + sendJSONError(w, "error getting project", + err.Error(), http.StatusInternalServerError) + return + } + + creationDatePlusMonth := project.CreatedAt.AddDate(0, 1, 0) + if time.Now().After(creationDatePlusMonth) { + sendJSONError(w, "this project was created more than a month ago", + "we should update user agent only for recently created projects", http.StatusBadRequest) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + sendJSONError(w, "failed to read body", + err.Error(), http.StatusInternalServerError) + return + } + + var input struct { + UserAgent string `json:"userAgent"` + } + + err = json.Unmarshal(body, &input) + if err != nil { + sendJSONError(w, "failed to unmarshal request", + err.Error(), http.StatusBadRequest) + return + } + + if input.UserAgent == "" { + sendJSONError(w, "UserAgent was not provided", + "", http.StatusBadRequest) + return + } + + newUserAgent := []byte(input.UserAgent) + + if bytes.Equal(project.UserAgent, newUserAgent) { + sendJSONError(w, "new UserAgent is equal to existing projects UserAgent", + "", http.StatusBadRequest) + return + } + + err = server._updateProjectsUserAgent(ctx, project.ID, newUserAgent) + if err != nil { + sendJSONError(w, "failed to update projects user agent", + err.Error(), http.StatusInternalServerError) + } +} + func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -502,6 +580,45 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) { } } +func (server *Server) _updateProjectsUserAgent(ctx context.Context, projectID uuid.UUID, newUserAgent []byte) (err error) { + err = server.db.Console().Projects().UpdateUserAgent(ctx, projectID, newUserAgent) + if err != nil { + return err + } + + listOptions := buckets.ListOptions{ + Direction: buckets.DirectionForward, + } + + allowedBuckets := macaroon.AllowedBuckets{ + All: true, + } + + projectBuckets, err := server.db.Buckets().ListBuckets(ctx, projectID, listOptions, allowedBuckets) + if err != nil { + return err + } + + var errList errs.Group + for _, bucket := range projectBuckets.Items { + err = server.db.Buckets().UpdateUserAgent(ctx, projectID, bucket.Name, newUserAgent) + if err != nil { + errList.Append(err) + } + + err = server.db.Attribution().UpdateUserAgent(ctx, projectID, bucket.Name, newUserAgent) + if err != nil { + errList.Append(err) + } + } + + if errList.Err() != nil { + return errList.Err() + } + + return nil +} + func (server *Server) checkInvoicing(ctx context.Context, w http.ResponseWriter, projectID uuid.UUID) (openInvoices bool) { year, month, _ := server.nowFn().UTC().Date() firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) diff --git a/satellite/admin/project_test.go b/satellite/admin/project_test.go index 3b209a523..fa9e46d88 100644 --- a/satellite/admin/project_test.go +++ b/satellite/admin/project_test.go @@ -23,6 +23,7 @@ import ( "storj.io/storj/private/testplanet" "storj.io/storj/satellite" "storj.io/storj/satellite/accounting" + "storj.io/storj/satellite/attribution" "storj.io/storj/satellite/buckets" "storj.io/storj/satellite/console" "storj.io/storj/satellite/payments/stripe" @@ -244,6 +245,109 @@ func TestProjectAdd(t *testing.T) { }) } +func TestUpdateProjectsUserAgent(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] + newUserAgent := "awesome user agent value" + + t.Run("OK", func(t *testing.T) { + bucketName := "testName" + bucketName1 := "testName1" + + bucketID, err := uuid.New() + require.NoError(t, err) + bucketID1, err := uuid.New() + require.NoError(t, err) + + _, err = db.Buckets().CreateBucket(ctx, buckets.Bucket{ + ID: bucketID, + Name: bucketName, + ProjectID: project.ID, + UserAgent: nil, + }) + require.NoError(t, err) + + _, err = db.Buckets().CreateBucket(ctx, buckets.Bucket{ + ID: bucketID1, + Name: bucketName1, + ProjectID: project.ID, + UserAgent: nil, + }) + require.NoError(t, err) + + _, err = db.Attribution().Insert(ctx, &attribution.Info{ + ProjectID: project.ID, + BucketName: []byte(bucketName), + UserAgent: nil, + }) + require.NoError(t, err) + + _, err = db.Attribution().Insert(ctx, &attribution.Info{ + ProjectID: project.ID, + BucketName: []byte(bucketName1), + UserAgent: nil, + }) + require.NoError(t, err) + + body := strings.NewReader(fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent)) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("http://"+address.String()+"/api/projects/%s/useragent", project.ID.String()), body) + require.NoError(t, err) + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) + + response, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + require.NoError(t, response.Body.Close()) + + newUserAgentBytes := []byte(newUserAgent) + + updatedProject, err := db.Console().Projects().Get(ctx, project.ID) + require.NoError(t, err) + require.Equal(t, newUserAgentBytes, updatedProject.UserAgent) + + projectBuckets, err := planet.Satellites[0].API.Buckets.Service.ListBuckets(ctx, updatedProject.ID, buckets.ListOptions{Direction: buckets.DirectionForward}, macaroon.AllowedBuckets{All: true}) + require.NoError(t, err) + require.Equal(t, 2, len(projectBuckets.Items)) + + for _, bucket := range projectBuckets.Items { + require.Equal(t, newUserAgentBytes, bucket.UserAgent) + } + + bucketAttribution, err := db.Attribution().Get(ctx, project.ID, []byte(bucketName)) + require.Equal(t, newUserAgentBytes, bucketAttribution.UserAgent) + require.NoError(t, err) + + bucketAttribution1, err := db.Attribution().Get(ctx, project.ID, []byte(bucketName1)) + require.Equal(t, newUserAgentBytes, bucketAttribution1.UserAgent) + require.NoError(t, err) + }) + + t.Run("Same UserAgent", func(t *testing.T) { + newProject, err := db.Console().Projects().Insert(ctx, &console.Project{ + Name: "test", + UserAgent: []byte(newUserAgent), + }) + require.NoError(t, err) + + link := fmt.Sprintf("http://"+address.String()+"/api/projects/%s/useragent", newProject.ID) + body := fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent) + responseBody := assertReq(ctx, t, link, http.MethodPatch, body, http.StatusBadRequest, "", planet.Satellites[0].Config.Console.AuthToken) + require.Contains(t, string(responseBody), "new UserAgent is equal to existing projects UserAgent") + }) + }) +} + func TestProjectRename(t *testing.T) { testplanet.Run(t, testplanet.Config{ SatelliteCount: 1, diff --git a/satellite/admin/server.go b/satellite/admin/server.go index 19000c810..d27bfa435 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -21,6 +21,7 @@ import ( "storj.io/common/errs2" "storj.io/storj/satellite/accounting" adminui "storj.io/storj/satellite/admin/ui" + "storj.io/storj/satellite/attribution" "storj.io/storj/satellite/buckets" "storj.io/storj/satellite/console" "storj.io/storj/satellite/console/consoleweb" @@ -64,6 +65,10 @@ type DB interface { OIDC() oidc.DB // StripeCoinPayments returns database for satellite stripe coin payments StripeCoinPayments() stripe.DB + // Buckets returns database for buckets metainfo. + Buckets() buckets.DB + // Attribution returns database for value attribution. + Attribution() attribution.DB } // Server provides endpoints for administrative tasks. @@ -117,6 +122,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S fullAccessAPI.HandleFunc("/users/{useremail}", server.updateUser).Methods("PUT") 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("/oauth/clients", server.createOAuthClient).Methods("POST") fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.updateOAuthClient).Methods("PUT") fullAccessAPI.HandleFunc("/oauth/clients/{id}", server.deleteOAuthClient).Methods("DELETE") @@ -131,6 +137,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S fullAccessAPI.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.createGeofenceForBucket).Methods("POST") 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("/apikeys/{apikey}", server.getAPIKey).Methods("GET") fullAccessAPI.HandleFunc("/apikeys/{apikey}", server.deleteAPIKey).Methods("DELETE") fullAccessAPI.HandleFunc("/restkeys/{useremail}", server.addRESTKey).Methods("POST") diff --git a/satellite/admin/user.go b/satellite/admin/user.go index fb8f41e56..b7a456b99 100644 --- a/satellite/admin/user.go +++ b/satellite/admin/user.go @@ -4,6 +4,7 @@ package admin import ( + "bytes" "database/sql" "encoding/json" "errors" @@ -11,8 +12,10 @@ import ( "io" "net/http" "strconv" + "time" "github.com/gorilla/mux" + "github.com/zeebo/errs" "golang.org/x/crypto/bcrypt" "storj.io/common/memory" @@ -343,6 +346,101 @@ func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) { } } +func (server *Server) updateUsersUserAgent(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 + } + + creationDatePlusMonth := user.CreatedAt.AddDate(0, 1, 0) + if time.Now().After(creationDatePlusMonth) { + sendJSONError(w, "this user was created more than a month ago", + "we should update user agent only for recently created users", http.StatusBadRequest) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + sendJSONError(w, "failed to read body", + err.Error(), http.StatusInternalServerError) + return + } + + var input struct { + UserAgent string `json:"userAgent"` + } + + err = json.Unmarshal(body, &input) + if err != nil { + sendJSONError(w, "failed to unmarshal request", + err.Error(), http.StatusBadRequest) + return + } + + if input.UserAgent == "" { + sendJSONError(w, "UserAgent was not provided", + "", http.StatusBadRequest) + return + } + + newUserAgent := []byte(input.UserAgent) + + if bytes.Equal(user.UserAgent, newUserAgent) { + sendJSONError(w, "new UserAgent is equal to existing users UserAgent", + "", http.StatusBadRequest) + return + } + + err = server.db.Console().Users().UpdateUserAgent(ctx, user.ID, newUserAgent) + if err != nil { + sendJSONError(w, "failed to update user's user agent", + err.Error(), http.StatusInternalServerError) + return + } + + projects, err := server.db.Console().Projects().GetOwn(ctx, user.ID) + if err != nil { + sendJSONError(w, "failed to get users projects", + err.Error(), http.StatusInternalServerError) + return + } + + var errList errs.Group + for _, project := range projects { + if bytes.Equal(project.UserAgent, newUserAgent) { + errList.Add(errs.New("projectID: %s. New UserAgent is equal to existing users UserAgent", project.ID)) + continue + } + + err = server._updateProjectsUserAgent(ctx, project.ID, newUserAgent) + if err != nil { + errList.Add(errs.New("projectID: %s. Failed to update projects user agent: %s", project.ID, err)) + } + } + + if errList.Err() != nil { + sendJSONError(w, "failed to update projects user agent", + errList.Err().Error(), http.StatusInternalServerError) + } +} + // 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() diff --git a/satellite/admin/user_test.go b/satellite/admin/user_test.go index f5a4022a9..39ba7a694 100644 --- a/satellite/admin/user_test.go +++ b/satellite/admin/user_test.go @@ -272,6 +272,56 @@ func TestUserUpdate(t *testing.T) { }) } +func TestUpdateUsersUserAgent(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] + newUserAgent := "awesome user agent value" + + t.Run("OK", func(t *testing.T) { + body := strings.NewReader(fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent)) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("http://"+address.String()+"/api/users/%s/useragent", project.Owner.Email), body) + require.NoError(t, err) + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) + + response, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + require.NoError(t, response.Body.Close()) + + newUserAgentBytes := []byte(newUserAgent) + + updatedUser, err := db.Console().Users().Get(ctx, project.Owner.ID) + require.NoError(t, err) + require.Equal(t, newUserAgentBytes, updatedUser.UserAgent) + + updatedProject, err := db.Console().Projects().Get(ctx, project.ID) + require.NoError(t, err) + require.Equal(t, newUserAgentBytes, updatedProject.UserAgent) + }) + + t.Run("Same UserAgent", func(t *testing.T) { + err := db.Console().Users().UpdateUserAgent(ctx, project.Owner.ID, []byte(newUserAgent)) + require.NoError(t, err) + + link := fmt.Sprintf("http://"+address.String()+"/api/users/%s/useragent", project.Owner.Email) + body := fmt.Sprintf(`{"userAgent":"%s"}`, newUserAgent) + responseBody := assertReq(ctx, t, link, http.MethodPatch, body, http.StatusBadRequest, "", planet.Satellites[0].Config.Console.AuthToken) + require.Contains(t, string(responseBody), "new UserAgent is equal to existing users UserAgent") + }) + }) +} + func TestDisableMFA(t *testing.T) { testplanet.Run(t, testplanet.Config{ SatelliteCount: 1, diff --git a/satellite/attribution/db.go b/satellite/attribution/db.go index 2ed40585b..96d510af6 100644 --- a/satellite/attribution/db.go +++ b/satellite/attribution/db.go @@ -44,6 +44,8 @@ type DB interface { Get(ctx context.Context, projectID uuid.UUID, bucketName []byte) (*Info, error) // Insert creates and stores new Info. Insert(ctx context.Context, info *Info) (*Info, error) + // UpdateUserAgent updates bucket attribution data. + UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) error // QueryAttribution queries partner bucket attribution data. QueryAttribution(ctx context.Context, userAgent []byte, start time.Time, end time.Time) ([]*BucketUsage, error) // QueryAllAttribution queries all partner bucket usage data. diff --git a/satellite/buckets/db.go b/satellite/buckets/db.go index 9199c61b9..2b8d987c1 100644 --- a/satellite/buckets/db.go +++ b/satellite/buckets/db.go @@ -101,6 +101,8 @@ type DB interface { GetBucketID(ctx context.Context, bucket metabase.BucketLocation) (id uuid.UUID, err error) // UpdateBucket updates an existing bucket UpdateBucket(ctx context.Context, bucket Bucket) (_ Bucket, err error) + // UpdateUserAgent updates buckets user agent. + UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) error // DeleteBucket deletes a bucket DeleteBucket(ctx context.Context, bucketName []byte, projectID uuid.UUID) (err error) // ListBuckets returns all buckets for a project diff --git a/satellite/console/projects.go b/satellite/console/projects.go index 6cce09050..b7f8cbbae 100644 --- a/satellite/console/projects.go +++ b/satellite/console/projects.go @@ -55,6 +55,9 @@ type Projects interface { // UpdateUsageLimits is a method for updating project's usage limits. UpdateUsageLimits(ctx context.Context, id uuid.UUID, limits UsageLimits) error + + // UpdateUserAgent is a method for updating projects user agent. + UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) error } // UsageLimitsConfig is a configuration struct for default per-project usage limits. diff --git a/satellite/console/users.go b/satellite/console/users.go index 1422fd4d5..6d8126e4f 100644 --- a/satellite/console/users.go +++ b/satellite/console/users.go @@ -42,6 +42,8 @@ type Users interface { Update(ctx context.Context, userID uuid.UUID, request UpdateUserRequest) error // UpdatePaidTier sets whether the user is in the paid tier. UpdatePaidTier(ctx context.Context, id uuid.UUID, paidTier bool, projectBandwidthLimit, projectStorageLimit memory.Size, projectSegmentLimit int64, projectLimit int) error + // UpdateUserAgent is a method to update the user's user agent. + UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) error // UpdateUserProjectLimits is a method to update the user's usage limits for new projects. UpdateUserProjectLimits(ctx context.Context, id uuid.UUID, limits UsageLimits) error // GetProjectLimit is a method to get the users project limit diff --git a/satellite/satellitedb/attribution.go b/satellite/satellitedb/attribution.go index a12bc7688..24b76a026 100644 --- a/satellite/satellitedb/attribution.go +++ b/satellite/satellitedb/attribution.go @@ -243,6 +243,20 @@ func (keys *attributionDB) Get(ctx context.Context, projectID uuid.UUID, bucketN return attributionFromDBX(dbxInfo) } +// UpdateUserAgent updates bucket attribution data. +func (keys *attributionDB) UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) (err error) { + defer mon.Task()(&ctx)(&err) + + _, err = keys.db.Update_ValueAttribution_By_ProjectId_And_BucketName(ctx, + dbx.ValueAttribution_ProjectId(projectID[:]), + dbx.ValueAttribution_BucketName([]byte(bucketName)), + dbx.ValueAttribution_Update_Fields{ + UserAgent: dbx.ValueAttribution_UserAgent(userAgent), + }) + + return err +} + // Insert implements create partner info. func (keys *attributionDB) Insert(ctx context.Context, info *attribution.Info) (_ *attribution.Info, err error) { defer mon.Task()(&ctx)(&err) diff --git a/satellite/satellitedb/bucketsdb.go b/satellite/satellitedb/bucketsdb.go index 7986960a2..ae7de2a1f 100644 --- a/satellite/satellitedb/bucketsdb.go +++ b/satellite/satellitedb/bucketsdb.go @@ -168,6 +168,20 @@ func (db *bucketsDB) UpdateBucket(ctx context.Context, bucket buckets.Bucket) (_ return convertDBXtoBucket(dbxBucket) } +// UpdateUserAgent updates buckets user agent. +func (db *bucketsDB) UpdateUserAgent(ctx context.Context, projectID uuid.UUID, bucketName string, userAgent []byte) (err error) { + defer mon.Task()(&ctx)(&err) + + _, err = db.db.Update_BucketMetainfo_By_ProjectId_And_Name(ctx, + dbx.BucketMetainfo_ProjectId(projectID[:]), + dbx.BucketMetainfo_Name([]byte(bucketName)), + dbx.BucketMetainfo_Update_Fields{ + UserAgent: dbx.BucketMetainfo_UserAgent(userAgent), + }) + + return err +} + // DeleteBucket deletes a bucket. func (db *bucketsDB) DeleteBucket(ctx context.Context, bucketName []byte, projectID uuid.UUID) (err error) { defer mon.Task()(&ctx)(&err) diff --git a/satellite/satellitedb/dbx/project.dbx b/satellite/satellitedb/dbx/project.dbx index 11449a4b0..c4d6cfded 100644 --- a/satellite/satellitedb/dbx/project.dbx +++ b/satellite/satellitedb/dbx/project.dbx @@ -40,7 +40,7 @@ model project ( // max_buckets is the maximum number of buckets that can be created for the project. field max_buckets int ( nullable, updatable ) // user_agent is the referred partner who created the project. - field user_agent blob ( nullable ) + field user_agent blob ( nullable, updatable ) // owner_id refers to the user UUID in user.id. field owner_id blob // salt is used for salting the user passphrase for the content. diff --git a/satellite/satellitedb/dbx/project_bucket.dbx b/satellite/satellitedb/dbx/project_bucket.dbx index 2a6b0e299..fa3ac511a 100644 --- a/satellite/satellitedb/dbx/project_bucket.dbx +++ b/satellite/satellitedb/dbx/project_bucket.dbx @@ -148,7 +148,7 @@ model value_attribution ( // user_agent is the first User-Agent that was used to upload data. // unless the user signed up with a specific partner. // note: this field is duplicated in bucket_metainfo.user_agent. - field user_agent blob ( nullable ) + field user_agent blob ( updatable, nullable ) // TODO remove as part of release 1.81 or after field partner_id blob (nullable, default null) // last_updated is updated whenever the row changes. @@ -156,6 +156,10 @@ model value_attribution ( ) create value_attribution () +update value_attribution ( + where value_attribution.project_id = ? + where value_attribution.bucket_name = ? +) read one ( select value_attribution diff --git a/satellite/satellitedb/dbx/satellitedb.dbx.go b/satellite/satellitedb/dbx/satellitedb.dbx.go index b3cd6323e..1f54ec16a 100644 --- a/satellite/satellitedb/dbx/satellitedb.dbx.go +++ b/satellite/satellitedb/dbx/satellitedb.dbx.go @@ -5573,6 +5573,7 @@ type Project_Update_Fields struct { RateLimit Project_RateLimit_Field BurstLimit Project_BurstLimit_Field MaxBuckets Project_MaxBuckets_Field + UserAgent Project_UserAgent_Field DefaultPlacement Project_DefaultPlacement_Field } @@ -9335,6 +9336,7 @@ type User_Update_Fields struct { ShortName User_ShortName_Field PasswordHash User_PasswordHash_Field Status User_Status_Field + UserAgent User_UserAgent_Field ProjectLimit User_ProjectLimit_Field ProjectBandwidthLimit User_ProjectBandwidthLimit_Field ProjectStorageLimit User_ProjectStorageLimit_Field @@ -10313,6 +10315,7 @@ type ValueAttribution_Create_Fields struct { } type ValueAttribution_Update_Fields struct { + UserAgent ValueAttribution_UserAgent_Field } type ValueAttribution_ProjectId_Field struct { @@ -18304,6 +18307,11 @@ func (obj *pgxImpl) Update_Project_By_Id(ctx context.Context, __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("max_buckets = ?")) } + if update.UserAgent._set { + __values = append(__values, update.UserAgent.value()) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?")) + } + if update.DefaultPlacement._set { __values = append(__values, update.DefaultPlacement.value()) __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("default_placement = ?")) @@ -18461,6 +18469,49 @@ func (obj *pgxImpl) Update_BucketMetainfo_By_ProjectId_And_Name(ctx context.Cont return bucket_metainfo, nil } +func (obj *pgxImpl) Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context, + value_attribution_project_id ValueAttribution_ProjectId_Field, + value_attribution_bucket_name ValueAttribution_BucketName_Field, + update ValueAttribution_Update_Fields) ( + value_attribution *ValueAttribution, err error) { + defer mon.Task()(&ctx)(&err) + var __sets = &__sqlbundle_Hole{} + + var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("UPDATE value_attributions SET "), __sets, __sqlbundle_Literal(" WHERE value_attributions.project_id = ? AND value_attributions.bucket_name = ? RETURNING value_attributions.project_id, value_attributions.bucket_name, value_attributions.user_agent, value_attributions.partner_id, value_attributions.last_updated")}} + + __sets_sql := __sqlbundle_Literals{Join: ", "} + var __values []interface{} + var __args []interface{} + + if update.UserAgent._set { + __values = append(__values, update.UserAgent.value()) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?")) + } + + __now := obj.db.Hooks.Now().UTC() + + __values = append(__values, __now) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("last_updated = ?")) + + __args = append(__args, value_attribution_project_id.value(), value_attribution_bucket_name.value()) + + __values = append(__values, __args...) + __sets.SQL = __sets_sql + + var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) + obj.logStmt(__stmt, __values...) + + value_attribution = &ValueAttribution{} + err = obj.queryRowContext(ctx, __stmt, __values...).Scan(&value_attribution.ProjectId, &value_attribution.BucketName, &value_attribution.UserAgent, &value_attribution.PartnerId, &value_attribution.LastUpdated) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, obj.makeErr(err) + } + return value_attribution, nil +} + func (obj *pgxImpl) Update_User_By_Id(ctx context.Context, user_id User_Id_Field, update User_Update_Fields) ( @@ -18504,6 +18555,11 @@ func (obj *pgxImpl) Update_User_By_Id(ctx context.Context, __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("status = ?")) } + if update.UserAgent._set { + __values = append(__values, update.UserAgent.value()) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?")) + } + if update.ProjectLimit._set { __values = append(__values, update.ProjectLimit.value()) __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("project_limit = ?")) @@ -26211,6 +26267,11 @@ func (obj *pgxcockroachImpl) Update_Project_By_Id(ctx context.Context, __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("max_buckets = ?")) } + if update.UserAgent._set { + __values = append(__values, update.UserAgent.value()) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?")) + } + if update.DefaultPlacement._set { __values = append(__values, update.DefaultPlacement.value()) __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("default_placement = ?")) @@ -26368,6 +26429,49 @@ func (obj *pgxcockroachImpl) Update_BucketMetainfo_By_ProjectId_And_Name(ctx con return bucket_metainfo, nil } +func (obj *pgxcockroachImpl) Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context, + value_attribution_project_id ValueAttribution_ProjectId_Field, + value_attribution_bucket_name ValueAttribution_BucketName_Field, + update ValueAttribution_Update_Fields) ( + value_attribution *ValueAttribution, err error) { + defer mon.Task()(&ctx)(&err) + var __sets = &__sqlbundle_Hole{} + + var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("UPDATE value_attributions SET "), __sets, __sqlbundle_Literal(" WHERE value_attributions.project_id = ? AND value_attributions.bucket_name = ? RETURNING value_attributions.project_id, value_attributions.bucket_name, value_attributions.user_agent, value_attributions.partner_id, value_attributions.last_updated")}} + + __sets_sql := __sqlbundle_Literals{Join: ", "} + var __values []interface{} + var __args []interface{} + + if update.UserAgent._set { + __values = append(__values, update.UserAgent.value()) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?")) + } + + __now := obj.db.Hooks.Now().UTC() + + __values = append(__values, __now) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("last_updated = ?")) + + __args = append(__args, value_attribution_project_id.value(), value_attribution_bucket_name.value()) + + __values = append(__values, __args...) + __sets.SQL = __sets_sql + + var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) + obj.logStmt(__stmt, __values...) + + value_attribution = &ValueAttribution{} + err = obj.queryRowContext(ctx, __stmt, __values...).Scan(&value_attribution.ProjectId, &value_attribution.BucketName, &value_attribution.UserAgent, &value_attribution.PartnerId, &value_attribution.LastUpdated) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, obj.makeErr(err) + } + return value_attribution, nil +} + func (obj *pgxcockroachImpl) Update_User_By_Id(ctx context.Context, user_id User_Id_Field, update User_Update_Fields) ( @@ -26411,6 +26515,11 @@ func (obj *pgxcockroachImpl) Update_User_By_Id(ctx context.Context, __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("status = ?")) } + if update.UserAgent._set { + __values = append(__values, update.UserAgent.value()) + __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("user_agent = ?")) + } + if update.ProjectLimit._set { __values = append(__values, update.ProjectLimit.value()) __sets_sql.SQLs = append(__sets_sql.SQLs, __sqlbundle_Literal("project_limit = ?")) @@ -29786,6 +29895,18 @@ func (rx *Rx) Update_User_By_Id(ctx context.Context, return tx.Update_User_By_Id(ctx, user_id, update) } +func (rx *Rx) Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context, + value_attribution_project_id ValueAttribution_ProjectId_Field, + value_attribution_bucket_name ValueAttribution_BucketName_Field, + update ValueAttribution_Update_Fields) ( + value_attribution *ValueAttribution, err error) { + var tx *Tx + if tx, err = rx.getTx(ctx); err != nil { + return + } + return tx.Update_ValueAttribution_By_ProjectId_And_BucketName(ctx, value_attribution_project_id, value_attribution_bucket_name, update) +} + func (rx *Rx) Update_WebappSession_By_Id(ctx context.Context, webapp_session_id WebappSession_Id_Field, update WebappSession_Update_Fields) ( @@ -30713,6 +30834,12 @@ type Methods interface { update User_Update_Fields) ( user *User, err error) + Update_ValueAttribution_By_ProjectId_And_BucketName(ctx context.Context, + value_attribution_project_id ValueAttribution_ProjectId_Field, + value_attribution_bucket_name ValueAttribution_BucketName_Field, + update ValueAttribution_Update_Fields) ( + value_attribution *ValueAttribution, err error) + Update_WebappSession_By_Id(ctx context.Context, webapp_session_id WebappSession_Id_Field, update WebappSession_Update_Fields) ( diff --git a/satellite/satellitedb/dbx/user.dbx b/satellite/satellitedb/dbx/user.dbx index 0ee3badef..87d941ab2 100644 --- a/satellite/satellitedb/dbx/user.dbx +++ b/satellite/satellitedb/dbx/user.dbx @@ -26,7 +26,7 @@ model user ( // status indicates whether the user is inactive=0, active=1, or deleted=2. See console.UserStatus for details. field status int ( updatable, autoinsert ) // user_agent contains the partner parameter from registration. - field user_agent blob ( nullable ) + field user_agent blob ( updatable, nullable ) // created_at indicates when the user was created. field created_at timestamp ( autoinsert ) diff --git a/satellite/satellitedb/projects.go b/satellite/satellitedb/projects.go index f5d2c452a..f67fa147c 100644 --- a/satellite/satellitedb/projects.go +++ b/satellite/satellitedb/projects.go @@ -291,6 +291,19 @@ func (projects *projects) UpdateBucketLimit(ctx context.Context, id uuid.UUID, n return err } +// UpdateUserAgent is a method for updating projects user agent. +func (projects *projects) UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) (err error) { + defer mon.Task()(&ctx)(&err) + + _, err = projects.db.Update_Project_By_Id(ctx, + dbx.Project_Id(id[:]), + dbx.Project_Update_Fields{ + UserAgent: dbx.Project_UserAgent(userAgent), + }) + + return err +} + // List returns paginated projects, created before provided timestamp. func (projects *projects) List(ctx context.Context, offset int64, limit int, before time.Time) (_ console.ProjectsPage, err error) { defer mon.Task()(&ctx)(&err) diff --git a/satellite/satellitedb/users.go b/satellite/satellitedb/users.go index 8c1859867..aa84f3f72 100644 --- a/satellite/satellitedb/users.go +++ b/satellite/satellitedb/users.go @@ -305,6 +305,21 @@ func (users *users) UpdatePaidTier(ctx context.Context, id uuid.UUID, paidTier b return err } +// UpdateUserAgent is a method to update the user's user agent. +func (users *users) UpdateUserAgent(ctx context.Context, id uuid.UUID, userAgent []byte) (err error) { + defer mon.Task()(&ctx)(&err) + + _, err = users.db.Update_User_By_Id( + ctx, + dbx.User_Id(id[:]), + dbx.User_Update_Fields{ + UserAgent: dbx.User_UserAgent(userAgent), + }, + ) + + return err +} + // UpdateUserProjectLimits is a method to update the user's usage limits for new projects. func (users *users) UpdateUserProjectLimits(ctx context.Context, id uuid.UUID, limits console.UsageLimits) (err error) { defer mon.Task()(&ctx)(&err) diff --git a/testsuite/storjscan/go.mod b/testsuite/storjscan/go.mod index 46642080b..cb33d3545 100644 --- a/testsuite/storjscan/go.mod +++ b/testsuite/storjscan/go.mod @@ -125,6 +125,7 @@ require ( github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect github.com/zeebo/admission/v3 v3.0.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect + github.com/zeebo/errs/v2 v2.0.3 // indirect github.com/zeebo/float16 v0.1.0 // indirect github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 // indirect github.com/zeebo/mwc v0.0.4 // indirect diff --git a/testsuite/storjscan/go.sum b/testsuite/storjscan/go.sum index 889c8d5d9..c92663253 100644 --- a/testsuite/storjscan/go.sum +++ b/testsuite/storjscan/go.sum @@ -836,6 +836,7 @@ github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/errs/v2 v2.0.3 h1:WwqAmopgot4ZC+CgIveP+H91Nf78NDEGWjtAXen45Hw= +github.com/zeebo/errs/v2 v2.0.3/go.mod h1:OKmvVZt4UqpyJrYFykDKm168ZquJ55pbbIVUICNmLN0= github.com/zeebo/float16 v0.1.0 h1:kRqxv5og6z1emEyz5FpW0/BVHe5VfxEAw6b1ljCZlUc= github.com/zeebo/float16 v0.1.0/go.mod h1:fssGvvXu+XS8MH57cKmyrLB/cqioYeYX/2mXCN3a5wo= github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 h1:+cwNE5KJ3pika4HuzmDHkDlK5myo0G9Sv+eO7WWxnUQ= diff --git a/testsuite/ui/go.mod b/testsuite/ui/go.mod index 50e08884d..e3733ad2a 100644 --- a/testsuite/ui/go.mod +++ b/testsuite/ui/go.mod @@ -185,6 +185,7 @@ require ( github.com/zeebo/admission/v3 v3.0.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/errs v1.3.0 // indirect + github.com/zeebo/errs/v2 v2.0.3 // indirect github.com/zeebo/float16 v0.1.0 // indirect github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 // indirect github.com/zeebo/mwc v0.0.4 // indirect diff --git a/testsuite/ui/go.sum b/testsuite/ui/go.sum index 5e9864b3a..f75f13ad0 100644 --- a/testsuite/ui/go.sum +++ b/testsuite/ui/go.sum @@ -1203,6 +1203,7 @@ github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtC github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/errs/v2 v2.0.3 h1:WwqAmopgot4ZC+CgIveP+H91Nf78NDEGWjtAXen45Hw= +github.com/zeebo/errs/v2 v2.0.3/go.mod h1:OKmvVZt4UqpyJrYFykDKm168ZquJ55pbbIVUICNmLN0= github.com/zeebo/float16 v0.1.0 h1:kRqxv5og6z1emEyz5FpW0/BVHe5VfxEAw6b1ljCZlUc= github.com/zeebo/float16 v0.1.0/go.mod h1:fssGvvXu+XS8MH57cKmyrLB/cqioYeYX/2mXCN3a5wo= github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 h1:+cwNE5KJ3pika4HuzmDHkDlK5myo0G9Sv+eO7WWxnUQ=