[CLD-7421][CLD-7420] Deprecate Self Serve: First Pass (#26668)

* Deprecate Self Serve: First Pass

* Fix ci

* Fix more ci

* Remmove outdated server tests

* Fix a missed spot opening purchase modal in Self Hosted

* Fix i18n

* Clean up some more server code, fix webapp test

* Fix alignment of button

* Fix linter

* Fix i18n server side

* Add back translation

* Remove client functions

* Put back client functions

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Nick Misasi
2024-04-23 14:25:37 -04:00
коммит произвёл GitHub
родитель a40550136f
Коммит 437f90e184
92 изменённых файлов: 173 добавлений и 13211 удалений

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

@@ -13,7 +13,6 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/audit"
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
)
@@ -25,11 +24,6 @@ func (api *API) InitCloud() {
api.BaseRoutes.Cloud.Handle("/products/selfhosted", api.APISessionRequired(getSelfHostedProducts)).Methods("GET")
// POST /api/v4/cloud/payment
// POST /api/v4/cloud/payment/confirm
api.BaseRoutes.Cloud.Handle("/payment", api.APISessionRequired(createCustomerPayment)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/payment/confirm", api.APISessionRequired(confirmCustomerPayment)).Methods("POST")
// GET /api/v4/cloud/customer
// PUT /api/v4/cloud/customer
// PUT /api/v4/cloud/customer/address
@@ -42,10 +36,6 @@ func (api *API) InitCloud() {
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT")
// GET /api/v4/cloud/request-trial
api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT")
// GET /api/v4/cloud/validate-business-email
api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST")
@@ -59,8 +49,6 @@ func (api *API) InitCloud() {
// GET /api/v4/cloud/cws-health-check
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/delete-workspace", api.APISessionRequired(selfServeDeleteWorkspace)).Methods("DELETE")
}
func ensureCloudInterface(c *Context, where string) bool {
@@ -134,131 +122,6 @@ func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write(json)
}
func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.changeSubscription")
if !ensured {
return
}
userId := c.AppContext.Session().UserId
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.license_error", nil, "", http.StatusInternalServerError)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var subscriptionChange *model.SubscriptionChange
if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil || subscriptionChange == nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
currentSubscription, appErr := c.App.Cloud().GetSubscription(userId)
if appErr != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
changedSub, err := c.App.Cloud().ChangeSubscription(userId, currentSubscription.ID, subscriptionChange)
if err != nil {
appErr := model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
if err.Error() == "compliance-failed" {
c.Logger.Error("Compliance check failed", mlog.Err(err))
appErr.StatusCode = http.StatusUnprocessableEntity
}
c.Err = appErr
return
}
if subscriptionChange.Feedback != nil {
c.App.Srv().GetTelemetryService().SendTelemetry("downgrade_feedback", subscriptionChange.Feedback.ToMap())
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID)
if err != nil || product == nil {
c.Logger.Error("Error finding the new cloud product", mlog.Err(err))
}
if product.SKU == string(model.SkuCloudStarter) {
w.Write(json)
return
}
isYearly := product.IsYearly()
// Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them
// At this point, the upgrade is complete.
if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil {
c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr))
}
w.Write(json)
}
func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.requestCloudTrial")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
// check if the email needs to be set
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
// a new business email was provided via the request business email modal
var startTrialRequest *model.StartCloudTrialRequest
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil || startTrialRequest == nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, startTrialRequest.SubscriptionID, startTrialRequest.Email)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
defer c.App.Srv().Cloud.InvalidateCaches()
w.Write(json)
}
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.validateBusinessEmail")
if !ensured {
@@ -635,84 +498,6 @@ func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Reque
w.Write(json)
}
func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.createCustomerPayment")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("createCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
intent, err := c.App.Cloud().CreateCustomerPayment(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(intent)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(json)
}
func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.confirmCustomerPayment")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("confirmCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var confirmRequest *model.ConfirmPaymentMethodRequest
if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil || confirmRequest == nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
err = c.App.Cloud().ConfirmCustomerPayment(c.AppContext.Session().UserId, confirmRequest)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getInvoicesForSubscription")
if !ensured {
@@ -809,40 +594,6 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
}
switch event.Event {
case model.EventTypeFailedPayment:
if nErr := c.App.SendPaymentFailedEmail(event.FailedPayment); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeFailedPaymentNoCard:
if nErr := c.App.SendNoCardPaymentFailedEmail(); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendUpgradeConfirmationEmail:
// isYearly determines whether to send the yearly or monthly Upgrade email
isYearly := false
if event.Subscription != nil && event.CloudWorkspaceOwner != nil {
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, "", appErr.StatusCode).Wrap(appErr)
return
}
// Get the current cloud product to determine whether it's a monthly or yearly product
product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
isYearly = product.IsYearly()
}
if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendAdminWelcomeEmail:
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
@@ -868,19 +619,6 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
case model.EventTypeTriggerDelinquencyEmail:
var emailToTrigger model.DelinquencyEmail
if event.DelinquencyEmail != nil {
emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger)
} else {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError)
return
}
if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil {
c.Err = nErr
return
}
default:
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
return
@@ -902,37 +640,3 @@ func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request
ReturnStatusOK(w)
}
func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.selfServeDeleteWorkspace")
if !ensured {
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
defer r.Body.Close()
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
var deleteRequest *model.WorkspaceDeletionRequest
if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil || deleteRequest == nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if err := c.App.Cloud().SelfServeDeleteWorkspace(c.AppContext.Session().UserId, deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.server.cws.delete_workspace.app_error", nil, "CWS Server failed to delete workspace.", http.StatusInternalServerError)
return
}
c.App.Srv().GetTelemetryService().SendTelemetry("delete_workspace_feedback", deleteRequest.Feedback.ToMap())
ReturnStatusOK(w)
}

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

