[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>
Этот коммит содержится в:
@@ -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)
|
||||
|
||||
Ссылка в новой задаче
Block a user