[CLD-6324] Cloud IP Filtering (#24726)
* Initial comit for ip filtering service implementation * Add audit logs for IP Filters * start of webapp work * Stashing * Updates based on Agniva's feedback around service vs einterface * Updates completed * Commit before refactoring, everything's working * First pass of cleanup complete, front-end tests added * actually add files * Updates to some translation strings, running i18n-extract * Lock everything behind a feature flag * Fix tests, try to fix some linter stuff * Fixed linter for JS, on to scss * Fixed linter for scss * Fix linter * More fixes for pipeline * Support for IPV6 * Remove tsx file that was removed in masteR * Revert package.json and package-lock.json to master, add cidr-regex dep into channels/package.json * Another commit to force fix Github * Fixes around IPV6. Some suggestions from Matt re: UX review. Fixing pipelines for tests and types on new cidr-regex package * Changes to address Matt's feedback * A few more changes for clean up * Add support for permissions * Fix vet for OpenAPI spec * Actually add the yaml file for openapi * Add permission migration to allow support for IP Filtering * Fix tests * Final fixes from Matt * Remove cancel button from page, update link outs to documentation * Update test to account for removed cancel button * Adjustments based on feedback from Harrison * More fixes from PR feedback * Add a t to fix translations that doesn't seem to be breaking anyone else? * More fix * updates for PR feedback * Fix linter * Fix types * Now fix the linter again * Add back tests because Harrison was able to get them running * Adjustments for PR feedback * Remove admin_definition.jsx * Fix linter * [CLD-6453] IP Filtering notification email for sysadmins (#25224) * Initial commit for IP filtering alert email * Updates to style for email, addition of ip_filtering email: * Fix pipelines * Adjustments from Matt's feedback * Padding changes * template diff (#25249) Co-authored-by: Gabe Jackson <3694686+gabrieljackson@users.noreply.github.com> * Fix hardcoded true, remove bool return value --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Gabe Jackson <3694686+gabrieljackson@users.noreply.github.com> * Lock feature behind enterprise license. Drop cidr-regex in favour of ipaddr.js dependency. Refactor isIpAddressWithinRanges to use ipaddr.js * Add a couple server tests * fix linter * Fix types from merge conflicts --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Gabe Jackson <3694686+gabrieljackson@users.noreply.github.com>
Этот коммит содержится в:
@@ -137,6 +137,8 @@ type Routes struct {
|
||||
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
|
||||
|
||||
Drafts *mux.Router // 'api/v4/drafts'
|
||||
|
||||
IPFiltering *mux.Router // 'api/v4/ip_filtering'
|
||||
}
|
||||
|
||||
type API struct {
|
||||
@@ -261,6 +263,8 @@ func Init(srv *app.Server) (*API, error) {
|
||||
|
||||
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
|
||||
|
||||
api.BaseRoutes.IPFiltering = api.BaseRoutes.APIRoot.PathPrefix("/ip_filtering").Subrouter()
|
||||
|
||||
api.InitUser()
|
||||
api.InitBot()
|
||||
api.InitTeam()
|
||||
@@ -304,6 +308,7 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.InitUsage()
|
||||
api.InitHostedCustomer()
|
||||
api.InitDrafts()
|
||||
api.InitIPFiltering()
|
||||
|
||||
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||
|
||||
|
||||
139
server/channels/api4/ip_filtering.go
Обычный файл
139
server/channels/api4/ip_filtering.go
Обычный файл
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
)
|
||||
|
||||
func (api *API) InitIPFiltering() {
|
||||
api.BaseRoutes.IPFiltering.Handle("", api.APISessionRequired(getIPFilters)).Methods("GET")
|
||||
api.BaseRoutes.IPFiltering.Handle("", api.APISessionRequired(applyIPFilters)).Methods("POST")
|
||||
api.BaseRoutes.IPFiltering.Handle("/my_ip", api.APISessionRequired(myIP)).Methods("GET")
|
||||
}
|
||||
|
||||
func ensureIPFilteringInterface(c *Context, where string) (einterfaces.IPFilteringInterface, bool) {
|
||||
if c.App.IPFiltering() == nil || !c.App.Config().FeatureFlags.CloudIPFiltering || c.App.License() == nil || c.App.License().SkuShortName != model.LicenseShortSkuEnterprise {
|
||||
c.Err = model.NewAppError(where, "api.context.ip_filtering.not_available.app_error", nil, "", http.StatusNotImplemented)
|
||||
return nil, false
|
||||
}
|
||||
return c.App.IPFiltering(), true
|
||||
}
|
||||
|
||||
func getIPFilters(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ipFiltering, ok := ensureIPFilteringInterface(c, "getIPFilters")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadIPFilters) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadIPFilters)
|
||||
return
|
||||
}
|
||||
|
||||
allowedRanges, err := ipFiltering.GetIPFilters()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("getIPFilters", "api.context.ip_filtering.get_ip_filters.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(allowedRanges); err != nil {
|
||||
c.Err = model.NewAppError("getIPFilters", "api.context.ip_filtering.get_ip_filters.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func applyIPFilters(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ipFiltering, ok := ensureIPFilteringInterface(c, "applyIPFilters")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteIPFilters) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleWriteIPFilters)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord("applyIPFilters", audit.Fail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
|
||||
allowedRanges := &model.AllowedIPRanges{} // Initialize the allowedRanges variable
|
||||
if err := json.NewDecoder(r.Body).Decode(allowedRanges); err != nil {
|
||||
c.Err = model.NewAppError("applyIPFilters", "api.context.ip_filtering.apply_ip_filters.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
audit.AddEventParameterAuditable(auditRec, "IPFilter", allowedRanges)
|
||||
|
||||
updatedAllowedRanges, err := ipFiltering.ApplyIPFilters(allowedRanges)
|
||||
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("applyIPFilters", "api.context.ip_filtering.apply_ip_filters.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
c.App.Srv().Go(func() {
|
||||
initiatingUser, err := c.App.Srv().Store().User().GetProfileByIds(context.Background(), []string{c.AppContext.Session().UserId}, nil, true)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to get initiating user", mlog.Err(err))
|
||||
}
|
||||
|
||||
users, err := c.App.Srv().Store().User().GetSystemAdminProfiles()
|
||||
if err != nil {
|
||||
mlog.Error("Failed to get system admins", mlog.Err(err))
|
||||
}
|
||||
|
||||
cloudWorkspaceOwnerEmailAddress := ""
|
||||
if c.App.License().IsCloud() {
|
||||
portalUserCustomer, cErr := c.App.Cloud().GetCloudCustomer(c.AppContext.Session().UserId)
|
||||
if cErr != nil {
|
||||
mlog.Error("Failed to get portal user customer", mlog.Err(cErr))
|
||||
}
|
||||
if cErr == nil && portalUserCustomer != nil {
|
||||
cloudWorkspaceOwnerEmailAddress = portalUserCustomer.Email
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if err = c.App.Srv().EmailService.SendIPFiltersChangedEmail(user.Email, initiatingUser[0], *c.App.Config().ServiceSettings.SiteURL, *c.App.Config().CloudSettings.CWSURL, user.Locale, cloudWorkspaceOwnerEmailAddress == user.Email); err != nil {
|
||||
mlog.Error("Error while sending IP filters changed email", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedAllowedRanges); err != nil {
|
||||
c.Err = model.NewAppError("getIPFilters", "api.context.ip_filtering.get_ip_filters.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func myIP(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := ensureIPFilteringInterface(c, "myIP")
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
response := &model.GetIPAddressResponse{
|
||||
IP: c.AppContext.IPAddress(),
|
||||
}
|
||||
|
||||
json, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("myIP", "api.context.ip_filtering.get_my_ip.failed", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(json)
|
||||
}
|
||||
310
server/channels/api4/ip_filtering_test.go
Обычный файл
310
server/channels/api4/ip_filtering_test.go
Обычный файл
@@ -0,0 +1,310 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getIPFilters(t *testing.T) {
|
||||
lic := &model.License{
|
||||
Features: &model.Features{
|
||||
CustomPermissionsSchemes: model.NewBool(false),
|
||||
Cloud: model.NewBool(true),
|
||||
},
|
||||
Customer: &model.Customer{
|
||||
Name: "TestName",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
SkuName: "SKU NAME",
|
||||
SkuShortName: model.LicenseShortSkuEnterprise,
|
||||
StartsAt: model.GetMillis() - 1000,
|
||||
ExpiresAt: model.GetMillis() + 100000,
|
||||
}
|
||||
|
||||
t.Run("No license returns 501", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().RemoveLicense()
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.GetIPFilters(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ipFilters)
|
||||
require.Equal(t, 501, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("No feature flag returns 501", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.GetIPFilters(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ipFilters)
|
||||
require.Equal(t, 501, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Feature flag and license but no permission", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.GetIPFilters(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ipFilters)
|
||||
require.Equal(t, 403, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Feature flag and license and permission", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFiltering.Mock.On("GetIPFilters").Return(&model.AllowedIPRanges{
|
||||
model.AllowedIPRange{
|
||||
CIDRBlock: "127.0.0.1/32",
|
||||
Description: "test",
|
||||
},
|
||||
}, nil)
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
th.Client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.GetIPFilters(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ipFilters)
|
||||
require.Equal(t, 200, r.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_applyIPFilters(t *testing.T) {
|
||||
allowedRanges := &model.AllowedIPRanges{
|
||||
model.AllowedIPRange{
|
||||
CIDRBlock: "127.0.0.1/32",
|
||||
Description: "test",
|
||||
},
|
||||
}
|
||||
|
||||
lic := &model.License{
|
||||
Features: &model.Features{
|
||||
CustomPermissionsSchemes: model.NewBool(false),
|
||||
Cloud: model.NewBool(true),
|
||||
},
|
||||
Customer: &model.Customer{
|
||||
Name: "TestName",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
SkuName: "SKU NAME",
|
||||
SkuShortName: model.LicenseShortSkuEnterprise,
|
||||
StartsAt: model.GetMillis() - 1000,
|
||||
ExpiresAt: model.GetMillis() + 100000,
|
||||
}
|
||||
// Initialize the allowedRanges variable
|
||||
t.Run("No license returns 501", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().RemoveLicense()
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.ApplyIPFilters(context.Background(), allowedRanges)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ipFilters)
|
||||
require.Equal(t, 501, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("License but no feature flag returns 501", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.ApplyIPFilters(context.Background(), allowedRanges)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ipFilters)
|
||||
require.Equal(t, 501, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("feature flag and license but no permission", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
ipFilters, r, err := th.Client.ApplyIPFilters(context.Background(), allowedRanges)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, ipFilters)
|
||||
require.Equal(t, 403, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Feature flag and license and permission", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFiltering.Mock.On("ApplyIPFilters", mock.Anything).Return(&model.AllowedIPRanges{
|
||||
model.AllowedIPRange{
|
||||
CIDRBlock: "127.0.0.1/32",
|
||||
Description: "test",
|
||||
},
|
||||
}, nil)
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
th.Client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password)
|
||||
|
||||
ipFilters, r, err := th.Client.ApplyIPFilters(context.Background(), allowedRanges)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ipFilters)
|
||||
require.Equal(t, 200, r.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getMyIP(t *testing.T) {
|
||||
lic := &model.License{
|
||||
Features: &model.Features{
|
||||
CustomPermissionsSchemes: model.NewBool(false),
|
||||
Cloud: model.NewBool(true),
|
||||
},
|
||||
Customer: &model.Customer{
|
||||
Name: "TestName",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
SkuName: "SKU NAME",
|
||||
SkuShortName: model.LicenseShortSkuEnterprise,
|
||||
StartsAt: model.GetMillis() - 1000,
|
||||
ExpiresAt: model.GetMillis() + 100000,
|
||||
}
|
||||
t.Run("No license returns 501", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
|
||||
th.App.Srv().RemoveLicense()
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
myIP, r, err := th.Client.GetMyIP(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, myIP)
|
||||
require.Equal(t, 501, r.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Licensed, but no feature flag returns 501", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_CLOUDIPFILTERING", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDIPFILTERING")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
|
||||
|
||||
ipFiltering := &mocks.IPFilteringInterface{}
|
||||
ipFilteringImpl := th.App.Srv().IPFiltering
|
||||
defer func() {
|
||||
th.App.Srv().IPFiltering = ipFilteringImpl
|
||||
}()
|
||||
th.App.Srv().IPFiltering = ipFiltering
|
||||
th.App.Srv().SetLicense(lic)
|
||||
|
||||
myIP, r, err := th.Client.GetMyIP(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, myIP)
|
||||
require.Equal(t, 501, r.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -103,6 +103,11 @@ func (a *App) Saml() einterfaces.SamlInterface {
|
||||
func (a *App) Cloud() einterfaces.CloudInterface {
|
||||
return a.ch.srv.Cloud
|
||||
}
|
||||
|
||||
func (a *App) IPFiltering() einterfaces.IPFilteringInterface {
|
||||
return a.ch.srv.IPFiltering
|
||||
}
|
||||
|
||||
func (a *App) HTTPService() httpservice.HTTPService {
|
||||
return a.ch.srv.httpService
|
||||
}
|
||||
|
||||
@@ -868,6 +868,7 @@ type AppIface interface {
|
||||
HasPermissionToTeam(c request.CTX, askingUserId string, teamID string, permission *model.Permission) bool
|
||||
HasPermissionToUser(askingUserId string, userID string) bool
|
||||
HasSharedChannel(channelID string) (bool, error)
|
||||
IPFiltering() einterfaces.IPFilteringInterface
|
||||
ImageProxy() *imageproxy.ImageProxy
|
||||
ImageProxyAdder() func(string) string
|
||||
ImageProxyRemover() (f func(string) string)
|
||||
|
||||
@@ -1275,3 +1275,40 @@ func (es *Service) SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *Service) SendIPFiltersChangedEmail(email string, initiatingUser *model.User, siteURL, portalURL, locale string, isWorkspaceOwner bool) error {
|
||||
T := i18n.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.ip_filters_changed.subject")
|
||||
|
||||
data := es.NewEmailTemplateData(locale)
|
||||
data.Props["SiteURL"] = siteURL
|
||||
data.Props["Title"] = T("api.templates.ip_filters_changed.title")
|
||||
data.Props["SubTitle"] = T("api.templates.ip_filters_changed.subTitle", map[string]any{"InitiatingUsername": initiatingUser.Username, "SiteURL": siteURL})
|
||||
data.Props["ButtonURL"] = siteURL + "/admin_console/site_config/ip_filtering"
|
||||
data.Props["Button"] = T("api.templates.ip_filters_changed.button")
|
||||
data.Props["TroubleAccessingTitle"] = T("api.templates.ip_filters_changed_footer.title")
|
||||
data.Props["SendAnEmailTo"] = T("api.templates.ip_filters_changed_footer.send_an_email_to", map[string]any{"InitiatingUserEmail": initiatingUser.Email})
|
||||
data.Props["PortalURL"] = portalURL
|
||||
// If the email we're sending to was the one who initiated the change, we don't want to show their email address as a mailto
|
||||
if email != initiatingUser.Email {
|
||||
data.Props["ActorEmail"] = initiatingUser.Email
|
||||
}
|
||||
|
||||
if isWorkspaceOwner {
|
||||
data.Props["LogInToCustomerPortal"] = T("api.templates.ip_filters_changed_footer.log_in_to_customer_portal")
|
||||
}
|
||||
data.Props["ContactSupport"] = T("api.templates.ip_filters_changed_footer.contact_support")
|
||||
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
|
||||
|
||||
body, err := es.templatesContainer.RenderToString("ip_filters_changed", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := es.sendMail(email, subject, body, "PasswordResetEmail"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -310,6 +310,20 @@ func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []*
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendIPFiltersChangedEmail provides a mock function with given fields: _a0, userWhoChangedFilter, siteURL, portalURL, locale, isWorkspaceOwner
|
||||
func (_m *ServiceInterface) SendIPFiltersChangedEmail(_a0 string, userWhoChangedFilter *model.User, siteURL string, portalURL string, locale string, isWorkspaceOwner bool) error {
|
||||
ret := _m.Called(_a0, userWhoChangedFilter, siteURL, portalURL, locale, isWorkspaceOwner)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, *model.User, string, string, string, bool) error); ok {
|
||||
r0 = rf(_a0, userWhoChangedFilter, siteURL, portalURL, locale, isWorkspaceOwner)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendInviteEmails provides a mock function with given fields: team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin
|
||||
func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error {
|
||||
ret := _m.Called(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
|
||||
@@ -163,6 +163,7 @@ type ServiceInterface interface {
|
||||
InitEmailBatching()
|
||||
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
|
||||
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
|
||||
SendIPFiltersChangedEmail(email string, userWhoChangedFilter *model.User, siteURL, portalURL, locale string, isWorkspaceOwner bool) error
|
||||
Stop()
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,12 @@ func RegisterNotificationInterface(f func(*App) einterfaces.NotificationInterfac
|
||||
notificationInterface = f
|
||||
}
|
||||
|
||||
var ipFilteringInterface func(*App) einterfaces.IPFilteringInterface
|
||||
|
||||
func RegisterIPFilteringInterface(f func(*App) einterfaces.IPFilteringInterface) {
|
||||
ipFilteringInterface = f
|
||||
}
|
||||
|
||||
func (s *Server) initEnterprise() {
|
||||
if cloudInterface != nil {
|
||||
s.Cloud = cloudInterface(s)
|
||||
|
||||
@@ -11560,6 +11560,23 @@ func (a *OpenTracingAppLayer) HubUnregister(webConn *platform.WebConn) {
|
||||
a.app.HubUnregister(webConn)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) IPFiltering() einterfaces.IPFilteringInterface {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IPFiltering")
|
||||
|
||||
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.IPFiltering()
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ImageProxyAdder() func(string) string {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ImageProxyAdder")
|
||||
|
||||
@@ -1117,6 +1117,30 @@ func (a *App) getAddChannelReadContentPermissions() (permissionsMap, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (a *App) getAddIPFilterPermissionsMigration() (permissionsMap, error) {
|
||||
t := []permissionTransformation{}
|
||||
|
||||
ipFilterPermissionsRead := []string{
|
||||
model.PermissionSysconsoleReadIPFilters.Id,
|
||||
}
|
||||
|
||||
ipFilterPermissionsWrite := []string{
|
||||
model.PermissionSysconsoleWriteIPFilters.Id,
|
||||
}
|
||||
|
||||
t = append(t, permissionTransformation{
|
||||
On: permissionOr(isExactRole(model.SystemAdminRoleId)),
|
||||
Add: ipFilterPermissionsRead,
|
||||
})
|
||||
|
||||
t = append(t, permissionTransformation{
|
||||
On: permissionOr(isExactRole(model.SystemAdminRoleId)),
|
||||
Add: ipFilterPermissionsWrite,
|
||||
})
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
|
||||
func (a *App) DoPermissionsMigrations() error {
|
||||
return a.Srv().doPermissionsMigrations()
|
||||
@@ -1161,6 +1185,7 @@ func (s *Server) doPermissionsMigrations() error {
|
||||
{Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions},
|
||||
{Key: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Migration: a.getAddCustomUserGroupsPermissionRestore},
|
||||
{Key: model.MigrationKeyAddReadChannelContentPermissions, Migration: a.getAddChannelReadContentPermissions},
|
||||
{Key: model.MigrationKeyAddIPFilteringPermissions, Migration: a.getAddIPFilterPermissionsMigration},
|
||||
}
|
||||
|
||||
roles, err := s.Store().Role().GetAll()
|
||||
|
||||
@@ -141,7 +141,8 @@ type Server struct {
|
||||
// startSearchEngine bool
|
||||
skipPostInit bool
|
||||
|
||||
Cloud einterfaces.CloudInterface
|
||||
Cloud einterfaces.CloudInterface
|
||||
IPFiltering einterfaces.IPFilteringInterface
|
||||
|
||||
tracer *tracing.Tracer
|
||||
|
||||
@@ -396,6 +397,10 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
|
||||
s.initJobs()
|
||||
|
||||
if ipFilteringInterface != nil {
|
||||
s.IPFiltering = ipFilteringInterface(app)
|
||||
}
|
||||
|
||||
s.clusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() {
|
||||
mlog.Info("Cluster leader changed. Determining if job schedulers should be running:", mlog.Bool("isLeader", s.IsLeader()))
|
||||
if s.Jobs != nil {
|
||||
|
||||
@@ -72,6 +72,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
|
||||
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissionRestore).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", model.MigrationKeyAddReadChannelContentPermissions).Return(&model.System{Name: model.MigrationKeyAddReadChannelContentPermissions, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", model.MigrationKeyDeleteEmptyDrafts).Return(&model.System{Name: model.MigrationKeyDeleteEmptyDrafts, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", model.MigrationKeyAddIPFilteringPermissions).Return(&model.System{Name: model.MigrationKeyAddIPFilteringPermissions, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
|
||||
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
|
||||
systemStore.On("GetByName", "elasticsearch_fix_channel_index_migration").Return(&model.System{Name: "elasticsearch_fix_channel_index_migration", Value: "true"}, nil)
|
||||
|
||||
@@ -53,4 +53,7 @@ type CloudInterface interface {
|
||||
|
||||
// Used only for when a customer has telemetry disabled. In this scenario, true up review telemetry will be submitted via CWS.
|
||||
SubmitTrueUpReview(userID string, trueUpReviewProfile map[string]any) error
|
||||
|
||||
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
||||
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
|
||||
}
|
||||
|
||||
11
server/einterfaces/ip_filtering.go
Обычный файл
11
server/einterfaces/ip_filtering.go
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package einterfaces
|
||||
|
||||
import "github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
type IPFilteringInterface interface {
|
||||
ApplyIPFilters(allowedIPRanges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
||||
GetIPFilters() (*model.AllowedIPRanges, error)
|
||||
}
|
||||
@@ -15,6 +15,32 @@ type CloudInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ApplyIPFilters provides a mock function with given fields: userID, ranges
|
||||
func (_m *CloudInterface) ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error) {
|
||||
ret := _m.Called(userID, ranges)
|
||||
|
||||
var r0 *model.AllowedIPRanges
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, *model.AllowedIPRanges) (*model.AllowedIPRanges, error)); ok {
|
||||
return rf(userID, ranges)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, *model.AllowedIPRanges) *model.AllowedIPRanges); ok {
|
||||
r0 = rf(userID, ranges)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AllowedIPRanges)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, *model.AllowedIPRanges) error); ok {
|
||||
r1 = rf(userID, ranges)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// BootstrapSelfHostedSignup provides a mock function with given fields: req
|
||||
func (_m *CloudInterface) BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error) {
|
||||
ret := _m.Called(req)
|
||||
@@ -343,6 +369,32 @@ func (_m *CloudInterface) GetCloudProducts(userID string, includeLegacyProducts
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetIPFilters provides a mock function with given fields: userID
|
||||
func (_m *CloudInterface) GetIPFilters(userID string) (*model.AllowedIPRanges, error) {
|
||||
ret := _m.Called(userID)
|
||||
|
||||
var r0 *model.AllowedIPRanges
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (*model.AllowedIPRanges, error)); ok {
|
||||
return rf(userID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) *model.AllowedIPRanges); ok {
|
||||
r0 = rf(userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AllowedIPRanges)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetInvoicePDF provides a mock function with given fields: userID, invoiceID
|
||||
func (_m *CloudInterface) GetInvoicePDF(userID string, invoiceID string) ([]byte, string, error) {
|
||||
ret := _m.Called(userID, invoiceID)
|
||||
|
||||
82
server/einterfaces/mocks/IPFilteringInterface.go
Обычный файл
82
server/einterfaces/mocks/IPFilteringInterface.go
Обычный файл
@@ -0,0 +1,82 @@
|
||||
// Code generated by mockery v2.23.2. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make einterfaces-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// IPFilteringInterface is an autogenerated mock type for the IPFilteringInterface type
|
||||
type IPFilteringInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ApplyIPFilters provides a mock function with given fields: allowedIPRanges
|
||||
func (_m *IPFilteringInterface) ApplyIPFilters(allowedIPRanges *model.AllowedIPRanges) (*model.AllowedIPRanges, error) {
|
||||
ret := _m.Called(allowedIPRanges)
|
||||
|
||||
var r0 *model.AllowedIPRanges
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*model.AllowedIPRanges) (*model.AllowedIPRanges, error)); ok {
|
||||
return rf(allowedIPRanges)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*model.AllowedIPRanges) *model.AllowedIPRanges); ok {
|
||||
r0 = rf(allowedIPRanges)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AllowedIPRanges)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*model.AllowedIPRanges) error); ok {
|
||||
r1 = rf(allowedIPRanges)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetIPFilters provides a mock function with given fields:
|
||||
func (_m *IPFilteringInterface) GetIPFilters() (*model.AllowedIPRanges, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *model.AllowedIPRanges
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (*model.AllowedIPRanges, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() *model.AllowedIPRanges); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AllowedIPRanges)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewIPFilteringInterface interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewIPFilteringInterface creates a new instance of IPFilteringInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewIPFilteringInterface(t mockConstructorTestingTNewIPFilteringInterface) *IPFilteringInterface {
|
||||
mock := &IPFilteringInterface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1625,6 +1625,22 @@
|
||||
"id": "api.context.invitation_expired.error",
|
||||
"translation": "Invitation is expired."
|
||||
},
|
||||
{
|
||||
"id": "api.context.ip_filtering.apply_ip_filters.app_error",
|
||||
"translation": "An error has occurred while applying IP Filters"
|
||||
},
|
||||
{
|
||||
"id": "api.context.ip_filtering.get_ip_filters.app_error",
|
||||
"translation": "An error has occurred while fetching IP Filters"
|
||||
},
|
||||
{
|
||||
"id": "api.context.ip_filtering.get_my_ip.failed",
|
||||
"translation": "An error has occurred while fetching the client's IP address"
|
||||
},
|
||||
{
|
||||
"id": "api.context.ip_filtering.not_available.app_error",
|
||||
"translation": "IP Filtering is not available on this server"
|
||||
},
|
||||
{
|
||||
"id": "api.context.json_encoding.app_error",
|
||||
"translation": "Error encoding JSON."
|
||||
@@ -3706,6 +3722,38 @@
|
||||
"id": "api.templates.invite_team_and_channels_subject",
|
||||
"translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .ChannelsLen }} channels on the {{ .TeamDisplayName }} Team"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed.button",
|
||||
"translation": "Review changes"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed.subTitle",
|
||||
"translation": "@{{ .InitiatingUsername }} changed the IP filtering settings for your workspace at the URL: {{ .SiteURL }}"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed.subject",
|
||||
"translation": "Changes to Your Workspace's IP Filters"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed.title",
|
||||
"translation": "IP filtering changes for your workspace"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed_footer.contact_support",
|
||||
"translation": "Contact support"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed_footer.log_in_to_customer_portal",
|
||||
"translation": "Log in to the customer portal to reset IP filtering"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed_footer.send_an_email_to",
|
||||
"translation": "Send an email to {{ .InitiatingUserEmail }}"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.ip_filters_changed_footer.title",
|
||||
"translation": "Having trouble accessing your workspace?"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.license_up_for_renewal_contact_sales",
|
||||
"translation": "Contact sales"
|
||||
|
||||
@@ -555,6 +555,10 @@ func (c *Client4) sharedChannelsRoute() string {
|
||||
return "/sharedchannels"
|
||||
}
|
||||
|
||||
func (c *Client4) ipFiltersRoute() string {
|
||||
return "/ip_filtering"
|
||||
}
|
||||
|
||||
func (c *Client4) permissionsRoute() string {
|
||||
return "/permissions"
|
||||
}
|
||||
@@ -8028,6 +8032,52 @@ func (c *Client4) GetProductLimits(ctx context.Context) (*ProductLimits, *Respon
|
||||
return productLimits, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) GetIPFilters(ctx context.Context) (*AllowedIPRanges, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.ipFiltersRoute(), "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
|
||||
defer closeBody(r)
|
||||
|
||||
var allowedIPRanges *AllowedIPRanges
|
||||
json.NewDecoder(r.Body).Decode(&allowedIPRanges)
|
||||
return allowedIPRanges, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) ApplyIPFilters(ctx context.Context, allowedRanges *AllowedIPRanges) (*AllowedIPRanges, *Response, error) {
|
||||
payload, err := json.Marshal(allowedRanges)
|
||||
if err != nil {
|
||||
return nil, nil, NewAppError("ApplyIPFilters", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIPostBytes(ctx, c.ipFiltersRoute(), payload)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
|
||||
defer closeBody(r)
|
||||
|
||||
var allowedIPRanges *AllowedIPRanges
|
||||
json.NewDecoder(r.Body).Decode(&allowedIPRanges)
|
||||
|
||||
return allowedIPRanges, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) GetMyIP(ctx context.Context) (*GetIPAddressResponse, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.ipFiltersRoute()+"/my_ip", "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
|
||||
defer closeBody(r)
|
||||
|
||||
var response *GetIPAddressResponse
|
||||
json.NewDecoder(r.Body).Decode(&response)
|
||||
|
||||
return response, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) CreateCustomerPayment(ctx context.Context) (*StripeSetupIntent, *Response, error) {
|
||||
r, err := c.DoAPIPost(ctx, c.cloudRoute()+"/payment", "")
|
||||
if err != nil {
|
||||
|
||||
@@ -44,7 +44,8 @@ type FeatureFlags struct {
|
||||
|
||||
StreamlinedMarketplace bool
|
||||
|
||||
ConsumePostHook bool
|
||||
CloudIPFiltering bool
|
||||
ConsumePostHook bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -60,6 +61,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.CloudReverseTrial = false
|
||||
f.EnableExportDirectDownload = false
|
||||
f.StreamlinedMarketplace = true
|
||||
f.CloudIPFiltering = false
|
||||
f.ConsumePostHook = false
|
||||
}
|
||||
|
||||
|
||||
20
server/public/model/ip_filtering.go
Обычный файл
20
server/public/model/ip_filtering.go
Обычный файл
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
type AllowedIPRanges []AllowedIPRange
|
||||
|
||||
type AllowedIPRange struct {
|
||||
CIDRBlock string `json:"cidr_block"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
}
|
||||
|
||||
func (air *AllowedIPRanges) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"AllowedIPRanges": air,
|
||||
}
|
||||
}
|
||||
|
||||
type GetIPAddressResponse struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
@@ -44,4 +44,5 @@ const (
|
||||
MigrationKeyElasticsearchFixChannelIndex = "elasticsearch_fix_channel_index_migration"
|
||||
MigrationKeyS3Path = "s3_path_migration"
|
||||
MigrationKeyDeleteEmptyDrafts = "delete_empty_drafts_migration"
|
||||
MigrationKeyAddIPFilteringPermissions = "add_ip_filtering_permissions"
|
||||
)
|
||||
|
||||
@@ -267,6 +267,9 @@ var PermissionSysconsoleWriteSitePublicLinks *Permission
|
||||
var PermissionSysconsoleReadSiteNotices *Permission
|
||||
var PermissionSysconsoleWriteSiteNotices *Permission
|
||||
|
||||
var PermissionSysconsoleReadIPFilters *Permission
|
||||
var PermissionSysconsoleWriteIPFilters *Permission
|
||||
|
||||
var PermissionSysconsoleReadAuthentication *Permission
|
||||
var PermissionSysconsoleWriteAuthentication *Permission
|
||||
|
||||
@@ -1646,6 +1649,20 @@ func initializePermissions() {
|
||||
PermissionScopeSystem,
|
||||
}
|
||||
|
||||
PermissionSysconsoleReadIPFilters = &Permission{
|
||||
"sysconsole_read_site_ip_filters",
|
||||
"",
|
||||
"",
|
||||
PermissionScopeSystem,
|
||||
}
|
||||
|
||||
PermissionSysconsoleWriteIPFilters = &Permission{
|
||||
"sysconsole_write_site_ip_filters",
|
||||
"",
|
||||
"",
|
||||
PermissionScopeSystem,
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
PermissionSysconsoleReadAuthentication = &Permission{
|
||||
"sysconsole_read_authentication",
|
||||
@@ -2160,6 +2177,7 @@ func initializePermissions() {
|
||||
PermissionSysconsoleReadExperimentalFeatureFlags,
|
||||
PermissionSysconsoleReadExperimentalBleve,
|
||||
PermissionSysconsoleReadProductsBoards,
|
||||
PermissionSysconsoleReadIPFilters,
|
||||
}
|
||||
|
||||
SysconsoleWritePermissions = []*Permission{
|
||||
@@ -2218,6 +2236,7 @@ func initializePermissions() {
|
||||
PermissionSysconsoleWriteExperimentalFeatureFlags,
|
||||
PermissionSysconsoleWriteExperimentalBleve,
|
||||
PermissionSysconsoleWriteProductsBoards,
|
||||
PermissionSysconsoleWriteIPFilters,
|
||||
}
|
||||
|
||||
SystemScopedPermissionsMinusSysconsole := []*Permission{
|
||||
|
||||
@@ -296,6 +296,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -296,6 +296,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -296,6 +296,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -296,6 +296,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -296,6 +296,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -286,6 +286,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -306,6 +306,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
580
server/templates/ip_filters_changed.html
Обычный файл
580
server/templates/ip_filters_changed.html
Обычный файл
@@ -0,0 +1,580 @@
|
||||
{{define "ip_filters_changed"}}
|
||||
|
||||
<!-- FILE: ip_filters_changed.mjml -->
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700);
|
||||
|
||||
.emailBody {
|
||||
background-color: #F3F3F3
|
||||
}
|
||||
|
||||
.emailBody a {
|
||||
text-decoration: none !important;
|
||||
color: #1C58D9;
|
||||
}
|
||||
|
||||
.title div {
|
||||
font-weight: 600 !important;
|
||||
font-size: 28px !important;
|
||||
line-height: 36px !important;
|
||||
letter-spacing: -0.01em !important;
|
||||
color: #3F4350 !important;
|
||||
font-family: Open Sans, sans-serif !important;
|
||||
}
|
||||
|
||||
.subTitle div {
|
||||
font-size: 16px !important;
|
||||
line-height: 24px !important;
|
||||
color: rgba(63, 67, 80, 0.64) !important;
|
||||
}
|
||||
|
||||
.subTitle a {
|
||||
color: rgb(28, 88, 217) !important;
|
||||
}
|
||||
|
||||
.button a {
|
||||
background-color: #1C58D9 !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 18px !important;
|
||||
color: #FFFFFF !important;
|
||||
padding: 15px 24px !important;
|
||||
}
|
||||
|
||||
.button-cloud a {
|
||||
background-color: #1C58D9 !important;
|
||||
font-weight: 400 !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 18px !important;
|
||||
color: #FFFFFF !important;
|
||||
padding: 15px 24px !important;
|
||||
}
|
||||
|
||||
.messageButton a {
|
||||
background-color: #FFFFFF !important;
|
||||
border: 1px solid #FFFFFF !important;
|
||||
box-sizing: border-box !important;
|
||||
color: #1C58D9 !important;
|
||||
padding: 12px 20px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
|
||||
.info div {
|
||||
font-size: 14px !important;
|
||||
line-height: 20px !important;
|
||||
color: #3F4350 !important;
|
||||
padding: 40px 0px !important;
|
||||
}
|
||||
|
||||
.footerTitle div {
|
||||
font-weight: 600 !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 24px !important;
|
||||
color: #3F4350 !important;
|
||||
padding: 0px 0px 4px 0px !important;
|
||||
}
|
||||
|
||||
.footerInfo div {
|
||||
font-size: 14px !important;
|
||||
line-height: 20px !important;
|
||||
color: #3F4350 !important;
|
||||
padding: 0px 48px 0px 48px !important;
|
||||
}
|
||||
|
||||
.footerInfo a {
|
||||
color: #1C58D9 !important;
|
||||
}
|
||||
|
||||
.appDownloadButton a {
|
||||
background-color: #FFFFFF !important;
|
||||
border: 1px solid #1C58D9 !important;
|
||||
box-sizing: border-box !important;
|
||||
color: #1C58D9 !important;
|
||||
padding: 13px 20px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
|
||||
.emailFooter div {
|
||||
font-size: 12px !important;
|
||||
line-height: 16px !important;
|
||||
color: rgba(63, 67, 80, 0.56) !important;
|
||||
padding: 8px 24px 8px 24px !important;
|
||||
}
|
||||
|
||||
.postCard {
|
||||
padding: 0px 24px 40px 24px !important;
|
||||
}
|
||||
|
||||
.messageCard {
|
||||
background: #FFFFFF !important;
|
||||
border: 1px solid rgba(61, 60, 64, 0.08) !important;
|
||||
box-sizing: border-box !important;
|
||||
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12) !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 32px !important;
|
||||
}
|
||||
|
||||
.messageAvatar img {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
padding: 0px !important;
|
||||
border-radius: 32px !important;
|
||||
}
|
||||
|
||||
.messageAvatarCol {
|
||||
width: 32px !important;
|
||||
}
|
||||
|
||||
.postNameAndTime {
|
||||
padding: 0px 0px 4px 0px !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.senderName {
|
||||
font-family: Open Sans, sans-serif;
|
||||
text-align: left !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 20px !important;
|
||||
color: #3F4350 !important;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-family: Open Sans, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(63, 67, 80, 0.56);
|
||||
padding: 2px 6px;
|
||||
align-items: center;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.channelBg {
|
||||
background: rgba(63, 67, 80, 0.08);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.channelLogo {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
padding: 5px 4px 5px 6px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.channelName {
|
||||
font-family: Open Sans, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(63, 67, 80, 0.64);
|
||||
padding: 2px 6px 2px 0px;
|
||||
}
|
||||
|
||||
.gmChannelCount {
|
||||
background-color: rgba(63, 67, 80, 0.2);
|
||||
padding: 0 5px;
|
||||
border-radius: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.senderMessage div {
|
||||
text-align: left !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 20px !important;
|
||||
color: #3F4350 !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.senderInfoCol {
|
||||
width: 394px !important;
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 540px) and (min-width: 401px) {
|
||||
.emailBody {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.messageCard {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.senderInfoCol {
|
||||
width: 80% !important;
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 400px) {
|
||||
.emailBody {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.footerInfo div {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.messageCard {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.postCard {
|
||||
padding: 0px 0px 40px 0px !important;
|
||||
}
|
||||
|
||||
.senderInfoCol {
|
||||
width: 80% !important;
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-50 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;background-color:#FFFFFF;">
|
||||
<div class="emailBody" style="background-color: #FFFFFF;">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;border-radius:8px;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;border-radius:8px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:552px;" width="552" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:552px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 0px 40px 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:552px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:132px;">
|
||||
<img alt height="21" src="{{.Props.SiteURL}}/static/images/logo_email_dark.png" style="border:0;display:block;outline:none;text-decoration:none;height:21.76px;width:100%;font-size:13px;" width="132">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:552px;" width="552" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:552px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 24px 40px 24px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:504px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" class="title" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<div style="text-align: center; font-weight: 600; font-size: 28px; line-height: 36px; letter-spacing: -0.01em; color: #3F4350; font-family: Open Sans, sans-serif;">{{.Props.Title}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" class="subTitle" style="font-size:0px;padding:16px 24px 16px 24px;word-break:break-word;">
|
||||
<div style="font-family: Open Sans, sans-serif; text-align: center; font-size: 16px; line-height: 24px; color: rgba(63, 67, 80, 0.64);">{{.Props.SubTitle}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" class="button" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#FFFFFF" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:10px 25px;background:#FFFFFF;" valign="middle">
|
||||
<a href="{{.Props.ButtonURL}}" style="display: inline-block; background: #FFFFFF; font-family: Open Sans, sans-serif; margin: 0; text-transform: none; mso-padding-alt: 0px; border-radius: 4px; text-decoration: none; background-color: #1C58D9; font-weight: 600; font-size: 16px; line-height: 18px; color: #FFFFFF; padding: 15px 24px;" target="_blank">
|
||||
{{.Props.Button}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:552px;" width="552" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:552px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:552px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:312px;">
|
||||
<img alt height="auto" src="{{.Props.SiteURL}}/static/images/forgot_password_illustration.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="312">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:552px;" width="552" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:552px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:40px 0px 40px 0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:552px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" class="footerTitle" style="font-size:0px;padding:0px;padding-bottom:9px;word-break:break-word;">
|
||||
<div style="font-family: Open Sans, sans-serif; text-align: center; font-weight: 600; font-size: 16px; line-height: 24px; color: #3F4350; padding: 0px 0px 4px 0px;">{{.Props.TroubleAccessingTitle}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{if .Props.ActorEmail}}
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:0px;padding-top:0px;padding-bottom:1px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="transparent" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:10px 25px;background:transparent;" valign="middle">
|
||||
<a href="mailto:{{.Props.ActorEmail}}" style="display: inline-block; background: transparent; color: #1C58D9; font-family: Open Sans, sans-serif; font-size: 14px; font-weight: normal; line-height: 20px; margin: 0; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 4px; text-decoration: none;" target="_blank">
|
||||
{{.Props.SendAnEmailTo}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" class="divider" style="opacity: 12%; font-size: 0px; padding: 0; word-break: break-word;">
|
||||
<p style="border-top:solid 1px #3F4350;font-size:1px;margin:0px auto;width:313px;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #3F4350;font-size:1px;margin:0px auto;width:313px;" role="presentation" width="313px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}{{ if .Props.LogInToCustomerPortal}}
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:0px;padding-top:6px;padding-bottom:1px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="transparent" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:10px 25px;background:transparent;" valign="middle">
|
||||
<a href="{{.Props.PortalURL}}/console/cloud/ip-filtering" style="display: inline-block; background: transparent; color: #1C58D9; font-family: Open Sans, sans-serif; font-size: 14px; font-weight: normal; line-height: 20px; margin: 0; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 4px; text-decoration: none;" target="_blank">
|
||||
{{.Props.LogInToCustomerPortal}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" class="divider" style="opacity: 12%; font-size: 0px; padding: 0px; word-break: break-word;">
|
||||
<p style="border-top:solid 1px #3F4350;font-size:1px;margin:0px auto;width:313px;">
|
||||
</p>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #3F4350;font-size:1px;margin:0px auto;width:313px;" role="presentation" width="313px" ><tr><td style="height:0;line-height:0;">
|
||||
</td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:0px;padding-top:6px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="transparent" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:10px 25px;background:transparent;" valign="middle">
|
||||
<a href="mailto:{{.Props.SupportEmail}}" style="display: inline-block; background: transparent; color: #1C58D9; font-family: Open Sans, sans-serif; font-size: 14px; font-weight: normal; line-height: 20px; margin: 0; text-transform: none; padding: 10px 25px; mso-padding-alt: 0px; border-radius: 4px; text-decoration: none;" target="_blank">
|
||||
{{.Props.ContactSupport}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:552px;" width="552" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:552px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:552px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" class="emailFooter" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
<div style="font-family: Open Sans, sans-serif; text-align: center; font-size: 12px; line-height: 16px; color: rgba(63, 67, 80, 0.56); padding: 8px 24px 8px 24px;">{{.Props.Organization}}
|
||||
{{.Props.FooterV2}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
{{end}}
|
||||
40
server/templates/ip_filters_changed.mjml
Обычный файл
40
server/templates/ip_filters_changed.mjml
Обычный файл
@@ -0,0 +1,40 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="./partials/style.mjml" />
|
||||
</mj-head>
|
||||
<mj-body css-class="emailBody" background-color="#FFFFFF">
|
||||
<mj-wrapper mj-class="email">
|
||||
<mj-include path="./partials/logo.mjml" />
|
||||
<mj-include path="./partials/header.mjml" />
|
||||
<mj-section padding="0px">
|
||||
<mj-column>
|
||||
<mj-image src="{{.Props.SiteURL}}/static/images/forgot_password_illustration.png" width="312px"
|
||||
padding="0px" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding="40px 0px 40px 0px">
|
||||
<mj-column>
|
||||
<mj-text padding-bottom="9px" css-class="footerTitle" padding="0px">
|
||||
{{.Props.TroubleAccessingTitle}}
|
||||
</mj-text>
|
||||
<mj-raw>{{if .Props.ActorEmail}}</mj-raw>
|
||||
<mj-button padding-top="0px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.ActorEmail}}">
|
||||
{{.Props.SendAnEmailTo}}
|
||||
</mj-button>
|
||||
<mj-divider padding="0" css-class="divider" width="313px" border-width="1px" border-color="#3F4350"/>
|
||||
<mj-raw>{{end}}</mj-raw>
|
||||
<mj-raw>{{ if .Props.LogInToCustomerPortal}}</mj-raw>
|
||||
<mj-button padding-top="6px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="{{.Props.PortalURL}}/console/cloud/ip-filtering">
|
||||
{{.Props.LogInToCustomerPortal}}
|
||||
</mj-button>
|
||||
<mj-divider padding="0px" css-class="divider" width="313px" border-width="1px" border-color="#3F4350"/>
|
||||
<mj-raw>{{end}}</mj-raw>
|
||||
<mj-button padding-top="6px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.SupportEmail}}">
|
||||
{{.Props.ContactSupport}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-include path="./partials/email_footer.mjml" />
|
||||
</mj-wrapper>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -286,6 +286,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -306,6 +306,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -193,6 +193,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -296,6 +296,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -286,6 +286,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -286,6 +286,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
@@ -286,6 +286,10 @@
|
||||
padding: 0px 0px 0px 12px !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
opacity: 12%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 541px) {
|
||||
.emailBody {
|
||||
padding: 32px !important;
|
||||
|
||||
Ссылка в новой задаче
Block a user