MM-53147 Support for embedding Mattermost in an MSTeams iframe (#23776)
* add teams to allowed frame-ancestors * fix unit tests * set SameSite attribute for session cookie * further restrict ancestors * skip landing page if in iframe * Only set cookie SameSite=None if embedded in iframe * don't set MMEMBED cookie on landing page (check only) * fully parse MMEMBED cookie * add comment * more comments --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -2241,6 +2241,10 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
Secure: secure,
|
||||
}
|
||||
|
||||
if secure && utils.CheckEmbeddedCookie(r) {
|
||||
sessionCookie.SameSite = http.SameSiteNoneMode
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessionCookie)
|
||||
|
||||
if err := c.App.AttachDeviceId(c.AppContext.Session().Id, deviceId, c.AppContext.Session().ExpiresAt); err != nil {
|
||||
|
||||
@@ -323,6 +323,12 @@ func (a *App) AttachSessionCookies(c *request.Context, w http.ResponseWriter, r
|
||||
Secure: secure,
|
||||
}
|
||||
|
||||
if secure && utils.CheckEmbeddedCookie(r) {
|
||||
sessionCookie.SameSite = http.SameSiteNoneMode
|
||||
userCookie.SameSite = http.SameSiteNoneMode
|
||||
csrfCookie.SameSite = http.SameSiteNoneMode
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessionCookie)
|
||||
http.SetCookie(w, userCookie)
|
||||
http.SetCookie(w, csrfCookie)
|
||||
|
||||
@@ -41,6 +41,16 @@ func OriginChecker(allowedOrigins string) func(*http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckEmbeddedCookie returns true if the MMEMBED cookie is set to 1.
|
||||
// MMEMBED is set via any plugin that facilitates Mattermost embedded in an iframe (e.g. mattermost-plugin-msteams-sync).
|
||||
func CheckEmbeddedCookie(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("MMEMBED")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return cookie.Value == "1"
|
||||
}
|
||||
|
||||
func RenderWebAppError(config *model.Config, w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) {
|
||||
RenderWebError(config, w, r, err.StatusCode, url.Values{
|
||||
"message": []string{err.Message},
|
||||
|
||||
@@ -30,6 +30,10 @@ import (
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/tracing"
|
||||
)
|
||||
|
||||
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, ".")
|
||||
@@ -241,7 +245,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 'self'; script-src 'self' cdn.rudderlabs.com%s%s%s",
|
||||
"frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s%s",
|
||||
frameAncestors,
|
||||
cloudCSP,
|
||||
h.cspShaDirective,
|
||||
devCSP,
|
||||
|
||||
@@ -340,7 +340,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||
})
|
||||
|
||||
t.Run("static, without subpath or SelfHostedPurchase, does not allow Stripe in CSP", func(t *testing.T) {
|
||||
@@ -363,7 +363,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 'self'; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
||||
})
|
||||
|
||||
t.Run("static, with subpath", func(t *testing.T) {
|
||||
@@ -404,7 +404,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, 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
|
||||
@@ -419,7 +419,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, 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")
|
||||
})
|
||||
@@ -449,7 +449,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 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"])
|
||||
assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"])
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -177,7 +178,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||
w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
|
||||
w.Header().Set("Content-Security-Policy", fmt.Sprintf("frame-ancestors %s", frameAncestors))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, max-age=31556926")
|
||||
|
||||
|
||||
@@ -79,6 +79,20 @@ export default class LinkingLandingPage extends PureComponent<Props, State> {
|
||||
return landingPreference && landingPreference === LandingPreferenceTypes.BROWSER;
|
||||
};
|
||||
|
||||
isEmbedded = () => {
|
||||
// this cookie is set by any plugin that facilitates iframe embedding (e.g. mattermost-plugin-msteams-sync).
|
||||
const cookieName = 'MMEMBED';
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.startsWith(cookieName + '=')) {
|
||||
const value = cookie.substring(cookieName.length + 1);
|
||||
return decodeURIComponent(value) === '1';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
checkLandingPreferenceApp = () => {
|
||||
const landingPreference = BrowserStore.getLandingPreference(this.props.siteUrl);
|
||||
return landingPreference && landingPreference === LandingPreferenceTypes.MATTERMOSTAPP;
|
||||
@@ -435,7 +449,7 @@ export default class LinkingLandingPage extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const isMobile = UserAgent.isMobile();
|
||||
|
||||
if (this.checkLandingPreferenceBrowser()) {
|
||||
if (this.checkLandingPreferenceBrowser() || this.isEmbedded()) {
|
||||
this.openInBrowser();
|
||||
return null;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user