@@ -16,119 +16,6 @@ import (
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
)
func Test_getCloudLimits(t *testing.T) {
t.Run("no license returns not implemented", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.App.Srv().RemoveLicense()
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits(context.Background())
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusForbidden, r.StatusCode, "Expected 403 forbidden")
})
t.Run("non cloud license returns not implemented", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.App.Srv().SetLicense(model.NewTestLicense())
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits(context.Background())
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusForbidden, r.StatusCode, "Expected 403 forbidden")
})
t.Run("error fetching limits returns internal server error", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits(context.Background())
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusInternalServerError, r.StatusCode, "Expected 500 Internal Server Error")
})
t.Run("unauthenticated users can not access", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Logout(context.Background())
limits, r, err := th.Client.GetProductLimits(context.Background())
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusUnauthorized, r.StatusCode, "Expected 401 Unauthorized")
})
t.Run("good request with cloud server", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := &mocks.CloudInterface{}
ten := 10
mockLimits := &model.ProductLimits{
Messages: &model.MessagesLimits{
History: &ten,
},
}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(mockLimits, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits(context.Background())
require.NoError(t, err)
require.Equal(t, http.StatusOK, r.StatusCode, "Expected 200 OK")
require.Equal(t, mockLimits, limits)
require.Equal(t, *mockLimits.Messages.History, *limits.Messages.History)
})
}
func Test_GetSubscription(t *testing.T) {
deliquencySince := int64(2000000000)
@@ -215,119 +102,6 @@ func Test_GetSubscription(t *testing.T) {
})
}
func Test_requestTrial(t *testing.T) {
subscription := &model.Subscription{
ID: "MySubscriptionID",
CustomerID: "MyCustomer",
ProductID: "SomeProductId",
AddOns: []string{},
StartAt: 1000000000,
EndAt: 2000000000,
CreateAt: 1000000000,
Seats: 10,
DNS: "some.dns.server",
}
newValidBusinessEmail := model.StartCloudTrialRequest{Email: ""}
t.Run("NON Admin users are UNABLE to request the trial", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil)
cloud.Mock.On("InvalidateCaches").Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionChanged, r, err := th.Client.RequestCloudTrial(context.Background(), &newValidBusinessEmail)
require.Error(t, err)
require.Nil(t, subscriptionChanged)
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
})
t.Run("ADMIN user are ABLE to request the trial", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil)
cloud.Mock.On("InvalidateCaches").Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(context.Background(), &newValidBusinessEmail)
require.NoError(t, err)
require.Equal(t, subscriptionChanged, subscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
t.Run("ADMIN user are ABLE to request the trial with valid business email", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
// patch the customer with the additional contact updated with the valid business email
newValidBusinessEmail.Email = *model.NewString("valid.email@mattermost.com")
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "valid.email@mattermost.com").Return(subscription, nil)
cloud.Mock.On("InvalidateCaches").Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(context.Background(), &newValidBusinessEmail)
require.NoError(t, err)
require.Equal(t, subscriptionChanged, subscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
t.Run("Empty body returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
r, err := th.SystemAdminClient.DoAPIPutBytes(context.Background(), "/cloud/request-trial", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func Test_validateBusinessEmail(t *testing.T) {
t.Run("Returns forbidden for invalid business email", func(t *testing.T) {
th := Setup(t).InitBasic()
@@ -643,58 +417,6 @@ func TestGetCloudProducts(t *testing.T) {
})
}
func Test_GetExpandStatsForSubscription(t *testing.T) {
status := &model.SubscriptionLicenseSelfServeStatusResponse{
IsExpandable: true,
}
licenseId := "licenseID"
t.Run("NON Admin users are UNABLE to request expand stats for the subscription", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
checksMade, r, err := th.Client.GetSubscriptionStatus(context.Background(), licenseId)
require.Error(t, err)
require.Nil(t, checksMade)
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
})
t.Run("Admin users are UNABLE to request licenses is expendable due missing the id", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password)
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
checks, r, err := th.Client.GetSubscriptionStatus(context.Background(), "")
require.Error(t, err)
require.Nil(t, checks)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request")
})
}
func TestGetSelfHostedProducts(t *testing.T) {
products := []*model.Product{
{

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

@@ -4,20 +4,11 @@
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"io"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
)
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
@@ -25,276 +16,12 @@ import (
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/available
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods("GET")
// POST /api/v4/hosted_customer/bootstrap
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
// POST /api/v4/hosted_customer/customer
api.BaseRoutes.HostedCustomer.Handle("/customer", api.APISessionRequired(selfHostedCustomer)).Methods("POST")
// POST /api/v4/hosted_customer/confirm
api.BaseRoutes.HostedCustomer.Handle("/confirm", api.APISessionRequired(selfHostedConfirm)).Methods("POST")
// POST /api.v4/hosted_customer/confirm-expand
api.BaseRoutes.HostedCustomer.Handle("/confirm-expand", api.APISessionRequired(selfHostedConfirmExpand)).Methods("POST")
// GET /api/v4/hosted_customer/invoices
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
api.BaseRoutes.HostedCustomer.Handle("/subscribe-newsletter", api.APIHandler(handleSubscribeToNewsletter)).Methods("POST")
}
func ensureSelfHostedAdmin(c *Context, where string) {
ensured := ensureCloudInterface(c, where)
if !ensured {
return
}
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 checkSelfHostedPurchaseEnabled(c *Context) bool {
config := c.App.Config()
if config == nil {
return false
}
enabled := config.ServiceSettings.SelfHostedPurchase
return enabled != nil && *enabled
}
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedBootstrap"
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
reset := r.URL.Query().Get("reset") == "true"
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, Reset: reset})
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)
}
func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedCustomer"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var form *model.SelfHostedCustomerForm
if err = json.Unmarshal(bodyBytes, &form); err != nil || form == nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
customerResponse, err := c.App.Cloud().CreateCustomerSelfHostedSignup(*form, user.Email)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customerResponse)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedConfirm(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedConfirm"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var confirm model.SelfHostedConfirmPaymentMethodRequest
err = json.Unmarshal(bodyBytes, &confirm)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
confirmResponse, err := c.App.Cloud().ConfirmSelfHostedSignup(confirm, user.Email)
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
if err.Error() == strconv.Itoa(http.StatusUnprocessableEntity) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
license, appErr := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License))
if appErr != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{
License: utils.GetClientLicense(license),
Progress: confirmResponse.Progress,
})
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
go func() {
err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication()
if err != nil {
c.Logger.Warn("Unable to confirm license application", mlog.Err(err))
}
}()
_, _ = w.Write(clientResponse)
}
func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.handleSignupAvailable"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.Cloud().SelfHostedSignupAvailable(); err != nil {
if err.Error() == "upstream_off" {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusServiceUnavailable)
} else {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
}
return
}
systemValue, err := c.App.Srv().Store().System().GetByName(model.SystemHostedPurchaseNeedsScreening)
if err == nil && systemValue != nil {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusTooEarly)
return
}
ReturnStatusOK(w)
}
func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoices"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
invoices, err := c.App.Cloud().GetSelfHostedInvoices(c.AppContext)
if err != nil {
if err.Error() == "404" {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotFound).Wrap(errors.New("invoices for license not found"))
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoicePDF"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
pdfData, filename, appErr := c.App.Cloud().GetSelfHostedInvoicePDF(c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
}
func handleSubscribeToNewsletter(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -326,80 +53,3 @@ func handleSubscribeToNewsletter(c *Context, w http.ResponseWriter, r *http.Requ
ReturnStatusOK(w)
}
func selfHostedConfirmExpand(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedConfirmExpand"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var confirm model.SelfHostedConfirmPaymentMethodRequest
err = json.Unmarshal(bodyBytes, &confirm)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
confirmResponse, err := c.App.Cloud().ConfirmSelfHostedExpansion(confirm, user.Email)
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
if err.Error() == strconv.Itoa(http.StatusUnprocessableEntity) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
license, appErr := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License))
// dealing with an AppError
if appErr != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{
License: utils.GetClientLicense(license),
Progress: confirmResponse.Progress,
})
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
go func() {
err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication()
if err != nil {
c.Logger.Warn("Unable to confirm license application", mlog.Err(err))
}
}()
_, _ = w.Write(clientResponse)
}

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

@@ -1,145 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
)
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()
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.Client.Login(context.Background(), 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.SelfHostedPurchase = &valFalse })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(context.Background(), 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()
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.Client.Login(context.Background(), 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.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(context.Background(), 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()
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.Client.Login(context.Background(), 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.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(context.Background(), 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(context.Background(), 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.SelfHostedPurchase = &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(context.Background(), model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, err)
require.Equal(t, "START", response.Progress)
})
t.Run("team edition returns bad request instead of panicking", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = nil
th.Client.Login(context.Background(), 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.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(context.Background(), model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusBadRequest, r.StatusCode)
require.Error(t, err)
})
}