diff --git a/satellite/admin/project_testplanet_test.go b/satellite/admin/project_testplanet_test.go index c3a683bd0..af0bb8843 100644 --- a/satellite/admin/project_testplanet_test.go +++ b/satellite/admin/project_testplanet_test.go @@ -36,10 +36,6 @@ func TestAdminProjectGeofenceAPI(t *testing.T) { 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 diff --git a/satellite/console/config.go b/satellite/console/config.go new file mode 100644 index 000000000..9f8ec021a --- /dev/null +++ b/satellite/console/config.go @@ -0,0 +1,113 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +package console + +import ( + "encoding/json" + "time" + + "github.com/spf13/pflag" + + "storj.io/common/storj" +) + +// Config keeps track of core console service configuration parameters. +type Config struct { + PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"` + OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"` + DefaultProjectLimit int `help:"default project limits for users" default:"1" testDefault:"5"` + AsOfSystemTimeDuration time.Duration `help:"default duration for AS OF SYSTEM TIME" devDefault:"-5m" releaseDefault:"-5m" testDefault:"0"` + LoginAttemptsWithoutPenalty int `help:"number of times user can try to login without penalty" default:"3"` + FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"` + ProjectInvitationExpiration time.Duration `help:"duration that project member invitations are valid for" default:"168h"` + UserBalanceForUpgrade int64 `help:"amount of base units of US micro dollars needed to upgrade user's tier status" default:"10000000"` + PlacementEdgeURLOverrides PlacementEdgeURLOverrides `help:"placement-specific edge service URL overrides in the format {\"placementID\": {\"authService\": \"...\", \"publicLinksharing\": \"...\", \"internalLinksharing\": \"...\"}, \"placementID2\": ...}"` + UsageLimits UsageLimitsConfig + Captcha CaptchaConfig + Session SessionConfig +} + +// CaptchaConfig contains configurations for login/registration captcha system. +type CaptchaConfig struct { + Login MultiCaptchaConfig `json:"login"` + Registration MultiCaptchaConfig `json:"registration"` +} + +// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems. +type MultiCaptchaConfig struct { + Recaptcha SingleCaptchaConfig `json:"recaptcha"` + Hcaptcha SingleCaptchaConfig `json:"hcaptcha"` +} + +// SingleCaptchaConfig contains configurations abstract captcha system. +type SingleCaptchaConfig struct { + Enabled bool `help:"whether or not captcha is enabled" default:"false" json:"enabled"` + SiteKey string `help:"captcha site key" json:"siteKey"` + SecretKey string `help:"captcha secret key" json:"-"` +} + +// SessionConfig contains configurations for session management. +type SessionConfig struct { + InactivityTimerEnabled bool `help:"indicates if session can be timed out due inactivity" default:"true"` + InactivityTimerDuration int `help:"inactivity timer delay in seconds" default:"600"` + InactivityTimerViewerEnabled bool `help:"indicates whether remaining session time is shown for debugging" default:"false"` + Duration time.Duration `help:"duration a session is valid for (superseded by inactivity timer delay if inactivity timer is enabled)" default:"168h"` +} + +// EdgeURLOverrides contains edge service URL overrides. +type EdgeURLOverrides struct { + AuthService string `json:"authService,omitempty"` + PublicLinksharing string `json:"publicLinksharing,omitempty"` + InternalLinksharing string `json:"internalLinksharing,omitempty"` +} + +// PlacementEdgeURLOverrides represents a mapping between placement IDs and edge service URL overrides. +type PlacementEdgeURLOverrides struct { + overrideMap map[storj.PlacementConstraint]EdgeURLOverrides +} + +// Ensure that PlacementEdgeOverrides implements pflag.Value. +var _ pflag.Value = (*PlacementEdgeURLOverrides)(nil) + +// Type implements pflag.Value. +func (PlacementEdgeURLOverrides) Type() string { return "console.PlacementEdgeURLOverrides" } + +// String implements pflag.Value. +func (ov *PlacementEdgeURLOverrides) String() string { + if ov == nil || len(ov.overrideMap) == 0 { + return "" + } + + overrides, err := json.Marshal(ov.overrideMap) + if err != nil { + return "" + } + + return string(overrides) +} + +// Set implements pflag.Value. +func (ov *PlacementEdgeURLOverrides) Set(s string) error { + if s == "" { + return nil + } + + overrides := make(map[storj.PlacementConstraint]EdgeURLOverrides) + err := json.Unmarshal([]byte(s), &overrides) + if err != nil { + return err + } + ov.overrideMap = overrides + + return nil +} + +// Get returns the edge service URL overrides for the given placement ID. +func (ov *PlacementEdgeURLOverrides) Get(placement storj.PlacementConstraint) (overrides EdgeURLOverrides, ok bool) { + if ov == nil { + return EdgeURLOverrides{}, false + } + overrides, ok = ov.overrideMap[placement] + return overrides, ok +} diff --git a/satellite/console/consoleweb/consoleapi/projects.go b/satellite/console/consoleweb/consoleapi/projects.go index 212b8f592..c007195a9 100644 --- a/satellite/console/consoleweb/consoleapi/projects.go +++ b/satellite/console/consoleweb/consoleapi/projects.go @@ -83,7 +83,7 @@ func (p *Projects) GetUserProjects(w http.ResponseWriter, r *http.Request) { response := make([]console.ProjectInfo, 0) for _, project := range projects { - response = append(response, project.GetMinimal()) + response = append(response, p.service.GetMinimalProject(&project)) } err = json.NewEncoder(w).Encode(response) @@ -156,7 +156,7 @@ func (p *Projects) GetPagedProjects(w http.ResponseWriter, r *http.Request) { } for _, project := range projectsPage.Projects { - pageToSend.Projects = append(pageToSend.Projects, project.GetMinimal()) + pageToSend.Projects = append(pageToSend.Projects, p.service.GetMinimalProject(&project)) } err = json.NewEncoder(w).Encode(pageToSend) @@ -287,7 +287,7 @@ func (p *Projects) CreateProject(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(project.GetMinimal()) + err = json.NewEncoder(w).Encode(p.service.GetMinimalProject(project)) if err != nil { p.serveJSONError(ctx, w, http.StatusInternalServerError, err) } diff --git a/satellite/console/consoleweb/consoleapi/projects_test.go b/satellite/console/consoleweb/consoleapi/projects_test.go index d994a39c7..86ee813c0 100644 --- a/satellite/console/consoleweb/consoleapi/projects_test.go +++ b/satellite/console/consoleweb/consoleapi/projects_test.go @@ -14,12 +14,15 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/zap" "storj.io/common/memory" + "storj.io/common/storj" "storj.io/common/testcontext" "storj.io/common/testrand" "storj.io/common/uuid" "storj.io/storj/private/testplanet" + "storj.io/storj/satellite" "storj.io/storj/satellite/console" "storj.io/storj/satellite/console/consoleweb/consoleapi" ) @@ -373,3 +376,95 @@ func TestDeleteProjectMembers(t *testing.T) { require.Contains(t, string(body), "error") }) } + +func TestEdgeURLOverrides(t *testing.T) { + var ( + noOverridePlacementID storj.PlacementConstraint + partialOverridePlacementID storj.PlacementConstraint = 1 + fullOverridePlacementID storj.PlacementConstraint = 2 + + authServiceURL = "auth.storj.io" + publicLinksharingURL = "public-link.storj.io" + internalLinksharingURL = "link.storj.io" + ) + + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, UplinkCount: 1, + Reconfigure: testplanet.Reconfigure{ + Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + err := config.Console.PlacementEdgeURLOverrides.Set( + fmt.Sprintf( + `{ + "%d": {"authService": "%s"}, + "%d": { + "authService": "%s", + "publicLinksharing": "%s", + "internalLinksharing": "%s" + } + }`, + partialOverridePlacementID, authServiceURL, + fullOverridePlacementID, authServiceURL, publicLinksharingURL, internalLinksharingURL, + ), + ) + require.NoError(t, err) + }, + }, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + sat := planet.Satellites[0] + + project, err := sat.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID) + require.NoError(t, err) + + user, err := sat.API.Console.Service.GetUser(ctx, project.OwnerID) + require.NoError(t, err) + + for _, tt := range []struct { + name string + placement *storj.PlacementConstraint + expectedEdgeURLs *console.EdgeURLOverrides + }{ + {"nil placement", nil, nil}, + {"placement with no overrides", &noOverridePlacementID, nil}, + { + "placement with partial override", + &partialOverridePlacementID, + &console.EdgeURLOverrides{AuthService: authServiceURL}, + }, { + "placement with full override", + &fullOverridePlacementID, + &console.EdgeURLOverrides{ + AuthService: authServiceURL, + PublicLinksharing: publicLinksharingURL, + InternalLinksharing: internalLinksharingURL, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + result, err := sat.DB.Testing().RawDB().ExecContext(ctx, + "UPDATE projects SET default_placement = $1 WHERE id = $2", + tt.placement, project.ID, + ) + require.NoError(t, err) + + count, err := result.RowsAffected() + require.NoError(t, err) + require.EqualValues(t, 1, count) + + body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodGet, "projects", nil) + require.NoError(t, err) + require.Equal(t, http.StatusOK, status) + + var infos []console.ProjectInfo + require.NoError(t, json.Unmarshal(body, &infos)) + require.NotEmpty(t, infos) + + if tt.expectedEdgeURLs == nil { + require.Nil(t, infos[0].EdgeURLOverrides) + return + } + require.NotNil(t, infos[0].EdgeURLOverrides) + require.Equal(t, *tt.expectedEdgeURLs, *infos[0].EdgeURLOverrides) + }) + } + }) +} diff --git a/satellite/console/projects.go b/satellite/console/projects.go index 06735c93a..e701404cf 100644 --- a/satellite/console/projects.go +++ b/satellite/console/projects.go @@ -125,12 +125,13 @@ type UpsertProjectInfo struct { // ProjectInfo holds data sent via user facing http endpoints. type ProjectInfo struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - OwnerID uuid.UUID `json:"ownerId"` - Description string `json:"description"` - MemberCount int `json:"memberCount"` - CreatedAt time.Time `json:"createdAt"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + OwnerID uuid.UUID `json:"ownerId"` + Description string `json:"description"` + MemberCount int `json:"memberCount"` + CreatedAt time.Time `json:"createdAt"` + EdgeURLOverrides *EdgeURLOverrides `json:"edgeURLOverrides,omitempty"` } // ProjectsCursor holds info for project @@ -194,15 +195,3 @@ func ValidateNameAndDescription(name string, description string) error { return nil } - -// GetMinimal returns a ProjectInfo copy of a project. -func (p Project) GetMinimal() ProjectInfo { - return ProjectInfo{ - ID: p.PublicID, - Name: p.Name, - OwnerID: p.OwnerID, - Description: p.Description, - MemberCount: p.MemberCount, - CreatedAt: p.CreatedAt, - } -} diff --git a/satellite/console/service.go b/satellite/console/service.go index 1d7dfbb82..0893b9b9f 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -187,48 +187,6 @@ func init() { } } -// Config keeps track of core console service configuration parameters. -type Config struct { - PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"` - OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"` - DefaultProjectLimit int `help:"default project limits for users" default:"1" testDefault:"5"` - AsOfSystemTimeDuration time.Duration `help:"default duration for AS OF SYSTEM TIME" devDefault:"-5m" releaseDefault:"-5m" testDefault:"0"` - LoginAttemptsWithoutPenalty int `help:"number of times user can try to login without penalty" default:"3"` - FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"` - ProjectInvitationExpiration time.Duration `help:"duration that project member invitations are valid for" default:"168h"` - UserBalanceForUpgrade int64 `help:"amount of base units of US micro dollars needed to upgrade user's tier status" default:"10000000"` - UsageLimits UsageLimitsConfig - Captcha CaptchaConfig - Session SessionConfig -} - -// CaptchaConfig contains configurations for login/registration captcha system. -type CaptchaConfig struct { - Login MultiCaptchaConfig `json:"login"` - Registration MultiCaptchaConfig `json:"registration"` -} - -// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems. -type MultiCaptchaConfig struct { - Recaptcha SingleCaptchaConfig `json:"recaptcha"` - Hcaptcha SingleCaptchaConfig `json:"hcaptcha"` -} - -// SingleCaptchaConfig contains configurations abstract captcha system. -type SingleCaptchaConfig struct { - Enabled bool `help:"whether or not captcha is enabled" default:"false" json:"enabled"` - SiteKey string `help:"captcha site key" json:"siteKey"` - SecretKey string `help:"captcha secret key" json:"-"` -} - -// SessionConfig contains configurations for session management. -type SessionConfig struct { - InactivityTimerEnabled bool `help:"indicates if session can be timed out due inactivity" default:"true"` - InactivityTimerDuration int `help:"inactivity timer delay in seconds" default:"600"` - InactivityTimerViewerEnabled bool `help:"indicates whether remaining session time is shown for debugging" default:"false"` - Duration time.Duration `help:"duration a session is valid for (superseded by inactivity timer delay if inactivity timer is enabled)" default:"168h"` -} - // Payments separates all payment related functionality. type Payments struct { service *Service @@ -1614,6 +1572,24 @@ func (s *Service) GetUsersProjects(ctx context.Context) (ps []Project, err error return } +// GetMinimalProject returns a ProjectInfo copy of a project. +func (s *Service) GetMinimalProject(project *Project) ProjectInfo { + info := ProjectInfo{ + ID: project.PublicID, + Name: project.Name, + OwnerID: project.OwnerID, + Description: project.Description, + MemberCount: project.MemberCount, + CreatedAt: project.CreatedAt, + } + + if edgeURLs, ok := s.config.PlacementEdgeURLOverrides.Get(project.DefaultPlacement); ok { + info.EdgeURLOverrides = &edgeURLs + } + + return info +} + // GenGetUsersProjects is a method for querying all projects for generated api. func (s *Service) GenGetUsersProjects(ctx context.Context) (ps []Project, httpErr api.HTTPError) { var err error diff --git a/satellite/satellitedb/projects.go b/satellite/satellitedb/projects.go index f67fa147c..05fb455bc 100644 --- a/satellite/satellitedb/projects.go +++ b/satellite/satellitedb/projects.go @@ -68,13 +68,22 @@ func (projects *projects) GetByUserID(ctx context.Context, userID uuid.UUID) (_ defer mon.Task()(&ctx)(&err) rows, err := projects.sdb.Query(ctx, projects.sdb.Rebind(` - SELECT projects.id, projects.public_id, projects.name, projects.description, projects.owner_id, projects.rate_limit, projects.max_buckets, projects.created_at, + SELECT + projects.id, + projects.public_id, + projects.name, + projects.description, + projects.owner_id, + projects.rate_limit, + projects.max_buckets, + projects.created_at, + COALESCE(projects.default_placement, 0), (SELECT COUNT(*) FROM project_members WHERE project_id = projects.id) AS member_count - FROM projects - JOIN project_members ON projects.id = project_members.project_id - WHERE project_members.member_id = ? - ORDER BY name ASC - `), userID) + FROM projects + JOIN project_members ON projects.id = project_members.project_id + WHERE project_members.member_id = ? + ORDER BY name ASC + `), userID) if err != nil { return nil, err } @@ -84,7 +93,18 @@ func (projects *projects) GetByUserID(ctx context.Context, userID uuid.UUID) (_ var rateLimit, maxBuckets sql.NullInt32 projectsToSend := make([]console.Project, 0) for rows.Next() { - err = rows.Scan(&nextProject.ID, &nextProject.PublicID, &nextProject.Name, &nextProject.Description, &nextProject.OwnerID, &rateLimit, &maxBuckets, &nextProject.CreatedAt, &nextProject.MemberCount) + err = rows.Scan( + &nextProject.ID, + &nextProject.PublicID, + &nextProject.Name, + &nextProject.Description, + &nextProject.OwnerID, + &rateLimit, + &maxBuckets, + &nextProject.CreatedAt, + &nextProject.DefaultPlacement, + &nextProject.MemberCount, + ) if err != nil { return nil, err } @@ -366,14 +386,23 @@ func (projects *projects) ListByOwnerID(ctx context.Context, ownerID uuid.UUID, } rows, err := projects.sdb.Query(ctx, projects.sdb.Rebind(` - SELECT id, public_id, name, description, owner_id, rate_limit, max_buckets, created_at, + SELECT + id, + public_id, + name, + description, + owner_id, + rate_limit, + max_buckets, + created_at, + COALESCE(default_placement, 0), (SELECT COUNT(*) FROM project_members WHERE project_id = projects.id) AS member_count - FROM projects - WHERE owner_id = ? - ORDER BY name ASC - OFFSET ? ROWS - LIMIT ? - `), ownerID, page.Offset, page.Limit+1) // add 1 to limit to see if there is another page + FROM projects + WHERE owner_id = ? + ORDER BY name ASC + OFFSET ? ROWS + LIMIT ? + `), ownerID, page.Offset, page.Limit+1) // add 1 to limit to see if there is another page if err != nil { return console.ProjectsPage{}, err } @@ -391,7 +420,18 @@ func (projects *projects) ListByOwnerID(ctx context.Context, ownerID uuid.UUID, } var rateLimit, maxBuckets sql.NullInt32 nextProject := &console.Project{} - err = rows.Scan(&nextProject.ID, &nextProject.PublicID, &nextProject.Name, &nextProject.Description, &nextProject.OwnerID, &rateLimit, &maxBuckets, &nextProject.CreatedAt, &nextProject.MemberCount) + err = rows.Scan( + &nextProject.ID, + &nextProject.PublicID, + &nextProject.Name, + &nextProject.Description, + &nextProject.OwnerID, + &rateLimit, + &maxBuckets, + &nextProject.CreatedAt, + &nextProject.DefaultPlacement, + &nextProject.MemberCount, + ) if err != nil { return console.ProjectsPage{}, err } diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index 1a3abdf0e..d2d1b0e2f 100755 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -337,6 +337,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0 # indicates if the overview onboarding step should render with pathways # console.pathway-overview-enabled: true +# placement-specific edge service URL overrides in the format {"placementID": {"authService": "...", "publicLinksharing": "...", "internalLinksharing": "..."}, "placementID2": ...} +# console.placement-edge-url-overrides: "" + # whether to allow purchasing pricing packages # console.pricing-packages-enabled: false