diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go
index 9d3531f1e05..6314ce629f5 100644
--- a/server/channels/web/handlers.go
+++ b/server/channels/web/handlers.go
@@ -25,10 +25,6 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/utils"
)
-const (
- frameAncestors = "'self' teams.microsoft.com"
-)
-
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
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.
w.Header().Set("Content-Security-Policy", fmt.Sprintf(
- "frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s",
- frameAncestors,
+ "frame-ancestors 'self' %s; script-src 'self' cdn.rudderlabs.com%s%s",
+ *c.App.Config().ServiceSettings.FrameAncestors,
h.cspShaDirective,
devCSP,
))
diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go
index 37fe2b21b38..97a8722b853 100644
--- a/server/channels/web/handlers_test.go
+++ b/server/channels/web/handlers_test.go
@@ -343,10 +343,10 @@ func TestHandlerServeCSPHeader(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
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)
defer th.TearDown()
@@ -367,6 +367,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath"
+ *cfg.ServiceSettings.FrameAncestors = "teams.microsoft.com *.cloud.microsoft"
})
web := New(th.Server)
@@ -384,7 +385,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
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
// 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()
handler.ServeHTTP(response, request)
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.
// 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()
handler.ServeHTTP(response, request)
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"])
})
}
diff --git a/server/channels/web/oauth.go b/server/channels/web/oauth.go
index 077baf0c5a7..b0a22413286 100644
--- a/server/channels/web/oauth.go
+++ b/server/channels/web/oauth.go
@@ -195,7 +195,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("success")
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("Cache-Control", "no-cache, max-age=31556926")
diff --git a/server/public/model/config.go b/server/public/model/config.go
index 43700a026cb..05ddb3114d8 100644
--- a/server/public/model/config.go
+++ b/server/public/model/config.go
@@ -440,6 +440,7 @@ type ServiceSettings struct {
MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ScheduledPosts *bool `access:"site_posts"`
EnableWebHubChannelIteration *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
+ FrameAncestors *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none
}
var MattermostGiphySdkKey string
@@ -967,6 +968,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
if s.EnableWebHubChannelIteration == nil {
s.EnableWebHubChannelIteration = NewPointer(false)
}
+
+ if s.FrameAncestors == nil {
+ s.FrameAncestors = NewPointer("")
+ }
}
type CacheSettings struct {
diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx
index ac5f0fed6d1..8178c41fbbc 100644
--- a/webapp/channels/src/components/admin_console/admin_definition.tsx
+++ b/webapp/channels/src/components/admin_console/admin_definition.tsx
@@ -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: {
diff --git a/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap b/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap
index 9bccb1107ce..961d99e17fe 100644
--- a/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/admin_sidebar/__snapshots__/admin_sidebar.test.tsx.snap
@@ -1121,6 +1121,17 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
/>
}
/>
+
+ }
+ />
}
/>
+
+ }
+ />
}
/>
+
+ }
+ />
}
/>
+
+ }
+ />
}
/>
+
+ }
+ />
}
/>
+
+ }
+ />