* When service setting flag is on, self hosted workspaces can initiate process for self-serve sign up
* Add hosted_customer api.
Этот коммит содержится в:
Nathaniel Allred
2022-11-29 13:32:08 -06:00
коммит произвёл GitHub
родитель 2455de99ff
Коммит 4f3f6e6496
12 изменённых файлов: 249 добавлений и 1 удалений

Просмотреть файл

@@ -140,6 +140,8 @@ type Routes struct {
Usage *mux.Router // 'api/v4/usage'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
Drafts *mux.Router // 'api/v4/drafts'
}
@@ -267,6 +269,8 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
api.InitUser()
@@ -312,6 +316,7 @@ func Init(srv *app.Server) (*API, error) {
api.InitExport()
api.InitInsights()
api.InitUsage()
api.InitHostedCustomer()
api.InitDrafts()
if err := api.InitGraphQL(); err != nil {
return nil, err

73
api4/hosted_customer.go Обычный файл
Просмотреть файл

@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
// Endpoints for cloud installations should not go in this file.
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/bootstrap
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
}
func ensureSelfHostedAdmin(c *Context, where string) {
license := c.App.Channels().License()
if license.IsCloud() {
c.Err = model.NewAppError(where, "api.cloud.license_error", nil, "Cloud installations do not use this endpoint", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
}
func checkSelfHostedFirstTimePurchaseEnabled(c *Context) bool {
config := c.App.Config()
if config == nil {
return false
}
enabled := config.ServiceSettings.SelfHostedFirstTimePurchase
return enabled != nil && *enabled
}
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
where := "Api4.selfHostedBootstrap"
if !checkSelfHostedFirstTimePurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email})
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
json, err := json.Marshal(signupProgress)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}

100
api4/hosted_customer_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"os"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
"github.com/mattermost/mattermost-server/v6/model"
)
var valFalse = false
var valTrue = true
func TestSelfHostedBootstrap(t *testing.T) {
t.Run("feature flag off returns not implemented", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "false")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valFalse })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusNotImplemented, r.StatusCode)
require.Error(t, err)
})
t.Run("cloud instances not allowed to bootstrap self-hosted signup", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusBadRequest, r.StatusCode)
require.Error(t, err)
})
t.Run("non-admins not allowed to bootstrap self-hosted signup", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusForbidden, r.StatusCode)
require.Error(t, err)
})
t.Run("self-hosted admins can bootstrap self-hosted signup", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue })
th.App.ReloadConfig()
cloud := mocks.CloudInterface{}
cloud.Mock.On("BootstrapSelfHostedSignup", mock.Anything).Return(&model.BootstrapSelfHostedSignupResponse{Progress: "START"}, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
response, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, err)
require.Equal(t, "START", response.Progress)
})
}

Просмотреть файл

@@ -32,6 +32,7 @@ type CloudInterface interface {
GetLicenseRenewalStatus(userID, token string) error
InvalidateCaches() error
BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error)
CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error)
HandleLicenseChange() error
}

Просмотреть файл

@@ -14,6 +14,29 @@ type CloudInterface struct {
mock.Mock
}
// BootstrapSelfHostedSignup provides a mock function with given fields: req
func (_m *CloudInterface) BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error) {
ret := _m.Called(req)
var r0 *model.BootstrapSelfHostedSignupResponse
if rf, ok := ret.Get(0).(func(model.BootstrapSelfHostedSignupRequest) *model.BootstrapSelfHostedSignupResponse); ok {
r0 = rf(req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.BootstrapSelfHostedSignupResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(model.BootstrapSelfHostedSignupRequest) error); ok {
r1 = rf(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ChangeSubscription provides a mock function with given fields: userID, subscriptionID, subscriptionChange
func (_m *CloudInterface) ChangeSubscription(userID string, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) {
ret := _m.Called(userID, subscriptionID, subscriptionChange)

Просмотреть файл

@@ -326,6 +326,10 @@ func (c *Client4) cloudRoute() string {
return "/cloud"
}
func (c *Client4) hostedCustomerRoute() string {
return "/hosted_customer"
}
func (c *Client4) testEmailRoute() string {
return "/email/test"
}
@@ -8220,6 +8224,23 @@ func (c *Client4) UpdateCloudCustomerAddress(address *Address) (*CloudCustomer,
return customer, BuildResponse(r), nil
}
func (c *Client4) BootstrapSelfHostedSignup(req BootstrapSelfHostedSignupRequest) (*BootstrapSelfHostedSignupResponse, *Response, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, nil, NewAppError("BootstrapSelfHostedSignup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPostBytes(c.hostedCustomerRoute()+"/bootstrap", reqBytes)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var res *BootstrapSelfHostedSignupResponse
json.NewDecoder(r.Body).Decode(&res)
return res, BuildResponse(r), nil
}
func (c *Client4) ListImports() ([]string, *Response, error) {
r, err := c.DoAPIGet(c.importsRoute(), "")
if err != nil {

Просмотреть файл

@@ -274,6 +274,19 @@ type ProductLimits struct {
Teams *TeamsLimits `json:"teams,omitempty"`
}
type BootstrapSelfHostedSignupRequest struct {
Email string `json:"email"`
}
type BootstrapSelfHostedSignupResponse struct {
Progress string `json:"progress"`
}
type BootstrapSelfHostedSignupResponseInternal struct {
Progress string `json:"progress"`
License string `json:"license"`
}
func (p *Product) IsYearly() bool {
return p.RecurringInterval == RecurringIntervalYearly
}

Просмотреть файл

@@ -383,6 +383,7 @@ type ServiceSettings struct {
CollapsedThreads *string `access:"experimental_features"`
ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
EnableCustomGroups *bool `access:"site_users_and_teams"`
SelfHostedFirstTimePurchase *bool `access:"write_restrictable,cloud_restrictable"`
AllowSyncedDrafts *bool `access:"site_posts"`
}
@@ -852,6 +853,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
if s.AllowSyncedDrafts == nil {
s.AllowSyncedDrafts = NewBool(true)
}
if s.SelfHostedFirstTimePurchase == nil {
s.SelfHostedFirstTimePurchase = NewBool(false)
}
}
type ClusterSettings struct {

Просмотреть файл

@@ -56,6 +56,7 @@ type License struct {
SkuShortName string `json:"sku_short_name"`
IsTrial bool `json:"is_trial"`
IsGovSku bool `json:"is_gov_sku"`
SignupJWT *string `json:"signup_jwt"`
}
type Customer struct {

Просмотреть файл

@@ -158,6 +158,11 @@ func TestIsCloud(t *testing.T) {
l1.Features = nil
assert.False(t, l1.IsCloud())
t.Run("false if license is nil", func(t *testing.T) {
var license *License
assert.False(t, license.IsCloud())
})
}
func TestLicenseRecordIsValid(t *testing.T) {

Просмотреть файл

@@ -450,6 +450,7 @@ func (ts *TelemetryService) trackConfig() {
"restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""),
"enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups,
"post_priority": *cfg.ServiceSettings.PostPriority,
"self_hosted_first_time_purchase": *cfg.ServiceSettings.SelfHostedFirstTimePurchase,
"allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts,
})

Просмотреть файл

@@ -236,7 +236,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
cloudCSP := ""
if c.App.Channels().License().IsCloud() {
if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedFirstTimePurchase {
cloudCSP = " js.stripe.com/v3"
}