[MM-30539] New renewal link logic (#16539)
* New renewal link logic Endpoint and logic that returns the renewal link to be used to start the license renewal process. * Limit access for restricted sysadmins * Include active users in the renewal token
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e057e5b10b
Коммит
94fe01dc1d
@@ -5,21 +5,34 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
requestTrialURL = "https://customers.mattermost.com/api/v1/trials"
|
||||
LicenseEnv = "MM_LICENSE"
|
||||
requestTrialURL = "https://customers.mattermost.com/api/v1/trials"
|
||||
LicenseEnv = "MM_LICENSE"
|
||||
LicenseRenewalURL = "https://customers.mattermost.com/subscribe/renew"
|
||||
JWTDefaultTokenExpiration = 7 * 24 * time.Hour // 7 days of expiration
|
||||
)
|
||||
|
||||
// JWTClaims custom JWT claims with the needed information for the
|
||||
// renewal process
|
||||
type JWTClaims struct {
|
||||
LicenseID string `json:"license_id"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
func (s *Server) LoadLicense() {
|
||||
// ENV var overrides all other sources of license.
|
||||
licenseStr := os.Getenv(LicenseEnv)
|
||||
@@ -245,3 +258,76 @@ func (s *Server) RequestTrialLicense(trialRequest *model.TrialLicenseRequest) *m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateRenewalToken returns the current active token or generate a new one if
|
||||
// the current active one has expired
|
||||
func (s *Server) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) {
|
||||
license := s.License()
|
||||
if license == nil {
|
||||
// Clean renewal token if there is no license present
|
||||
if _, err := s.Store.System().PermanentDeleteByName(model.SYSTEM_LICENSE_RENEWAL_TOKEN); err != nil {
|
||||
mlog.Error("error removing the renewal token", mlog.Err(err))
|
||||
}
|
||||
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.no_license", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *license.Features.Cloud {
|
||||
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.bad_license", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
currentToken, _ := s.Store.System().GetByName(model.SYSTEM_LICENSE_RENEWAL_TOKEN)
|
||||
if currentToken != nil {
|
||||
tokenIsValid, err := s.renewalTokenValid(currentToken.Value, license.Customer.Email)
|
||||
if err != nil {
|
||||
mlog.Warn("error checking license renewal token validation", mlog.Err(err))
|
||||
}
|
||||
if currentToken.Value != "" && tokenIsValid {
|
||||
return currentToken.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
activeUsers, err := s.Store.User().Count(model.UserCountOptions{})
|
||||
if err != nil {
|
||||
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error",
|
||||
nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
expirationTime := time.Now().UTC().Add(expiration)
|
||||
claims := &JWTClaims{
|
||||
LicenseID: license.Id,
|
||||
ActiveUsers: activeUsers,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expirationTime.Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(license.Customer.Email))
|
||||
if err != nil {
|
||||
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
err = s.Store.System().SaveOrUpdate(&model.System{
|
||||
Name: model.SYSTEM_LICENSE_RENEWAL_TOKEN,
|
||||
Value: tokenString,
|
||||
})
|
||||
if err != nil {
|
||||
return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (s *Server) renewalTokenValid(tokenString, signingKey string) (bool, error) {
|
||||
claims := &JWTClaims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(signingKey), nil
|
||||
})
|
||||
if err != nil && !token.Valid {
|
||||
return false, errors.Wrapf(err, "Error validating JWT token")
|
||||
}
|
||||
expirationTime := time.Unix(claims.ExpiresAt, 0)
|
||||
if expirationTime.Before(time.Now().UTC()) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -62,15 +63,7 @@ func TestGetSanitizedClientLicense(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
l1 := &model.License{}
|
||||
l1.Features = &model.Features{}
|
||||
l1.Customer = &model.Customer{}
|
||||
l1.Customer.Name = "TestName"
|
||||
l1.SkuName = "SKU NAME"
|
||||
l1.SkuShortName = "SKU SHORT NAME"
|
||||
l1.StartsAt = model.GetMillis() - 1000
|
||||
l1.ExpiresAt = model.GetMillis() + 100000
|
||||
th.App.Srv().SetLicense(l1)
|
||||
setLicense(th, nil)
|
||||
|
||||
m := th.App.Srv().GetSanitizedClientLicense()
|
||||
|
||||
@@ -81,3 +74,85 @@ func TestGetSanitizedClientLicense(t *testing.T) {
|
||||
_, ok = m["SkuShortName"]
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestGenerateRenewalToken(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("renewal token generated correctly", func(t *testing.T) {
|
||||
setLicense(th, nil)
|
||||
token, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEmpty(t, token)
|
||||
defer th.App.Srv().Store.System().PermanentDeleteByName(model.SYSTEM_LICENSE_RENEWAL_TOKEN)
|
||||
|
||||
customerEmail := th.App.Srv().License().Customer.Email
|
||||
validToken, err := th.App.Srv().renewalTokenValid(token, customerEmail)
|
||||
require.NoError(t, err)
|
||||
require.True(t, validToken)
|
||||
})
|
||||
|
||||
t.Run("only one token should be active", func(t *testing.T) {
|
||||
setLicense(th, nil)
|
||||
token, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEmpty(t, token)
|
||||
defer th.App.Srv().Store.System().PermanentDeleteByName(model.SYSTEM_LICENSE_RENEWAL_TOKEN)
|
||||
|
||||
newToken, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, token, newToken)
|
||||
})
|
||||
|
||||
t.Run("return error if there is no active license", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(nil)
|
||||
_, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.NotNil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("return another token if the license owner change", func(t *testing.T) {
|
||||
setLicense(th, nil)
|
||||
token, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEmpty(t, token)
|
||||
defer th.App.Srv().Store.System().PermanentDeleteByName(model.SYSTEM_LICENSE_RENEWAL_TOKEN)
|
||||
setLicense(th, &model.Customer{
|
||||
Name: "another customer",
|
||||
Email: "another@example.com",
|
||||
})
|
||||
newToken, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEqual(t, token, newToken)
|
||||
})
|
||||
|
||||
t.Run("return another token if the active one has expired", func(t *testing.T) {
|
||||
setLicense(th, nil)
|
||||
token, appErr := th.App.Srv().GenerateRenewalToken(1 * time.Second)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEmpty(t, token)
|
||||
defer th.App.Srv().Store.System().PermanentDeleteByName(model.SYSTEM_LICENSE_RENEWAL_TOKEN)
|
||||
// The small time unit for expiration we're using is seconds
|
||||
time.Sleep(1 * time.Second)
|
||||
newToken, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration)
|
||||
require.Nil(t, appErr)
|
||||
require.NotEqual(t, token, newToken)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func setLicense(th *TestHelper, customer *model.Customer) {
|
||||
l1 := &model.License{}
|
||||
l1.Features = &model.Features{}
|
||||
if customer != nil {
|
||||
l1.Customer = customer
|
||||
} else {
|
||||
l1.Customer = &model.Customer{}
|
||||
l1.Customer.Name = "TestName"
|
||||
l1.Customer.Email = "test@example.com"
|
||||
}
|
||||
l1.SkuName = "SKU NAME"
|
||||
l1.SkuShortName = "SKU SHORT NAME"
|
||||
l1.StartsAt = model.GetMillis() - 1000
|
||||
l1.ExpiresAt = model.GetMillis() + 100000
|
||||
th.App.Srv().SetLicense(l1)
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user