[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
Этот коммит содержится в:
Mario de Frutos Dieguez
2020-12-18 16:40:46 +01:00
коммит произвёл GitHub
родитель e057e5b10b
Коммит 94fe01dc1d
27 изменённых файлов: 1867 добавлений и 13 удалений

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

@@ -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)
}