Merge branch 'master' into azure-monitor

This commit is contained in:
Daniel Lee
2019-01-28 19:23:34 +01:00
378 changed files with 8050 additions and 3821 deletions
+6 -6
View File
@@ -23,9 +23,9 @@ func (hs *HTTPServer) registerRoutes() {
// not logged in views
r.Get("/", reqSignedIn, hs.Index)
r.Get("/logout", Logout)
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
r.Get("/login/:name", quota("session"), OAuthLogin)
r.Get("/logout", hs.Logout)
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(hs.LoginPost))
r.Get("/login/:name", quota("session"), hs.OAuthLogin)
r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index)
@@ -84,11 +84,11 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/signup", hs.Index)
r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(hs.SignUpStep2))
// invited
r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(hs.CompleteInvite))
// reset password
r.Get("/user/password/send-reset-email", hs.Index)
@@ -109,7 +109,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
// api renew session based on remember cookie
r.Get("/api/login/ping", quota("session"), LoginAPIPing)
r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
// authed api
r.Group("/api", func(apiRoute routing.RouteRegister) {
+32 -10
View File
@@ -5,7 +5,6 @@ import (
"net/http/httptest"
"path/filepath"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@@ -95,13 +94,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
}
type scenarioContext struct {
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
userAuthTokenService *fakeUserAuthTokenService
}
func (sc *scenarioContext) exec() {
@@ -123,8 +123,30 @@ func setupScenarioContext(url string) *scenarioContext {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService))
return sc
}
type fakeUserAuthTokenService struct {
initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
}
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
return &fakeUserAuthTokenService{
initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
return false
},
}
}
func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
return s.initContextWithTokenProvider(ctx, orgID)
}
func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
return nil
}
func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
+2
View File
@@ -165,6 +165,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
"externalUserMngInfo": setting.ExternalUserMngInfo,
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
"viewersCanEdit": setting.ViewersCanEdit,
"disableSanitizeHtml": hs.Cfg.DisableSanitizeHtml,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
+17 -16
View File
@@ -11,14 +11,8 @@ import (
"path"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/live"
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -27,11 +21,16 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1"
)
func init() {
@@ -49,13 +48,14 @@ type HTTPServer struct {
streamManager *live.StreamManager
httpSrv *http.Server
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
AuthTokenService auth.UserAuthTokenService `inject:""`
}
func (hs *HTTPServer) Init() error {
@@ -65,6 +65,8 @@ func (hs *HTTPServer) Init() error {
hs.macaron = hs.newMacaron()
hs.registerRoutes()
session.Init(&setting.SessionOptions, setting.SessionConnMaxLifetime)
return nil
}
@@ -223,8 +225,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.healthHandler)
m.Use(hs.metricsEndpoint)
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
m.Use(middleware.GetContextHandler(hs.AuthTokenService))
m.Use(middleware.OrgRedirect())
// needs to be after context handler
+1 -1
View File
@@ -140,7 +140,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
Children: dashboardChildNavs,
})
if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || setting.ViewersCanEdit) {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Explore",
Id: "explore",
+59 -67
View File
@@ -1,6 +1,8 @@
package api
import (
"encoding/hex"
"net/http"
"net/url"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -9,12 +11,13 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
ViewIndex = "index"
ViewIndex = "index"
LoginErrorCookieName = "login_error"
)
func (hs *HTTPServer) LoginView(c *m.ReqContext) {
@@ -34,8 +37,8 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
if loginError, ok := c.Session.Get("loginError").(string); ok {
c.Session.Delete("loginError")
if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
deleteCookie(c, LoginErrorCookieName)
viewData.Settings["loginError"] = loginError
}
@@ -43,7 +46,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
return
}
if !tryLoginUsingRememberCookie(c) {
if !c.IsSignedIn {
c.HTML(200, ViewIndex, viewData)
return
}
@@ -75,56 +78,15 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool {
return false
}
func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
// Check auto-login.
uname := c.GetCookie(setting.CookieUserName)
if len(uname) == 0 {
return false
func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response {
if c.IsSignedIn || c.IsAnonymous {
return JSON(200, "Logged in")
}
isSucceed := false
defer func() {
if !isSucceed {
log.Trace("auto-login cookie cleared: %s", uname)
c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
return
}
}()
userQuery := m.GetUserByLoginQuery{LoginOrEmail: uname}
if err := bus.Dispatch(&userQuery); err != nil {
return false
}
user := userQuery.Result
// validate remember me cookie
signingKey := user.Rands + user.Password
if len(signingKey) < 10 {
c.Logger.Error("Invalid user signingKey")
return false
}
if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
return false
}
isSucceed = true
loginUserWithUser(user, c)
return true
return Error(401, "Unauthorized", nil)
}
func LoginAPIPing(c *m.ReqContext) {
if !tryLoginUsingRememberCookie(c) {
c.JsonApiErr(401, "Unauthorized", nil)
return
}
c.JsonOK("Logged in")
}
func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
if setting.DisableLoginForm {
return Error(401, "Login is disabled", nil)
}
@@ -146,7 +108,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
user := authQuery.User
loginUserWithUser(user, c)
hs.loginUserWithUser(user, c)
result := map[string]interface{}{
"message": "Logged in",
@@ -162,30 +124,60 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
return JSON(200, result)
}
func loginUserWithUser(user *m.User, c *m.ReqContext) {
func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
if user == nil {
log.Error(3, "User login with nil user")
hs.log.Error("User login with nil user")
}
c.Resp.Header().Del("Set-Cookie")
days := 86400 * setting.LogInRememberDays
if days > 0 {
c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/")
c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
err := hs.AuthTokenService.UserAuthenticatedHook(user, c)
if err != nil {
hs.log.Error("User auth hook failed", "error", err)
}
c.Session.RegenerateId(c.Context)
c.Session.Set(session.SESS_KEY_USERID, user.Id)
}
func Logout(c *m.ReqContext) {
c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
c.Session.Destory(c.Context)
func (hs *HTTPServer) Logout(c *m.ReqContext) {
hs.AuthTokenService.UserSignedOutHook(c)
if setting.SignoutRedirectUrl != "" {
c.Redirect(setting.SignoutRedirectUrl)
} else {
c.Redirect(setting.AppSubUrl + "/login")
}
}
func tryGetEncryptedCookie(ctx *m.ReqContext, cookieName string) (string, bool) {
cookie := ctx.GetCookie(cookieName)
if cookie == "" {
return "", false
}
decoded, err := hex.DecodeString(cookie)
if err != nil {
return "", false
}
decryptedError, err := util.Decrypt([]byte(decoded), setting.SecretKey)
return string(decryptedError), err == nil
}
func deleteCookie(ctx *m.ReqContext, cookieName string) {
ctx.SetCookie(cookieName, "", -1, setting.AppSubUrl+"/")
}
func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string, value string, maxAge int) error {
encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey)
if err != nil {
return err
}
http.SetCookie(ctx.Resp, &http.Cookie{
Name: cookieName,
MaxAge: 60,
Value: hex.EncodeToString(encryptedError),
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.SecurityHTTPSCookies,
})
return nil
}
+49 -16
View File
@@ -3,9 +3,11 @@ package api
import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
@@ -18,12 +20,14 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
var oauthLogger = log.New("oauth")
var (
oauthLogger = log.New("oauth")
OauthStateCookieName = "oauth_state"
)
func GenStateString() string {
rnd := make([]byte, 32)
@@ -31,7 +35,7 @@ func GenStateString() string {
return base64.URLEncoding.EncodeToString(rnd)
}
func OAuthLogin(ctx *m.ReqContext) {
func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
if setting.OAuthService == nil {
ctx.Handle(404, "OAuth not enabled", nil)
return
@@ -48,14 +52,15 @@ func OAuthLogin(ctx *m.ReqContext) {
if errorParam != "" {
errorDesc := ctx.Query("error_description")
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
hs.redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
return
}
code := ctx.Query("code")
if code == "" {
state := GenStateString()
ctx.Session.Set(session.SESS_KEY_OAUTH_STATE, state)
hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60)
if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
} else {
@@ -64,14 +69,20 @@ func OAuthLogin(ctx *m.ReqContext) {
return
}
savedState, ok := ctx.Session.Get(session.SESS_KEY_OAUTH_STATE).(string)
if !ok {
cookieState := ctx.GetCookie(OauthStateCookieName)
// delete cookie
ctx.Resp.Header().Del("Set-Cookie")
hs.deleteCookie(ctx.Resp, OauthStateCookieName)
if cookieState == "" {
ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
return
}
queryState := ctx.Query("state")
if savedState != queryState {
queryState := hashStatecode(ctx.Query("state"), setting.OAuthService.OAuthInfos[name].ClientSecret)
oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState)
if cookieState != queryState {
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
return
}
@@ -131,7 +142,7 @@ func OAuthLogin(ctx *m.ReqContext) {
userInfo, err := connect.UserInfo(client, token)
if err != nil {
if sErr, ok := err.(*social.Error); ok {
redirectWithError(ctx, sErr)
hs.redirectWithError(ctx, sErr)
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
@@ -142,13 +153,13 @@ func OAuthLogin(ctx *m.ReqContext) {
// validate that we got at least an email address
if userInfo.Email == "" {
redirectWithError(ctx, login.ErrNoEmail)
hs.redirectWithError(ctx, login.ErrNoEmail)
return
}
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
redirectWithError(ctx, login.ErrEmailNotAllowed)
hs.redirectWithError(ctx, login.ErrEmailNotAllowed)
return
}
@@ -171,14 +182,15 @@ func OAuthLogin(ctx *m.ReqContext) {
ExternalUser: extUser,
SignupAllowed: connect.IsSignupAllowed(),
}
err = bus.Dispatch(cmd)
if err != nil {
redirectWithError(ctx, err)
hs.redirectWithError(ctx, err)
return
}
// login
loginUserWithUser(cmd.Result, ctx)
hs.loginUserWithUser(cmd.Result, ctx)
metrics.M_Api_Login_OAuth.Inc()
@@ -191,8 +203,29 @@ func OAuthLogin(ctx *m.ReqContext) {
ctx.Redirect(setting.AppSubUrl + "/")
}
func redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
func (hs *HTTPServer) deleteCookie(w http.ResponseWriter, name string) {
hs.writeCookie(w, name, "", -1)
}
func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value string, maxAge int) {
http.SetCookie(w, &http.Cookie{
Name: name,
MaxAge: maxAge,
Value: value,
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.SecurityHTTPSCookies,
})
}
func hashStatecode(code, seed string) string {
hashBytes := sha256.Sum256([]byte(code + setting.SecretKey + seed))
return hex.EncodeToString(hashBytes[:])
}
func (hs *HTTPServer) redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
ctx.Logger.Error(err.Error(), v...)
ctx.Session.Set("loginError", err.Error())
hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60)
ctx.Redirect(setting.AppSubUrl + "/login")
}
+2 -2
View File
@@ -148,7 +148,7 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
})
}
func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
func (hs *HTTPServer) CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
if err := bus.Dispatch(&query); err != nil {
@@ -186,7 +186,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
return rsp
}
loginUserWithUser(user, c)
hs.loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc()
metrics.M_Api_User_SignUpInvite.Inc()
+1 -1
View File
@@ -54,7 +54,7 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
func newHTTPClient() httpClient {
return &http.Client{
Timeout: time.Second * 30,
Timeout: time.Duration(setting.DataProxyTimeout) * time.Second,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
}
+2 -2
View File
@@ -51,7 +51,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
return JSON(200, util.DynMap{"status": "SignUpCreated"})
}
func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
func (hs *HTTPServer) SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
if !setting.AllowUserSignUp {
return Error(401, "User signup is disabled", nil)
}
@@ -109,7 +109,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
apiResponse["code"] = "redirect-to-select-org"
}
loginUserWithUser(user, c)
hs.loginUserWithUser(user, c)
metrics.M_Api_User_SignUpCompleted.Inc()
return JSON(200, apiResponse)
-11
View File
@@ -7,7 +7,6 @@ import (
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -17,16 +16,6 @@ type AuthOptions struct {
ReqSignedIn bool
}
func getRequestUserId(c *m.ReqContext) int64 {
userID := c.Session.Get(session.SESS_KEY_USERID)
if userID != nil {
return userID.(int64)
}
return 0
}
func getApiKey(c *m.ReqContext) string {
header := c.Req.Header.Get("Authorization")
parts := strings.SplitN(header, " ", 2)
+19 -1
View File
@@ -16,7 +16,9 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
var (
AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
)
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
if !setting.AuthProxyEnabled {
@@ -40,6 +42,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
return false
}
defer func() {
if err := ctx.Session.Release(); err != nil {
ctx.Logger.Error("failed to save session data", "error", err)
}
}()
query := &m.GetSignedInUserQuery{OrgId: orgID}
// if this session has already been authenticated by authProxy just load the user
@@ -192,6 +200,16 @@ var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
return nil
}
func getRequestUserId(c *m.ReqContext) int64 {
userID := c.Session.Get(session.SESS_KEY_USERID)
if userID != nil {
return userID.(int64)
}
return 0
}
func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
return nil
+5 -28
View File
@@ -3,15 +3,15 @@ package middleware
import (
"strconv"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1"
)
var (
@@ -21,12 +21,12 @@ var (
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
)
func GetContextHandler() macaron.Handler {
func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
return func(c *macaron.Context) {
ctx := &m.ReqContext{
Context: c,
SignedInUser: &m.SignedInUser{},
Session: session.GetSession(),
Session: session.GetSession(), // should only be used by auth_proxy
IsSignedIn: false,
AllowAnonymous: false,
SkipCache: false,
@@ -49,7 +49,7 @@ func GetContextHandler() macaron.Handler {
case initContextWithApiKey(ctx):
case initContextWithBasicAuth(ctx, orgId):
case initContextWithAuthProxy(ctx, orgId):
case initContextWithUserSessionCookie(ctx, orgId):
case ats.InitContextWithToken(ctx, orgId):
case initContextWithAnonymousUser(ctx):
}
@@ -88,29 +88,6 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
return true
}
func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool {
// initialize session
if err := ctx.Session.Start(ctx.Context); err != nil {
ctx.Logger.Error("Failed to start session", "error", err)
return false
}
var userId int64
if userId = getRequestUserId(ctx); userId == 0 {
return false
}
query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("Failed to get user with id", "userId", userId, "error", err)
return false
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
func initContextWithApiKey(ctx *m.ReqContext) bool {
var keyString string
if keyString = getApiKey(ctx); keyString == "" {
+48 -30
View File
@@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"
ms "github.com/go-macaron/session"
msession "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
@@ -43,11 +43,6 @@ func TestMiddlewareContext(t *testing.T) {
So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
})
middlewareScenario("Non api request should init session", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").exec()
So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess")
})
middlewareScenario("Invalid api key", func(sc *scenarioContext) {
sc.apiKey = "invalid_key_test"
sc.fakeReq("GET", "/").exec()
@@ -151,22 +146,17 @@ func TestMiddlewareContext(t *testing.T) {
})
})
middlewareScenario("UserId in session", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
middlewareScenario("Auth token service", func(sc *scenarioContext) {
var wasCalled bool
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
wasCalled = true
return false
}
sc.fakeReq("GET", "/").exec()
Convey("should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
Convey("should call middleware", func() {
So(wasCalled, ShouldBeTrue)
})
})
@@ -211,6 +201,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
setting.SessionOptions = msession.Options{}
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.exec()
@@ -479,6 +470,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
defer bus.ClearBusHandlers()
sc := &scenarioContext{}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
@@ -487,10 +479,13 @@ func middlewareScenario(desc string, fn scenarioFunc) {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(GetContextHandler())
session.Init(&msession.Options{}, 0)
sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
sc.m.Use(Sessioner(&ms.Options{}, 0))
setting.SessionOptions = msession.Options{}
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
@@ -508,15 +503,16 @@ func middlewareScenario(desc string, fn scenarioFunc) {
}
type scenarioContext struct {
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
apiKey string
authHeader string
respJson map[string]interface{}
handlerFunc handlerFunc
defaultHandler macaron.Handler
url string
m *macaron.Macaron
context *m.ReqContext
resp *httptest.ResponseRecorder
apiKey string
authHeader string
respJson map[string]interface{}
handlerFunc handlerFunc
defaultHandler macaron.Handler
url string
userAuthTokenService *fakeUserAuthTokenService
req *http.Request
}
@@ -585,3 +581,25 @@ func (sc *scenarioContext) exec() {
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *m.ReqContext)
type fakeUserAuthTokenService struct {
initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
}
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
return &fakeUserAuthTokenService{
initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
return false
},
}
}
func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
return s.initContextWithTokenProvider(ctx, orgID)
}
func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
return nil
}
func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
-1
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
)
+11 -13
View File
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
. "github.com/smartystreets/goconvey/convey"
)
@@ -15,18 +14,15 @@ func TestOrgRedirectMiddleware(t *testing.T) {
Convey("Can redirect to correct org", t, func() {
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return nil
})
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
ctx.IsSignedIn = true
return true
}
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
@@ -37,14 +33,16 @@ func TestOrgRedirectMiddleware(t *testing.T) {
})
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
ctx.IsSignedIn = true
return true
}
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
return nil
+5 -8
View File
@@ -74,15 +74,12 @@ func TestMiddlewareQuota(t *testing.T) {
})
middlewareScenario("with user logged in", func(sc *scenarioContext) {
// log us in, so we have a user_id and org_id in the context
sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
c.Session.Set(session.SESS_KEY_USERID, int64(12))
}).exec()
sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12}
ctx.IsSignedIn = true
return true
}
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
query.Result = &m.GlobalQuotaDTO{
Target: query.Target,
+3 -4
View File
@@ -4,13 +4,12 @@ import (
"path/filepath"
"testing"
ms "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
macaron "gopkg.in/macaron.v1"
)
func TestRecoveryMiddleware(t *testing.T) {
@@ -64,10 +63,10 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(GetContextHandler())
sc.userAuthTokenService = newFakeUserAuthTokenService()
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
// mock out gc goroutine
session.StartSessionGC = func() {}
sc.m.Use(Sessioner(&ms.Options{}, 0))
sc.m.Use(OrgRedirect())
sc.m.Use(AddDefaultResponseHeaders())
-21
View File
@@ -1,21 +0,0 @@
package middleware
import (
ms "github.com/go-macaron/session"
"gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
)
func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler {
session.Init(options, sessionConnMaxLifetime)
return func(ctx *m.ReqContext) {
ctx.Next()
if err := ctx.Session.Release(); err != nil {
panic("session(release): " + err.Error())
}
}
}
+3 -3
View File
@@ -3,18 +3,18 @@ package models
import (
"strings"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/macaron.v1"
)
type ReqContext struct {
*macaron.Context
*SignedInUser
// This should only be used by the auth_proxy
Session session.SessionStore
IsSignedIn bool
+13 -3
View File
@@ -105,8 +105,9 @@ func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
var (
unfinishedWorkTimeout = time.Second * 5
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
alertTimeout = time.Second * 30
alertMaxAttempts = 3
alertTimeout = time.Second * 30
resultHandleTimeout = time.Second * 30
alertMaxAttempts = 3
)
func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
@@ -116,7 +117,7 @@ func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *J
}
}()
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
cancelChan := make(chan context.CancelFunc, alertMaxAttempts*2)
attemptChan := make(chan int, 1)
// Initialize with first attemptID=1
@@ -204,6 +205,15 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel
}
}
// create new context with timeout for notifications
resultHandleCtx, resultHandleCancelFn := context.WithTimeout(context.Background(), resultHandleTimeout)
cancelChan <- resultHandleCancelFn
// override the context used for evaluation with a new context for notifications.
// This makes it possible for notifiers to execute when datasources
// dont respond within the timeout limit. We should rewrite this so notifications
// dont reuse the evalContext and get its own context.
evalContext.Ctx = resultHandleCtx
evalContext.Rule.State = evalContext.GetNewState()
e.resultHandler.Handle(evalContext)
span.Finish()
@@ -0,0 +1,148 @@
// +build integration
package alerting
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestEngineTimeouts(t *testing.T) {
Convey("Alerting engine timeout tests", t, func() {
engine := NewEngine()
engine.resultHandler = &FakeResultHandler{}
job := &Job{Running: true, Rule: &Rule{}}
Convey("Should trigger as many retries as needed", func() {
Convey("pended alert for datasource -> result handler should be worked", func() {
// reduce alert timeout to test quickly
originAlertTimeout := alertTimeout
alertTimeout = 2 * time.Second
transportTimeoutInterval := 2 * time.Second
serverBusySleepDuration := 1 * time.Second
evalHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
resultHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
engine.evalHandler = evalHandler
engine.resultHandler = resultHandler
engine.processJobWithRetry(context.TODO(), job)
So(evalHandler.EvalSucceed, ShouldEqual, true)
So(resultHandler.ResultHandleSucceed, ShouldEqual, true)
// initialize for other tests.
alertTimeout = originAlertTimeout
engine.resultHandler = &FakeResultHandler{}
})
})
})
}
type FakeCommonTimeoutHandler struct {
TransportTimeoutDuration time.Duration
ServerBusySleepDuration time.Duration
EvalSucceed bool
ResultHandleSucceed bool
}
func NewFakeCommonTimeoutHandler(transportTimeoutDuration time.Duration, serverBusySleepDuration time.Duration) *FakeCommonTimeoutHandler {
return &FakeCommonTimeoutHandler{
TransportTimeoutDuration: transportTimeoutDuration,
ServerBusySleepDuration: serverBusySleepDuration,
EvalSucceed: false,
ResultHandleSucceed: false,
}
}
func (handler *FakeCommonTimeoutHandler) Eval(evalContext *EvalContext) {
// 1. prepare mock server
path := "/evaltimeout"
srv := runBusyServer(path, handler.ServerBusySleepDuration)
defer srv.Close()
// 2. send requests
url := srv.URL + path
res, err := sendRequest(evalContext.Ctx, url, handler.TransportTimeoutDuration)
if res != nil {
defer res.Body.Close()
}
if err != nil {
evalContext.Error = errors.New("Fake evaluation timeout test failure")
return
}
if res.StatusCode == 200 {
handler.EvalSucceed = true
}
evalContext.Error = errors.New("Fake evaluation timeout test failure; wrong response")
}
func (handler *FakeCommonTimeoutHandler) Handle(evalContext *EvalContext) error {
// 1. prepare mock server
path := "/resulthandle"
srv := runBusyServer(path, handler.ServerBusySleepDuration)
defer srv.Close()
// 2. send requests
url := srv.URL + path
res, err := sendRequest(evalContext.Ctx, url, handler.TransportTimeoutDuration)
if res != nil {
defer res.Body.Close()
}
if err != nil {
evalContext.Error = errors.New("Fake result handle timeout test failure")
return evalContext.Error
}
if res.StatusCode == 200 {
handler.ResultHandleSucceed = true
return nil
}
evalContext.Error = errors.New("Fake result handle timeout test failure; wrong response")
return evalContext.Error
}
func runBusyServer(path string, serverBusySleepDuration time.Duration) *httptest.Server {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
time.Sleep(serverBusySleepDuration)
})
return server
}
func sendRequest(context context.Context, url string, transportTimeoutInterval time.Duration) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req = req.WithContext(context)
transport := http.Transport{
Dial: (&net.Dialer{
Timeout: transportTimeoutInterval,
KeepAlive: transportTimeoutInterval,
}).Dial,
}
client := http.Client{
Transport: &transport,
}
return client.Do(req)
}
+266
View File
@@ -0,0 +1,266 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"net/url"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func init() {
registry.RegisterService(&UserAuthTokenServiceImpl{})
}
var (
getTime = time.Now
UrgentRotateTime = 1 * time.Minute
oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often.
)
// UserAuthTokenService are used for generating and validating user auth tokens
type UserAuthTokenService interface {
InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
UserSignedOutHook(c *models.ReqContext)
}
type UserAuthTokenServiceImpl struct {
SQLStore *sqlstore.SqlStore `inject:""`
ServerLockService *serverlock.ServerLockService `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger
}
// Init this service
func (s *UserAuthTokenServiceImpl) Init() error {
s.log = log.New("auth")
return nil
}
func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool {
//auth User
unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName)
if unhashedToken == "" {
return false
}
userToken, err := s.LookupToken(unhashedToken)
if err != nil {
ctx.Logger.Info("failed to look up user based on cookie", "error", err)
return false
}
query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err)
return false
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
//rotate session token if needed.
rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent())
if err != nil {
ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id)
return true
}
if rotated {
s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds)
}
return true
}
func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
if setting.Env == setting.DEV {
ctx.Logger.Info("new token", "unhashed token", value)
}
ctx.Resp.Header().Del("Set-Cookie")
cookie := http.Cookie{
Name: s.Cfg.LoginCookieName,
Value: url.QueryEscape(value),
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: s.Cfg.SecurityHTTPSCookies,
MaxAge: maxAge,
}
http.SetCookie(ctx.Resp, &cookie)
}
func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error {
userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
if err != nil {
return err
}
s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds)
return nil
}
func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) {
s.writeSessionCookie(c, "", -1)
}
func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
clientIP = util.ParseIPAddress(clientIP)
token, err := util.RandomHex(16)
if err != nil {
return nil, err
}
hashedToken := hashToken(token)
now := getTime().Unix()
userToken := userAuthToken{
UserId: userId,
AuthToken: hashedToken,
PrevAuthToken: hashedToken,
ClientIp: clientIP,
UserAgent: userAgent,
RotatedAt: now,
CreatedAt: now,
UpdatedAt: now,
SeenAt: 0,
AuthTokenSeen: false,
}
_, err = s.SQLStore.NewSession().Insert(&userToken)
if err != nil {
return nil, err
}
userToken.UnhashedToken = token
return &userToken, nil
}
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
hashedToken := hashToken(unhashedToken)
if setting.Env == setting.DEV {
s.log.Info("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
}
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
var userToken userAuthToken
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken)
if err != nil {
return nil, err
}
if !exists {
return nil, ErrAuthTokenNotFound
}
if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen {
userTokenCopy := userToken
userTokenCopy.AuthTokenSeen = false
expireBefore := getTime().Add(-UrgentRotateTime).Unix()
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy)
if err != nil {
return nil, err
}
if affectedRows == 0 {
s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
} else {
s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
}
}
if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken {
userTokenCopy := userToken
userTokenCopy.AuthTokenSeen = true
userTokenCopy.SeenAt = getTime().Unix()
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy)
if err != nil {
return nil, err
}
if affectedRows == 1 {
userToken = userTokenCopy
}
if affectedRows == 0 {
s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
} else {
s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
}
}
userToken.UnhashedToken = unhashedToken
return &userToken, nil
}
func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) {
if token == nil {
return false, nil
}
now := getTime()
needsRotation := false
rotatedAt := time.Unix(token.RotatedAt, 0)
if token.AuthTokenSeen {
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute))
} else {
needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime))
}
if !needsRotation {
return false, nil
}
s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id)
clientIP = util.ParseIPAddress(clientIP)
newToken, _ := util.RandomHex(16)
hashedToken := hashToken(newToken)
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
sql := `
UPDATE user_auth_token
SET
seen_at = 0,
user_agent = ?,
client_ip = ?,
prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
auth_token = ?,
auth_token_seen = ?,
rotated_at = ?
WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
if err != nil {
return false, err
}
affected, _ := res.RowsAffected()
s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId)
if affected > 0 {
token.UnhashedToken = newToken
return true, nil
}
return false, nil
}
func hashToken(token string) string {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:])
}
+339
View File
@@ -0,0 +1,339 @@
package auth
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserAuthToken(t *testing.T) {
Convey("Test user auth token", t, func() {
ctx := createTestContext(t)
userAuthTokenService := ctx.tokenService
userID := int64(10)
t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
getTime = func() time.Time {
return t
}
Convey("When creating token", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
So(token.AuthTokenSeen, ShouldBeFalse)
Convey("When lookup unhashed token should return user auth token", func() {
LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(LookupToken, ShouldNotBeNil)
So(LookupToken.UserId, ShouldEqual, userID)
So(LookupToken.AuthTokenSeen, ShouldBeTrue)
storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id)
So(err, ShouldBeNil)
So(storedAuthToken, ShouldNotBeNil)
So(storedAuthToken.AuthTokenSeen, ShouldBeTrue)
})
Convey("When lookup hashed token should return user auth token not found error", func() {
LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(LookupToken, ShouldBeNil)
})
})
Convey("expires correctly", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
getTime = func() time.Time {
return t.Add(time.Hour)
}
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(stillGood, ShouldNotBeNil)
getTime = func() time.Time {
return t.Add(24 * 7 * time.Hour)
}
notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldEqual, ErrAuthTokenNotFound)
So(notGood, ShouldBeNil)
})
Convey("can properly rotate tokens", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
prevToken := token.AuthToken
unhashedPrev := token.UnhashedToken
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeFalse)
updated, err := ctx.markAuthTokenAsSeen(token.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
getTime = func() time.Time {
return t.Add(time.Hour)
}
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
unhashedToken := token.UnhashedToken
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
token.UnhashedToken = unhashedToken
So(token.RotatedAt, ShouldEqual, getTime().Unix())
So(token.ClientIp, ShouldEqual, "192.168.10.12")
So(token.UserAgent, ShouldEqual, "a new user agent")
So(token.AuthTokenSeen, ShouldBeFalse)
So(token.SeenAt, ShouldEqual, 0)
So(token.PrevAuthToken, ShouldEqual, prevToken)
// ability to auth using an old token
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
So(lookedUp.SeenAt, ShouldEqual, getTime().Unix())
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.Id, ShouldEqual, token.Id)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(time.Hour + (2 * time.Minute))
}
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeFalse)
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
token, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
So(token.SeenAt, ShouldEqual, 0)
})
Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
getTime = func() time.Time {
return t.Add(10 * time.Minute)
}
prevToken := token.UnhashedToken
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(20 * time.Minute)
}
current, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(current, ShouldNotBeNil)
prev, err := userAuthTokenService.LookupToken(prevToken)
So(err, ShouldBeNil)
So(prev, ShouldNotBeNil)
})
Convey("will not mark token unseen when prev and current are the same", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
So(err, ShouldBeNil)
So(lookedUp, ShouldNotBeNil)
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
})
Convey("Rotate token", func() {
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
So(err, ShouldBeNil)
So(token, ShouldNotBeNil)
prevToken := token.AuthToken
Convey("Should rotate current token and previous token when auth token seen", func() {
updated, err := ctx.markAuthTokenAsSeen(token.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(10 * time.Minute)
}
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
prevToken = storedToken.AuthToken
updated, err = ctx.markAuthTokenAsSeen(token.Id)
So(err, ShouldBeNil)
So(updated, ShouldBeTrue)
getTime = func() time.Time {
return t.Add(20 * time.Minute)
}
refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
storedToken, err = ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
})
Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
token.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
getTime = func() time.Time {
return t.Add(2 * time.Minute)
}
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
So(err, ShouldBeNil)
So(refreshed, ShouldBeTrue)
storedToken, err := ctx.getAuthTokenByID(token.Id)
So(err, ShouldBeNil)
So(storedToken, ShouldNotBeNil)
So(storedToken.AuthTokenSeen, ShouldBeFalse)
So(storedToken.PrevAuthToken, ShouldEqual, prevToken)
So(storedToken.AuthToken, ShouldNotEqual, prevToken)
})
})
Reset(func() {
getTime = time.Now
})
})
}
func createTestContext(t *testing.T) *testContext {
t.Helper()
sqlstore := sqlstore.InitTestDB(t)
tokenService := &UserAuthTokenServiceImpl{
SQLStore: sqlstore,
Cfg: &setting.Cfg{
LoginCookieName: "grafana_session",
LoginCookieMaxDays: 7,
LoginDeleteExpiredTokensAfterDays: 30,
LoginCookieRotation: 10,
},
log: log.New("test-logger"),
}
UrgentRotateTime = time.Minute
return &testContext{
sqlstore: sqlstore,
tokenService: tokenService,
}
}
type testContext struct {
sqlstore *sqlstore.SqlStore
tokenService *UserAuthTokenServiceImpl
}
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
sess := c.sqlstore.NewSession()
var t userAuthToken
found, err := sess.ID(id).Get(&t)
if err != nil || !found {
return nil, err
}
return &t, nil
}
func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
sess := c.sqlstore.NewSession()
res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id)
if err != nil {
return false, err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return false, err
}
return rowsAffected == 1, nil
}
+25
View File
@@ -0,0 +1,25 @@
package auth
import (
"errors"
)
// Typed errors
var (
ErrAuthTokenNotFound = errors.New("User auth token not found")
)
type userAuthToken struct {
Id int64
UserId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
UnhashedToken string `xorm:"-"`
}
+38
View File
@@ -0,0 +1,38 @@
package auth
import (
"context"
"time"
)
func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error {
ticker := time.NewTicker(time.Hour * 12)
deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.LoginDeleteExpiredTokensAfterDays)
for {
select {
case <-ticker.C:
srv.ServerLockService.LockAndExecute(ctx, "delete old sessions", time.Hour*12, func() {
srv.deleteOldSession(deleteSessionAfter)
})
case <-ctx.Done():
return ctx.Err()
}
}
}
func (srv *UserAuthTokenServiceImpl) deleteOldSession(deleteSessionAfter time.Duration) (int64, error) {
sql := `DELETE from user_auth_token WHERE rotated_at < ?`
deleteBefore := getTime().Add(-deleteSessionAfter)
res, err := srv.SQLStore.NewSession().Exec(sql, deleteBefore.Unix())
if err != nil {
return 0, err
}
affected, err := res.RowsAffected()
srv.log.Info("deleted old sessions", "count", affected)
return affected, err
}
+36
View File
@@ -0,0 +1,36 @@
package auth
import (
"fmt"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserAuthTokenCleanup(t *testing.T) {
Convey("Test user auth token cleanup", t, func() {
ctx := createTestContext(t)
insertToken := func(token string, prev string, rotatedAt int64) {
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
_, err := ctx.sqlstore.NewSession().Insert(&ut)
So(err, ShouldBeNil)
}
// insert three old tokens that should be deleted
for i := 0; i < 3; i++ {
insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), int64(i))
}
// insert three active tokens that should not be deleted
for i := 0; i < 3; i++ {
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), getTime().Unix())
}
affected, err := ctx.tokenService.deleteOldSession(time.Hour)
So(err, ShouldBeNil)
So(affected, ShouldEqual, 3)
})
}
+1 -5
View File
@@ -164,11 +164,7 @@ func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand,
User: dto.User,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return err
}
return nil
return bus.Dispatch(&alertCmd)
}
func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
-2
View File
@@ -14,8 +14,6 @@ import (
const (
SESS_KEY_USERID = "uid"
SESS_KEY_OAUTH_STATE = "state"
SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys
SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
)
@@ -32,6 +32,7 @@ func AddMigrations(mg *Migrator) {
addLoginAttemptMigrations(mg)
addUserAuthMigrations(mg)
addServerlockMigrations(mg)
addUserAuthTokenMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
@@ -0,0 +1,32 @@
package migrations
import (
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func addUserAuthTokenMigrations(mg *Migrator) {
userAuthTokenV1 := Table{
Name: "user_auth_token",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "user_id", Type: DB_BigInt, Nullable: false},
{Name: "auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
{Name: "prev_auth_token", Type: DB_NVarchar, Length: 100, Nullable: false},
{Name: "user_agent", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "client_ip", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "auth_token_seen", Type: DB_Bool, Nullable: false},
{Name: "seen_at", Type: DB_Int, Nullable: true},
{Name: "rotated_at", Type: DB_Int, Nullable: false},
{Name: "created_at", Type: DB_Int, Nullable: false},
{Name: "updated_at", Type: DB_Int, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"auth_token"}, Type: UniqueIndex},
{Cols: []string{"prev_auth_token"}, Type: UniqueIndex},
},
}
mg.AddMigration("create user auth token table", NewAddTableMigration(userAuthTokenV1))
mg.AddMigration("add unique index user_auth_token.auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[0]))
mg.AddMigration("add unique index user_auth_token.prev_auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[1]))
}
+4 -7
View File
@@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
@@ -222,13 +223,9 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
cnnstr += "&tls=custom"
}
case migrator.POSTGRES:
var host, port = "127.0.0.1", "5432"
fields := strings.Split(ss.dbCfg.Host, ":")
if len(fields) > 0 && len(strings.TrimSpace(fields[0])) > 0 {
host = fields[0]
}
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
port = fields[1]
host, port, err := util.SplitIpPort(ss.dbCfg.Host, "5432")
if err != nil {
return "", err
}
if ss.dbCfg.Pwd == "" {
ss.dbCfg.Pwd = "''"
+101
View File
@@ -0,0 +1,101 @@
package sqlstore
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/setting"
)
type sqlStoreTest struct {
name string
dbType string
dbHost string
connStrValues []string
}
var sqlStoreTestCases = []sqlStoreTest{
{
name: "MySQL IPv4",
dbType: "mysql",
dbHost: "1.2.3.4:5678",
connStrValues: []string{"tcp(1.2.3.4:5678)"},
},
{
name: "Postgres IPv4",
dbType: "postgres",
dbHost: "1.2.3.4:5678",
connStrValues: []string{"host=1.2.3.4", "port=5678"},
},
{
name: "Postgres IPv4 (Default Port)",
dbType: "postgres",
dbHost: "1.2.3.4",
connStrValues: []string{"host=1.2.3.4", "port=5432"},
},
{
name: "MySQL IPv4 (Default Port)",
dbType: "mysql",
dbHost: "1.2.3.4",
connStrValues: []string{"tcp(1.2.3.4)"},
},
{
name: "MySQL IPv6",
dbType: "mysql",
dbHost: "[fe80::24e8:31b2:91df:b177]:1234",
connStrValues: []string{"tcp([fe80::24e8:31b2:91df:b177]:1234)"},
},
{
name: "Postgres IPv6",
dbType: "postgres",
dbHost: "[fe80::24e8:31b2:91df:b177]:1234",
connStrValues: []string{"host=fe80::24e8:31b2:91df:b177", "port=1234"},
},
{
name: "MySQL IPv6 (Default Port)",
dbType: "mysql",
dbHost: "::1",
connStrValues: []string{"tcp(::1)"},
},
{
name: "Postgres IPv6 (Default Port)",
dbType: "postgres",
dbHost: "::1",
connStrValues: []string{"host=::1", "port=5432"},
},
}
func TestSqlConnectionString(t *testing.T) {
Convey("Testing SQL Connection Strings", t, func() {
t.Helper()
for _, testCase := range sqlStoreTestCases {
Convey(testCase.name, func() {
sqlstore := &SqlStore{}
sqlstore.Cfg = makeSqlStoreTestConfig(testCase.dbType, testCase.dbHost)
sqlstore.readConfig()
connStr, err := sqlstore.buildConnectionString()
So(err, ShouldBeNil)
for _, connSubStr := range testCase.connStrValues {
So(connStr, ShouldContainSubstring, connSubStr)
}
})
}
})
}
func makeSqlStoreTestConfig(dbType string, host string) *setting.Cfg {
cfg := setting.NewCfg()
sec, _ := cfg.Raw.NewSection("database")
sec.NewKey("type", dbType)
sec.NewKey("host", host)
sec.NewKey("user", "user")
sec.NewKey("name", "test_db")
sec.NewKey("password", "pass")
return cfg
}
+24 -8
View File
@@ -18,7 +18,7 @@ import (
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"gopkg.in/ini.v1"
ini "gopkg.in/ini.v1"
)
type Scheme string
@@ -77,15 +77,13 @@ var (
SocketPath string
RouterLogging bool
DataProxyLogging bool
DataProxyTimeout int
StaticRootPath string
EnableGzip bool
EnforceDomain bool
// Security settings.
SecretKey string
LogInRememberDays int
CookieUserName string
CookieRememberName string
DisableGravatar bool
EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool
@@ -222,7 +220,15 @@ type Cfg struct {
MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string
EnableAlphaPanels bool
DisableSanitizeHtml bool
EnterpriseLicensePath string
LoginCookieName string
LoginCookieMaxDays int
LoginCookieRotation int
LoginDeleteExpiredTokensAfterDays int
SecurityHTTPSCookies bool
}
type CommandLineArgs struct {
@@ -546,6 +552,16 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
ApplicationName = APP_NAME_ENTERPRISE
}
//login
login := iniFile.Section("login")
cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session")
cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7)
cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30)
cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10)
if cfg.LoginCookieRotation < 2 {
cfg.LoginCookieRotation = 2
}
Env = iniFile.Section("").Key("app_mode").MustString("development")
InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
@@ -582,15 +598,14 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// read data proxy settings
dataproxy := iniFile.Section("dataproxy")
DataProxyLogging = dataproxy.Key("logging").MustBool(false)
DataProxyTimeout = dataproxy.Key("timeout").MustInt(30)
// read security settings
security := iniFile.Section("security")
SecretKey = security.Key("secret_key").String()
LogInRememberDays = security.Key("login_remember_days").MustInt()
CookieUserName = security.Key("cookie_username").String()
CookieRememberName = security.Key("cookie_remember_name").String()
DisableGravatar = security.Key("disable_gravatar").MustBool(true)
cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
cfg.SecurityHTTPSCookies = security.Key("https_flag_cookies").MustBool(false)
DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection
// read snapshots settings
@@ -705,10 +720,11 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data")
explore := iniFile.Section("explore")
ExploreEnabled = explore.Key("enabled").MustBool(false)
ExploreEnabled = explore.Key("enabled").MustBool(true)
panels := iniFile.Section("panels")
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
cfg.DisableSanitizeHtml = panels.Key("disable_sanitize_html").MustBool(false)
cfg.readSessionConfig()
cfg.readSmtpSettings()
+10 -8
View File
@@ -4,13 +4,13 @@ import (
"database/sql"
"fmt"
"strconv"
"strings"
_ "github.com/denisenkom/go-mssqldb"
"github.com/go-xorm/core"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util"
)
func init() {
@@ -20,7 +20,10 @@ func init() {
func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
logger := log.New("tsdb.mssql")
cnnstr := generateConnectionString(datasource)
cnnstr, err := generateConnectionString(datasource)
if err != nil {
return nil, err
}
logger.Debug("getEngine", "connection", cnnstr)
config := tsdb.SqlQueryEndpointConfiguration{
@@ -37,7 +40,7 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMssqlMacroEngine(), logger)
}
func generateConnectionString(datasource *models.DataSource) string {
func generateConnectionString(datasource *models.DataSource) (string, error) {
password := ""
for key, value := range datasource.SecureJsonData.Decrypt() {
if key == "password" {
@@ -46,12 +49,11 @@ func generateConnectionString(datasource *models.DataSource) string {
}
}
hostParts := strings.Split(datasource.Url, ":")
if len(hostParts) < 2 {
hostParts = append(hostParts, "1433")
server, port, err := util.SplitIpPort(datasource.Url, "1433")
if err != nil {
return "", err
}
server, port := hostParts[0], hostParts[1]
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
server,
@@ -63,7 +65,7 @@ func generateConnectionString(datasource *models.DataSource) string {
if encrypt != "false" {
connStr += fmt.Sprintf("encrypt=%s;", encrypt)
}
return connStr
return connStr, nil
}
type mssqlRowTransformer struct {
+8
View File
@@ -101,3 +101,11 @@ func DecodeBasicAuthHeader(header string) (string, string, error) {
return userAndPass[0], userAndPass[1], nil
}
func RandomHex(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
+24
View File
@@ -0,0 +1,24 @@
package util
import (
"net"
)
func SplitIpPort(ipStr string, portDefault string) (ip string, port string, err error) {
ipAddr := net.ParseIP(ipStr)
if ipAddr == nil {
// Port was included
ip, port, err = net.SplitHostPort(ipStr)
if err != nil {
return "", "", err
}
} else {
// No port was included
ip = ipAddr.String()
port = portDefault
}
return ip, port, nil
}
+29
View File
@@ -0,0 +1,29 @@
package util
import (
"net"
"strings"
)
// ParseIPAddress parses an IP address and removes port and/or IPV6 format
func ParseIPAddress(input string) string {
s := input
lastIndex := strings.LastIndex(input, ":")
if lastIndex != -1 {
if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" {
s = input[:lastIndex]
}
}
s = strings.Replace(s, "[", "", -1)
s = strings.Replace(s, "]", "", -1)
ip := net.ParseIP(s)
if ip.IsLoopback() {
return "127.0.0.1"
}
return ip.String()
}
+16
View File
@@ -0,0 +1,16 @@
package util
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestParseIPAddress(t *testing.T) {
Convey("Test parse ip address", t, func() {
So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140")
So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1")
So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140")
})
}
+43
View File
@@ -0,0 +1,43 @@
package util
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestSplitIpPort(t *testing.T) {
Convey("When parsing an IPv4 without explicit port", t, func() {
ip, port, err := SplitIpPort("1.2.3.4", "5678")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "1.2.3.4")
So(port, ShouldEqual, "5678")
})
Convey("When parsing an IPv6 without explicit port", t, func() {
ip, port, err := SplitIpPort("::1", "5678")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "::1")
So(port, ShouldEqual, "5678")
})
Convey("When parsing an IPv4 with explicit port", t, func() {
ip, port, err := SplitIpPort("1.2.3.4:56", "78")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "1.2.3.4")
So(port, ShouldEqual, "56")
})
Convey("When parsing an IPv6 with explicit port", t, func() {
ip, port, err := SplitIpPort("[::1]:56", "78")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "::1")
So(port, ShouldEqual, "56")
})
}