MM-63327 Config setting for ServiceSettings.FrameAncestors (#30409)
* Add Embedding page to system console, with single setting for Frame Ancestors
Этот коммит содержится в:
@@ -25,10 +25,6 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
frameAncestors = "'self' teams.microsoft.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
|
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
|
||||||
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
|
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
|
||||||
pos := strings.LastIndex(handlerName, ".")
|
pos := strings.LastIndex(handlerName, ".")
|
||||||
@@ -242,8 +238,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
|
// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
|
||||||
w.Header().Set("Content-Security-Policy", fmt.Sprintf(
|
w.Header().Set("Content-Security-Policy", fmt.Sprintf(
|
||||||
"frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s",
|
"frame-ancestors 'self' %s; script-src 'self' cdn.rudderlabs.com%s%s",
|
||||||
frameAncestors,
|
*c.App.Config().ServiceSettings.FrameAncestors,
|
||||||
h.cspShaDirective,
|
h.cspShaDirective,
|
||||||
devCSP,
|
devCSP,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -343,10 +343,10 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("static, with subpath", func(t *testing.T) {
|
t.Run("static, with subpath and frame ancestors", func(t *testing.T) {
|
||||||
th := SetupWithStoreMock(t)
|
th := SetupWithStoreMock(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
@@ -367,6 +367,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath"
|
*cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath"
|
||||||
|
*cfg.ServiceSettings.FrameAncestors = "teams.microsoft.com *.cloud.microsoft"
|
||||||
})
|
})
|
||||||
|
|
||||||
web := New(th.Server)
|
web := New(th.Server)
|
||||||
@@ -384,7 +385,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
||||||
|
|
||||||
// TODO: It's hard to unit test this now that the CSP directive is effectively
|
// TODO: It's hard to unit test this now that the CSP directive is effectively
|
||||||
// decided in Setup(). Circle back to this in master once the memory store is
|
// decided in Setup(). Circle back to this in master once the memory store is
|
||||||
@@ -399,7 +400,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response = httptest.NewRecorder()
|
response = httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
||||||
// TODO: See above.
|
// TODO: See above.
|
||||||
// assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed")
|
// assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed")
|
||||||
})
|
})
|
||||||
@@ -429,7 +430,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.LogAudit("success")
|
c.LogAudit("success")
|
||||||
|
|
||||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||||
w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors %s", frameAncestors))
|
w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors 'self' %s", *c.App.Config().ServiceSettings.FrameAncestors))
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
|
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
|
||||||
|
|
||||||
|
|||||||
@@ -440,6 +440,7 @@ type ServiceSettings struct {
|
|||||||
MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
|
MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
|
||||||
ScheduledPosts *bool `access:"site_posts"`
|
ScheduledPosts *bool `access:"site_posts"`
|
||||||
EnableWebHubChannelIteration *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
|
EnableWebHubChannelIteration *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
|
FrameAncestors *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
}
|
}
|
||||||
|
|
||||||
var MattermostGiphySdkKey string
|
var MattermostGiphySdkKey string
|
||||||
@@ -967,6 +968,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
|
|||||||
if s.EnableWebHubChannelIteration == nil {
|
if s.EnableWebHubChannelIteration == nil {
|
||||||
s.EnableWebHubChannelIteration = NewPointer(false)
|
s.EnableWebHubChannelIteration = NewPointer(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.FrameAncestors == nil {
|
||||||
|
s.FrameAncestors = NewPointer("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheSettings struct {
|
type CacheSettings struct {
|
||||||
|
|||||||
@@ -5833,6 +5833,25 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
embedding: {
|
||||||
|
url: 'integrations/embedding',
|
||||||
|
title: defineMessage({id: 'admin.sidebar.embedding', defaultMessage: 'Embedding'}),
|
||||||
|
isHidden: it.not(it.userHasReadPermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.CORS)),
|
||||||
|
schema: {
|
||||||
|
id: 'EmbeddingSettings',
|
||||||
|
name: defineMessage({id: 'admin.integrations.embedding', defaultMessage: 'Embedding'}),
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
key: 'ServiceSettings.FrameAncestors',
|
||||||
|
label: defineMessage({id: 'admin.customization.frameAncestorTitle', defaultMessage: 'Frame Ancestors:'}),
|
||||||
|
help_text: defineMessage({id: 'admin.customization.frameAncestorDesc', defaultMessage: 'Allows the Mattermost web client to be embedded in other websites. Enter a space-separated list of domains that are allowed to embed the Mattermost web client. Leave blank to disallow embedding.'}),
|
||||||
|
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.CORS)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compliance: {
|
compliance: {
|
||||||
|
|||||||
@@ -1121,6 +1121,17 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<AdminSidebarSection
|
||||||
|
definitionKey="integrations.embedding"
|
||||||
|
key="integrations.embedding"
|
||||||
|
name="integrations/embedding"
|
||||||
|
title={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Embedding"
|
||||||
|
id="admin.sidebar.embedding"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
<AdminSidebarCategory
|
<AdminSidebarCategory
|
||||||
definitionKey="experimental"
|
definitionKey="experimental"
|
||||||
@@ -1815,6 +1826,17 @@ exports[`components/AdminSidebar should match snapshot with workspace optimizati
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<AdminSidebarSection
|
||||||
|
definitionKey="integrations.embedding"
|
||||||
|
key="integrations.embedding"
|
||||||
|
name="integrations/embedding"
|
||||||
|
title={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Embedding"
|
||||||
|
id="admin.sidebar.embedding"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
<AdminSidebarCategory
|
<AdminSidebarCategory
|
||||||
definitionKey="experimental"
|
definitionKey="experimental"
|
||||||
@@ -2564,6 +2586,17 @@ exports[`components/AdminSidebar should match snapshot, not prevent the console
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<AdminSidebarSection
|
||||||
|
definitionKey="integrations.embedding"
|
||||||
|
key="integrations.embedding"
|
||||||
|
name="integrations/embedding"
|
||||||
|
title={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Embedding"
|
||||||
|
id="admin.sidebar.embedding"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
<AdminSidebarCategory
|
<AdminSidebarCategory
|
||||||
definitionKey="experimental"
|
definitionKey="experimental"
|
||||||
@@ -3258,6 +3291,17 @@ exports[`components/AdminSidebar should match snapshot, render plugins without a
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<AdminSidebarSection
|
||||||
|
definitionKey="integrations.embedding"
|
||||||
|
key="integrations.embedding"
|
||||||
|
name="integrations/embedding"
|
||||||
|
title={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Embedding"
|
||||||
|
id="admin.sidebar.embedding"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
<AdminSidebarCategory
|
<AdminSidebarCategory
|
||||||
definitionKey="experimental"
|
definitionKey="experimental"
|
||||||
@@ -4062,6 +4106,17 @@ exports[`components/AdminSidebar should match snapshot, with license (with all f
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<AdminSidebarSection
|
||||||
|
definitionKey="integrations.embedding"
|
||||||
|
key="integrations.embedding"
|
||||||
|
name="integrations/embedding"
|
||||||
|
title={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Embedding"
|
||||||
|
id="admin.sidebar.embedding"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
<AdminSidebarCategory
|
<AdminSidebarCategory
|
||||||
definitionKey="compliance"
|
definitionKey="compliance"
|
||||||
@@ -4987,6 +5042,17 @@ exports[`components/AdminSidebar should match snapshot, with license (without an
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<AdminSidebarSection
|
||||||
|
definitionKey="integrations.embedding"
|
||||||
|
key="integrations.embedding"
|
||||||
|
name="integrations/embedding"
|
||||||
|
title={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Embedding"
|
||||||
|
id="admin.sidebar.embedding"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</AdminSidebarCategory>
|
</AdminSidebarCategory>
|
||||||
<AdminSidebarCategory
|
<AdminSidebarCategory
|
||||||
definitionKey="compliance"
|
definitionKey="compliance"
|
||||||
|
|||||||
@@ -680,6 +680,8 @@
|
|||||||
"admin.customization.enablePermalinkPreviewsTitle": "Enable message link previews:",
|
"admin.customization.enablePermalinkPreviewsTitle": "Enable message link previews:",
|
||||||
"admin.customization.enableSVGsDesc": "Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.",
|
"admin.customization.enableSVGsDesc": "Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.",
|
||||||
"admin.customization.enableSVGsTitle": "Enable SVGs:",
|
"admin.customization.enableSVGsTitle": "Enable SVGs:",
|
||||||
|
"admin.customization.frameAncestorDesc": "Allows the Mattermost web client to be embedded in other websites. Enter a space-separated list of domains that are allowed to embed the Mattermost web client. Leave blank to disallow embedding.",
|
||||||
|
"admin.customization.frameAncestorTitle": "Frame Ancestors:",
|
||||||
"admin.customization.iosAppDownloadLinkDesc": "Add a link to download the iOS app. Users who access the site on a mobile web browser will be prompted with a page giving them the option to download the app. Leave this field blank to prevent the page from appearing.",
|
"admin.customization.iosAppDownloadLinkDesc": "Add a link to download the iOS app. Users who access the site on a mobile web browser will be prompted with a page giving them the option to download the app. Leave this field blank to prevent the page from appearing.",
|
||||||
"admin.customization.iosAppDownloadLinkTitle": "iOS App Download Link:",
|
"admin.customization.iosAppDownloadLinkTitle": "iOS App Download Link:",
|
||||||
"admin.customization.maxMarkdownNodesDesc": "When rendering Markdown text in the mobile app, controls the maximum number of Markdown elements (eg. emojis, links, table cells, etc) that can be in a single piece of text. If set to 0, a default limit will be used.",
|
"admin.customization.maxMarkdownNodesDesc": "When rendering Markdown text in the mobile app, controls the maximum number of Markdown elements (eg. emojis, links, table cells, etc) that can be in a single piece of text. If set to 0, a default limit will be used.",
|
||||||
@@ -1211,6 +1213,7 @@
|
|||||||
"admin.integrations.botAccounts": "Bot Accounts",
|
"admin.integrations.botAccounts": "Bot Accounts",
|
||||||
"admin.integrations.botAccounts.title": "Bot Accounts",
|
"admin.integrations.botAccounts.title": "Bot Accounts",
|
||||||
"admin.integrations.cors": "CORS",
|
"admin.integrations.cors": "CORS",
|
||||||
|
"admin.integrations.embedding": "Embedding",
|
||||||
"admin.integrations.gif": "GIF",
|
"admin.integrations.gif": "GIF",
|
||||||
"admin.integrations.integrationManagement": "Integration Management",
|
"admin.integrations.integrationManagement": "Integration Management",
|
||||||
"admin.integrations.integrationManagement.title": "Integration Management",
|
"admin.integrations.integrationManagement.title": "Integration Management",
|
||||||
@@ -2441,6 +2444,7 @@
|
|||||||
"admin.sidebar.developer": "Developer",
|
"admin.sidebar.developer": "Developer",
|
||||||
"admin.sidebar.elasticsearch": "Elasticsearch",
|
"admin.sidebar.elasticsearch": "Elasticsearch",
|
||||||
"admin.sidebar.email": "Email",
|
"admin.sidebar.email": "Email",
|
||||||
|
"admin.sidebar.embedding": "Embedding",
|
||||||
"admin.sidebar.emoji": "Emoji",
|
"admin.sidebar.emoji": "Emoji",
|
||||||
"admin.sidebar.environment": "Environment",
|
"admin.sidebar.environment": "Environment",
|
||||||
"admin.sidebar.experimental": "Experimental",
|
"admin.sidebar.experimental": "Experimental",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user