From a2967565deb39f8633da1fb4d653c909f505b409 Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 30 Jul 2018 17:19:41 +0200 Subject: [PATCH 001/167] added urlescape formatting option --- docs/sources/reference/templating.md | 1 + public/app/features/templating/specs/template_srv.jest.ts | 5 +++++ public/app/features/templating/template_srv.ts | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index ce1a1299d26..d59117fefea 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -52,6 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string `distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB. `lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression. +`urlescape` | ${servers:urlescape} | `'foo()bar baz', 'test2'` | `{foo%28%29bar%20baz%2Ctest2}` | Formats multi-value variable into a glob, url escaped Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1). diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 86b6aa7ec99..040597888b6 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -275,6 +275,11 @@ describe('templateSrv', function() { expect(result).toBe('test,test2'); }); + it('multi value and urlescape format should render url-escaped string', function() { + var result = _templateSrv.formatValue(['foo()bar baz', 'test2'], 'urlescape'); + expect(result).toBe('foo%28%29bar%20baz%2Ctest2'); + }); + it('slash should be properly escaped in regex format', function() { var result = _templateSrv.formatValue('Gi3/14', 'regex'); expect(result).toBe('Gi3\\/14'); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index fc79d12ff9e..7ce539b6506 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -124,6 +124,13 @@ export class TemplateSrv { } return value; } + case 'urlescape': { + // like glob, but url escaped + if (_.isArray(value)) { + return escape('{' + value.join(',') + '}'); + } + return escape(value); + } default: { if (_.isArray(value)) { return '{' + value.join(',') + '}'; From 7da9c33ae4dae4232d242e8ab6710be720bdfd35 Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 30 Jul 2018 17:28:50 +0200 Subject: [PATCH 002/167] fixed test result --- public/app/features/templating/specs/template_srv.jest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 040597888b6..85a159fc098 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -277,7 +277,7 @@ describe('templateSrv', function() { it('multi value and urlescape format should render url-escaped string', function() { var result = _templateSrv.formatValue(['foo()bar baz', 'test2'], 'urlescape'); - expect(result).toBe('foo%28%29bar%20baz%2Ctest2'); + expect(result).toBe('%7Bfoo%28%29bar%20baz%2Ctest2%7D'); }); it('slash should be properly escaped in regex format', function() { From 9220f83b3dcd4376d1ec8a5ab5bdf0a0e031a65f Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 6 Aug 2018 21:54:12 +0200 Subject: [PATCH 003/167] replaced escape() call, renamed formatter to be more expressive --- docs/sources/reference/templating.md | 2 +- .../features/templating/specs/template_srv.jest.ts | 6 +++--- public/app/features/templating/template_srv.ts | 13 ++++++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index d59117fefea..1482eb34350 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -52,7 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string `distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB. `lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression. -`urlescape` | ${servers:urlescape} | `'foo()bar baz', 'test2'` | `{foo%28%29bar%20baz%2Ctest2}` | Formats multi-value variable into a glob, url escaped +`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-escaped Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1). diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 85a159fc098..b4501e81f59 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -275,9 +275,9 @@ describe('templateSrv', function() { expect(result).toBe('test,test2'); }); - it('multi value and urlescape format should render url-escaped string', function() { - var result = _templateSrv.formatValue(['foo()bar baz', 'test2'], 'urlescape'); - expect(result).toBe('%7Bfoo%28%29bar%20baz%2Ctest2%7D'); + it('multi value and percentencode format should render percent-encoded string', function() { + var result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); + expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D'); }); it('slash should be properly escaped in regex format', function() { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 7ce539b6506..3d462f1bcde 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -83,6 +83,13 @@ export class TemplateSrv { return '(' + quotedValues.join(' OR ') + ')'; } + // like encodeURIComponent() but for all characters except alpha-numerics + encodeURIQueryValue(str) { + return str.replace(/[^a-z0-9]/gi, function(c) { + return '%' + c.charCodeAt(0).toString(16); + }); + } + formatValue(value, format, variable) { // for some scopedVars there is no variable variable = variable || {}; @@ -124,12 +131,12 @@ export class TemplateSrv { } return value; } - case 'urlescape': { + case 'percentencode': { // like glob, but url escaped if (_.isArray(value)) { - return escape('{' + value.join(',') + '}'); + return this.encodeURIQueryValue('{' + value.join(',') + '}'); } - return escape(value); + return this.encodeURIQueryValue(value); } default: { if (_.isArray(value)) { From a653b277f312766ebfe0bb00ea0f6268139591d1 Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 6 Aug 2018 22:04:33 +0200 Subject: [PATCH 004/167] switched to lowercase --- public/app/features/templating/specs/template_srv.jest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index b4501e81f59..3d8b3af1ddd 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -277,7 +277,7 @@ describe('templateSrv', function() { it('multi value and percentencode format should render percent-encoded string', function() { var result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); - expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D'); + expect(result).toBe('%7bfoo%28%29bar%20BAZ%2ctest2%7d'); }); it('slash should be properly escaped in regex format', function() { From f4b29b5782bea2efe8d428f9cc164678306cd7dd Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 3 Sep 2018 16:08:52 +0200 Subject: [PATCH 005/167] fixed testcase --- public/app/features/templating/specs/template_srv.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 06c7ac552c9..3e5ddf8bf54 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -276,7 +276,7 @@ describe('templateSrv', function() { }); it('multi value and percentencode format should render percent-encoded string', function() { - var result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); + const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); expect(result).toBe('%7bfoo%28%29bar%20BAZ%2ctest2%7d'); }); From e172bade40b71d932eed46364fd2e3e597091639 Mon Sep 17 00:00:00 2001 From: sharkpc0813 Date: Tue, 15 Jan 2019 17:30:51 +0900 Subject: [PATCH 006/167] fix that alert context and result handle context do not use the same derived context. --- pkg/services/alerting/engine.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 0f8e24bcef5..f784a322513 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -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,9 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } } + resultHandleCtx, resultHandleCancelFn := context.WithTimeout(context.Background(), resultHandleTimeout) + cancelChan <- resultHandleCancelFn + evalContext.Ctx = resultHandleCtx evalContext.Rule.State = evalContext.GetNewState() e.resultHandler.Handle(evalContext) span.Finish() From e08f61059bcbc60753268eae06aced2cc31e7f9d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Jan 2019 11:11:32 +0100 Subject: [PATCH 007/167] utils --- pkg/util/encoding.go | 8 ++++++++ pkg/util/ip_address.go | 27 +++++++++++++++++++++++++++ pkg/util/ip_address_test.go | 14 ++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 pkg/util/ip_address.go create mode 100644 pkg/util/ip_address_test.go diff --git a/pkg/util/encoding.go b/pkg/util/encoding.go index 0edb721e422..e82344d73f9 100644 --- a/pkg/util/encoding.go +++ b/pkg/util/encoding.go @@ -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 +} diff --git a/pkg/util/ip_address.go b/pkg/util/ip_address.go new file mode 100644 index 00000000000..4e9a9378c6b --- /dev/null +++ b/pkg/util/ip_address.go @@ -0,0 +1,27 @@ +package util + +import ( + "net" + "strings" +) + +// ParseIPAddress parses an IP address and removes port and/or IPV6 format +func ParseIPAddress(input string) string { + var s string + lastIndex := strings.LastIndex(input, ":") + + if lastIndex != -1 { + 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() +} diff --git a/pkg/util/ip_address_test.go b/pkg/util/ip_address_test.go new file mode 100644 index 00000000000..644340a5e82 --- /dev/null +++ b/pkg/util/ip_address_test.go @@ -0,0 +1,14 @@ +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") + }) +} From b0df7280be60be815078e572f5975a14521695bf Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Jan 2019 15:15:17 +0100 Subject: [PATCH 008/167] begin user auth token implementation --- pkg/services/auth/auth_token.go | 170 +++++++++++++++ pkg/services/auth/auth_token_test.go | 206 ++++++++++++++++++ pkg/services/auth/model.go | 25 +++ .../sqlstore/migrations/migrations.go | 1 + .../migrations/user_auth_token_mig.go | 32 +++ 5 files changed, 434 insertions(+) create mode 100644 pkg/services/auth/auth_token.go create mode 100644 pkg/services/auth/auth_token_test.go create mode 100644 pkg/services/auth/model.go create mode 100644 pkg/services/sqlstore/migrations/user_auth_token_mig.go diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go new file mode 100644 index 00000000000..aefcccadbd6 --- /dev/null +++ b/pkg/services/auth/auth_token.go @@ -0,0 +1,170 @@ +package auth + +import ( + "crypto/sha256" + "encoding/hex" + "time" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" + macaron "gopkg.in/macaron.v1" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func init() { + registry.RegisterService(&UserAuthTokenService{}) +} + +var now = time.Now + +// UserAuthTokenService are used for generating and validating user auth tokens +type UserAuthTokenService struct { + SQLStore *sqlstore.SqlStore `inject:""` + log log.Logger +} + +// Init this service +func (s *UserAuthTokenService) Init() error { + s.log = log.New("auth") + return nil +} + +const sessionCookieKey = "grafana_session" + +func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error { + userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) + if err != nil { + return err + } + + c.Resp.Header().Del("Set-Cookie") + c.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) + + return nil +} + +func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { + c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true) +} + +func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler { + return func(ctx *models.ReqContext) { + authToken := ctx.GetCookie(sessionCookieKey) + userToken, err := s.lookupToken(authToken) + if err != nil { + + } + + ctx.Next() + + refreshed, err := s.refreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + + } + + if refreshed { + ctx.Resp.Header().Del("Set-Cookie") + ctx.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) + } + } +} + +func (s *UserAuthTokenService) 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) + + userToken := userAuthToken{ + UserId: userId, + AuthToken: hashedToken, + PrevAuthToken: hashedToken, + ClientIp: clientIP, + UserAgent: userAgent, + RotatedAt: now().Unix(), + CreatedAt: now().Unix(), + UpdatedAt: now().Unix(), + SeenAt: 0, + AuthTokenSeen: false, + } + _, err = s.SQLStore.NewSession().Insert(&userToken) + if err != nil { + return nil, err + } + + userToken.unhashedToken = token + + return &userToken, nil +} + +func (s *UserAuthTokenService) lookupToken(unhashedToken string) (*userAuthToken, error) { + hashedToken := hashToken(unhashedToken) + + var userToken userAuthToken + exists, err := s.SQLStore.NewSession().Where("auth_token = ? OR prev_auth_token = ?", hashedToken, hashedToken).Get(&userToken) + if err != nil { + return nil, err + } + + if !exists { + return nil, ErrAuthTokenNotFound + } + + if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { + userToken.AuthTokenSeen = false + expireBefore := now().Add(-1 * time.Minute).Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userToken.Id, userToken.PrevAuthToken, expireBefore).AllCols().Update(&userToken) + 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 = now().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 *UserAuthTokenService) refreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { + // lookup token in db + // refresh token if needed + + return false, nil +} + +func hashToken(token string) string { + hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) + return hex.EncodeToString(hashBytes[:]) +} diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go new file mode 100644 index 00000000000..2e4618c10ed --- /dev/null +++ b/pkg/services/auth/auth_token_test.go @@ -0,0 +1,206 @@ +package auth + +import ( + "testing" + "time" + + "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) + now = 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) + + // set now (now - 23 hours) + _, err = userAuthTokenService.refreshToken(token, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + + _, err = userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + + stillGood, err := userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + So(stillGood, ShouldNotBeNil) + + // set now (new - 2 hours) + 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) + + ctx.markAuthTokenAsSeen(token.Id) + token, err = ctx.getAuthTokenByID(token.Id) + So(err, ShouldBeNil) + + // ability to auth using an old token + now = func() time.Time { + return t + } + + 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, t.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) + + lookedUp, err := userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + So(lookedUp.AuthTokenSeen, ShouldBeTrue) + So(lookedUp.SeenAt, ShouldEqual, t.Unix()) + + lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + So(lookedUp.Id, ShouldEqual, token.Id) + + now = func() time.Time { + return t.Add(2 * time.Minute) + } + + lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + + 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() { + + }) + + Convey("will not mark token unseen when prev and current are the same", func() { + + }) + + Reset(func() { + now = time.Now + }) + }) +} + +func createTestContext(t *testing.T) *testContext { + t.Helper() + + sqlstore := sqlstore.InitTestDB(t) + tokenService := &UserAuthTokenService{ + SQLStore: sqlstore, + log: log.New("test-logger"), + } + + return &testContext{ + sqlstore: sqlstore, + tokenService: tokenService, + } +} + +type testContext struct { + sqlstore *sqlstore.SqlStore + tokenService *UserAuthTokenService +} + +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 +} diff --git a/pkg/services/auth/model.go b/pkg/services/auth/model.go new file mode 100644 index 00000000000..a033b96be31 --- /dev/null +++ b/pkg/services/auth/model.go @@ -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:"-"` +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 36cd8e5ed62..931259ec3ed 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -32,6 +32,7 @@ func AddMigrations(mg *Migrator) { addLoginAttemptMigrations(mg) addUserAuthMigrations(mg) addServerlockMigrations(mg) + addUserAuthTokenMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/user_auth_token_mig.go b/pkg/services/sqlstore/migrations/user_auth_token_mig.go new file mode 100644 index 00000000000..9794b7a78c7 --- /dev/null +++ b/pkg/services/sqlstore/migrations/user_auth_token_mig.go @@ -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])) +} From 8764fb5aa6eee9c8df08e5fc23dec5d78d3f5682 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Jan 2019 15:15:52 +0100 Subject: [PATCH 009/167] inject login/logout hooks --- pkg/api/api.go | 12 ++++++------ pkg/api/http_server.go | 17 ++++++++++------- pkg/api/login.go | 38 +++++++++++++++----------------------- pkg/api/login_oauth.go | 4 ++-- pkg/api/org_invite.go | 4 ++-- pkg/api/signup.go | 4 ++-- 6 files changed, 37 insertions(+), 42 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0526ee80afe..07cb712f794 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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) { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index d4d7b41bec5..600157878fe 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -11,6 +11,8 @@ import ( "path" "time" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/api/routing" "github.com/prometheus/client_golang/prometheus" @@ -49,13 +51,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 { diff --git a/pkg/api/login.go b/pkg/api/login.go index 05afc40e59a..f0902a60f58 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -9,7 +9,6 @@ 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" ) @@ -43,7 +42,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { return } - if !tryLoginUsingRememberCookie(c) { + if !hs.tryLoginUsingRememberCookie(c) { c.HTML(200, ViewIndex, viewData) return } @@ -75,7 +74,7 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool { return false } -func tryLoginUsingRememberCookie(c *m.ReqContext) bool { +func (hs *HTTPServer) tryLoginUsingRememberCookie(c *m.ReqContext) bool { // Check auto-login. uname := c.GetCookie(setting.CookieUserName) if len(uname) == 0 { @@ -111,12 +110,12 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool { } isSucceed = true - loginUserWithUser(user, c) + hs.loginUserWithUser(user, c) return true } -func LoginAPIPing(c *m.ReqContext) { - if !tryLoginUsingRememberCookie(c) { +func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) { + if !hs.tryLoginUsingRememberCookie(c) { c.JsonApiErr(401, "Unauthorized", nil) return } @@ -124,7 +123,7 @@ func LoginAPIPing(c *m.ReqContext) { 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 +145,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,27 +161,20 @@ 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", 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 { diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index fe4fa93b621..6013df8ea02 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -31,7 +31,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 @@ -178,7 +178,7 @@ func OAuthLogin(ctx *m.ReqContext) { } // login - loginUserWithUser(cmd.Result, ctx) + hs.loginUserWithUser(cmd.Result, ctx) metrics.M_Api_Login_OAuth.Inc() diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index dfb2cf045ed..835b03a2cc9 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -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() diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 200a3ebc9d1..fe577dd9ef9 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -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) From aba6148c4322ca914dd564f5a1d64d5d9ce39804 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 16 Jan 2019 14:53:59 +0100 Subject: [PATCH 010/167] login users based on token cookie --- pkg/api/http_server.go | 16 +++---- pkg/api/login.go | 8 ++-- pkg/middleware/middleware.go | 33 +++++++++++++-- pkg/middleware/org_redirect.go | 1 - pkg/services/auth/auth_token.go | 62 +++++++++++++++++----------- pkg/services/auth/auth_token_test.go | 36 ++++++++-------- 6 files changed, 95 insertions(+), 61 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 600157878fe..54f0601e577 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -11,16 +11,8 @@ import ( "path" "time" - "github.com/grafana/grafana/pkg/services/auth" - - "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" @@ -29,11 +21,15 @@ 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/setting" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + macaron "gopkg.in/macaron.v1" ) func init() { @@ -226,7 +222,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(hs.healthHandler) m.Use(hs.metricsEndpoint) - m.Use(middleware.GetContextHandler()) + m.Use(middleware.GetContextHandler(hs.AuthTokenService)) m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime)) m.Use(middleware.OrgRedirect()) diff --git a/pkg/api/login.go b/pkg/api/login.go index f0902a60f58..37b12d03299 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -42,10 +42,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { return } - if !hs.tryLoginUsingRememberCookie(c) { - c.HTML(200, ViewIndex, viewData) - return - } + //if !hs.tryLoginUsingRememberCookie(c) { + c.HTML(200, ViewIndex, viewData) + return + //} if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 { c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/") diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index ace72d998eb..28f08869425 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -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" + "gopkg.in/macaron.v1" ) var ( @@ -21,7 +21,7 @@ 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, @@ -49,7 +49,8 @@ func GetContextHandler() macaron.Handler { case initContextWithApiKey(ctx): case initContextWithBasicAuth(ctx, orgId): case initContextWithAuthProxy(ctx, orgId): - case initContextWithUserSessionCookie(ctx, orgId): + //case initContextWithUserSessionCookie(ctx, orgId): + case initContextWithToken(ctx, orgId, ats): case initContextWithAnonymousUser(ctx): } @@ -58,6 +59,11 @@ func GetContextHandler() macaron.Handler { c.Map(ctx) + c.Next() + + //if signed in with token + //ats.RefreshToken() + // update last seen every 5min if ctx.ShouldUpdateLastSeenAt() { ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId) @@ -88,6 +94,25 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool { return true } +func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthTokenService) bool { + user, err := ts.LookupToken(ctx) + if err != nil { + ctx.Logger.Info("failed to look up user based on cookie") + return false + } + + query := m.GetSignedInUserQuery{UserId: user.UserId, OrgId: orgID} + if err := bus.Dispatch(&query); err != nil { + ctx.Logger.Error("Failed to get user with id", "userId", user.UserId, "error", err) + return false + } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + + return true +} + func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool { // initialize session if err := ctx.Session.Start(ctx.Context); err != nil { diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index db263c2a17a..ca63733946c 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -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" ) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index aefcccadbd6..d812124f1c1 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -3,16 +3,17 @@ package auth import ( "crypto/sha256" "encoding/hex" + "fmt" + "net/http" + "net/url" "time" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" - macaron "gopkg.in/macaron.v1" - "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() { @@ -42,7 +43,15 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model } c.Resp.Header().Del("Set-Cookie") - c.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) + cookie := http.Cookie{ + Name: sessionCookieKey, + Value: url.QueryEscape(userToken.unhashedToken), + HttpOnly: true, + Expires: time.Now().Add(time.Minute * 10), + Domain: setting.Domain, + } + + c.Resp.Header().Add("Set-Cookie", cookie.String()) return nil } @@ -51,27 +60,27 @@ func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true) } -func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler { - return func(ctx *models.ReqContext) { - authToken := ctx.GetCookie(sessionCookieKey) - userToken, err := s.lookupToken(authToken) - if err != nil { +// func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler { +// return func(ctx *models.ReqContext) { +// authToken := ctx.GetCookie(sessionCookieKey) +// userToken, err := s.LookupToken(authToken) +// if err != nil { - } +// } - ctx.Next() +// ctx.Next() - refreshed, err := s.refreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) - if err != nil { +// refreshed, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) +// if err != nil { - } +// } - if refreshed { - ctx.Resp.Header().Del("Set-Cookie") - ctx.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) - } - } -} +// if refreshed { +// ctx.Resp.Header().Del("Set-Cookie") +// ctx.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) +// } +// } +// } func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { clientIP = util.ParseIPAddress(clientIP) @@ -104,7 +113,12 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str return &userToken, nil } -func (s *UserAuthTokenService) lookupToken(unhashedToken string) (*userAuthToken, error) { +func (s *UserAuthTokenService) LookupToken(ctx *models.ReqContext) (*userAuthToken, error) { + unhashedToken := ctx.GetCookie(sessionCookieKey) + if unhashedToken == "" { + return nil, fmt.Errorf("session token cookie is empty") + } + hashedToken := hashToken(unhashedToken) var userToken userAuthToken @@ -157,7 +171,7 @@ func (s *UserAuthTokenService) lookupToken(unhashedToken string) (*userAuthToken return &userToken, nil } -func (s *UserAuthTokenService) refreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { +func (s *UserAuthTokenService) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { // lookup token in db // refresh token if needed diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 2e4618c10ed..4a0ca952bd1 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -27,22 +27,22 @@ func TestUserAuthToken(t *testing.T) { So(token.AuthTokenSeen, ShouldBeFalse) Convey("When lookup unhashed token should return user auth token", func() { - lookupToken, err := userAuthTokenService.lookupToken(token.unhashedToken) + LookupToken, err := userAuthTokenService.LookupToken(token.unhashedToken) So(err, ShouldBeNil) - So(lookupToken, ShouldNotBeNil) - So(lookupToken.UserId, ShouldEqual, userID) - So(lookupToken.AuthTokenSeen, ShouldBeTrue) + So(LookupToken, ShouldNotBeNil) + So(LookupToken.UserId, ShouldEqual, userID) + So(LookupToken.AuthTokenSeen, ShouldBeTrue) - storedAuthToken, err := ctx.getAuthTokenByID(lookupToken.Id) + 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) + LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken) So(err, ShouldEqual, ErrAuthTokenNotFound) - So(lookupToken, ShouldBeNil) + So(LookupToken, ShouldBeNil) }) }) @@ -51,25 +51,25 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(token, ShouldNotBeNil) - _, err = userAuthTokenService.lookupToken(token.unhashedToken) + _, err = userAuthTokenService.LookupToken(token.unhashedToken) So(err, ShouldBeNil) token, err = ctx.getAuthTokenByID(token.Id) So(err, ShouldBeNil) // set now (now - 23 hours) - _, err = userAuthTokenService.refreshToken(token, "192.168.10.11:1234", "some user agent") + _, err = userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) - _, err = userAuthTokenService.lookupToken(token.unhashedToken) + _, err = userAuthTokenService.LookupToken(token.unhashedToken) So(err, ShouldBeNil) - stillGood, err := userAuthTokenService.lookupToken(token.unhashedToken) + stillGood, err := userAuthTokenService.LookupToken(token.unhashedToken) So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) // set now (new - 2 hours) - notGood, err := userAuthTokenService.lookupToken(token.unhashedToken) + notGood, err := userAuthTokenService.LookupToken(token.unhashedToken) So(err, ShouldEqual, ErrAuthTokenNotFound) So(notGood, ShouldBeNil) }) @@ -82,7 +82,7 @@ func TestUserAuthToken(t *testing.T) { prevToken := token.AuthToken unhashedPrev := token.unhashedToken - refreshed, err := userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent") + refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) So(refreshed, ShouldBeFalse) @@ -95,7 +95,7 @@ func TestUserAuthToken(t *testing.T) { return t } - refreshed, err = userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent") + refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) So(refreshed, ShouldBeTrue) @@ -112,13 +112,13 @@ func TestUserAuthToken(t *testing.T) { So(token.SeenAt, ShouldEqual, 0) So(token.PrevAuthToken, ShouldEqual, prevToken) - lookedUp, err := userAuthTokenService.lookupToken(token.unhashedToken) + lookedUp, err := userAuthTokenService.LookupToken(token.unhashedToken) So(err, ShouldBeNil) So(lookedUp, ShouldNotBeNil) So(lookedUp.AuthTokenSeen, ShouldBeTrue) So(lookedUp.SeenAt, ShouldEqual, t.Unix()) - lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev) + lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) So(err, ShouldBeNil) So(lookedUp, ShouldNotBeNil) So(lookedUp.Id, ShouldEqual, token.Id) @@ -127,7 +127,7 @@ func TestUserAuthToken(t *testing.T) { return t.Add(2 * time.Minute) } - lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev) + lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) So(err, ShouldBeNil) So(lookedUp, ShouldNotBeNil) @@ -136,7 +136,7 @@ func TestUserAuthToken(t *testing.T) { So(lookedUp, ShouldNotBeNil) So(lookedUp.AuthTokenSeen, ShouldBeFalse) - refreshed, err = userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent") + refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) So(refreshed, ShouldBeTrue) From c2accfa4c031617ca55afb3ea9fca0ec1e0ea889 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 17 Jan 2019 17:11:52 +0100 Subject: [PATCH 011/167] inital code for rotate --- pkg/middleware/middleware.go | 35 +++++++++++- pkg/models/context.go | 21 ++++++- pkg/services/auth/auth_token.go | 83 ++++++++++++++++++++++------ pkg/services/auth/auth_token_test.go | 23 ++++---- pkg/services/auth/model.go | 28 +++++----- 5 files changed, 141 insertions(+), 49 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 28f08869425..57e47bd2860 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,7 +1,10 @@ package middleware import ( + "net/http" + "net/url" "strconv" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" @@ -11,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" ) var ( @@ -62,7 +65,27 @@ func GetContextHandler(ats *auth.UserAuthTokenService) macaron.Handler { c.Next() //if signed in with token - //ats.RefreshToken() + rotated, err := ats.RefreshToken(ctx.UserToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + ctx.Logger.Error("failed to rotate token", "error", err) + return + } + + if rotated { + ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) + //c.SetCookie("grafana_session", url.QueryEscape(ctx.UserToken.UnhashedToken), nil, setting.AppSubUrl+"/", setting.Domain, false, true) + // ctx.Resp.Header().Del("Set-Cookie") + cookie := http.Cookie{ + Name: "grafana_session", + Value: url.QueryEscape(ctx.UserToken.UnhashedToken), + HttpOnly: true, + MaxAge: int(time.Minute * 10), + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", + } + + ctx.Resp.Header().Add("Set-Cookie", cookie.String()) + } // update last seen every 5min if ctx.ShouldUpdateLastSeenAt() { @@ -95,7 +118,12 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool { } func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthTokenService) bool { - user, err := ts.LookupToken(ctx) + unhashedToken := ctx.GetCookie("grafana_session") + if unhashedToken == "" { + return false + } + + user, err := ts.LookupToken(unhashedToken) if err != nil { ctx.Logger.Info("failed to look up user based on cookie") return false @@ -109,6 +137,7 @@ func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthToken ctx.SignedInUser = query.Result ctx.IsSignedIn = true + ctx.UserToken = user return true } diff --git a/pkg/models/context.go b/pkg/models/context.go index 7cb80a957c3..f6df8b1c2f0 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -3,17 +3,32 @@ 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 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:"-"` +} + type ReqContext struct { *macaron.Context *SignedInUser + UserToken *UserAuthToken Session session.SessionStore diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index d812124f1c1..e393239ef9d 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -3,7 +3,6 @@ package auth import ( "crypto/sha256" "encoding/hex" - "fmt" "net/http" "net/url" "time" @@ -45,10 +44,11 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model c.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ Name: sessionCookieKey, - Value: url.QueryEscape(userToken.unhashedToken), + Value: url.QueryEscape(userToken.UnhashedToken), HttpOnly: true, - Expires: time.Now().Add(time.Minute * 10), + MaxAge: int(time.Minute * 10), Domain: setting.Domain, + Path: setting.AppSubUrl + "/", } c.Resp.Header().Add("Set-Cookie", cookie.String()) @@ -57,7 +57,18 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model } func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { - c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true) + //c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true) + c.Resp.Header().Del("Set-Cookie") + cookie := http.Cookie{ + Name: sessionCookieKey, + Value: "", + HttpOnly: true, + MaxAge: -1, + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", + } + + c.Resp.Header().Add("Set-Cookie", cookie.String()) } // func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler { @@ -82,7 +93,7 @@ func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { // } // } -func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { +func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserAuthToken, error) { clientIP = util.ParseIPAddress(clientIP) token, err := util.RandomHex(16) if err != nil { @@ -91,7 +102,7 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str hashedToken := hashToken(token) - userToken := userAuthToken{ + userToken := models.UserAuthToken{ UserId: userId, AuthToken: hashedToken, PrevAuthToken: hashedToken, @@ -108,20 +119,15 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str return nil, err } - userToken.unhashedToken = token + userToken.UnhashedToken = token return &userToken, nil } -func (s *UserAuthTokenService) LookupToken(ctx *models.ReqContext) (*userAuthToken, error) { - unhashedToken := ctx.GetCookie(sessionCookieKey) - if unhashedToken == "" { - return nil, fmt.Errorf("session token cookie is empty") - } - +func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAuthToken, error) { hashedToken := hashToken(unhashedToken) - var userToken userAuthToken + var userToken models.UserAuthToken exists, err := s.SQLStore.NewSession().Where("auth_token = ? OR prev_auth_token = ?", hashedToken, hashedToken).Get(&userToken) if err != nil { return nil, err @@ -166,14 +172,55 @@ func (s *UserAuthTokenService) LookupToken(ctx *models.ReqContext) (*userAuthTok } } - userToken.unhashedToken = unhashedToken + userToken.UnhashedToken = unhashedToken return &userToken, nil } -func (s *UserAuthTokenService) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { - // lookup token in db - // refresh token if needed +func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientIP, userAgent string) (bool, error) { + if token == nil { + return false, nil + } + + var needsRotation = false + rotatedAt := time.Unix(token.RotatedAt, 0) + if token.AuthTokenSeen { + needsRotation = rotatedAt.Before(now().Add(time.Duration(-1) * time.Minute)) + } else { + needsRotation = rotatedAt.Before(now().Add(time.Duration(-30) * time.Second)) + } + + s.log.Info("refresh token", "needs rotation?", needsRotation, "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) + if !needsRotation { + return false, nil + } + + newToken, _ := util.RandomHex(16) + hashedToken := hashToken(newToken) + + sql := ` + UPDATE user_auth_token + SET + auth_token_seen = false, + seen_at = null, + user_agent = ?, + client_ip = ?, + prev_auth_token = case when auth_token_seen then auth_token else prev_auth_token end, + auth_token = ?, + rotated_at = ? + WHERE id = ? AND (auth_token_seen or rotated_at < ?)` + + res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, hashedToken, now().Unix(), token.Id, now().Add(time.Duration(-30)*time.Second)) + if err != nil { + return false, err + } + + affected, _ := res.RowsAffected() + s.log.Info("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId, "user_agent", userAgent, "client_ip", clientIP) + if affected > 0 { + token.UnhashedToken = newToken + return true, nil + } return false, nil } diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 4a0ca952bd1..2ee7e2d67be 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" . "github.com/smartystreets/goconvey/convey" ) @@ -27,7 +28,7 @@ func TestUserAuthToken(t *testing.T) { So(token.AuthTokenSeen, ShouldBeFalse) Convey("When lookup unhashed token should return user auth token", func() { - LookupToken, err := userAuthTokenService.LookupToken(token.unhashedToken) + LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldBeNil) So(LookupToken, ShouldNotBeNil) So(LookupToken.UserId, ShouldEqual, userID) @@ -51,7 +52,7 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(token, ShouldNotBeNil) - _, err = userAuthTokenService.LookupToken(token.unhashedToken) + _, err = userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldBeNil) token, err = ctx.getAuthTokenByID(token.Id) @@ -61,15 +62,15 @@ func TestUserAuthToken(t *testing.T) { _, err = userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) - _, err = userAuthTokenService.LookupToken(token.unhashedToken) + _, err = userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldBeNil) - stillGood, err := userAuthTokenService.LookupToken(token.unhashedToken) + stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) // set now (new - 2 hours) - notGood, err := userAuthTokenService.LookupToken(token.unhashedToken) + notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldEqual, ErrAuthTokenNotFound) So(notGood, ShouldBeNil) }) @@ -80,7 +81,7 @@ func TestUserAuthToken(t *testing.T) { So(token, ShouldNotBeNil) prevToken := token.AuthToken - unhashedPrev := token.unhashedToken + unhashedPrev := token.UnhashedToken refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) @@ -99,11 +100,11 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(refreshed, ShouldBeTrue) - unhashedToken := token.unhashedToken + unhashedToken := token.UnhashedToken token, err = ctx.getAuthTokenByID(token.Id) So(err, ShouldBeNil) - token.unhashedToken = unhashedToken + token.UnhashedToken = unhashedToken So(token.RotatedAt, ShouldEqual, t.Unix()) So(token.ClientIp, ShouldEqual, "192.168.10.12") @@ -112,7 +113,7 @@ func TestUserAuthToken(t *testing.T) { So(token.SeenAt, ShouldEqual, 0) So(token.PrevAuthToken, ShouldEqual, prevToken) - lookedUp, err := userAuthTokenService.LookupToken(token.unhashedToken) + lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldBeNil) So(lookedUp, ShouldNotBeNil) So(lookedUp.AuthTokenSeen, ShouldBeTrue) @@ -180,9 +181,9 @@ type testContext struct { tokenService *UserAuthTokenService } -func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { +func (c *testContext) getAuthTokenByID(id int64) (*models.UserAuthToken, error) { sess := c.sqlstore.NewSession() - var t userAuthToken + var t models.UserAuthToken found, err := sess.ID(id).Get(&t) if err != nil || !found { return nil, err diff --git a/pkg/services/auth/model.go b/pkg/services/auth/model.go index a033b96be31..4347f6d2d6a 100644 --- a/pkg/services/auth/model.go +++ b/pkg/services/auth/model.go @@ -9,17 +9,17 @@ 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:"-"` -} +// 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:"-"` +// } From 8b3fe41b0a9c7cba9a1d77f7c6ccafbd43949462 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 17 Jan 2019 17:32:33 +0100 Subject: [PATCH 012/167] log fix --- pkg/api/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index 37b12d03299..b4c6f8af58e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -168,7 +168,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { err := hs.AuthTokenService.UserAuthenticatedHook(user, c) if err != nil { - hs.log.Error("User auth hook failed", err) + hs.log.Error("User auth hook failed", "error", err) } } From 97c7963f176576c39d2bc5f7921e25dcf1ce29ac Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 17 Jan 2019 20:27:53 +0100 Subject: [PATCH 013/167] fix cannot set cookie when response is written --- pkg/middleware/middleware.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 57e47bd2860..aad47fccdac 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -62,8 +62,6 @@ func GetContextHandler(ats *auth.UserAuthTokenService) macaron.Handler { c.Map(ctx) - c.Next() - //if signed in with token rotated, err := ats.RefreshToken(ctx.UserToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) if err != nil { From 81879f0162652bf51e6f760df7ae0854766a3bac Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 17 Jan 2019 20:29:26 +0100 Subject: [PATCH 014/167] fix broken code --- pkg/api/common_test.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/middleware/recovery_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 8b66a7a468b..f99902aac51 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -123,7 +123,7 @@ func setupScenarioContext(url string) *scenarioContext { Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) - sc.m.Use(middleware.GetContextHandler()) + sc.m.Use(middleware.GetContextHandler(nil)) sc.m.Use(middleware.Sessioner(&session.Options{}, 0)) return sc diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index b9a8afce6c6..73c84af09fd 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -487,7 +487,7 @@ func middlewareScenario(desc string, fn scenarioFunc) { Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) - sc.m.Use(GetContextHandler()) + sc.m.Use(GetContextHandler(nil)) // mock out gc goroutine session.StartSessionGC = func() {} sc.m.Use(Sessioner(&ms.Options{}, 0)) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index c92150f3b7d..5e70fffc45e 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -64,7 +64,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) - sc.m.Use(GetContextHandler()) + sc.m.Use(GetContextHandler(nil)) // mock out gc goroutine session.StartSessionGC = func() {} sc.m.Use(Sessioner(&ms.Options{}, 0)) From fd937e3d95ce817292dfc4a6df53dee07cad726a Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 17 Jan 2019 21:03:27 +0100 Subject: [PATCH 015/167] remove maxage from session token --- pkg/middleware/middleware.go | 10 ++++------ pkg/services/auth/auth_token.go | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index aad47fccdac..60869d7bd1f 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -4,7 +4,6 @@ import ( "net/http" "net/url" "strconv" - "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" @@ -71,15 +70,14 @@ func GetContextHandler(ats *auth.UserAuthTokenService) macaron.Handler { if rotated { ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) - //c.SetCookie("grafana_session", url.QueryEscape(ctx.UserToken.UnhashedToken), nil, setting.AppSubUrl+"/", setting.Domain, false, true) - // ctx.Resp.Header().Del("Set-Cookie") + ctx.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ Name: "grafana_session", Value: url.QueryEscape(ctx.UserToken.UnhashedToken), HttpOnly: true, - MaxAge: int(time.Minute * 10), - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", + //MaxAge: 600, + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", } ctx.Resp.Header().Add("Set-Cookie", cookie.String()) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index e393239ef9d..1b2c7307923 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -46,9 +46,9 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model Name: sessionCookieKey, Value: url.QueryEscape(userToken.UnhashedToken), HttpOnly: true, - MaxAge: int(time.Minute * 10), - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", + //MaxAge: 600, + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", } c.Resp.Header().Add("Set-Cookie", cookie.String()) From 35013a28cde302c7d37349737ed326ffb592679c Mon Sep 17 00:00:00 2001 From: sharkpc0813 Date: Wed, 16 Jan 2019 15:30:35 +0900 Subject: [PATCH 016/167] add timeout test for alert handling. --- pkg/services/alerting/engine_test.go | 127 +++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go index 63108bbb9aa..7c2a0888c04 100644 --- a/pkg/services/alerting/engine_test.go +++ b/pkg/services/alerting/engine_test.go @@ -4,7 +4,11 @@ import ( "context" "errors" "math" + "net" + "net/http" + "net/http/httptest" "testing" + "time" . "github.com/smartystreets/goconvey/convey" ) @@ -34,6 +38,106 @@ func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error { return nil } +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) +} func TestEngineProcessJob(t *testing.T) { Convey("Alerting engine job processing", t, func() { engine := NewEngine() @@ -113,6 +217,29 @@ func TestEngineProcessJob(t *testing.T) { engine.processJobWithRetry(context.TODO(), job) So(evalHandler.CallNb, ShouldEqual, expectedAttempts) }) + + Convey("pended alert for datasource -> result handler should be worked", func() { + // reduce alert timeout to test quickly + originAlertTimeout := alertTimeout + alertTimeout = 5 * time.Second + transportTimeoutInterval := 5 * time.Second + serverBusySleepDuration := 4 * 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{} + }) + }) }) } From 4712a87733ebf870950c932ef2802c4f2e15f9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 16:37:43 +0100 Subject: [PATCH 017/167] Revert "Specify expected encoding for access/secret key" --- docs/sources/http_api/data_source.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 9aaf29ec5f4..364b55b0cfc 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "defaultRegion": "us-west-1" }, "secureJsonData": { - "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded - "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded + "accessKey": "Ol4pIDpeKSA6XikgOl4p", + "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" } } ``` From 382a12c0fd2da62525c66a9a440a4c4fceafc4c7 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Sat, 19 Jan 2019 16:58:26 +0100 Subject: [PATCH 018/167] Add loop counter for full refresh in playlist --- public/app/features/playlist/playlist_srv.ts | 22 ++-- .../playlist/specs/playlist_srv.test.ts | 103 ++++++++++++++++++ 2 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 public/app/features/playlist/specs/playlist_srv.test.ts diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index 9d3b635a1e5..0a80ce0cdf0 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -4,12 +4,13 @@ import appEvents from 'app/core/app_events'; import _ from 'lodash'; import { toUrlParams } from 'app/core/utils/url'; -class PlaylistSrv { +export class PlaylistSrv { private cancelPromise: any; - private dashboards: any; + private dashboards: Array<{ uri: string }>; private index: number; - private interval: any; + private interval: number; private startUrl: string; + private numberOfLoops = 0; isPlaying: boolean; /** @ngInject */ @@ -20,8 +21,15 @@ class PlaylistSrv { const playedAllDashboards = this.index > this.dashboards.length - 1; if (playedAllDashboards) { - window.location.href = this.startUrl; - return; + this.numberOfLoops++; + + // This does full reload of the playlist to keep memory in check due to existing leaks but at the same time + // we do not want page to flicker after each full loop. + if (this.numberOfLoops >= 3) { + window.location.href = this.startUrl; + return; + } + this.index = 0; } const dash = this.dashboards[this.index]; @@ -46,8 +54,8 @@ class PlaylistSrv { this.index = 0; this.isPlaying = true; - this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => { - this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => { + return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => { + return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => { this.dashboards = dashboards; this.interval = kbn.interval_to_ms(playlist.interval); this.next(); diff --git a/public/app/features/playlist/specs/playlist_srv.test.ts b/public/app/features/playlist/specs/playlist_srv.test.ts new file mode 100644 index 00000000000..e6b7671c964 --- /dev/null +++ b/public/app/features/playlist/specs/playlist_srv.test.ts @@ -0,0 +1,103 @@ +import { PlaylistSrv } from '../playlist_srv'; + +const dashboards = [{ uri: 'dash1' }, { uri: 'dash2' }]; + +const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance }] => { + const mockBackendSrv = { + get: jest.fn(url => { + switch (url) { + case '/api/playlists/1': + return Promise.resolve({ interval: '1s' }); + case '/api/playlists/1/dashboards': + return Promise.resolve(dashboards); + default: + throw new Error(`Unexpected url=${url}`); + } + }), + }; + + const mockLocation = { + url: jest.fn(), + search: () => ({}), + }; + + const mockTimeout = jest.fn(); + (mockTimeout as any).cancel = jest.fn(); + + return [new PlaylistSrv(mockLocation, mockTimeout, mockBackendSrv), mockLocation]; +}; + +const mockWindowLocation = (): [jest.MockInstance, () => void] => { + const oldLocation = window.location; + const hrefMock = jest.fn(); + + // JSDom defines window in a way that you cannot tamper with location so this seems to be the only way to change it. + // https://github.com/facebook/jest/issues/5124#issuecomment-446659510 + delete window.location; + window.location = {} as any; + + // Only mocking href as that is all this test needs, but otherwise there is lots of things missing, so keep that + // in mind if this is reused. + Object.defineProperty(window.location, 'href', { + set: hrefMock, + get: hrefMock, + }); + const unmock = () => { + window.location = oldLocation; + }; + return [hrefMock, unmock]; +}; + +describe('PlaylistSrv', () => { + let srv: PlaylistSrv; + let mockLocationService: { url: jest.MockInstance }; + let hrefMock: jest.MockInstance; + let unmockLocation: () => void; + const initialUrl = 'http://localhost/playlist'; + + beforeEach(() => { + [srv, mockLocationService] = createPlaylistSrv(); + [hrefMock, unmockLocation] = mockWindowLocation(); + + // This will be cached in the srv when start() is called + hrefMock.mockReturnValue(initialUrl); + }); + + afterEach(() => { + unmockLocation(); + }); + + it('runs all dashboards in cycle and reloads page after 3 cycles', async () => { + await srv.start(1); + + for (let i = 0; i < 6; i++) { + expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); + srv.next(); + } + + expect(hrefMock).toHaveBeenCalledTimes(2); + expect(hrefMock).toHaveBeenLastCalledWith(initialUrl); + }); + + it('keeps the refresh counter value after restarting', async () => { + await srv.start(1); + + // 1 complete loop + for (let i = 0; i < 3; i++) { + expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); + srv.next(); + } + + srv.stop(); + await srv.start(1); + + // Another 2 loops + for (let i = 0; i < 4; i++) { + expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); + srv.next(); + } + + expect(hrefMock).toHaveBeenCalledTimes(3); + expect(hrefMock).toHaveBeenLastCalledWith(initialUrl); + }); +}); From 47a7d93fd9a1569d61800a2399158267f16e9684 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Jan 2019 08:59:01 +0100 Subject: [PATCH 019/167] moves rotation into auth since both happens before c.Next() --- pkg/middleware/middleware.go | 44 ++++++++++++++++----------------- pkg/services/auth/auth_token.go | 4 +-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 60869d7bd1f..0635ad55c64 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -61,28 +61,6 @@ func GetContextHandler(ats *auth.UserAuthTokenService) macaron.Handler { c.Map(ctx) - //if signed in with token - rotated, err := ats.RefreshToken(ctx.UserToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) - if err != nil { - ctx.Logger.Error("failed to rotate token", "error", err) - return - } - - if rotated { - ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) - ctx.Resp.Header().Del("Set-Cookie") - cookie := http.Cookie{ - Name: "grafana_session", - Value: url.QueryEscape(ctx.UserToken.UnhashedToken), - HttpOnly: true, - //MaxAge: 600, - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", - } - - ctx.Resp.Header().Add("Set-Cookie", cookie.String()) - } - // update last seen every 5min if ctx.ShouldUpdateLastSeenAt() { ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId) @@ -114,6 +92,7 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool { } func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthTokenService) bool { + //auth User unhashedToken := ctx.GetCookie("grafana_session") if unhashedToken == "" { return false @@ -135,6 +114,27 @@ func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthToken ctx.IsSignedIn = true ctx.UserToken = user + //rotate session token if needed. + rotated, err := ts.RefreshToken(ctx.UserToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + ctx.Logger.Error("failed to rotate token", "error", err, "user.id", user.UserId, "user_token.id", user.Id) + return true + } + + if rotated { + ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) + ctx.Resp.Header().Del("Set-Cookie") + cookie := http.Cookie{ + Name: "grafana_session", + Value: url.QueryEscape(ctx.UserToken.UnhashedToken), + HttpOnly: true, + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", + } + + ctx.Resp.Header().Add("Set-Cookie", cookie.String()) + } + return true } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 1b2c7307923..5a5b5fb005c 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -190,7 +190,7 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI needsRotation = rotatedAt.Before(now().Add(time.Duration(-30) * time.Second)) } - s.log.Info("refresh token", "needs rotation?", needsRotation, "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) + s.log.Debug("refresh token", "needs rotation?", needsRotation, "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) if !needsRotation { return false, nil } @@ -216,7 +216,7 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI } affected, _ := res.RowsAffected() - s.log.Info("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId, "user_agent", userAgent, "client_ip", clientIP) + s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId, "user_agent", userAgent, "client_ip", clientIP) if affected > 0 { token.UnhashedToken = newToken return true, nil From 2e97d39abedf041968b8acf396b1e8cc315ba450 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Jan 2019 10:01:48 +0100 Subject: [PATCH 020/167] removes commented code --- pkg/services/auth/auth_token.go | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 5a5b5fb005c..db5b938e0fb 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -46,9 +46,8 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model Name: sessionCookieKey, Value: url.QueryEscape(userToken.UnhashedToken), HttpOnly: true, - //MaxAge: 600, - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", } c.Resp.Header().Add("Set-Cookie", cookie.String()) @@ -57,7 +56,6 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model } func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { - //c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true) c.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ Name: sessionCookieKey, @@ -71,28 +69,6 @@ func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { c.Resp.Header().Add("Set-Cookie", cookie.String()) } -// func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler { -// return func(ctx *models.ReqContext) { -// authToken := ctx.GetCookie(sessionCookieKey) -// userToken, err := s.LookupToken(authToken) -// if err != nil { - -// } - -// ctx.Next() - -// refreshed, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) -// if err != nil { - -// } - -// if refreshed { -// ctx.Resp.Header().Del("Set-Cookie") -// ctx.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) -// } -// } -// } - func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserAuthToken, error) { clientIP = util.ParseIPAddress(clientIP) token, err := util.RandomHex(16) From 0495499b4f0f82eae5599b4c44e06dcb9030771e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 09:13:55 +0100 Subject: [PATCH 021/167] fix ip address parsing of loopback address --- pkg/util/ip_address.go | 6 ++++-- pkg/util/ip_address_test.go | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/util/ip_address.go b/pkg/util/ip_address.go index 4e9a9378c6b..d8d95ef3acd 100644 --- a/pkg/util/ip_address.go +++ b/pkg/util/ip_address.go @@ -7,11 +7,13 @@ import ( // ParseIPAddress parses an IP address and removes port and/or IPV6 format func ParseIPAddress(input string) string { - var s string + s := input lastIndex := strings.LastIndex(input, ":") if lastIndex != -1 { - s = input[:lastIndex] + if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" { + s = input[:lastIndex] + } } s = strings.Replace(s, "[", "", -1) diff --git a/pkg/util/ip_address_test.go b/pkg/util/ip_address_test.go index 644340a5e82..fd3e3ea8587 100644 --- a/pkg/util/ip_address_test.go +++ b/pkg/util/ip_address_test.go @@ -10,5 +10,7 @@ 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") }) } From f3125b447bc1f578f06d00a7c7a68cc80390d3dd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 09:53:53 +0100 Subject: [PATCH 022/167] dead code --- pkg/middleware/middleware.go | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 0635ad55c64..109def9ff2c 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -138,28 +138,28 @@ func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthToken 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 - } +// 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 - } +// 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 - } +// 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 -} +// ctx.SignedInUser = query.Result +// ctx.IsSignedIn = true +// return true +// } func initContextWithApiKey(ctx *m.ReqContext) bool { var keyString string From 0d1e3759ebbaf74644b32a96c791461d0c0abdce Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 10:20:06 +0100 Subject: [PATCH 023/167] mixor fixes --- pkg/middleware/middleware.go | 2 +- pkg/services/auth/auth_token.go | 23 +++++++++++++---------- pkg/services/auth/auth_token_test.go | 3 +++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 109def9ff2c..a6800971f4f 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -132,7 +132,7 @@ func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthToken Path: setting.AppSubUrl + "/", } - ctx.Resp.Header().Add("Set-Cookie", cookie.String()) + http.SetCookie(ctx.Resp, &cookie) } return true diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index db5b938e0fb..aefacd7788d 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -19,7 +19,11 @@ func init() { registry.RegisterService(&UserAuthTokenService{}) } -var now = time.Now +var ( + now = time.Now + RotateTime = 10 * time.Second + UrgentRotateTime = 5 * time.Second +) // UserAuthTokenService are used for generating and validating user auth tokens type UserAuthTokenService struct { @@ -50,7 +54,7 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model Path: setting.AppSubUrl + "/", } - c.Resp.Header().Add("Set-Cookie", cookie.String()) + http.SetCookie(c.Resp, &cookie) return nil } @@ -61,12 +65,10 @@ func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { Name: sessionCookieKey, Value: "", HttpOnly: true, - MaxAge: -1, Domain: setting.Domain, Path: setting.AppSubUrl + "/", } - - c.Resp.Header().Add("Set-Cookie", cookie.String()) + http.SetCookie(c.Resp, &cookie) } func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserAuthToken, error) { @@ -115,7 +117,7 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { userToken.AuthTokenSeen = false - expireBefore := now().Add(-1 * time.Minute).Unix() + expireBefore := now().Add(-RotateTime).Unix() affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userToken.Id, userToken.PrevAuthToken, expireBefore).AllCols().Update(&userToken) if err != nil { return nil, err @@ -158,12 +160,12 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI return false, nil } - var needsRotation = false + needsRotation := false rotatedAt := time.Unix(token.RotatedAt, 0) if token.AuthTokenSeen { - needsRotation = rotatedAt.Before(now().Add(time.Duration(-1) * time.Minute)) + needsRotation = rotatedAt.Before(now().Add(-RotateTime)) } else { - needsRotation = rotatedAt.Before(now().Add(time.Duration(-30) * time.Second)) + needsRotation = rotatedAt.Before(now().Add(-UrgentRotateTime)) } s.log.Debug("refresh token", "needs rotation?", needsRotation, "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) @@ -171,6 +173,7 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI return false, nil } + clientIP = util.ParseIPAddress(clientIP) newToken, _ := util.RandomHex(16) hashedToken := hashToken(newToken) @@ -186,7 +189,7 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI rotated_at = ? WHERE id = ? AND (auth_token_seen or rotated_at < ?)` - res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, hashedToken, now().Unix(), token.Id, now().Add(time.Duration(-30)*time.Second)) + res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, hashedToken, now().Unix(), token.Id, now().Add(-UrgentRotateTime)) if err != nil { return false, err } diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 2ee7e2d67be..bb146252fa4 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -170,6 +170,9 @@ func createTestContext(t *testing.T) *testContext { log: log.New("test-logger"), } + RotateTime = 10 * time.Minute + UrgentRotateTime = time.Minute + return &testContext{ sqlstore: sqlstore, tokenService: tokenService, From 766cfab374bd8ed7aad8870fd5c28734c92a6505 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 10:22:18 +0100 Subject: [PATCH 024/167] change rotate time --- pkg/services/auth/auth_token.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index aefacd7788d..1c687841f7e 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -21,8 +21,8 @@ func init() { var ( now = time.Now - RotateTime = 10 * time.Second - UrgentRotateTime = 5 * time.Second + RotateTime = 1 * time.Minute + UrgentRotateTime = 30 * time.Second ) // UserAuthTokenService are used for generating and validating user auth tokens From 734a7d38b28c15849389328cbf7ca0d3286f198c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 11:21:43 +0100 Subject: [PATCH 025/167] set cookie name from configuration --- pkg/middleware/middleware.go | 2 +- pkg/services/auth/auth_token.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index a6800971f4f..6cec2b9ad05 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -125,7 +125,7 @@ func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthToken ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) ctx.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ - Name: "grafana_session", + Name: setting.SessionOptions.CookieName, Value: url.QueryEscape(ctx.UserToken.UnhashedToken), HttpOnly: true, Domain: setting.Domain, diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 1c687841f7e..5929043573e 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -37,8 +37,6 @@ func (s *UserAuthTokenService) Init() error { return nil } -const sessionCookieKey = "grafana_session" - func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error { userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) if err != nil { @@ -47,11 +45,12 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model c.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ - Name: sessionCookieKey, + Name: setting.SessionOptions.CookieName, Value: url.QueryEscape(userToken.UnhashedToken), HttpOnly: true, Domain: setting.Domain, Path: setting.AppSubUrl + "/", + Secure: setting.SessionOptions.Secure, } http.SetCookie(c.Resp, &cookie) @@ -62,11 +61,12 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { c.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ - Name: sessionCookieKey, + Name: setting.SessionOptions.CookieName, Value: "", HttpOnly: true, Domain: setting.Domain, Path: setting.AppSubUrl + "/", + Secure: setting.SessionOptions.Secure, } http.SetCookie(c.Resp, &cookie) } From 55b3013eb398e70bd4545e15b028c58fcf436348 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Jan 2019 11:37:44 +0100 Subject: [PATCH 026/167] moves initWithToken to auth package --- pkg/middleware/middleware.go | 52 +--------------------- pkg/services/auth/auth_token.go | 78 +++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 73 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 6cec2b9ad05..6c4ce1c20ae 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,8 +1,6 @@ package middleware import ( - "net/http" - "net/url" "strconv" "github.com/grafana/grafana/pkg/bus" @@ -51,8 +49,7 @@ func GetContextHandler(ats *auth.UserAuthTokenService) macaron.Handler { case initContextWithApiKey(ctx): case initContextWithBasicAuth(ctx, orgId): case initContextWithAuthProxy(ctx, orgId): - //case initContextWithUserSessionCookie(ctx, orgId): - case initContextWithToken(ctx, orgId, ats): + case ats.InitContextWithToken(ctx, orgId): case initContextWithAnonymousUser(ctx): } @@ -91,53 +88,6 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool { return true } -func initContextWithToken(ctx *m.ReqContext, orgID int64, ts *auth.UserAuthTokenService) bool { - //auth User - unhashedToken := ctx.GetCookie("grafana_session") - if unhashedToken == "" { - return false - } - - user, err := ts.LookupToken(unhashedToken) - if err != nil { - ctx.Logger.Info("failed to look up user based on cookie") - return false - } - - query := m.GetSignedInUserQuery{UserId: user.UserId, OrgId: orgID} - if err := bus.Dispatch(&query); err != nil { - ctx.Logger.Error("Failed to get user with id", "userId", user.UserId, "error", err) - return false - } - - ctx.SignedInUser = query.Result - ctx.IsSignedIn = true - ctx.UserToken = user - - //rotate session token if needed. - rotated, err := ts.RefreshToken(ctx.UserToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) - if err != nil { - ctx.Logger.Error("failed to rotate token", "error", err, "user.id", user.UserId, "user_token.id", user.Id) - return true - } - - if rotated { - ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) - ctx.Resp.Header().Del("Set-Cookie") - cookie := http.Cookie{ - Name: setting.SessionOptions.CookieName, - Value: url.QueryEscape(ctx.UserToken.UnhashedToken), - HttpOnly: true, - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", - } - - http.SetCookie(ctx.Resp, &cookie) - } - - return true -} - // func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool { // // initialize session // if err := ctx.Session.Start(ctx.Context); err != nil { diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 5929043573e..49d40400205 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -7,6 +7,7 @@ import ( "net/url" "time" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" @@ -23,6 +24,7 @@ var ( now = time.Now RotateTime = 1 * time.Minute UrgentRotateTime = 30 * time.Second + 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 @@ -37,38 +39,70 @@ func (s *UserAuthTokenService) Init() error { return nil } +func (s *UserAuthTokenService) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool { + //auth User + unhashedToken := ctx.GetCookie(setting.SessionOptions.CookieName) + if unhashedToken == "" { + return false + } + + user, 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: user.UserId, OrgId: orgID} + if err := bus.Dispatch(&query); err != nil { + ctx.Logger.Error("Failed to get user with id", "userId", user.UserId, "error", err) + return false + } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + ctx.UserToken = user + + //rotate session token if needed. + rotated, err := s.RefreshToken(ctx.UserToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + ctx.Logger.Error("failed to rotate token", "error", err, "user.id", user.UserId, "user_token.id", user.Id) + return true + } + + if rotated { + s.writeSessionCookie(ctx, ctx.UserToken.UnhashedToken, oneYearInSeconds) + } + + return true +} + +func (s *UserAuthTokenService) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) { + ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) + ctx.Resp.Header().Del("Set-Cookie") + cookie := http.Cookie{ + Name: setting.SessionOptions.CookieName, + Value: url.QueryEscape(value), + HttpOnly: true, + Domain: setting.Domain, + Path: setting.AppSubUrl + "/", + Secure: setting.SessionOptions.Secure, + } + + http.SetCookie(ctx.Resp, &cookie) +} + func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error { userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) if err != nil { return err } - c.Resp.Header().Del("Set-Cookie") - cookie := http.Cookie{ - Name: setting.SessionOptions.CookieName, - Value: url.QueryEscape(userToken.UnhashedToken), - HttpOnly: true, - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", - Secure: setting.SessionOptions.Secure, - } - - http.SetCookie(c.Resp, &cookie) - + s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds) return nil } func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { - c.Resp.Header().Del("Set-Cookie") - cookie := http.Cookie{ - Name: setting.SessionOptions.CookieName, - Value: "", - HttpOnly: true, - Domain: setting.Domain, - Path: setting.AppSubUrl + "/", - Secure: setting.SessionOptions.Secure, - } - http.SetCookie(c.Resp, &cookie) + s.writeSessionCookie(c, "", -1) } func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserAuthToken, error) { From 697ddccd8ee6a6dd934d347342440ff3357394b9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Jan 2019 11:42:10 +0100 Subject: [PATCH 027/167] set userToken on request when logging in --- pkg/services/auth/auth_token.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 49d40400205..bab08778511 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -22,7 +22,7 @@ func init() { var ( now = time.Now - RotateTime = 1 * time.Minute + RotateTime = 1 * time.Minute // this should be read from [session] configuration. UrgentRotateTime = 30 * time.Second oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. ) @@ -77,7 +77,8 @@ func (s *UserAuthTokenService) InitContextWithToken(ctx *models.ReqContext, orgI } func (s *UserAuthTokenService) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) { - ctx.Logger.Info("new token", "unhashed token", ctx.UserToken.UnhashedToken) + ctx.Logger.Info("new token", "unhashed token", value) + ctx.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ Name: setting.SessionOptions.CookieName, @@ -97,6 +98,8 @@ func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *model return err } + c.UserToken = userToken + s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds) return nil } From 565408194aafc1714389ccc71e762a3fa41846ea Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 13:22:20 +0100 Subject: [PATCH 028/167] handle expired tokens --- pkg/services/auth/auth_token.go | 3 ++- pkg/services/auth/auth_token_test.go | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index bab08778511..0ab4e32c0ad 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -141,9 +141,10 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAuthToken, error) { hashedToken := hashToken(unhashedToken) + expireBefore := now().Add(time.Duration(-86400*setting.LogInRememberDays) * time.Second).Unix() var userToken models.UserAuthToken - exists, err := s.SQLStore.NewSession().Where("auth_token = ? OR prev_auth_token = ?", hashedToken, hashedToken).Get(&userToken) + 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 } diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index bb146252fa4..fa67cd62869 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -58,9 +60,13 @@ func TestUserAuthToken(t *testing.T) { token, err = ctx.getAuthTokenByID(token.Id) So(err, ShouldBeNil) - // set now (now - 23 hours) - _, err = userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent") + now = 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) @@ -69,7 +75,9 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) - // set now (new - 2 hours) + now = func() time.Time { + return t.Add(24 * 7 * time.Hour) + } notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) So(err, ShouldEqual, ErrAuthTokenNotFound) So(notGood, ShouldBeNil) @@ -93,7 +101,7 @@ func TestUserAuthToken(t *testing.T) { // ability to auth using an old token now = func() time.Time { - return t + return t.Add(time.Hour) } refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") @@ -172,6 +180,7 @@ func createTestContext(t *testing.T) *testContext { RotateTime = 10 * time.Minute UrgentRotateTime = time.Minute + setting.LogInRememberDays = 7 return &testContext{ sqlstore: sqlstore, From dd8476d81ac9e6dbd74b6946d287cc218d1840da Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 15:06:33 +0100 Subject: [PATCH 029/167] passing auth token tests --- pkg/services/auth/auth_token.go | 20 +++++++++++--------- pkg/services/auth/auth_token_test.go | 16 +++++++++++----- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 0ab4e32c0ad..54e938ecfbe 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -3,6 +3,7 @@ package auth import ( "crypto/sha256" "encoding/hex" + "fmt" "net/http" "net/url" "time" @@ -22,8 +23,8 @@ func init() { var ( now = time.Now - RotateTime = 1 * time.Minute // this should be read from [session] configuration. - UrgentRotateTime = 30 * time.Second + RotateTime = 30 * time.Second + UrgentRotateTime = 10 * time.Second oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. ) @@ -154,17 +155,18 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu } if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { - userToken.AuthTokenSeen = false - expireBefore := now().Add(-RotateTime).Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userToken.Id, userToken.PrevAuthToken, expireBefore).AllCols().Update(&userToken) + userTokenCopy := userToken + userTokenCopy.AuthTokenSeen = false + expireBefore := now().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) + fmt.Println("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) + fmt.Println("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) } } @@ -182,9 +184,9 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu } if affectedRows == 0 { - s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + fmt.Println("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) + fmt.Println("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) } } diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index fa67cd62869..a92fb7e1598 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -95,11 +95,13 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(refreshed, ShouldBeFalse) - ctx.markAuthTokenAsSeen(token.Id) + updated, err := ctx.markAuthTokenAsSeen(token.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + token, err = ctx.getAuthTokenByID(token.Id) So(err, ShouldBeNil) - // ability to auth using an old token now = func() time.Time { return t.Add(time.Hour) } @@ -114,31 +116,35 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) token.UnhashedToken = unhashedToken - So(token.RotatedAt, ShouldEqual, t.Unix()) + So(token.RotatedAt, ShouldEqual, now().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, t.Unix()) + So(lookedUp.SeenAt, ShouldEqual, now().Unix()) lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) So(err, ShouldBeNil) So(lookedUp, ShouldNotBeNil) So(lookedUp.Id, ShouldEqual, token.Id) + So(lookedUp.AuthTokenSeen, ShouldBeTrue) now = func() time.Time { - return t.Add(2 * time.Minute) + 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) From 92620af75f988e2ae8e87aeba51afef8ec5be284 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Jan 2019 15:30:08 +0100 Subject: [PATCH 030/167] avoid calling now() multiple times --- pkg/services/auth/auth_token.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 54e938ecfbe..a7a65b2aeca 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -22,7 +22,7 @@ func init() { } var ( - now = time.Now + getTime = time.Now RotateTime = 30 * time.Second UrgentRotateTime = 10 * time.Second oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. @@ -118,15 +118,17 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str hashedToken := hashToken(token) + now := getTime().Unix() + userToken := models.UserAuthToken{ UserId: userId, AuthToken: hashedToken, PrevAuthToken: hashedToken, ClientIp: clientIP, UserAgent: userAgent, - RotatedAt: now().Unix(), - CreatedAt: now().Unix(), - UpdatedAt: now().Unix(), + RotatedAt: now, + CreatedAt: now, + UpdatedAt: now, SeenAt: 0, AuthTokenSeen: false, } @@ -142,7 +144,7 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAuthToken, error) { hashedToken := hashToken(unhashedToken) - expireBefore := now().Add(time.Duration(-86400*setting.LogInRememberDays) * time.Second).Unix() + expireBefore := getTime().Add(time.Duration(-86400*setting.LogInRememberDays) * time.Second).Unix() var userToken models.UserAuthToken exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken) @@ -157,7 +159,7 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { userTokenCopy := userToken userTokenCopy.AuthTokenSeen = false - expireBefore := now().Add(-UrgentRotateTime).Unix() + 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 @@ -173,7 +175,7 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken { userTokenCopy := userToken userTokenCopy.AuthTokenSeen = true - userTokenCopy.SeenAt = now().Unix() + 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 @@ -200,19 +202,22 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI return false, nil } + now := getTime() + needsRotation := false rotatedAt := time.Unix(token.RotatedAt, 0) if token.AuthTokenSeen { - needsRotation = rotatedAt.Before(now().Add(-RotateTime)) + needsRotation = rotatedAt.Before(now.Add(-RotateTime)) } else { - needsRotation = rotatedAt.Before(now().Add(-UrgentRotateTime)) + needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime)) } - s.log.Debug("refresh token", "needs rotation?", needsRotation, "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) 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) @@ -229,7 +234,7 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI rotated_at = ? WHERE id = ? AND (auth_token_seen or rotated_at < ?)` - res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, hashedToken, now().Unix(), token.Id, now().Add(-UrgentRotateTime)) + res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, hashedToken, now.Unix(), token.Id, now.Add(-UrgentRotateTime)) if err != nil { return false, err } From 38efc1d7d2c1620b638b4f64b720908a99b3c4cb Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 15:51:00 +0100 Subject: [PATCH 031/167] s/print/log --- pkg/services/auth/auth_token.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index a7a65b2aeca..1fb3543d354 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -3,7 +3,6 @@ package auth import ( "crypto/sha256" "encoding/hex" - "fmt" "net/http" "net/url" "time" @@ -166,9 +165,9 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu } if affectedRows == 0 { - fmt.Println("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) } else { - fmt.Println("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) } } @@ -186,9 +185,9 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserAu } if affectedRows == 0 { - fmt.Println("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) } else { - fmt.Println("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) } } From 38ea11d110263ce8fb39291772989c81ea91c384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 21 Jan 2019 15:58:36 +0100 Subject: [PATCH 032/167] Added check for null value in ValueMappings and added tests --- .../src/components/Gauge/Gauge.test.tsx | 39 +++++++++++++++++++ .../grafana-ui/src/components/Gauge/Gauge.tsx | 22 +++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index b3396841d4d..1dbfb2bbb42 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -135,6 +135,45 @@ describe('Format value with value mappings', () => { expect(result.text).toEqual('1-20'); }); + it('should return if value is null and value to text mapping value is null', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: '', type: MappingType.ValueToText, value: 'null' }, + ]; + const value = null; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual(''); + }); + + it('should return if value is null and range to text mapping from is null', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: 'null', to: '10' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = null; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual(''); + }); + + it('should return if value is null and range to text mapping to is null', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: '1', to: 'null' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = null; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual(''); + }); + it('should return rangeToText mapping where value equals to', () => { const valueMappings: ValueMapping[] = [ { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 63d875e9cd5..e0c60177a34 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -60,10 +60,14 @@ export class Gauge extends PureComponent { } addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) { - if (!valueToTextMapping.value) { + if (valueToTextMapping.value === undefined) { return allValueMappings; } + if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') { + return allValueMappings.concat(valueToTextMapping); + } + const valueAsNumber = parseFloat(value as string); const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); @@ -79,10 +83,19 @@ export class Gauge extends PureComponent { } addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) { - if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) { + if (rangeToTextMapping.from === undefined || rangeToTextMapping.to === undefined || value === undefined) { return allValueMappings; } + if ( + value === null && + rangeToTextMapping.from && + rangeToTextMapping.to && + (rangeToTextMapping.from.toLowerCase() === 'null' || rangeToTextMapping.to.toLowerCase() === 'null') + ) { + return allValueMappings.concat(rangeToTextMapping); + } + const valueAsNumber = parseFloat(value as string); const fromAsNumber = parseFloat(rangeToTextMapping.from as string); const toAsNumber = parseFloat(rangeToTextMapping.to as string); @@ -139,8 +152,9 @@ export class Gauge extends PureComponent { const formatFunc = getValueFormat(unit); const formattedValue = formatFunc(value as number, decimals); + const handleNoValueValue = formattedValue || 'no value'; - return `${prefix} ${formattedValue} ${suffix}`; + return `${prefix} ${handleNoValueValue} ${suffix}`; } getFontColor(value: TimeSeriesValue) { @@ -204,7 +218,7 @@ export class Gauge extends PureComponent { if (timeSeries[0]) { value = timeSeries[0].stats[stat]; } else { - value = 'N/A'; + value = null; } const dimension = Math.min(width, height * 1.3); From f040f9a4002a4631b525f1948ed2ec4682257d93 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 16:53:00 +0100 Subject: [PATCH 033/167] fix tests after renaming now --- pkg/services/auth/auth_token_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index a92fb7e1598..2e876350e7b 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -19,7 +19,7 @@ func TestUserAuthToken(t *testing.T) { userID := int64(10) t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) - now = func() time.Time { + getTime = func() time.Time { return t } @@ -60,7 +60,7 @@ func TestUserAuthToken(t *testing.T) { token, err = ctx.getAuthTokenByID(token.Id) So(err, ShouldBeNil) - now = func() time.Time { + getTime = func() time.Time { return t.Add(time.Hour) } @@ -75,7 +75,7 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) - now = func() time.Time { + getTime = func() time.Time { return t.Add(24 * 7 * time.Hour) } notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) @@ -102,7 +102,7 @@ func TestUserAuthToken(t *testing.T) { token, err = ctx.getAuthTokenByID(token.Id) So(err, ShouldBeNil) - now = func() time.Time { + getTime = func() time.Time { return t.Add(time.Hour) } @@ -116,7 +116,7 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) token.UnhashedToken = unhashedToken - So(token.RotatedAt, ShouldEqual, now().Unix()) + 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) @@ -129,7 +129,7 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(lookedUp, ShouldNotBeNil) So(lookedUp.AuthTokenSeen, ShouldBeTrue) - So(lookedUp.SeenAt, ShouldEqual, now().Unix()) + So(lookedUp.SeenAt, ShouldEqual, getTime().Unix()) lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) So(err, ShouldBeNil) @@ -137,7 +137,7 @@ func TestUserAuthToken(t *testing.T) { So(lookedUp.Id, ShouldEqual, token.Id) So(lookedUp.AuthTokenSeen, ShouldBeTrue) - now = func() time.Time { + getTime = func() time.Time { return t.Add(time.Hour + (2 * time.Minute)) } @@ -170,7 +170,7 @@ func TestUserAuthToken(t *testing.T) { }) Reset(func() { - now = time.Now + getTime = time.Now }) }) } From bbb7596113b56747c86630915e21955b1dcd8a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 21 Jan 2019 17:30:47 +0100 Subject: [PATCH 034/167] Changed null logic for range value mappings after PR comments --- .../src/components/Gauge/Gauge.test.tsx | 17 ++--------------- .../grafana-ui/src/components/Gauge/Gauge.tsx | 3 ++- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 1dbfb2bbb42..254044847bd 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -148,22 +148,9 @@ describe('Format value with value mappings', () => { expect(result.text).toEqual(''); }); - it('should return if value is null and range to text mapping from is null', () => { + it('should return if value is null and range to text mapping from and to is null', () => { const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: 'null', to: '10' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = null; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual(''); - }); - - it('should return if value is null and range to text mapping to is null', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: '1', to: 'null' }, + { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: 'null', to: 'null' }, { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, ]; const value = null; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index e0c60177a34..ddc5b91f9b7 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -91,7 +91,8 @@ export class Gauge extends PureComponent { value === null && rangeToTextMapping.from && rangeToTextMapping.to && - (rangeToTextMapping.from.toLowerCase() === 'null' || rangeToTextMapping.to.toLowerCase() === 'null') + rangeToTextMapping.from.toLowerCase() === 'null' && + rangeToTextMapping.to.toLowerCase() === 'null' ) { return allValueMappings.concat(rangeToTextMapping); } From 777bd9ea1845893739fac9549bc4cbb0cdcc913e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Jan 2019 17:05:42 +0100 Subject: [PATCH 035/167] adds cleanup job for old session tokens --- pkg/services/auth/auth_token.go | 8 +++-- pkg/services/auth/session_cleanup.go | 38 +++++++++++++++++++++++ pkg/services/auth/session_cleanup_test.go | 37 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 pkg/services/auth/session_cleanup.go create mode 100644 pkg/services/auth/session_cleanup_test.go diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 1fb3543d354..3e3bd75869d 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -8,6 +8,7 @@ import ( "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" @@ -29,8 +30,9 @@ var ( // UserAuthTokenService are used for generating and validating user auth tokens type UserAuthTokenService struct { - SQLStore *sqlstore.SqlStore `inject:""` - log log.Logger + SQLStore *sqlstore.SqlStore `inject:""` + ServerLockService *serverlock.ServerLockService `inject:""` + log log.Logger } // Init this service @@ -239,7 +241,7 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI } affected, _ := res.RowsAffected() - s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId, "user_agent", userAgent, "client_ip", clientIP) + s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId) if affected > 0 { token.UnhashedToken = newToken return true, nil diff --git a/pkg/services/auth/session_cleanup.go b/pkg/services/auth/session_cleanup.go new file mode 100644 index 00000000000..16f6deb9005 --- /dev/null +++ b/pkg/services/auth/session_cleanup.go @@ -0,0 +1,38 @@ +package auth + +import ( + "context" + "time" +) + +func (srv *UserAuthTokenService) Run(ctx context.Context) error { + ticker := time.NewTicker(time.Hour * 12) + deleteSessionAfter := time.Hour * 24 * 7 * 30 + + 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 *UserAuthTokenService) 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 +} diff --git a/pkg/services/auth/session_cleanup_test.go b/pkg/services/auth/session_cleanup_test.go new file mode 100644 index 00000000000..1f3b7ad0c7d --- /dev/null +++ b/pkg/services/auth/session_cleanup_test.go @@ -0,0 +1,37 @@ +package auth + +import ( + "fmt" + "testing" + "time" + + "github.com/grafana/grafana/pkg/models" + . "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 := models.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) + }) +} From 79bd7f4077dae4312bf407d1b2797b934c03d050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Jan 2019 21:36:30 +0100 Subject: [PATCH 036/167] Fixed data source selection in explore --- .../app/features/explore/state/actionTypes.ts | 2 +- public/app/features/explore/state/actions.ts | 17 +++++++++-------- public/app/features/explore/state/reducers.ts | 3 ++- public/app/types/explore.ts | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 850f2137541..4e1d658f072 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -123,7 +123,7 @@ export interface LoadDatasourcePendingAction { type: ActionTypes.LoadDatasourcePending; payload: { exploreId: ExploreId; - datasourceId: number; + datasourceName: string; }; } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 34169a999a3..d4c42ffa9c7 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -33,7 +33,7 @@ import { } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -import { RawTimeRange, TimeRange } from '@grafana/ui'; +import { RawTimeRange, TimeRange, DataSourceApi } from '@grafana/ui'; import { Action as ThunkableAction, ActionTypes, @@ -216,11 +216,11 @@ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissi /** * Start the async process of loading a datasource to display a loading indicator */ -export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ +export const loadDatasourcePending = (exploreId: ExploreId, datasourceName: string): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, payload: { exploreId, - datasourceId, + datasourceName, }, }); @@ -266,12 +266,12 @@ export const loadDatasourceSuccess = ( /** * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback. */ -export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { +export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult { return async (dispatch, getState) => { - const datasourceId = instance.meta.id; + const datasourceName = instance.name; // Keep ID to track selection - dispatch(loadDatasourcePending(exploreId, datasourceId)); + dispatch(loadDatasourcePending(exploreId, datasourceName)); let datasourceError = null; try { @@ -280,12 +280,13 @@ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult } catch (error) { datasourceError = (error && error.statusText) || 'Network error'; } + if (datasourceError) { dispatch(loadDatasourceFailure(exploreId, datasourceError)); return; } - if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { + if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) { // User already changed datasource again, discard results return; } @@ -311,7 +312,7 @@ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult } } - if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { + if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) { // User already changed datasource again, discard results return; } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index ba87e8818df..8acf52340c9 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -185,7 +185,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.LoadDatasourcePending: { - return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId }; + return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.datasourceName }; } case ActionTypes.LoadDatasourceSuccess: { @@ -217,6 +217,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { supportsTable, datasourceLoading: false, datasourceMissing: false, + datasourceError: null, logsHighlighterExpressions: undefined, modifiedQueries: initialQueries.slice(), queryTransactions: [], diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c69e93ff88e..ce5ea1047dd 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -186,7 +186,7 @@ export interface ExploreItemState { * Allows the selection to be discarded if something went wrong during the asynchronous * loading of the datasource. */ - requestedDatasourceId?: number; + requestedDatasourceName?: string; /** * Time range for this Explore. Managed by the time picker and used by all query runs. */ From 366e356e080e4ccaf6e7bf988ae449aceda3fc19 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 21 Jan 2019 19:08:51 +0100 Subject: [PATCH 037/167] more auth token tests --- pkg/services/auth/auth_token.go | 11 +-- pkg/services/auth/auth_token_test.go | 111 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 3e3bd75869d..181cc4315c9 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -223,19 +223,20 @@ func (s *UserAuthTokenService) RefreshToken(token *models.UserAuthToken, clientI 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 - auth_token_seen = false, - seen_at = null, + seen_at = 0, user_agent = ?, client_ip = ?, - prev_auth_token = case when auth_token_seen then auth_token else prev_auth_token end, + 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 < ?)` + WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` - res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, hashedToken, now.Unix(), token.Id, now.Add(-UrgentRotateTime)) + 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 } diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 2e876350e7b..27405059e26 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -162,11 +162,122 @@ func TestUserAuthToken(t *testing.T) { }) 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() { From a9c33ab6580c0e9aea7d932b54bc95d3eb6dc13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 06:40:04 +0100 Subject: [PATCH 038/167] Moved ValueMapping logic and tests to separate files --- .../src/components/Gauge/Gauge.test.tsx | 103 ------------------ .../grafana-ui/src/components/Gauge/Gauge.tsx | 94 +--------------- .../src/utils/valueMappings.test.ts | 81 ++++++++++++++ .../grafana-ui/src/utils/valueMappings.ts | 89 +++++++++++++++ 4 files changed, 173 insertions(+), 194 deletions(-) create mode 100644 packages/grafana-ui/src/utils/valueMappings.test.ts create mode 100644 packages/grafana-ui/src/utils/valueMappings.ts diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 254044847bd..396b7a03162 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -98,109 +98,6 @@ describe('Get thresholds formatted', () => { }); }); -describe('Format value with value mappings', () => { - it('should return undefined with no valuemappings', () => { - const valueMappings: ValueMapping[] = []; - const value = '10'; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result).toBeUndefined(); - }); - - it('should return undefined with no matching valuemappings', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result).toBeUndefined(); - }); - - it('should return first matching mapping with lowest id', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, - { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual('1-20'); - }); - - it('should return if value is null and value to text mapping value is null', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, - { id: 1, operator: '', text: '', type: MappingType.ValueToText, value: 'null' }, - ]; - const value = null; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual(''); - }); - - it('should return if value is null and range to text mapping from and to is null', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: 'null', to: 'null' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = null; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual(''); - }); - - it('should return rangeToText mapping where value equals to', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual('1-10'); - }); - - it('should return rangeToText mapping where value equals from', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual('10-20'); - }); - - it('should return rangeToText mapping where value is between from and to', () => { - const valueMappings: ValueMapping[] = [ - { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, - { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, - ]; - const value = '10'; - const { instance } = setup({ valueMappings }); - - const result = instance.getFirstFormattedValueMapping(valueMappings, value); - - expect(result.text).toEqual('1-20'); - }); -}); - describe('Format value', () => { it('should return if value isNaN', () => { const valueMappings: ValueMapping[] = []; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index ddc5b91f9b7..2dce20543fd 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,20 +1,10 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { - ValueMapping, - Threshold, - ThemeName, - MappingType, - BasicGaugeColor, - ThemeNames, - ValueMap, - RangeMap, -} from '../../types/panel'; +import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel'; import { TimeSeriesVMs } from '../../types/series'; import { getValueFormat } from '../../utils/valueFormats/valueFormats'; - -type TimeSeriesValue = string | number | null; +import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings'; export interface Props { decimals: number; @@ -59,84 +49,6 @@ export class Gauge extends PureComponent { this.draw(); } - addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) { - if (valueToTextMapping.value === undefined) { - return allValueMappings; - } - - if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') { - return allValueMappings.concat(valueToTextMapping); - } - - const valueAsNumber = parseFloat(value as string); - const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); - - if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) { - return allValueMappings; - } - - if (valueAsNumber !== valueToTextMappingAsNumber) { - return allValueMappings; - } - - return allValueMappings.concat(valueToTextMapping); - } - - addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) { - if (rangeToTextMapping.from === undefined || rangeToTextMapping.to === undefined || value === undefined) { - return allValueMappings; - } - - if ( - value === null && - rangeToTextMapping.from && - rangeToTextMapping.to && - rangeToTextMapping.from.toLowerCase() === 'null' && - rangeToTextMapping.to.toLowerCase() === 'null' - ) { - return allValueMappings.concat(rangeToTextMapping); - } - - const valueAsNumber = parseFloat(value as string); - const fromAsNumber = parseFloat(rangeToTextMapping.from as string); - const toAsNumber = parseFloat(rangeToTextMapping.to as string); - - if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) { - return allValueMappings; - } - - if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) { - return allValueMappings.concat(rangeToTextMapping); - } - - return allValueMappings; - } - - getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { - const allFormattedValueMappings = valueMappings.reduce( - (allValueMappings, valueMapping) => { - if (valueMapping.type === MappingType.ValueToText) { - allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value); - } else if (valueMapping.type === MappingType.RangeToText) { - allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value); - } - - return allValueMappings; - }, - [] as ValueMapping[] - ); - - allFormattedValueMappings.sort((t1, t2) => { - return t1.id - t2.id; - }); - - return allFormattedValueMappings; - } - - getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) { - return this.getAllFormattedValueMappings(valueMappings, value)[0]; - } - formatValue(value: TimeSeriesValue) { const { decimals, valueMappings, prefix, suffix, unit } = this.props; @@ -145,7 +57,7 @@ export class Gauge extends PureComponent { } if (valueMappings.length > 0) { - const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value); + const valueMappedValue = getMappedValue(valueMappings, value); if (valueMappedValue) { return `${prefix} ${valueMappedValue.text} ${suffix}`; } diff --git a/packages/grafana-ui/src/utils/valueMappings.test.ts b/packages/grafana-ui/src/utils/valueMappings.test.ts new file mode 100644 index 00000000000..d37e0beedab --- /dev/null +++ b/packages/grafana-ui/src/utils/valueMappings.test.ts @@ -0,0 +1,81 @@ +import { getMappedValue } from './valueMappings'; +import { ValueMapping, MappingType } from '../types/panel'; + +describe('Format value with value mappings', () => { + it('should return undefined with no valuemappings', () => { + const valueMappings: ValueMapping[] = []; + const value = '10'; + + expect(getMappedValue(valueMappings, value)).toBeUndefined(); + }); + + it('should return undefined with no matching valuemappings', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' }, + ]; + const value = '10'; + + expect(getMappedValue(valueMappings, value)).toBeUndefined(); + }); + + it('should return first matching mapping with lowest id', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' }, + ]; + const value = '10'; + + expect(getMappedValue(valueMappings, value).text).toEqual('1-20'); + }); + + it('should return if value is null and value to text mapping value is null', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: '', type: MappingType.ValueToText, value: 'null' }, + ]; + const value = null; + + expect(getMappedValue(valueMappings, value).text).toEqual(''); + }); + + it('should return if value is null and range to text mapping from and to is null', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '', type: MappingType.RangeToText, from: 'null', to: 'null' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = null; + + expect(getMappedValue(valueMappings, value).text).toEqual(''); + }); + + it('should return rangeToText mapping where value equals to', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = '10'; + + expect(getMappedValue(valueMappings, value).text).toEqual('1-10'); + }); + + it('should return rangeToText mapping where value equals from', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = '10'; + + expect(getMappedValue(valueMappings, value).text).toEqual('10-20'); + }); + + it('should return rangeToText mapping where value is between from and to', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = '10'; + + expect(getMappedValue(valueMappings, value).text).toEqual('1-20'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueMappings.ts b/packages/grafana-ui/src/utils/valueMappings.ts new file mode 100644 index 00000000000..c9b926ea0a4 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueMappings.ts @@ -0,0 +1,89 @@ +import { ValueMapping, MappingType, ValueMap, RangeMap } from '../types'; + +export type TimeSeriesValue = string | number | null; + +const addValueToTextMappingText = ( + allValueMappings: ValueMapping[], + valueToTextMapping: ValueMap, + value: TimeSeriesValue +) => { + if (valueToTextMapping.value === undefined) { + return allValueMappings; + } + + if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') { + return allValueMappings.concat(valueToTextMapping); + } + + const valueAsNumber = parseFloat(value as string); + const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); + + if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) { + return allValueMappings; + } + + if (valueAsNumber !== valueToTextMappingAsNumber) { + return allValueMappings; + } + + return allValueMappings.concat(valueToTextMapping); +}; + +const addRangeToTextMappingText = ( + allValueMappings: ValueMapping[], + rangeToTextMapping: RangeMap, + value: TimeSeriesValue +) => { + if (rangeToTextMapping.from === undefined || rangeToTextMapping.to === undefined || value === undefined) { + return allValueMappings; + } + + if ( + value === null && + rangeToTextMapping.from && + rangeToTextMapping.to && + rangeToTextMapping.from.toLowerCase() === 'null' && + rangeToTextMapping.to.toLowerCase() === 'null' + ) { + return allValueMappings.concat(rangeToTextMapping); + } + + const valueAsNumber = parseFloat(value as string); + const fromAsNumber = parseFloat(rangeToTextMapping.from as string); + const toAsNumber = parseFloat(rangeToTextMapping.to as string); + + if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) { + return allValueMappings; + } + + if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) { + return allValueMappings.concat(rangeToTextMapping); + } + + return allValueMappings; +}; + +const getAllFormattedValueMappings = (valueMappings: ValueMapping[], value: TimeSeriesValue) => { + const allFormattedValueMappings = valueMappings.reduce( + (allValueMappings, valueMapping) => { + if (valueMapping.type === MappingType.ValueToText) { + allValueMappings = addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value); + } else if (valueMapping.type === MappingType.RangeToText) { + allValueMappings = addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value); + } + + return allValueMappings; + }, + [] as ValueMapping[] + ); + + allFormattedValueMappings.sort((t1, t2) => { + return t1.id - t2.id; + }); + + return allFormattedValueMappings; +}; + +export const getMappedValue = (valueMappings: ValueMapping[], value: TimeSeriesValue): ValueMapping => { + return getAllFormattedValueMappings(valueMappings, value)[0]; +}; From a6428fef0cce1a11e1290fd97bcb949a84672d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 08:12:48 +0100 Subject: [PATCH 039/167] Added link to side menu header and fixed styling --- .../components/sidemenu/SideMenuDropDown.tsx | 4 ++- .../SideMenuDropDown.test.tsx.snap | 26 ++++++++++++------- public/sass/components/_sidemenu.scss | 6 +++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.tsx index 4231e992b19..068298da381 100644 --- a/public/app/core/components/sidemenu/SideMenuDropDown.tsx +++ b/public/app/core/components/sidemenu/SideMenuDropDown.tsx @@ -10,7 +10,9 @@ const SideMenuDropDown: FC = props => { return (
  • - {link.text} + + {link.text}{' '} +
  • {link.children && link.children.map((child, index) => { diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap index 861168c1cc3..536bf141ef7 100644 --- a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap @@ -8,11 +8,14 @@ exports[`Render should render children 1`] = `
  • - - link - + + + link + + +
  • - - link - + + + link + + +
`; diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 237574b93bc..10565a4e0c7 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -149,6 +149,12 @@ color: #ebedf2; } +.side-menu-header > a { + // Removes left-brand-border-gradient from link + border: none !important; + padding: 0 !important; +} + .sidemenu-subtitle { padding: 0.5rem 1rem 0.5rem; font-size: $font-size-sm; From bf0333e329051bee0b0b5489020cad128ffe06ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 08:18:03 +0100 Subject: [PATCH 040/167] Removed strange edit --- public/app/core/components/sidemenu/SideMenuDropDown.tsx | 2 +- .../sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.tsx index 068298da381..4b930d605e5 100644 --- a/public/app/core/components/sidemenu/SideMenuDropDown.tsx +++ b/public/app/core/components/sidemenu/SideMenuDropDown.tsx @@ -11,7 +11,7 @@ const SideMenuDropDown: FC = props => {
  • - {link.text}{' '} + {link.text}
  • {link.children && diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap index 536bf141ef7..0e1a1486422 100644 --- a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap @@ -14,7 +14,6 @@ exports[`Render should render children 1`] = ` > link - link -
From 23202ab130d804b8e2ec89754d723059f6dd043f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 08:59:22 +0100 Subject: [PATCH 041/167] Refactored out LogRow to a separate file --- public/app/core/logs_model.ts | 20 +- public/app/features/explore/LogLabels.tsx | 6 +- public/app/features/explore/LogRow.tsx | 193 +++++++++++++++++ public/app/features/explore/Logs.tsx | 204 +----------------- .../datasource/loki/result_transformer.ts | 8 +- 5 files changed, 216 insertions(+), 215 deletions(-) create mode 100644 public/app/features/explore/LogRow.tsx diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 4cf9a029a2a..b8af798489d 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -42,7 +42,7 @@ export interface LogSearchMatch { text: string; } -export interface LogRow { +export interface LogRowModel { duplicates?: number; entry: string; key: string; // timestamp + labels @@ -78,7 +78,7 @@ export interface LogsMetaItem { export interface LogsModel { id: string; // Identify one logs result from another meta?: LogsMetaItem[]; - rows: LogRow[]; + rows: LogRowModel[]; series?: TimeSeries[]; } @@ -188,13 +188,13 @@ export const LogsParsers: { [name: string]: LogsParser } = { }, }; -export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] { +export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogsLabelStat[] { // Consider only rows that satisfy the matcher const rowsWithField = rows.filter(row => extractor.test(row.entry)); const rowCount = rowsWithField.length; // Get field value counts for eligible rows - const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]); + const countsByValue = _.countBy(rowsWithField, row => (row as LogRowModel).entry.match(extractor)[1]); const sortedCounts = _.chain(countsByValue) .map((count, value) => ({ count, value, proportion: count / rowCount })) .sortBy('count') @@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe return sortedCounts; } -export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] { +export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogsLabelStat[] { // Consider only rows that have the given label const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); const rowCount = rowsWithLabel.length; // Get label value counts for eligible rows - const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRow).labels[label]); + const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]); const sortedCounts = _.chain(countsByValue) .map((count, value) => ({ count, value, proportion: count / rowCount })) .sortBy('count') @@ -221,7 +221,7 @@ export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabe } const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g; -function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean { +function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedupStrategy): boolean { switch (strategy) { case LogsDedupStrategy.exact: // Exact still strips dates @@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs return logs; } - const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { + const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => { const previous = result[result.length - 1]; if (index > 0 && isDuplicateRow(row, previous, strategy)) { previous.duplicates++; @@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set) return logs; } - const filteredRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { + const filteredRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => { if (!hiddenLogLevels.has(row.logLevel)) { result.push(row); } @@ -291,7 +291,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set) }; } -export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] { +export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] { // currently interval is rangeMs / resolution, which is too low for showing series as bars. // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain // when executing queries & interval calculated and not here but this is a temporary fix. diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 7675fb13152..1dc82637f58 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import classnames from 'classnames'; -import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model'; +import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; function StatsRow({ active, count, proportion, value }: LogsLabelStat) { const percent = `${Math.round(proportion * 100)}%`; @@ -68,7 +68,7 @@ export class Stats extends PureComponent<{ class Label extends PureComponent< { - getRows?: () => LogRow[]; + getRows?: () => LogRowModel[]; label: string; plain?: boolean; value: string; @@ -133,7 +133,7 @@ class Label extends PureComponent< } export default class LogLabels extends PureComponent<{ - getRows?: () => LogRow[]; + getRows?: () => LogRowModel[]; labels: LogsStreamLabels; plain?: boolean; onClickLabel?: (label: string, value: string) => void; diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx new file mode 100644 index 00000000000..7b620da0ef6 --- /dev/null +++ b/public/app/features/explore/LogRow.tsx @@ -0,0 +1,193 @@ +import React, { PureComponent } from 'react'; +import _ from 'lodash'; +import Highlighter from 'react-highlight-words'; +import classnames from 'classnames'; + +import { LogRowModel, LogsLabelStat, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model'; +import LogLabels, { Stats } from './LogLabels'; +import { findHighlightChunksInText } from 'app/core/utils/text'; + +interface RowProps { + highlighterExpressions?: string[]; + row: LogRowModel; + showDuplicates: boolean; + showLabels: boolean | null; // Tristate: null means auto + showLocalTime: boolean; + showUtc: boolean; + getRows: () => LogRowModel[]; + onClickLabel?: (label: string, value: string) => void; +} + +interface RowState { + fieldCount: number; + fieldLabel: string; + fieldStats: LogsLabelStat[]; + fieldValue: string; + parsed: boolean; + parser?: LogsParser; + parsedFieldHighlights: string[]; + showFieldStats: boolean; +} + +/** + * Renders a highlighted field. + * When hovering, a stats icon is shown. + */ +const FieldHighlight = onClick => props => { + return ( + + {props.children} + onClick(props.children)} /> + + ); +}; + +/** + * Renders a log line. + * + * When user hovers over it for a certain time, it lazily parses the log line. + * Once a parser is found, it will determine fields, that will be highlighted. + * When the user requests stats for a field, they will be calculated and rendered below the row. + */ +export class LogRow extends PureComponent { + mouseMessageTimer: NodeJS.Timer; + + state = { + fieldCount: 0, + fieldLabel: null, + fieldStats: null, + fieldValue: null, + parsed: false, + parser: undefined, + parsedFieldHighlights: [], + showFieldStats: false, + }; + + componentWillUnmount() { + clearTimeout(this.mouseMessageTimer); + } + + onClickClose = () => { + this.setState({ showFieldStats: false }); + }; + + onClickHighlight = (fieldText: string) => { + const { getRows } = this.props; + const { parser } = this.state; + const allRows = getRows(); + + // Build value-agnostic row matcher based on the field label + const fieldLabel = parser.getLabelFromField(fieldText); + const fieldValue = parser.getValueFromField(fieldText); + const matcher = parser.buildMatcher(fieldLabel); + const fieldStats = calculateFieldStats(allRows, matcher); + const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0); + + this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true }); + }; + + onMouseOverMessage = () => { + // Don't parse right away, user might move along + this.mouseMessageTimer = setTimeout(this.parseMessage, 500); + }; + + onMouseOutMessage = () => { + clearTimeout(this.mouseMessageTimer); + this.setState({ parsed: false }); + }; + + parseMessage = () => { + if (!this.state.parsed) { + const { row } = this.props; + const parser = getParser(row.entry); + if (parser) { + // Use parser to highlight detected fields + const parsedFieldHighlights = parser.getFields(this.props.row.entry); + this.setState({ parsedFieldHighlights, parsed: true, parser }); + } + } + }; + + render() { + const { + getRows, + highlighterExpressions, + onClickLabel, + row, + showDuplicates, + showLabels, + showLocalTime, + showUtc, + } = this.props; + const { + fieldCount, + fieldLabel, + fieldStats, + fieldValue, + parsed, + parsedFieldHighlights, + showFieldStats, + } = this.state; + const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); + const highlights = previewHighlights ? highlighterExpressions : row.searchWords; + const needsHighlighter = highlights && highlights.length > 0; + const highlightClassName = classnames('logs-row__match-highlight', { + 'logs-row__match-highlight--preview': previewHighlights, + }); + return ( +
+ {showDuplicates && ( +
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
+ )} +
+ {showUtc && ( +
+ {row.timestamp} +
+ )} + {showLocalTime && ( +
+ {row.timeLocal} +
+ )} + {showLabels && ( +
+ +
+ )} +
+ {parsed && ( + + )} + {!parsed && + needsHighlighter && ( + + )} + {!parsed && !needsHighlighter && row.entry} + {showFieldStats && ( +
+ +
+ )} +
+
+ ); + } +} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index d07b31e2ff1..7c821179587 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -1,7 +1,5 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; -import Highlighter from 'react-highlight-words'; -import classnames from 'classnames'; import * as rangeUtil from 'app/core/utils/rangeutil'; import { RawTimeRange } from '@grafana/ui'; @@ -11,20 +9,16 @@ import { LogsModel, dedupLogRows, filterLogLevels, - getParser, LogLevel, LogsMetaKind, - LogsLabelStat, - LogsParser, - LogRow, - calculateFieldStats, } from 'app/core/logs_model'; -import { findHighlightChunksInText } from 'app/core/utils/text'; + import { Switch } from 'app/core/components/Switch/Switch'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import Graph from './Graph'; -import LogLabels, { Stats } from './LogLabels'; +import LogLabels from './LogLabels'; +import { LogRow } from './LogRow'; const PREVIEW_LIMIT = 100; @@ -43,191 +37,6 @@ const graphOptions = { }, }; -/** - * Renders a highlighted field. - * When hovering, a stats icon is shown. - */ -const FieldHighlight = onClick => props => { - return ( - - {props.children} - onClick(props.children)} /> - - ); -}; - -interface RowProps { - highlighterExpressions?: string[]; - row: LogRow; - showDuplicates: boolean; - showLabels: boolean | null; // Tristate: null means auto - showLocalTime: boolean; - showUtc: boolean; - getRows: () => LogRow[]; - onClickLabel?: (label: string, value: string) => void; -} - -interface RowState { - fieldCount: number; - fieldLabel: string; - fieldStats: LogsLabelStat[]; - fieldValue: string; - parsed: boolean; - parser?: LogsParser; - parsedFieldHighlights: string[]; - showFieldStats: boolean; -} - -/** - * Renders a log line. - * - * When user hovers over it for a certain time, it lazily parses the log line. - * Once a parser is found, it will determine fields, that will be highlighted. - * When the user requests stats for a field, they will be calculated and rendered below the row. - */ -class Row extends PureComponent { - mouseMessageTimer: NodeJS.Timer; - - state = { - fieldCount: 0, - fieldLabel: null, - fieldStats: null, - fieldValue: null, - parsed: false, - parser: undefined, - parsedFieldHighlights: [], - showFieldStats: false, - }; - - componentWillUnmount() { - clearTimeout(this.mouseMessageTimer); - } - - onClickClose = () => { - this.setState({ showFieldStats: false }); - }; - - onClickHighlight = (fieldText: string) => { - const { getRows } = this.props; - const { parser } = this.state; - const allRows = getRows(); - - // Build value-agnostic row matcher based on the field label - const fieldLabel = parser.getLabelFromField(fieldText); - const fieldValue = parser.getValueFromField(fieldText); - const matcher = parser.buildMatcher(fieldLabel); - const fieldStats = calculateFieldStats(allRows, matcher); - const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0); - - this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true }); - }; - - onMouseOverMessage = () => { - // Don't parse right away, user might move along - this.mouseMessageTimer = setTimeout(this.parseMessage, 500); - }; - - onMouseOutMessage = () => { - clearTimeout(this.mouseMessageTimer); - this.setState({ parsed: false }); - }; - - parseMessage = () => { - if (!this.state.parsed) { - const { row } = this.props; - const parser = getParser(row.entry); - if (parser) { - // Use parser to highlight detected fields - const parsedFieldHighlights = parser.getFields(this.props.row.entry); - this.setState({ parsedFieldHighlights, parsed: true, parser }); - } - } - }; - - render() { - const { - getRows, - highlighterExpressions, - onClickLabel, - row, - showDuplicates, - showLabels, - showLocalTime, - showUtc, - } = this.props; - const { - fieldCount, - fieldLabel, - fieldStats, - fieldValue, - parsed, - parsedFieldHighlights, - showFieldStats, - } = this.state; - const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); - const highlights = previewHighlights ? highlighterExpressions : row.searchWords; - const needsHighlighter = highlights && highlights.length > 0; - const highlightClassName = classnames('logs-row__match-highlight', { - 'logs-row__match-highlight--preview': previewHighlights, - }); - return ( -
- {showDuplicates && ( -
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
- )} -
- {showUtc && ( -
- {row.timestamp} -
- )} - {showLocalTime && ( -
- {row.timeLocal} -
- )} - {showLabels && ( -
- -
- )} -
- {parsed && ( - - )} - {!parsed && - needsHighlighter && ( - - )} - {!parsed && !needsHighlighter && row.entry} - {showFieldStats && ( -
- -
- )} -
-
- ); - } -} - function renderMetaItem(value: any, kind: LogsMetaKind) { if (kind === LogsMetaKind.LabelsMap) { return ( @@ -441,10 +250,9 @@ export default class Logs extends PureComponent {
{hasData && - !deferLogs && - // Only inject highlighterExpression in the first set for performance reasons + !deferLogs && // Only inject highlighterExpression in the first set for performance reasons firstRows.map(row => ( - { !deferLogs && renderAll && lastRows.map(row => ( - [ + (acc: LogRowModel[], stream: LogsStream) => [ ...acc, ...stream.entries.map(entry => processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search) From c369279401b6e94d84c035e828702385cfa304d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 09:22:38 +0100 Subject: [PATCH 042/167] Splitted up LogLabels into LogLabelStats and LogLabel --- public/app/core/logs_model.ts | 6 +- public/app/features/explore/LogLabel.tsx | 74 +++++++++ public/app/features/explore/LogLabelStats.tsx | 72 +++++++++ public/app/features/explore/LogLabels.tsx | 141 +----------------- public/app/features/explore/LogRow.tsx | 15 +- public/app/features/explore/Logs.tsx | 2 +- 6 files changed, 165 insertions(+), 145 deletions(-) create mode 100644 public/app/features/explore/LogLabel.tsx create mode 100644 public/app/features/explore/LogLabelStats.tsx diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index b8af798489d..a3f78e7152a 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -56,7 +56,7 @@ export interface LogRowModel { uniqueLabels?: LogsStreamLabels; } -export interface LogsLabelStat { +export interface LogLabelStatsModel { active?: boolean; count: number; proportion: number; @@ -188,7 +188,7 @@ export const LogsParsers: { [name: string]: LogsParser } = { }, }; -export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogsLabelStat[] { +export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogLabelStatsModel[] { // Consider only rows that satisfy the matcher const rowsWithField = rows.filter(row => extractor.test(row.entry)); const rowCount = rowsWithField.length; @@ -204,7 +204,7 @@ export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): Log return sortedCounts; } -export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogsLabelStat[] { +export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] { // Consider only rows that have the given label const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); const rowCount = rowsWithLabel.length; diff --git a/public/app/features/explore/LogLabel.tsx b/public/app/features/explore/LogLabel.tsx new file mode 100644 index 00000000000..b4570f10c82 --- /dev/null +++ b/public/app/features/explore/LogLabel.tsx @@ -0,0 +1,74 @@ +import React, { PureComponent } from 'react'; + +import { calculateLogsLabelStats, LogLabelStatsModel, LogRowModel } from 'app/core/logs_model'; +import { LogLabelStats } from './LogLabelStats'; + +interface Props { + getRows?: () => LogRowModel[]; + label: string; + plain?: boolean; + value: string; + onClickLabel?: (label: string, value: string) => void; +} + +interface State { + showStats: boolean; + stats: LogLabelStatsModel[]; +} + +export class LogLabel extends PureComponent { + state = { + stats: null, + showStats: false, + }; + + onClickClose = () => { + this.setState({ showStats: false }); + }; + + onClickLabel = () => { + const { onClickLabel, label, value } = this.props; + if (onClickLabel) { + onClickLabel(label, value); + } + }; + + onClickStats = () => { + this.setState(state => { + if (state.showStats) { + return { showStats: false, stats: null }; + } + const allRows = this.props.getRows(); + const stats = calculateLogsLabelStats(allRows, this.props.label); + return { showStats: true, stats }; + }); + }; + + render() { + const { getRows, label, plain, value } = this.props; + const { showStats, stats } = this.state; + const tooltip = `${label}: ${value}`; + return ( + + + {value} + + {!plain && ( + + )} + {!plain && getRows && } + {showStats && ( + + + + )} + + ); + } +} diff --git a/public/app/features/explore/LogLabelStats.tsx b/public/app/features/explore/LogLabelStats.tsx new file mode 100644 index 00000000000..b0bd69170c5 --- /dev/null +++ b/public/app/features/explore/LogLabelStats.tsx @@ -0,0 +1,72 @@ +import React, { PureComponent } from 'react'; +import classnames from 'classnames'; +import { LogLabelStatsModel } from 'app/core/logs_model'; + +function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) { + const { active, count, proportion, value } = logLabelStatsModel; + const percent = `${Math.round(proportion * 100)}%`; + const barStyle = { width: percent }; + const className = classnames('logs-stats-row', { 'logs-stats-row--active': active }); + + return ( +
+
+
{value}
+
{count}
+
{percent}
+
+
+
+
+
+ ); +} + +const STATS_ROW_LIMIT = 5; + +interface Props { + stats: LogLabelStatsModel[]; + label: string; + value: string; + rowCount: number; + onClickClose: () => void; +} + +export class LogLabelStats extends PureComponent { + render() { + const { label, rowCount, stats, value, onClickClose } = this.props; + const topRows = stats.slice(0, STATS_ROW_LIMIT); + let activeRow = topRows.find(row => row.value === value); + let otherRows = stats.slice(STATS_ROW_LIMIT); + const insertActiveRow = !activeRow; + + // Remove active row from other to show extra + if (insertActiveRow) { + activeRow = otherRows.find(row => row.value === value); + otherRows = otherRows.filter(row => row.value !== value); + } + + const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0); + const topCount = topRows.reduce((sum, row) => sum + row.count, 0); + const total = topCount + otherCount; + const otherProportion = otherCount / total; + + return ( +
+
+ + {label}: {total} of {rowCount} rows have that label + + +
+
+ {topRows.map(stat => )} + {insertActiveRow && activeRow && } + {otherCount > 0 && ( + + )} +
+
+ ); + } +} diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 1dc82637f58..7105a2a5370 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,147 +1,20 @@ import React, { PureComponent } from 'react'; -import classnames from 'classnames'; -import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; +import { LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; +import { LogLabel } from './LogLabel'; -function StatsRow({ active, count, proportion, value }: LogsLabelStat) { - const percent = `${Math.round(proportion * 100)}%`; - const barStyle = { width: percent }; - const className = classnames('logs-stats-row', { 'logs-stats-row--active': active }); - - return ( -
-
-
{value}
-
{count}
-
{percent}
-
-
-
-
-
- ); -} - -const STATS_ROW_LIMIT = 5; -export class Stats extends PureComponent<{ - stats: LogsLabelStat[]; - label: string; - value: string; - rowCount: number; - onClickClose: () => void; -}> { - render() { - const { label, rowCount, stats, value, onClickClose } = this.props; - const topRows = stats.slice(0, STATS_ROW_LIMIT); - let activeRow = topRows.find(row => row.value === value); - let otherRows = stats.slice(STATS_ROW_LIMIT); - const insertActiveRow = !activeRow; - // Remove active row from other to show extra - if (insertActiveRow) { - activeRow = otherRows.find(row => row.value === value); - otherRows = otherRows.filter(row => row.value !== value); - } - const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0); - const topCount = topRows.reduce((sum, row) => sum + row.count, 0); - const total = topCount + otherCount; - const otherProportion = otherCount / total; - - return ( -
-
- - {label}: {total} of {rowCount} rows have that label - - -
-
- {topRows.map(stat => )} - {insertActiveRow && activeRow && } - {otherCount > 0 && ( - - )} -
-
- ); - } -} - -class Label extends PureComponent< - { - getRows?: () => LogRowModel[]; - label: string; - plain?: boolean; - value: string; - onClickLabel?: (label: string, value: string) => void; - }, - { showStats: boolean; stats: LogsLabelStat[] } -> { - state = { - stats: null, - showStats: false, - }; - - onClickClose = () => { - this.setState({ showStats: false }); - }; - - onClickLabel = () => { - const { onClickLabel, label, value } = this.props; - if (onClickLabel) { - onClickLabel(label, value); - } - }; - - onClickStats = () => { - this.setState(state => { - if (state.showStats) { - return { showStats: false, stats: null }; - } - const allRows = this.props.getRows(); - const stats = calculateLogsLabelStats(allRows, this.props.label); - return { showStats: true, stats }; - }); - }; - - render() { - const { getRows, label, plain, value } = this.props; - const { showStats, stats } = this.state; - const tooltip = `${label}: ${value}`; - return ( - - - {value} - - {!plain && ( - - )} - {!plain && getRows && } - {showStats && ( - - - - )} - - ); - } -} - -export default class LogLabels extends PureComponent<{ +interface Props { getRows?: () => LogRowModel[]; labels: LogsStreamLabels; plain?: boolean; onClickLabel?: (label: string, value: string) => void; -}> { +} + +export class LogLabels extends PureComponent { render() { const { getRows, labels, onClickLabel, plain } = this.props; return Object.keys(labels).map(key => ( -