[MM-44307] On Limits Change, archive or restore teams according to the limits (#20247)

Automatic Merge
Этот коммит содержится в:
Nick Misasi
2022-05-24 09:16:24 -04:00
коммит произвёл GitHub
родитель 380a0ef827
Коммит 50d069e86d
13 изменённых файлов: 454 добавлений и 26 удалений

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

@@ -536,7 +536,9 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
return true
}, plugin.OnCloudLimitsUpdatedID)
}
c.App.AdjustInProductLimits(event.ProductLimits, event.Subscription)
}
if err := c.App.Cloud().UpdateSubscriptionFromHook(event.ProductLimits, event.Subscription); err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.subscription.update_error", nil, err.Error(), http.StatusInternalServerError)
return

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

@@ -43,6 +43,7 @@ func (api *API) InitTeam() {
api.BaseRoutes.Team.Handle("", api.APISessionRequired(getTeam)).Methods("GET")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(updateTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("", api.APISessionRequired(deleteTeam)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/except", api.APISessionRequired(softDeleteTeamsExcept)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/patch", api.APISessionRequired(patchTeam)).Methods("PUT")
api.BaseRoutes.Team.Handle("/restore", api.APISessionRequired(restoreTeam)).Methods("POST")
api.BaseRoutes.Team.Handle("/privacy", api.APISessionRequired(updateTeamPrivacy)).Methods("PUT")
@@ -402,6 +403,23 @@ func deleteTeam(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func softDeleteTeamsExcept(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeam) {
c.SetPermissionError(model.PermissionManageTeam)
return
}
err := c.App.SoftDeleteAllTeamsExcept(c.Params.TeamId)
if err != nil {
c.Err = err
}
}
func getTeamsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {

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

@@ -387,6 +387,8 @@ type AppIface interface {
AddUserToTeamByTeamId(c *request.Context, teamID string, user *model.User) *model.AppError
AddUserToTeamByToken(c *request.Context, userID string, tokenID string) (*model.Team, *model.TeamMember, *model.AppError)
AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError)
AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError
AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError
AllowOAuthAppAccessToUser(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError)
AppendFile(fr io.Reader, path string) (int64, *model.AppError)
AsymmetricSigningKey() *ecdsa.PrivateKey
@@ -1022,6 +1024,7 @@ type AppIface interface {
SetTeamIconFromFile(team *model.Team, file io.Reader) *model.AppError
SetTeamIconFromMultiPartFile(teamID string, file multipart.File) *model.AppError
SlackImport(c *request.Context, fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer)
SoftDeleteAllTeamsExcept(teamID string) *model.AppError
SoftDeleteTeam(teamID string) *model.AppError
Srv() *Server
SubmitInteractiveDialog(c *request.Context, request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError)

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

@@ -37,6 +37,17 @@ func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.
return nil
}
func (a *App) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
if limits.Teams != nil && limits.Teams.Active != nil && *limits.Teams.Active > 0 {
err := a.AdjustTeamsFromProductLimits(limits.Teams)
if err != nil {
return err
}
}
return nil
}
func (a *App) SendUpgradeConfirmationEmail() *model.AppError {
sysAdmins, e := a.getSysAdminsEmailRecipients()
if e != nil {

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

@@ -625,6 +625,50 @@ func (a *OpenTracingAppLayer) AdjustImage(file io.Reader) (*bytes.Buffer, *model
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustInProductLimits")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AdjustInProductLimits(limits, subscription)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustTeamsFromProductLimits")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AdjustTeamsFromProductLimits(teamLimits)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AllowOAuthAppAccessToUser(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AllowOAuthAppAccessToUser")
@@ -15583,6 +15627,28 @@ func (a *OpenTracingAppLayer) SlackImport(c *request.Context, fileData multipart
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SoftDeleteAllTeamsExcept")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SoftDeleteAllTeamsExcept(teamID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SoftDeleteTeam(teamID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SoftDeleteTeam")

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

@@ -14,6 +14,7 @@ import (
"mime/multipart"
"net/http"
"net/url"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/app/email"
@@ -29,6 +30,106 @@ import (
"github.com/mattermost/mattermost-server/v6/store/sqlstore"
)
func (a *App) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
maxActiveTeams := *teamLimits.Active
teams, appErr := a.GetAllTeams()
if appErr != nil {
return appErr
}
if teams == nil {
return nil
}
// Sort the list of teams based on their creation date
sort.Slice(teams, func(i, j int) bool {
return teams[i].CreateAt < teams[j].CreateAt
})
var activeTeams []*model.Team
var cloudArchivedTeams []*model.Team
for _, team := range teams {
if team.DeleteAt == 0 {
activeTeams = append(activeTeams, team)
}
if team.DeleteAt > 0 && team.CloudLimitsArchived {
cloudArchivedTeams = append(cloudArchivedTeams, team)
}
}
if len(activeTeams) > maxActiveTeams {
// If there are more active teams than allowed, we must archive them
// Remove the first n elements (where n is the allowed number of teams) so they aren't archived
teamsToArchive := activeTeams[maxActiveTeams:]
for _, team := range teamsToArchive {
cloudLimitsArchived := true
// Archive the remainder
patch := model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
_, err := a.PatchTeam(team.Id, &patch)
if err != nil {
return err
}
err = a.SoftDeleteTeam(team.Id)
if err != nil {
return err
}
}
} else if len(activeTeams) < maxActiveTeams && len(cloudArchivedTeams) > 0 {
// If the number of activeTeams is less than the allowed limit, and there are some cloudArchivedTeams, we can restore these cloudArchivedTeams
activeTeamsBeforeLimit := maxActiveTeams - len(activeTeams)
teamsToRestore := cloudArchivedTeams
// If the number of active teams remaining before the limit is hit is fewer than the number of cloudArchivedTeams, trim the list (still according to CreateAt)
// Otherwise, we can restore all of the cloudArchivedTeams without hitting the limit, so don't filter the list
if activeTeamsBeforeLimit < len(cloudArchivedTeams) {
teamsToRestore = cloudArchivedTeams[:(activeTeamsBeforeLimit)]
}
cloudLimitsArchived := false
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
for _, team := range teamsToRestore {
err := a.RestoreTeam(team.Id)
if err != nil {
return err
}
_, err = a.PatchTeam(team.Id, patch)
if err != nil {
return err
}
}
}
return nil
}
func (a *App) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
teams, appErr := a.GetAllTeams()
if appErr != nil {
return appErr
}
if teams == nil {
return nil
}
cloudLimitsArchived := true
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
for _, team := range teams {
if team.Id != teamID {
_, err := a.PatchTeam(team.Id, patch)
if err != nil {
return err
}
err = a.SoftDeleteTeam(team.Id)
if err != nil {
return err
}
}
}
return nil
}
func (a *App) CreateTeam(c *request.Context, team *model.Team) (*model.Team, *model.AppError) {
rteam, err := a.ch.srv.teamService.CreateTeam(team)
if err != nil {

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

@@ -466,6 +466,197 @@ func TestAddUserToTeamByTeamId(t *testing.T) {
}
func TestSoftDeleteAllTeamsExcept(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
teams := []*model.Team{
{
DisplayName: "team-1",
Name: "team-1",
Email: "foo@foo.com",
Type: model.TeamOpen,
},
}
teamId := ""
for _, create := range teams {
team, err := th.App.CreateTeam(th.Context, create)
require.Nil(t, err)
teamId = team.Id
}
err := th.App.SoftDeleteAllTeamsExcept(teamId)
assert.Nil(t, err)
allTeams, err := th.App.GetAllTeams()
require.Nil(t, err)
for _, team := range allTeams {
if team.Id == teamId {
require.Equal(t, int64(0), team.DeleteAt)
require.Equal(t, false, team.CloudLimitsArchived)
} else {
require.NotEqual(t, int64(0), team.DeleteAt)
require.Equal(t, true, team.CloudLimitsArchived)
}
}
}
func TestAdjustTeamsFromProductLimits(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
teams := []*model.Team{
{
DisplayName: "team-1",
Name: "team-1",
Email: "foo@foo.com",
Type: model.TeamOpen,
},
{
DisplayName: "team-2",
Name: "team-2",
Email: "foo@foo.com",
Type: model.TeamOpen,
},
{
DisplayName: "team-3",
Name: "team-3",
Email: "foo@foo.com",
Type: model.TeamOpen,
},
}
teamIds := []string{}
for _, create := range teams {
team, err := th.App.CreateTeam(th.Context, create)
require.Nil(t, err)
teamIds = append(teamIds, team.Id)
}
t.Run("Should soft delete teams if there are more teams than the limit", func(t *testing.T) {
activeLimit := 1
teamLimits := &model.TeamsLimits{Active: &activeLimit}
err := th.App.AdjustTeamsFromProductLimits(teamLimits)
require.Nil(t, err)
teamsList, err := th.App.GetTeams(teamIds)
require.Nil(t, err)
// Sort the list of teams based on their creation date
sort.Slice(teamsList, func(i, j int) bool {
return teamsList[i].CreateAt < teamsList[j].CreateAt
})
for i := range teamsList {
require.Equal(t, teamsList[i].DisplayName, teams[i].DisplayName)
require.NotEqual(t, 0, teamsList[i].DeleteAt)
require.Equal(t, true, teamsList[i].CloudLimitsArchived)
}
})
t.Run("Should not do anything if the amount of teams is equal to the limit", func(t *testing.T) {
expectedTeamsList, err := th.App.GetAllTeams()
var expectedActiveTeams []*model.Team
var expectedCloudArchivedTeams []*model.Team
for _, team := range expectedTeamsList {
if team.DeleteAt == 0 {
expectedActiveTeams = append(expectedActiveTeams, team)
}
if team.DeleteAt > 0 && team.CloudLimitsArchived {
expectedCloudArchivedTeams = append(expectedCloudArchivedTeams, team)
}
}
require.Nil(t, err)
activeLimit := len(expectedActiveTeams)
teamLimits := &model.TeamsLimits{Active: &activeLimit}
err = th.App.AdjustTeamsFromProductLimits(teamLimits)
require.Nil(t, err)
actualTeamsList, err := th.App.GetAllTeams()
require.Nil(t, err)
var actualActiveTeams []*model.Team
var actualCloudArchivedTeams []*model.Team
for _, team := range actualTeamsList {
if team.DeleteAt == 0 {
actualActiveTeams = append(actualActiveTeams, team)
}
if team.DeleteAt > 0 && team.CloudLimitsArchived {
actualCloudArchivedTeams = append(actualCloudArchivedTeams, team)
}
}
require.Equal(t, len(expectedActiveTeams), len(actualActiveTeams))
require.Equal(t, len(expectedCloudArchivedTeams), len(actualCloudArchivedTeams))
})
t.Run("Should restore archived teams if limit increases", func(t *testing.T) {
activeLimit := 1
teamLimits := &model.TeamsLimits{Active: &activeLimit}
err := th.App.AdjustTeamsFromProductLimits(teamLimits)
require.Nil(t, err)
activeLimit = 10000 // make the limit extremely high so all teams are enabled
teamLimits = &model.TeamsLimits{Active: &activeLimit}
err = th.App.AdjustTeamsFromProductLimits(teamLimits)
require.Nil(t, err)
teamsList, err := th.App.GetTeams(teamIds)
require.Nil(t, err)
// Sort the list of teams based on their creation date
sort.Slice(teamsList, func(i, j int) bool {
return teamsList[i].CreateAt < teamsList[j].CreateAt
})
for i := range teamsList {
require.Equal(t, teamsList[i].DisplayName, teams[i].DisplayName)
require.Equal(t, int64(0), teamsList[i].DeleteAt)
require.Equal(t, false, teamsList[i].CloudLimitsArchived)
}
})
t.Run("Should only restore teams that were archived by cloud limits", func(t *testing.T) {
activeLimit := 1
teamLimits := &model.TeamsLimits{Active: &activeLimit}
err := th.App.AdjustTeamsFromProductLimits(teamLimits)
require.Nil(t, err)
cloudLimitsArchived := false
patch := &model.TeamPatch{CloudLimitsArchived: &cloudLimitsArchived}
team, err := th.App.PatchTeam(teamIds[0], patch)
require.Nil(t, err)
require.Equal(t, false, team.CloudLimitsArchived)
activeLimit = 10000 // make the limit extremely high so all teams are enabled
teamLimits = &model.TeamsLimits{Active: &activeLimit}
err = th.App.AdjustTeamsFromProductLimits(teamLimits)
require.Nil(t, err)
teamsList, err := th.App.GetTeams(teamIds)
require.Nil(t, err)
// Sort the list of teams based on their creation date
sort.Slice(teamsList, func(i, j int) bool {
return teamsList[i].CreateAt < teamsList[j].CreateAt
})
require.NotEqual(t, int64(0), teamsList[0].DeleteAt)
require.Equal(t, int64(0), teamsList[1].DeleteAt)
require.Equal(t, int64(0), teamsList[2].DeleteAt)
})
}
func TestPermanentDeleteTeam(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

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

@@ -0,0 +1,14 @@
SET @preparedStatement = (SELECT IF(
EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Teams'
AND table_schema = DATABASE()
AND column_name = 'CloudLimitsArchived'
),
'ALTER TABLE Teams DROP COLUMN CloudLimitsArchived;',
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;

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

@@ -0,0 +1,14 @@
SET @preparedStatement = (SELECT IF(
NOT EXISTS(
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Teams'
AND table_schema = DATABASE()
AND column_name = 'CloudLimitsArchived'
),
'ALTER TABLE Teams ADD COLUMN CloudLimitsArchived BOOLEAN NOT NULL DEFAULT FALSE;',
'SELECT 1'
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

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

@@ -0,0 +1 @@
ALTER TABLE teams DROP COLUMN IF EXISTS CloudLimitsArchived;

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

@@ -0,0 +1 @@
ALTER TABLE teams ADD COLUMN IF NOT EXISTS CloudLimitsArchived bool NOT NULL DEFAULT FALSE;

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

@@ -24,32 +24,34 @@ const (
)
type Team struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Description string `json:"description"`
Email string `json:"email"`
Type string `json:"type"`
CompanyName string `json:"company_name"`
AllowedDomains string `json:"allowed_domains"`
InviteId string `json:"invite_id"`
AllowOpenInvite bool `json:"allow_open_invite"`
LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
SchemeId *string `json:"scheme_id"`
GroupConstrained *bool `json:"group_constrained"`
PolicyID *string `json:"policy_id"`
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Description string `json:"description"`
Email string `json:"email"`
Type string `json:"type"`
CompanyName string `json:"company_name"`
AllowedDomains string `json:"allowed_domains"`
InviteId string `json:"invite_id"`
AllowOpenInvite bool `json:"allow_open_invite"`
LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"`
SchemeId *string `json:"scheme_id"`
GroupConstrained *bool `json:"group_constrained"`
PolicyID *string `json:"policy_id"`
CloudLimitsArchived bool `json:"cloud_limits_archived"`
}
type TeamPatch struct {
DisplayName *string `json:"display_name"`
Description *string `json:"description"`
CompanyName *string `json:"company_name"`
AllowedDomains *string `json:"allowed_domains"`
AllowOpenInvite *bool `json:"allow_open_invite"`
GroupConstrained *bool `json:"group_constrained"`
DisplayName *string `json:"display_name"`
Description *string `json:"description"`
CompanyName *string `json:"company_name"`
AllowedDomains *string `json:"allowed_domains"`
AllowOpenInvite *bool `json:"allow_open_invite"`
GroupConstrained *bool `json:"group_constrained"`
CloudLimitsArchived *bool `json:"cloud_limits_archived"`
}
type TeamForExport struct {
@@ -246,6 +248,10 @@ func (o *Team) Patch(patch *TeamPatch) {
if patch.GroupConstrained != nil {
o.GroupConstrained = patch.GroupConstrained
}
if patch.CloudLimitsArchived != nil {
o.CloudLimitsArchived = *patch.CloudLimitsArchived
}
}
func (o *Team) IsGroupConstrained() bool {

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

@@ -227,10 +227,10 @@ func (s SqlTeamStore) Save(team *model.Team) (*model.Team, error) {
if _, err := s.GetMasterX().NamedExec(`INSERT INTO Teams
(Id, CreateAt, UpdateAt, DeleteAt, DisplayName, Name, Description, Email, Type, CompanyName, AllowedDomains,
InviteId, AllowOpenInvite, LastTeamIconUpdate, SchemeId, GroupConstrained)
InviteId, AllowOpenInvite, LastTeamIconUpdate, SchemeId, GroupConstrained, CloudLimitsArchived)
VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :DisplayName, :Name, :Description, :Email, :Type, :CompanyName, :AllowedDomains,
:InviteId, :AllowOpenInvite, :LastTeamIconUpdate, :SchemeId, :GroupConstrained)`, team); err != nil {
:InviteId, :AllowOpenInvite, :LastTeamIconUpdate, :SchemeId, :GroupConstrained, :CloudLimitsArchived)`, team); err != nil {
if IsUniqueConstraintError(err, []string{"Name", "teams_name_key"}) {
return nil, store.NewErrInvalidInput("Team", "id", team.Id)
}
@@ -268,7 +268,7 @@ func (s SqlTeamStore) Update(team *model.Team) (*model.Team, error) {
SET CreateAt=:CreateAt, UpdateAt=:UpdateAt, DeleteAt=:DeleteAt, DisplayName=:DisplayName, Name=:Name,
Description=:Description, Email=:Email, Type=:Type, CompanyName=:CompanyName, AllowedDomains=:AllowedDomains,
InviteId=:InviteId, AllowOpenInvite=:AllowOpenInvite, LastTeamIconUpdate=:LastTeamIconUpdate,
SchemeId=:SchemeId, GroupConstrained=:GroupConstrained
SchemeId=:SchemeId, GroupConstrained=:GroupConstrained, CloudLimitsArchived=:CloudLimitsArchived
WHERE Id=:Id`, team)
if err != nil {
return nil, errors.Wrapf(err, "failed to update Team with id=%s", team.Id)