[MM-29845] Add a new handler to allow authentication via CWS API Key (#16319)

* Add a new handler to allow authentication via CWS API Key

* Make error better

* Add tests and cases for new handler functions

* Move some code around

* Add test for GetCloudSession function

* unset the env after test completion

* Remove white space

* Change Info to Warn

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Nick Misasi
2020-11-23 14:34:10 -05:00
коммит произвёл GitHub
родитель 27462bbfc9
Коммит 3099128bbd
12 изменённых файлов: 144 добавлений и 4 удалений

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

@@ -51,6 +51,26 @@ func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.R
}
// CloudApiKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
func (api *API) CloudApiKeyRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
handler := &web.Handler{
GetGlobalAppOptions: api.GetGlobalAppOptions,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// ApiSessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed,
// if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
// authentication must be waived.

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

@@ -540,6 +540,7 @@ type AppIface interface {
GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError)
GetChannelsForUser(teamId string, userId string, includeDeleted bool, lastDeleteAt int) (*model.ChannelList, *model.AppError)
GetChannelsUserNotIn(teamId string, userId string, offset int, limit int) (*model.ChannelList, *model.AppError)
GetCloudSession(token string) (*model.Session, *model.AppError)
GetClusterId() string
GetClusterStatus() []*model.ClusterInfo
GetCommand(commandId string) (*model.Command, *model.AppError)

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

@@ -19,6 +19,7 @@ const (
TokenLocationHeader
TokenLocationCookie
TokenLocationQueryString
TokenLocationCloudHeader
)
func (tl TokenLocation) String() string {
@@ -31,6 +32,8 @@ func (tl TokenLocation) String() string {
return "Cookie"
case TokenLocationQueryString:
return "QueryString"
case TokenLocationCloudHeader:
return "CloudHeader"
default:
return "Unknown"
}
@@ -281,5 +284,9 @@ func ParseAuthTokenFromRequest(r *http.Request) (string, TokenLocation) {
return token, TokenLocationQueryString
}
if token := r.Header.Get(model.HEADER_CLOUD_TOKEN); token != "" {
return token, TokenLocationCloudHeader
}
return "", TokenLocationNotFound
}

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

@@ -26,6 +26,7 @@ func TestParseAuthTokenFromRequest(t *testing.T) {
{"BEARER mytoken", "", "", "mytoken", TokenLocationHeader},
{"", "mytoken", "", "mytoken", TokenLocationCookie},
{"", "", "mytoken", "mytoken", TokenLocationQueryString},
{"mytoken", "", "", "mytoken", TokenLocationCloudHeader},
}
for testnum, tc := range cases {
@@ -34,10 +35,12 @@ func TestParseAuthTokenFromRequest(t *testing.T) {
pathname += "?access_token=" + tc.query
}
req := httptest.NewRequest("GET", pathname, nil)
if tc.header != "" {
switch tc.expectedLocation {
case TokenLocationHeader:
req.Header.Add(model.HEADER_AUTH, tc.header)
}
if tc.cookie != "" {
case TokenLocationCloudHeader:
req.Header.Add(model.HEADER_CLOUD_TOKEN, tc.header)
case TokenLocationCookie:
req.AddCookie(&http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: tc.cookie,

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

@@ -4862,6 +4862,28 @@ func (a *OpenTracingAppLayer) GetChannelsUserNotIn(teamId string, userId string,
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetCloudSession(token string) (*model.Session, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCloudSession")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetCloudSession(token)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetClusterId() string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetClusterId")

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

@@ -7,6 +7,7 @@ import (
"errors"
"math"
"net/http"
"os"
"time"
"github.com/mattermost/mattermost-server/v5/audit"
@@ -34,6 +35,21 @@ func (a *App) CreateSession(session *model.Session) (*model.Session, *model.AppE
return session, nil
}
func (a *App) GetCloudSession(token string) (*model.Session, *model.AppError) {
apiKey := os.Getenv("MM_CLOUD_API_KEY")
if apiKey != "" && apiKey == token {
// Need a bare-bones session object for later checks
session := &model.Session{
Token: token,
IsOAuth: false,
}
session.AddProp(model.SESSION_PROP_TYPE, model.SESSION_TYPE_CLOUD_KEY)
return session, nil
}
return nil, model.NewAppError("GetCloudSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "The provided token is invalid", http.StatusUnauthorized)
}
func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
metrics := a.Metrics()

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

@@ -5,6 +5,7 @@ package app
import (
"fmt"
"os"
"testing"
"time"
@@ -404,3 +405,35 @@ func TestApp_SetSessionExpireInDays(t *testing.T) {
})
}
}
func TestGetCloudSession(t *testing.T) {
th := Setup(t)
defer func() {
os.Unsetenv("MM_CLOUD_API_KEY")
th.TearDown()
}()
t.Run("Matching environment variable and token should return non-nil session", func(t *testing.T) {
os.Setenv("MM_CLOUD_API_KEY", "mytoken")
session, err := th.App.GetCloudSession("mytoken")
require.Nil(t, err)
require.NotNil(t, session)
require.Equal(t, "mytoken", session.Token)
})
t.Run("Empty environment variable should return error", func(t *testing.T) {
os.Setenv("MM_CLOUD_API_KEY", "")
session, err := th.App.GetCloudSession("mytoken")
require.Nil(t, session)
require.NotNil(t, err)
require.Equal(t, "api.context.invalid_token.error", err.Id)
})
t.Run("Mismatched env variable and token should return error", func(t *testing.T) {
os.Setenv("MM_CLOUD_API_KEY", "mytoken")
session, err := th.App.GetCloudSession("myincorrecttoken")
require.Nil(t, session)
require.NotNil(t, err)
require.Equal(t, "api.context.invalid_token.error", err.Id)
})
}

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

@@ -31,6 +31,7 @@ const (
HEADER_CSRF_TOKEN = "X-CSRF-Token"
HEADER_BEARER = "BEARER"
HEADER_AUTH = "Authorization"
HEADER_CLOUD_TOKEN = "X-Cloud-Token"
HEADER_REQUESTED_WITH = "X-Requested-With"
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
STATUS = "status"

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

@@ -25,6 +25,7 @@ const (
SESSION_PROP_IS_BOT = "is_bot"
SESSION_PROP_IS_BOT_VALUE = "true"
SESSION_TYPE_USER_ACCESS_TOKEN = "UserAccessToken"
SESSION_TYPE_CLOUD_KEY = "CloudKey"
SESSION_PROP_IS_GUEST = "is_guest"
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes
SESSION_USER_ACCESS_TOKEN_EXPIRY = 100 * 365 // 100 years

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

@@ -143,6 +143,13 @@ func (c *Context) SessionRequired() {
}
}
func (c *Context) CloudKeyRequired() {
if license := c.App.Srv().License(); license == nil || !*license.Features.Cloud || c.App.Session().Props[model.SESSION_PROP_TYPE] != model.SESSION_TYPE_CLOUD_KEY {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "TokenRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
if license := c.App.Srv().License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {

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

@@ -32,6 +32,21 @@ func TestRequireHookId(t *testing.T) {
})
}
func TestCloudKeyRequired(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
c := &Context{
App: th.App,
}
c.CloudKeyRequired()
assert.Equal(t, c.Err.Id, "api.context.session_expired.app_error")
}
func TestMfaRequired(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()

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

@@ -72,6 +72,7 @@ type Handler struct {
HandleFunc func(*Context, http.ResponseWriter, *http.Request)
HandlerName string
RequireSession bool
RequireCloudKey bool
TrustRequester bool
RequireMfa bool
IsStatic bool
@@ -187,7 +188,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
token, tokenLocation := app.ParseAuthTokenFromRequest(r)
if len(token) != 0 {
if len(token) != 0 && tokenLocation != app.TokenLocationCloudHeader {
session, err := c.App.GetSession(token)
if err != nil {
c.Log.Info("Invalid session", mlog.Err(err))
@@ -209,6 +210,15 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
h.checkCSRFToken(c, r, token, tokenLocation, session)
} else if len(token) != 0 && c.App.Srv().License() != nil && *c.App.Srv().License().Features.Cloud && tokenLocation == app.TokenLocationCloudHeader {
// Check to see if this provided token matches our CWS Token
session, err := c.App.GetCloudSession(token)
if err != nil {
c.Log.Warn("Invalid CWS token", mlog.Err(err))
c.Err = err
} else {
c.App.SetSession(session)
}
}
c.Log = c.App.Log().With(
@@ -231,6 +241,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.SetServerBusyError()
}
if c.Err == nil && h.RequireCloudKey {
c.CloudKeyRequired()
}
if c.Err == nil && h.IsLocal {
// if the connection is local, RemoteAddr shouldn't have the
// shape IP:PORT (it will be "@" in Linux, for example)