Merge branch 'master' into 14701-fix-alert-context
* master: (262 commits) pkg/services/dashboards/dashboard_service.go: simplify return Updated url query param encoding to exctly match angular encoding Updated snapshot Added missing props not being passed to scrollbar component, fixes #15058 Document /api/health changelog: adds note for #15062 change default rotate_token_minutes to 10 minutes fix load test/ha fixes set low login cookie rotate time in ha mode fix multiple piechart instances bug scripts/build/*: Fix some golint issues scripts/build/*: Fix golint issues Url => URL build: fixes building grafana completely within docker. dont specify domain for auth cookies New snapshot reflecting changes Makes the clickable side menu header look great in light theme again org id fix for load test user auth token load tests using k6.io moves cookie https setting to [security] ...
This commit is contained in:
+6
-6
@@ -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
@@ -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) {}
|
||||
|
||||
@@ -336,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||
"id": 123123,
|
||||
"gridPos": map[string]interface{}{
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"y": 0,
|
||||
"w": 24,
|
||||
"h": 4,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("imguploader")
|
||||
)
|
||||
|
||||
func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
switch setting.ImageUploadProvider {
|
||||
@@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
}
|
||||
|
||||
if setting.ImageUploadProvider != "" {
|
||||
log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||
logger.Error("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||
}
|
||||
|
||||
return NopImageUploader{}, nil
|
||||
|
||||
+2
-20
@@ -10,13 +10,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/go-stack/stack"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/inconshreveable/log15"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var Root log15.Logger
|
||||
@@ -58,10 +56,6 @@ func Debug(format string, v ...interface{}) {
|
||||
Root.Debug(message)
|
||||
}
|
||||
|
||||
func Debug2(message string, v ...interface{}) {
|
||||
Root.Debug(message, v...)
|
||||
}
|
||||
|
||||
func Info(format string, v ...interface{}) {
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
@@ -73,10 +67,6 @@ func Info(format string, v ...interface{}) {
|
||||
Root.Info(message)
|
||||
}
|
||||
|
||||
func Info2(message string, v ...interface{}) {
|
||||
Root.Info(message, v...)
|
||||
}
|
||||
|
||||
func Warn(format string, v ...interface{}) {
|
||||
var message string
|
||||
if len(v) > 0 {
|
||||
@@ -88,18 +78,10 @@ func Warn(format string, v ...interface{}) {
|
||||
Root.Warn(message)
|
||||
}
|
||||
|
||||
func Warn2(message string, v ...interface{}) {
|
||||
Root.Warn(message, v...)
|
||||
}
|
||||
|
||||
func Error(skip int, format string, v ...interface{}) {
|
||||
Root.Error(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func Error2(message string, v ...interface{}) {
|
||||
Root.Error(message, v...)
|
||||
}
|
||||
|
||||
func Critical(skip int, format string, v ...interface{}) {
|
||||
Root.Crit(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ func init() {
|
||||
bus.AddHandler("auth", UpsertUser)
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("login.ext_user")
|
||||
)
|
||||
|
||||
func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
extUser := cmd.ExternalUser
|
||||
|
||||
@@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
return bus.Dispatch(updateCmd)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
|
||||
defer func() {
|
||||
err := imageFile.Close()
|
||||
if err != nil {
|
||||
log.Error2("Could not close Telegram inline image.", "err", err)
|
||||
this.log.Error("Could not close Telegram inline image.", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -18,9 +18,12 @@ type NotificationTestCommand struct {
|
||||
Settings *simplejson.Json
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("alerting.testnotification")
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("alerting", handleNotificationTestCommand)
|
||||
|
||||
}
|
||||
|
||||
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
@@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
notifiers, err := InitNotifier(model)
|
||||
|
||||
if err != nil {
|
||||
log.Error2("Failed to create notifier", "error", err.Error())
|
||||
logger.Error("Failed to create notifier", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -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[:])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -69,7 +70,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
|
||||
parsedDashboards, err := cr.parseConfigs(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("could not parse provisioning config file: %s error: %v", file.Name(), err)
|
||||
}
|
||||
|
||||
if len(parsedDashboards) > 0 {
|
||||
|
||||
@@ -118,6 +118,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
||||
if fr.Cfg.DisableDeletion {
|
||||
return
|
||||
@@ -180,7 +181,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
||||
dash.Dashboard.SetId(provisionedData.DashboardId)
|
||||
}
|
||||
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
fr.log.Debug("saving new dashboard", "provisoner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId)
|
||||
dp := &models.DashboardProvisioning{
|
||||
ExternalId: path,
|
||||
Name: fr.Cfg.Name,
|
||||
|
||||
@@ -166,6 +166,36 @@ func TestDashboardFileReader(t *testing.T) {
|
||||
_, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Two dashboard providers should be able to provisioned the same dashboard without uid", func() {
|
||||
cfg1 := &DashboardsAsConfig{Name: "1", Type: "file", OrgId: 1, Folder: "f1", Options: map[string]interface{}{"path": containingId}}
|
||||
cfg2 := &DashboardsAsConfig{Name: "2", Type: "file", OrgId: 1, Folder: "f2", Options: map[string]interface{}{"path": containingId}}
|
||||
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader1.startWalkingDisk()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader2.startWalkingDisk()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var folderCount int
|
||||
var dashCount int
|
||||
for _, o := range fakeService.inserted {
|
||||
if o.Dashboard.IsFolder {
|
||||
folderCount++
|
||||
} else {
|
||||
dashCount++
|
||||
}
|
||||
}
|
||||
|
||||
So(folderCount, ShouldEqual, 2)
|
||||
So(dashCount, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not create new folder if folder name is missing", func() {
|
||||
@@ -256,7 +286,9 @@ func (ffi FakeFileInfo) Sys() interface{} {
|
||||
}
|
||||
|
||||
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
||||
mock := fakeDashboardProvisioningService{}
|
||||
mock := fakeDashboardProvisioningService{
|
||||
provisioned: map[string][]*models.DashboardProvisioning{},
|
||||
}
|
||||
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
|
||||
return &mock
|
||||
}
|
||||
@@ -265,17 +297,26 @@ func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
||||
|
||||
type fakeDashboardProvisioningService struct {
|
||||
inserted []*dashboards.SaveDashboardDTO
|
||||
provisioned []*models.DashboardProvisioning
|
||||
provisioned map[string][]*models.DashboardProvisioning
|
||||
getDashboard []*models.Dashboard
|
||||
}
|
||||
|
||||
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
return s.provisioned, nil
|
||||
if _, ok := s.provisioned[name]; !ok {
|
||||
s.provisioned[name] = []*models.DashboardProvisioning{}
|
||||
}
|
||||
|
||||
return s.provisioned[name], nil
|
||||
}
|
||||
|
||||
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||
s.inserted = append(s.inserted, dto)
|
||||
s.provisioned = append(s.provisioned, provisioning)
|
||||
|
||||
if _, ok := s.provisioned[provisioning.Name]; !ok {
|
||||
s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{}
|
||||
}
|
||||
|
||||
s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], provisioning)
|
||||
return dto.Dashboard, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error
|
||||
func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
||||
result := &models.DashboardProvisioning{}
|
||||
|
||||
exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
|
||||
exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
}
|
||||
+21
-7
@@ -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
|
||||
@@ -83,9 +83,6 @@ var (
|
||||
|
||||
// Security settings.
|
||||
SecretKey string
|
||||
LogInRememberDays int
|
||||
CookieUserName string
|
||||
CookieRememberName string
|
||||
DisableGravatar bool
|
||||
EmailCodeValidMinutes int
|
||||
DataProxyWhiteList map[string]bool
|
||||
@@ -222,7 +219,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 +551,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)
|
||||
@@ -586,11 +601,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
// 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
|
||||
@@ -709,6 +722,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user