[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"
pass, resp := th.Client.AttachDeviceId(deviceId)
CheckNoError(t, resp)
if !pass { t.Run("success", func(t *testing.T) {
t.Fatal("should have passed") testCases := []struct {
} Description string
SiteURL string
if sessions, err := th.App.GetSessions(th.BasicUser.Id); err != nil { ExpectedSetCookieHeaderRegexp string
t.Fatal(err) }{
} else { {"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=[a-z0-9]+; Path=/"},
if sessions[0].DeviceId != deviceId { {"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=[a-z0-9]+; Path=/subpath"},
t.Fatal("Missing device Id")
} }
}
_, resp = th.Client.AttachDeviceId("") for _, tc := range testCases {
CheckBadRequestStatus(t, resp) t.Run(tc.Description, func(t *testing.T) {
th.Client.Logout() th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
_, resp = th.Client.AttachDeviceId("") pass, resp := th.Client.AttachDeviceId(deviceId)
CheckUnauthorizedStatus(t, resp) CheckNoError(t, resp)
cookies := resp.Header.Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
assert.True(t, pass)
sessions, err := th.App.GetSessions(th.BasicUser.Id)
require.Nil(t, err)
assert.Equal(t, deviceId, sessions[0].DeviceId, "Missing device Id")
})
}
})
t.Run("invalid device id", func(t *testing.T) {
_, resp := th.Client.AttachDeviceId("")
CheckBadRequestStatus(t, resp)
})
t.Run("not logged in", func(t *testing.T) {
th.Client.Logout()
_, resp := th.Client.AttachDeviceId("")
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,43 +468,120 @@ 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) {
userData := "Hello, World!" testCases := []struct {
Description string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { SiteURL string
switch r.URL.Path { ExpectedSetCookieHeaderRegexp string
case "/token": }{
json.NewEncoder(w).Encode(&model.AccessResponse{ {"no subpath", "http://localhost:8065", "^MMOAUTH=; Path=/"},
AccessToken: model.NewId(), {"subpath", "http://localhost:8065/subpath", "^MMOAUTH=; Path=/subpath"},
TokenType: model.ACCESS_TOKEN_TYPE,
})
case "/user":
w.WriteHeader(http.StatusOK)
w.Write([]byte(userData))
}
}))
defer server.Close()
th := setup(true, true, true, server.URL)
defer th.TearDown()
cookie := model.NewId()
request := makeRequest(t, cookie)
stateProps := map[string]string{
"team_id": model.NewId(),
"token": makeToken(th, cookie).Token,
} }
state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
body, receivedTeamId, receivedStateProps, err := th.App.AuthorizeOAuthUser(&httptest.ResponseRecorder{}, request, model.SERVICE_GITLAB, "", state, "") for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
userData := "Hello, World!"
require.NotNil(t, body) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
bodyBytes, bodyErr := ioutil.ReadAll(body) switch r.URL.Path {
require.Nil(t, bodyErr) case "/token":
assert.Equal(t, userData, string(bodyBytes)) json.NewEncoder(w).Encode(&model.AccessResponse{
AccessToken: model.NewId(),
TokenType: model.ACCESS_TOKEN_TYPE,
})
case "/user":
w.WriteHeader(http.StatusOK)
w.Write([]byte(userData))
}
}))
defer server.Close()
assert.Equal(t, stateProps["team_id"], receivedTeamId) th := setup(true, true, true, server.URL)
assert.Equal(t, stateProps, receivedStateProps) defer th.TearDown()
assert.Nil(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
cookie := model.NewId()
request := makeRequest(t, cookie)
stateProps := map[string]string{
"team_id": model.NewId(),
"token": makeToken(th, cookie).Token,
}
state := base64.StdEncoding.EncodeToString([]byte(model.MapToJson(stateProps)))
recorder := httptest.ResponseRecorder{}
body, receivedTeamId, receivedStateProps, err := th.App.AuthorizeOAuthUser(&recorder, request, model.SERVICE_GITLAB, "", state, "")
require.NotNil(t, body)
bodyBytes, bodyErr := ioutil.ReadAll(body)
require.Nil(t, bodyErr)
assert.Equal(t, userData, string(bodyBytes))
assert.Equal(t, stateProps["team_id"], receivedTeamId)
assert.Equal(t, stateProps, receivedStateProps)
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)
})
}
}