[MM-12958] Support running two Mattermost instances on the same domain using subpaths (#10493)

Этот коммит содержится в:
d28park
2019-05-03 13:52:32 -07:00
коммит произвёл Hanzei
родитель e063b74337
Коммит 4552c20d5b
7 изменённых файлов: 246 добавлений и 58 удалений

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

@@ -14,6 +14,7 @@ import (
"github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
) )
func (api *API) InitUser() { func (api *API) InitUser() {
@@ -1430,11 +1431,13 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
secure = true secure = true
} }
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0) expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
sessionCookie := &http.Cookie{ sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN, Name: model.SESSION_COOKIE_TOKEN,
Value: c.App.Session.Token, Value: c.App.Session.Token,
Path: "/", Path: subpath,
MaxAge: maxAge, MaxAge: maxAge,
Expires: expiresAt, Expires: expiresAt,
HttpOnly: true, HttpOnly: true,

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

@@ -2427,28 +2427,49 @@ func TestAttachDeviceId(t *testing.T) {
defer th.TearDown() defer th.TearDown()
deviceId := model.PUSH_NOTIFY_APPLE + ":1234567890" deviceId := model.PUSH_NOTIFY_APPLE + ":1234567890"
t.Run("success", func(t *testing.T) {
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=[a-z0-9]+; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=[a-z0-9]+; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
pass, resp := th.Client.AttachDeviceId(deviceId) pass, resp := th.Client.AttachDeviceId(deviceId)
CheckNoError(t, resp) CheckNoError(t, resp)
if !pass { cookies := resp.Header.Get("Set-Cookie")
t.Fatal("should have passed") assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
} assert.True(t, pass)
if sessions, err := th.App.GetSessions(th.BasicUser.Id); err != nil { sessions, err := th.App.GetSessions(th.BasicUser.Id)
t.Fatal(err) require.Nil(t, err)
} else { assert.Equal(t, deviceId, sessions[0].DeviceId, "Missing device Id")
if sessions[0].DeviceId != deviceId { })
t.Fatal("Missing device Id")
}
} }
})
_, resp = th.Client.AttachDeviceId("") t.Run("invalid device id", func(t *testing.T) {
_, resp := th.Client.AttachDeviceId("")
CheckBadRequestStatus(t, resp) CheckBadRequestStatus(t, resp)
})
t.Run("not logged in", func(t *testing.T) {
th.Client.Logout() th.Client.Logout()
_, resp = th.Client.AttachDeviceId("") _, resp := th.Client.AttachDeviceId("")
CheckUnauthorizedStatus(t, resp) CheckUnauthorizedStatus(t, resp)
})
} }
func TestGetUserAudits(t *testing.T) { func TestGetUserAudits(t *testing.T) {
@@ -2673,6 +2694,36 @@ func TestLogin(t *testing.T) {
}) })
} }
func TestLoginCookies(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.Client.Logout()
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=[a-z0-9]+; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=[a-z0-9]+; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
user, resp := th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
CheckNoError(t, resp)
assert.Equal(t, user.Id, th.BasicUser.Id)
cookies := resp.Header.Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
})
}
}
func TestCBALogin(t *testing.T) { func TestCBALogin(t *testing.T) {
t.Run("primary", func(t *testing.T) { t.Run("primary", func(t *testing.T) {
th := Setup().InitBasic() th := Setup().InitBasic()

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

@@ -13,6 +13,7 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/plugin"
"github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store"
"github.com/mattermost/mattermost-server/utils"
) )
func (a *App) CheckForClientSideCert(r *http.Request) (string, string, string) { func (a *App) CheckForClientSideCert(r *http.Request) (string, string, string) {
@@ -164,11 +165,13 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
} }
domain := a.GetCookieDomain() domain := a.GetCookieDomain()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0) expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
sessionCookie := &http.Cookie{ sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN, Name: model.SESSION_COOKIE_TOKEN,
Value: session.Token, Value: session.Token,
Path: "/", Path: subpath,
MaxAge: maxAge, MaxAge: maxAge,
Expires: expiresAt, Expires: expiresAt,
HttpOnly: true, HttpOnly: true,
@@ -179,7 +182,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
userCookie := &http.Cookie{ userCookie := &http.Cookie{
Name: model.SESSION_COOKIE_USER, Name: model.SESSION_COOKIE_USER,
Value: user.Id, Value: user.Id,
Path: "/", Path: subpath,
MaxAge: maxAge, MaxAge: maxAge,
Expires: expiresAt, Expires: expiresAt,
Domain: domain, Domain: domain,
@@ -189,7 +192,7 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
csrfCookie := &http.Cookie{ csrfCookie := &http.Cookie{
Name: model.SESSION_COOKIE_CSRF, Name: model.SESSION_COOKIE_CSRF,
Value: session.GetCSRF(), Value: session.GetCSRF(),
Path: "/", Path: subpath,
MaxAge: maxAge, MaxAge: maxAge,
Expires: expiresAt, Expires: expiresAt,
Domain: domain, Domain: domain,

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

@@ -654,11 +654,13 @@ func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, servi
} }
cookieValue := model.NewId() cookieValue := model.NewId()
subpath, _ := utils.GetSubpathFromConfig(a.Config())
expiresAt := time.Unix(model.GetMillis()/1000+int64(OAUTH_COOKIE_MAX_AGE_SECONDS), 0) expiresAt := time.Unix(model.GetMillis()/1000+int64(OAUTH_COOKIE_MAX_AGE_SECONDS), 0)
oauthCookie := &http.Cookie{ oauthCookie := &http.Cookie{
Name: COOKIE_OAUTH, Name: COOKIE_OAUTH,
Value: cookieValue, Value: cookieValue,
Path: "/", Path: subpath,
MaxAge: OAUTH_COOKIE_MAX_AGE_SECONDS, MaxAge: OAUTH_COOKIE_MAX_AGE_SECONDS,
Expires: expiresAt, Expires: expiresAt,
HttpOnly: true, HttpOnly: true,
@@ -741,10 +743,12 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service
mlog.Error(appErr.Error()) mlog.Error(appErr.Error())
} }
subpath, _ := utils.GetSubpathFromConfig(a.Config())
httpCookie := &http.Cookie{ httpCookie := &http.Cookie{
Name: COOKIE_OAUTH, Name: COOKIE_OAUTH,
Value: "", Value: "",
Path: "/", Path: subpath,
MaxAge: -1, MaxAge: -1,
HttpOnly: true, HttpOnly: true,
} }

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

@@ -468,6 +468,17 @@ func TestAuthorizeOAuthUser(t *testing.T) {
}) })
t.Run("enabled and properly configured", func(t *testing.T) { t.Run("enabled and properly configured", func(t *testing.T) {
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMOAUTH=; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMOAUTH=; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
userData := "Hello, World!" userData := "Hello, World!"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -487,6 +498,10 @@ func TestAuthorizeOAuthUser(t *testing.T) {
th := setup(true, true, true, server.URL) th := setup(true, true, true, server.URL)
defer th.TearDown() defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
cookie := model.NewId() cookie := model.NewId()
request := makeRequest(t, cookie) request := makeRequest(t, cookie)
@@ -496,7 +511,8 @@ func TestAuthorizeOAuthUser(t *testing.T) {
} }
state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps))) state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
body, receivedTeamId, receivedStateProps, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") recorder := httptest.ResponseRecorder{}
body, receivedTeamId, receivedStateProps, err := th.App.AuthorizeOAuthUser(&recorder, request, model.SERVICE_GITLAB, "", state, "")
require.NotNil(t, body) require.NotNil(t, body)
bodyBytes, bodyErr := ioutil.ReadAll(body) bodyBytes, bodyErr := ioutil.ReadAll(body)
@@ -506,5 +522,66 @@ func TestAuthorizeOAuthUser(t *testing.T) {
assert.Equal(t, stateProps["team_id"], receivedTeamId) assert.Equal(t, stateProps["team_id"], receivedTeamId)
assert.Equal(t, stateProps, receivedStateProps) assert.Equal(t, stateProps, receivedStateProps)
assert.Nil(t, err) assert.Nil(t, err)
cookies := recorder.Header().Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
})
}
})
}
func TestGetAuthorizationCode(t *testing.T) {
t.Run("not enabled", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GitLabSettings.Enable = false
})
_, err := th.App.GetAuthorizationCode(nil, nil, model.SERVICE_GITLAB, map[string]string{}, "")
require.NotNil(t, err)
assert.Equal(t, "api.user.get_authorization_code.unsupported.app_error", err.Id)
})
t.Run("enabled and properly configured", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GitLabSettings.Enable = true
})
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMOAUTH=[a-z0-9]+; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMOAUTH=[a-z0-9]+; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
request, _ := http.NewRequest(http.MethodGet, "https://mattermost.example.com", nil)
stateProps := map[string]string{
"email": "email@example.com",
"action": "action",
}
recorder := httptest.ResponseRecorder{}
url, err := th.App.GetAuthorizationCode(&recorder, request, model.SERVICE_GITLAB, stateProps, "")
require.Nil(t, err)
assert.NotEmpty(t, url)
cookies := recorder.Header().Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
})
}
}) })
} }

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

@@ -138,10 +138,12 @@ func (c *Context) MfaRequired() {
} }
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN, Name: model.SESSION_COOKIE_TOKEN,
Value: "", Value: "",
Path: "/", Path: subpath,
MaxAge: -1, MaxAge: -1,
HttpOnly: true, HttpOnly: true,
} }

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

@@ -10,6 +10,7 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func handlerForHTTPErrors(c *Context, w http.ResponseWriter, r *http.Request) { func handlerForHTTPErrors(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -289,3 +290,50 @@ func TestHandlerServeCSPHeader(t *testing.T) {
// assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/ '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.segment.com/analytics.js/ 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed")
}) })
} }
func TestHandlerServeInvalidToken(t *testing.T) {
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
web := New(th.Server, th.Server.AppOptions, th.Server.Router)
handler := Handler{
GetGlobalAppOptions: web.GetGlobalAppOptions,
HandleFunc: handlerForCSRFToken,
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
}
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: "invalid",
}
request := httptest.NewRequest("POST", "/api/v4/test", nil)
request.AddCookie(cookie)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
require.Equal(t, http.StatusUnauthorized, response.Code)
cookies := response.Header().Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
})
}
}