Merge branch 'master' into 12556-oauth-pass-thru
This commit is contained in:
@@ -95,52 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
}
|
||||
}
|
||||
case "diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
value = first - points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
allNull, value = calculateDiff(series, allNull, value, diff)
|
||||
case "percent_diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
allNull, value = calculateDiff(series, allNull, value, percentDiff)
|
||||
case "count_non_null":
|
||||
for _, v := range series.Points {
|
||||
if v[0].Valid {
|
||||
@@ -163,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||
return &SimpleReducer{Type: typ}
|
||||
}
|
||||
|
||||
func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) {
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
if i >= 1 {
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := fn(first, points[i][0].Float64)
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return allNull, value
|
||||
}
|
||||
|
||||
var diff = func(newest, oldest float64) float64 {
|
||||
return newest - oldest
|
||||
}
|
||||
|
||||
var percentDiff = func(newest, oldest float64) float64 {
|
||||
return (newest - oldest) / oldest * 100
|
||||
}
|
||||
|
||||
@@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) {
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
|
||||
Convey("diff with only nulls", func() {
|
||||
reducer := NewSimpleReducer("diff")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("percent_diff one point", func() {
|
||||
result := testReducer("percent_diff", 40)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
@@ -157,6 +169,18 @@ func TestSimpleReducer(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30, 40, 40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
|
||||
Convey("percent_diff with only nulls", func() {
|
||||
reducer := NewSimpleReducer("percent_diff")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
||||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
|
||||
const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d"
|
||||
|
||||
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if c.IsTestRun {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
)
|
||||
|
||||
type DefaultEvalHandler struct {
|
||||
|
||||
@@ -3,11 +3,12 @@ package alerting
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
@@ -126,7 +127,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
renderOpts := rendering.Opts{
|
||||
Width: 1000,
|
||||
Height: 500,
|
||||
Timeout: alertTimeout / 2,
|
||||
Timeout: time.Duration(float64(alertTimeout) * 0.9),
|
||||
OrgId: context.Rule.OrgId,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
ConcurrentLimit: setting.AlertingRenderLimit,
|
||||
@@ -137,7 +138,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, context.Rule.OrgId, context.Rule.PanelId)
|
||||
|
||||
result, err := n.renderService.Render(context.Ctx, renderOpts)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
@@ -8,19 +12,26 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "dingding",
|
||||
Name: "DingDing",
|
||||
Description: "Sends HTTP POST request to DingDing",
|
||||
Factory: NewDingDingNotifier,
|
||||
OptionsTemplate: `
|
||||
const DefaultDingdingMsgType = "link"
|
||||
const DingdingOptionsTemplate = `
|
||||
<h3 class="page-heading">DingDing settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
|
||||
<input type="text" required class="gf-form-input max-width-70" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
|
||||
</div>
|
||||
`,
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">MessageType</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.msgType" ng-options="s for s in ['link','actionCard']" ng-init="ctrl.model.settings.msgType=ctrl.model.settings.msgType || '` + DefaultDingdingMsgType + `'"></select>
|
||||
</div>
|
||||
`
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "dingding",
|
||||
Name: "DingDing",
|
||||
Description: "Sends HTTP POST request to DingDing",
|
||||
Factory: NewDingDingNotifier,
|
||||
OptionsTemplate: DingdingOptionsTemplate,
|
||||
})
|
||||
|
||||
}
|
||||
@@ -31,8 +42,11 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType)
|
||||
|
||||
return &DingDingNotifier{
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
MsgType: msgType,
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.dingding"),
|
||||
}, nil
|
||||
@@ -40,8 +54,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
|
||||
type DingDingNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
log log.Logger
|
||||
MsgType string
|
||||
Url string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
@@ -52,6 +67,16 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name)
|
||||
messageUrl = ""
|
||||
}
|
||||
|
||||
q := url.Values{
|
||||
"pc_slide": {"false"},
|
||||
"url": {messageUrl},
|
||||
}
|
||||
|
||||
// Use special link to auto open the message url outside of Dingding
|
||||
// Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9
|
||||
messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode()
|
||||
|
||||
this.log.Info("messageUrl:" + messageUrl)
|
||||
|
||||
message := evalContext.Rule.Message
|
||||
@@ -61,15 +86,39 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
message = title
|
||||
}
|
||||
|
||||
bodyJSON, err := simplejson.NewJson([]byte(`{
|
||||
"msgtype": "link",
|
||||
"link": {
|
||||
"text": "` + message + `",
|
||||
"title": "` + title + `",
|
||||
"picUrl": "` + picUrl + `",
|
||||
"messageUrl": "` + messageUrl + `"
|
||||
for i, match := range evalContext.EvalMatches {
|
||||
message += fmt.Sprintf("\\n%2d. %s: %s", i+1, match.Metric, match.Value)
|
||||
}
|
||||
|
||||
var bodyStr string
|
||||
if this.MsgType == "actionCard" {
|
||||
// Embed the pic into the markdown directly because actionCard doesn't have a picUrl field
|
||||
if picUrl != "" {
|
||||
message = "\\n\\n" + message
|
||||
}
|
||||
}`))
|
||||
|
||||
bodyStr = `{
|
||||
"msgtype": "actionCard",
|
||||
"actionCard": {
|
||||
"text": "` + strings.Replace(message, `"`, "'", -1) + `",
|
||||
"title": "` + strings.Replace(title, `"`, "'", -1) + `",
|
||||
"singleTitle": "More",
|
||||
"singleURL": "` + messageUrl + `"
|
||||
}
|
||||
}`
|
||||
} else {
|
||||
bodyStr = `{
|
||||
"msgtype": "link",
|
||||
"link": {
|
||||
"text": "` + message + `",
|
||||
"title": "` + title + `",
|
||||
"picUrl": "` + picUrl + `",
|
||||
"messageUrl": "` + messageUrl + `"
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
bodyJSON, err := simplejson.NewJson([]byte(bodyStr))
|
||||
|
||||
if err != nil {
|
||||
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
|
||||
|
||||
@@ -111,57 +111,20 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
json, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
content_type := "application/json"
|
||||
|
||||
var body []byte
|
||||
|
||||
if embeddedImage {
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
f, err := os.Open(evalContext.ImageOnDiskPath)
|
||||
|
||||
if err != nil {
|
||||
this.log.Error("Can't open graph file", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
fw, err := w.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fw.Write([]byte(string(json))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fw, err = w.CreateFormFile("file", "graph.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
body = b.Bytes()
|
||||
content_type = w.FormDataContentType()
|
||||
|
||||
} else {
|
||||
body = json
|
||||
}
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.WebhookURL,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
ContentType: content_type,
|
||||
ContentType: "application/json",
|
||||
}
|
||||
|
||||
if !embeddedImage {
|
||||
cmd.Body = string(json)
|
||||
} else {
|
||||
err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
|
||||
if err != nil {
|
||||
this.log.Error("failed to embed image", "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
@@ -171,3 +134,45 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
|
||||
f, err := os.Open(imagePath)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
cmd.Body = string(existingJSONBody)
|
||||
return nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
fw, err := w.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fw.Write([]byte(string(existingJSONBody))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fw, err = w.CreateFormFile("file", "graph.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
cmd.Body = string(b.Bytes())
|
||||
cmd.ContentType = w.FormDataContentType()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
|
||||
@@ -56,7 +56,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
|
||||
|
||||
ctx := NewEvalContext(context.Background(), testRule)
|
||||
if cmd.Settings.Get("uploadImage").MustBool(true) {
|
||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||
ctx.ImagePublicUrl = "https://grafana.com/assets/img/blog/mixed_styles.png"
|
||||
}
|
||||
ctx.IsTestRun = true
|
||||
ctx.Firing = true
|
||||
|
||||
+92
-122
@@ -3,12 +3,10 @@ package auth
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
@@ -18,105 +16,33 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&UserAuthTokenServiceImpl{})
|
||||
registry.RegisterService(&UserAuthTokenService{})
|
||||
}
|
||||
|
||||
var (
|
||||
getTime = time.Now
|
||||
UrgentRotateTime = 1 * time.Minute
|
||||
oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often.
|
||||
)
|
||||
var getTime = time.Now
|
||||
|
||||
// UserAuthTokenService are used for generating and validating user auth tokens
|
||||
type UserAuthTokenService interface {
|
||||
InitContextWithToken(ctx *models.ReqContext, orgID int64) bool
|
||||
UserAuthenticatedHook(user *models.User, c *models.ReqContext) error
|
||||
UserSignedOutHook(c *models.ReqContext)
|
||||
}
|
||||
const urgentRotateTime = 1 * time.Minute
|
||||
|
||||
type UserAuthTokenServiceImpl struct {
|
||||
type UserAuthTokenService struct {
|
||||
SQLStore *sqlstore.SqlStore `inject:""`
|
||||
ServerLockService *serverlock.ServerLockService `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// Init this service
|
||||
func (s *UserAuthTokenServiceImpl) Init() error {
|
||||
func (s *UserAuthTokenService) Init() error {
|
||||
s.log = log.New("auth")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool {
|
||||
//auth User
|
||||
unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName)
|
||||
if unhashedToken == "" {
|
||||
return false
|
||||
}
|
||||
func (s *UserAuthTokenService) ActiveTokenCount() (int64, error) {
|
||||
var model userAuthToken
|
||||
count, err := s.SQLStore.NewSession().Where(`created_at > ? AND rotated_at > ?`, s.createdAfterParam(), s.rotatedAfterParam()).Count(&model)
|
||||
|
||||
userToken, err := s.LookupToken(unhashedToken)
|
||||
if err != nil {
|
||||
ctx.Logger.Info("failed to look up user based on cookie", "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.SignedInUser = query.Result
|
||||
ctx.IsSignedIn = true
|
||||
|
||||
//rotate session token if needed.
|
||||
rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent())
|
||||
if err != nil {
|
||||
ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id)
|
||||
return true
|
||||
}
|
||||
|
||||
if rotated {
|
||||
s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds)
|
||||
}
|
||||
|
||||
return true
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) {
|
||||
if setting.Env == setting.DEV {
|
||||
ctx.Logger.Info("new token", "unhashed token", value)
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Del("Set-Cookie")
|
||||
cookie := http.Cookie{
|
||||
Name: s.Cfg.LoginCookieName,
|
||||
Value: url.QueryEscape(value),
|
||||
HttpOnly: true,
|
||||
Path: setting.AppSubUrl + "/",
|
||||
Secure: s.Cfg.SecurityHTTPSCookies,
|
||||
MaxAge: maxAge,
|
||||
SameSite: s.Cfg.LoginCookieSameSite,
|
||||
}
|
||||
|
||||
http.SetCookie(ctx.Resp, &cookie)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error {
|
||||
userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) {
|
||||
s.writeSessionCookie(c, "", -1)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) {
|
||||
func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
|
||||
clientIP = util.ParseIPAddress(clientIP)
|
||||
token, err := util.RandomHex(16)
|
||||
if err != nil {
|
||||
@@ -127,7 +53,7 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent
|
||||
|
||||
now := getTime().Unix()
|
||||
|
||||
userToken := userAuthToken{
|
||||
userAuthToken := userAuthToken{
|
||||
UserId: userId,
|
||||
AuthToken: hashedToken,
|
||||
PrevAuthToken: hashedToken,
|
||||
@@ -139,98 +65,109 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent
|
||||
SeenAt: 0,
|
||||
AuthTokenSeen: false,
|
||||
}
|
||||
_, err = s.SQLStore.NewSession().Insert(&userToken)
|
||||
_, err = s.SQLStore.NewSession().Insert(&userAuthToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userToken.UnhashedToken = token
|
||||
userAuthToken.UnhashedToken = token
|
||||
|
||||
return &userToken, nil
|
||||
s.log.Debug("user auth token created", "tokenId", userAuthToken.Id, "userId", userAuthToken.UserId, "clientIP", userAuthToken.ClientIp, "userAgent", userAuthToken.UserAgent, "authToken", userAuthToken.AuthToken)
|
||||
|
||||
var userToken models.UserToken
|
||||
err = userAuthToken.toUserToken(&userToken)
|
||||
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
|
||||
func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) {
|
||||
hashedToken := hashToken(unhashedToken)
|
||||
if setting.Env == setting.DEV {
|
||||
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
|
||||
}
|
||||
|
||||
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()
|
||||
|
||||
var userToken userAuthToken
|
||||
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken)
|
||||
var model userAuthToken
|
||||
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, s.createdAfterParam(), s.rotatedAfterParam()).Get(&model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, ErrAuthTokenNotFound
|
||||
return nil, models.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen {
|
||||
userTokenCopy := userToken
|
||||
userTokenCopy.AuthTokenSeen = false
|
||||
expireBefore := getTime().Add(-UrgentRotateTime).Unix()
|
||||
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy)
|
||||
if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = false
|
||||
expireBefore := getTime().Add(-urgentRotateTime).Unix()
|
||||
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", modelCopy.Id, modelCopy.PrevAuthToken, expireBefore).AllCols().Update(&modelCopy)
|
||||
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)
|
||||
s.log.Debug("prev seen token unchanged", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
} else {
|
||||
s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||
s.log.Debug("prev seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
}
|
||||
}
|
||||
|
||||
if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken {
|
||||
userTokenCopy := userToken
|
||||
userTokenCopy.AuthTokenSeen = true
|
||||
userTokenCopy.SeenAt = getTime().Unix()
|
||||
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy)
|
||||
if !model.AuthTokenSeen && model.AuthToken == hashedToken {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = true
|
||||
modelCopy.SeenAt = getTime().Unix()
|
||||
affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", modelCopy.Id, modelCopy.AuthToken).AllCols().Update(&modelCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if affectedRows == 1 {
|
||||
userToken = userTokenCopy
|
||||
model = modelCopy
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||
s.log.Debug("seen wrong token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
} else {
|
||||
s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent)
|
||||
s.log.Debug("seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken)
|
||||
}
|
||||
}
|
||||
|
||||
userToken.UnhashedToken = unhashedToken
|
||||
model.UnhashedToken = unhashedToken
|
||||
|
||||
return &userToken, nil
|
||||
var userToken models.UserToken
|
||||
err = model.toUserToken(&userToken)
|
||||
|
||||
return &userToken, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) {
|
||||
func (s *UserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) {
|
||||
if token == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
model := userAuthTokenFromUserToken(token)
|
||||
|
||||
now := getTime()
|
||||
|
||||
needsRotation := false
|
||||
rotatedAt := time.Unix(token.RotatedAt, 0)
|
||||
if token.AuthTokenSeen {
|
||||
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute))
|
||||
rotatedAt := time.Unix(model.RotatedAt, 0)
|
||||
if model.AuthTokenSeen {
|
||||
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.TokenRotationIntervalMinutes) * time.Minute))
|
||||
} else {
|
||||
needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime))
|
||||
needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
|
||||
}
|
||||
|
||||
if !needsRotation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id)
|
||||
s.log.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt)
|
||||
|
||||
clientIP = util.ParseIPAddress(clientIP)
|
||||
newToken, _ := util.RandomHex(16)
|
||||
newToken, err := util.RandomHex(16)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
hashedToken := hashToken(newToken)
|
||||
|
||||
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
|
||||
@@ -246,21 +183,54 @@ func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP,
|
||||
rotated_at = ?
|
||||
WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
|
||||
|
||||
res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
|
||||
res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
affected, _ := res.RowsAffected()
|
||||
s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId)
|
||||
s.log.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId)
|
||||
if affected > 0 {
|
||||
token.UnhashedToken = newToken
|
||||
model.UnhashedToken = newToken
|
||||
model.toUserToken(token)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
|
||||
if token == nil {
|
||||
return models.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
model := userAuthTokenFromUserToken(token)
|
||||
|
||||
rowsAffected, err := s.SQLStore.NewSession().Delete(model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
s.log.Debug("user auth token not found/revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
|
||||
return models.ErrUserTokenNotFound
|
||||
}
|
||||
|
||||
s.log.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) createdAfterParam() int64 {
|
||||
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||
return getTime().Add(-tokenMaxLifetime).Unix()
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) rotatedAfterParam() int64 {
|
||||
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
|
||||
return getTime().Add(-tokenMaxInactiveLifetime).Unix()
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
|
||||
return hex.EncodeToString(hashBytes[:])
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"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"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
@@ -23,202 +26,277 @@ func TestUserAuthToken(t *testing.T) {
|
||||
}
|
||||
|
||||
Convey("When creating token", func() {
|
||||
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeNil)
|
||||
So(token.AuthTokenSeen, ShouldBeFalse)
|
||||
So(userToken, ShouldNotBeNil)
|
||||
So(userToken.AuthTokenSeen, ShouldBeFalse)
|
||||
|
||||
Convey("Can count active tokens", func() {
|
||||
count, err := userAuthTokenService.ActiveTokenCount()
|
||||
So(err, ShouldBeNil)
|
||||
So(count, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("When lookup unhashed token should return user auth token", func() {
|
||||
LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(LookupToken, ShouldNotBeNil)
|
||||
So(LookupToken.UserId, ShouldEqual, userID)
|
||||
So(LookupToken.AuthTokenSeen, ShouldBeTrue)
|
||||
So(userToken, ShouldNotBeNil)
|
||||
So(userToken.UserId, ShouldEqual, userID)
|
||||
So(userToken.AuthTokenSeen, ShouldBeTrue)
|
||||
|
||||
storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id)
|
||||
storedAuthToken, err := ctx.getAuthTokenByID(userToken.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)
|
||||
userToken, err := userAuthTokenService.LookupToken(userToken.AuthToken)
|
||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
||||
So(userToken, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("revoking existing token should delete token", func() {
|
||||
err = userAuthTokenService.RevokeToken(userToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(model, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("revoking nil token should return error", func() {
|
||||
err = userAuthTokenService.RevokeToken(nil)
|
||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
||||
})
|
||||
|
||||
Convey("revoking non-existing token should return error", func() {
|
||||
userToken.Id = 1000
|
||||
err = userAuthTokenService.RevokeToken(userToken)
|
||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
token, err = ctx.getAuthTokenByID(token.Id)
|
||||
userToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return t.Add(time.Hour)
|
||||
}
|
||||
|
||||
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent")
|
||||
rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
_, err = userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
userToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
stillGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(stillGood, ShouldNotBeNil)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return t.Add(24 * 7 * time.Hour)
|
||||
}
|
||||
notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
So(err, ShouldEqual, ErrAuthTokenNotFound)
|
||||
So(notGood, ShouldBeNil)
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("when rotated_at is 6:59:59 ago should find token", func() {
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour).Add(-time.Second)
|
||||
}
|
||||
|
||||
stillGood, err = userAuthTokenService.LookupToken(stillGood.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(stillGood, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("when rotated_at is 7:00:00 ago should not find token", func() {
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour)
|
||||
}
|
||||
|
||||
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
||||
So(notGood, ShouldBeNil)
|
||||
|
||||
Convey("should not find active token when expired", func() {
|
||||
count, err := userAuthTokenService.ActiveTokenCount()
|
||||
So(err, ShouldBeNil)
|
||||
So(count, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() {
|
||||
updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix())
|
||||
So(err, ShouldBeNil)
|
||||
So(updated, ShouldBeTrue)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour).Add(-time.Second)
|
||||
}
|
||||
|
||||
stillGood, err = userAuthTokenService.LookupToken(stillGood.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(stillGood, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("when rotated_at is 5 days ago and created_at is 30 days ago should not find token", func() {
|
||||
updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix())
|
||||
So(err, ShouldBeNil)
|
||||
So(updated, ShouldBeTrue)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour)
|
||||
}
|
||||
|
||||
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
||||
So(notGood, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("can properly rotate tokens", func() {
|
||||
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeNil)
|
||||
|
||||
prevToken := token.AuthToken
|
||||
unhashedPrev := token.UnhashedToken
|
||||
prevToken := userToken.AuthToken
|
||||
unhashedPrev := userToken.UnhashedToken
|
||||
|
||||
refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
|
||||
rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeFalse)
|
||||
So(rotated, ShouldBeFalse)
|
||||
|
||||
updated, err := ctx.markAuthTokenAsSeen(token.Id)
|
||||
updated, err := ctx.markAuthTokenAsSeen(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(updated, ShouldBeTrue)
|
||||
|
||||
token, err = ctx.getAuthTokenByID(token.Id)
|
||||
model, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var tok models.UserToken
|
||||
err = model.toUserToken(&tok)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return t.Add(time.Hour)
|
||||
}
|
||||
|
||||
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
|
||||
rotated, err = userAuthTokenService.TryRotateToken(&tok, "192.168.10.12:1234", "a new user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
unhashedToken := token.UnhashedToken
|
||||
unhashedToken := tok.UnhashedToken
|
||||
|
||||
token, err = ctx.getAuthTokenByID(token.Id)
|
||||
model, err = ctx.getAuthTokenByID(tok.Id)
|
||||
So(err, ShouldBeNil)
|
||||
token.UnhashedToken = unhashedToken
|
||||
model.UnhashedToken = unhashedToken
|
||||
|
||||
So(token.RotatedAt, ShouldEqual, getTime().Unix())
|
||||
So(token.ClientIp, ShouldEqual, "192.168.10.12")
|
||||
So(token.UserAgent, ShouldEqual, "a new user agent")
|
||||
So(token.AuthTokenSeen, ShouldBeFalse)
|
||||
So(token.SeenAt, ShouldEqual, 0)
|
||||
So(token.PrevAuthToken, ShouldEqual, prevToken)
|
||||
So(model.RotatedAt, ShouldEqual, getTime().Unix())
|
||||
So(model.ClientIp, ShouldEqual, "192.168.10.12")
|
||||
So(model.UserAgent, ShouldEqual, "a new user agent")
|
||||
So(model.AuthTokenSeen, ShouldBeFalse)
|
||||
So(model.SeenAt, ShouldEqual, 0)
|
||||
So(model.PrevAuthToken, ShouldEqual, prevToken)
|
||||
|
||||
// ability to auth using an old token
|
||||
|
||||
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
|
||||
So(lookedUp.SeenAt, ShouldEqual, getTime().Unix())
|
||||
So(lookedUpUserToken, ShouldNotBeNil)
|
||||
So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue)
|
||||
So(lookedUpUserToken.SeenAt, ShouldEqual, getTime().Unix())
|
||||
|
||||
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
|
||||
lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUp.Id, ShouldEqual, token.Id)
|
||||
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
|
||||
So(lookedUpUserToken, ShouldNotBeNil)
|
||||
So(lookedUpUserToken.Id, ShouldEqual, model.Id)
|
||||
So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return t.Add(time.Hour + (2 * time.Minute))
|
||||
}
|
||||
|
||||
lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev)
|
||||
lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
|
||||
So(lookedUpUserToken, ShouldNotBeNil)
|
||||
So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue)
|
||||
|
||||
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
|
||||
lookedUpModel, err := ctx.getAuthTokenByID(lookedUpUserToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUp.AuthTokenSeen, ShouldBeFalse)
|
||||
So(lookedUpModel, ShouldNotBeNil)
|
||||
So(lookedUpModel.AuthTokenSeen, ShouldBeFalse)
|
||||
|
||||
refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent")
|
||||
rotated, err = userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
token, err = ctx.getAuthTokenByID(token.Id)
|
||||
model, err = ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeNil)
|
||||
So(token.SeenAt, ShouldEqual, 0)
|
||||
So(model, ShouldNotBeNil)
|
||||
So(model.SeenAt, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("keeps prev token valid for 1 minute after it is confirmed", func() {
|
||||
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeNil)
|
||||
So(userToken, ShouldNotBeNil)
|
||||
|
||||
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
lookedUpUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUpUserToken, ShouldNotBeNil)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return t.Add(10 * time.Minute)
|
||||
}
|
||||
|
||||
prevToken := token.UnhashedToken
|
||||
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
|
||||
prevToken := userToken.UnhashedToken
|
||||
rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
getTime = func() time.Time {
|
||||
return t.Add(20 * time.Minute)
|
||||
}
|
||||
|
||||
current, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
currentUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(current, ShouldNotBeNil)
|
||||
So(currentUserToken, ShouldNotBeNil)
|
||||
|
||||
prev, err := userAuthTokenService.LookupToken(prevToken)
|
||||
prevUserToken, err := userAuthTokenService.LookupToken(prevToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(prev, ShouldNotBeNil)
|
||||
So(prevUserToken, 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")
|
||||
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeNil)
|
||||
So(userToken, ShouldNotBeNil)
|
||||
|
||||
lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
lookedUpUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUpUserToken, ShouldNotBeNil)
|
||||
|
||||
lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken)
|
||||
lookedUpUserToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUpUserToken, ShouldNotBeNil)
|
||||
|
||||
lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id)
|
||||
lookedUpModel, err := ctx.getAuthTokenByID(lookedUpUserToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(lookedUp, ShouldNotBeNil)
|
||||
So(lookedUp.AuthTokenSeen, ShouldBeTrue)
|
||||
So(lookedUpModel, ShouldNotBeNil)
|
||||
So(lookedUpModel.AuthTokenSeen, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Rotate token", func() {
|
||||
token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent")
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeNil)
|
||||
So(userToken, ShouldNotBeNil)
|
||||
|
||||
prevToken := token.AuthToken
|
||||
prevToken := userToken.AuthToken
|
||||
|
||||
Convey("Should rotate current token and previous token when auth token seen", func() {
|
||||
updated, err := ctx.markAuthTokenAsSeen(token.Id)
|
||||
updated, err := ctx.markAuthTokenAsSeen(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(updated, ShouldBeTrue)
|
||||
|
||||
@@ -226,11 +304,11 @@ func TestUserAuthToken(t *testing.T) {
|
||||
return t.Add(10 * time.Minute)
|
||||
}
|
||||
|
||||
refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
|
||||
rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
storedToken, err := ctx.getAuthTokenByID(token.Id)
|
||||
storedToken, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(storedToken, ShouldNotBeNil)
|
||||
So(storedToken.AuthTokenSeen, ShouldBeFalse)
|
||||
@@ -239,7 +317,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
|
||||
prevToken = storedToken.AuthToken
|
||||
|
||||
updated, err = ctx.markAuthTokenAsSeen(token.Id)
|
||||
updated, err = ctx.markAuthTokenAsSeen(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(updated, ShouldBeTrue)
|
||||
|
||||
@@ -247,11 +325,11 @@ func TestUserAuthToken(t *testing.T) {
|
||||
return t.Add(20 * time.Minute)
|
||||
}
|
||||
|
||||
refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox")
|
||||
rotated, err = userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
storedToken, err = ctx.getAuthTokenByID(token.Id)
|
||||
storedToken, err = ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(storedToken, ShouldNotBeNil)
|
||||
So(storedToken.AuthTokenSeen, ShouldBeFalse)
|
||||
@@ -260,17 +338,17 @@ func TestUserAuthToken(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should rotate current token, but keep previous token when auth token not seen", func() {
|
||||
token.RotatedAt = getTime().Add(-2 * time.Minute).Unix()
|
||||
userToken.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")
|
||||
rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox")
|
||||
So(err, ShouldBeNil)
|
||||
So(refreshed, ShouldBeTrue)
|
||||
So(rotated, ShouldBeTrue)
|
||||
|
||||
storedToken, err := ctx.getAuthTokenByID(token.Id)
|
||||
storedToken, err := ctx.getAuthTokenByID(userToken.Id)
|
||||
So(err, ShouldBeNil)
|
||||
So(storedToken, ShouldNotBeNil)
|
||||
So(storedToken.AuthTokenSeen, ShouldBeFalse)
|
||||
@@ -279,6 +357,71 @@ func TestUserAuthToken(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When populating userAuthToken from UserToken should copy all properties", func() {
|
||||
ut := models.UserToken{
|
||||
Id: 1,
|
||||
UserId: 2,
|
||||
AuthToken: "a",
|
||||
PrevAuthToken: "b",
|
||||
UserAgent: "c",
|
||||
ClientIp: "d",
|
||||
AuthTokenSeen: true,
|
||||
SeenAt: 3,
|
||||
RotatedAt: 4,
|
||||
CreatedAt: 5,
|
||||
UpdatedAt: 6,
|
||||
UnhashedToken: "e",
|
||||
}
|
||||
utBytes, err := json.Marshal(ut)
|
||||
So(err, ShouldBeNil)
|
||||
utJSON, err := simplejson.NewJson(utBytes)
|
||||
So(err, ShouldBeNil)
|
||||
utMap := utJSON.MustMap()
|
||||
|
||||
var uat userAuthToken
|
||||
uat.fromUserToken(&ut)
|
||||
uatBytes, err := json.Marshal(uat)
|
||||
So(err, ShouldBeNil)
|
||||
uatJSON, err := simplejson.NewJson(uatBytes)
|
||||
So(err, ShouldBeNil)
|
||||
uatMap := uatJSON.MustMap()
|
||||
|
||||
So(uatMap, ShouldResemble, utMap)
|
||||
})
|
||||
|
||||
Convey("When populating userToken from userAuthToken should copy all properties", func() {
|
||||
uat := userAuthToken{
|
||||
Id: 1,
|
||||
UserId: 2,
|
||||
AuthToken: "a",
|
||||
PrevAuthToken: "b",
|
||||
UserAgent: "c",
|
||||
ClientIp: "d",
|
||||
AuthTokenSeen: true,
|
||||
SeenAt: 3,
|
||||
RotatedAt: 4,
|
||||
CreatedAt: 5,
|
||||
UpdatedAt: 6,
|
||||
UnhashedToken: "e",
|
||||
}
|
||||
uatBytes, err := json.Marshal(uat)
|
||||
So(err, ShouldBeNil)
|
||||
uatJSON, err := simplejson.NewJson(uatBytes)
|
||||
So(err, ShouldBeNil)
|
||||
uatMap := uatJSON.MustMap()
|
||||
|
||||
var ut models.UserToken
|
||||
err = uat.toUserToken(&ut)
|
||||
So(err, ShouldBeNil)
|
||||
utBytes, err := json.Marshal(ut)
|
||||
So(err, ShouldBeNil)
|
||||
utJSON, err := simplejson.NewJson(utBytes)
|
||||
So(err, ShouldBeNil)
|
||||
utMap := utJSON.MustMap()
|
||||
|
||||
So(utMap, ShouldResemble, uatMap)
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
getTime = time.Now
|
||||
})
|
||||
@@ -289,19 +432,16 @@ func createTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
|
||||
sqlstore := sqlstore.InitTestDB(t)
|
||||
tokenService := &UserAuthTokenServiceImpl{
|
||||
tokenService := &UserAuthTokenService{
|
||||
SQLStore: sqlstore,
|
||||
Cfg: &setting.Cfg{
|
||||
LoginCookieName: "grafana_session",
|
||||
LoginCookieMaxDays: 7,
|
||||
LoginDeleteExpiredTokensAfterDays: 30,
|
||||
LoginCookieRotation: 10,
|
||||
LoginMaxInactiveLifetimeDays: 7,
|
||||
LoginMaxLifetimeDays: 30,
|
||||
TokenRotationIntervalMinutes: 10,
|
||||
},
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
UrgentRotateTime = time.Minute
|
||||
|
||||
return &testContext{
|
||||
sqlstore: sqlstore,
|
||||
tokenService: tokenService,
|
||||
@@ -310,7 +450,7 @@ func createTestContext(t *testing.T) *testContext {
|
||||
|
||||
type testContext struct {
|
||||
sqlstore *sqlstore.SqlStore
|
||||
tokenService *UserAuthTokenServiceImpl
|
||||
tokenService *UserAuthTokenService
|
||||
}
|
||||
|
||||
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
|
||||
@@ -337,3 +477,17 @@ func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) {
|
||||
}
|
||||
return rowsAffected == 1, nil
|
||||
}
|
||||
|
||||
func (c *testContext) updateRotatedAt(id, rotatedAt int64) (bool, error) {
|
||||
sess := c.sqlstore.NewSession()
|
||||
res, err := sess.Exec("UPDATE user_auth_token SET rotated_at = ? WHERE id = ?", rotatedAt, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return rowsAffected == 1, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
"fmt"
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrAuthTokenNotFound = errors.New("User auth token not found")
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type userAuthToken struct {
|
||||
@@ -23,3 +20,51 @@ type userAuthToken struct {
|
||||
UpdatedAt int64
|
||||
UnhashedToken string `xorm:"-"`
|
||||
}
|
||||
|
||||
func userAuthTokenFromUserToken(ut *models.UserToken) *userAuthToken {
|
||||
var uat userAuthToken
|
||||
uat.fromUserToken(ut)
|
||||
return &uat
|
||||
}
|
||||
|
||||
func (uat *userAuthToken) fromUserToken(ut *models.UserToken) error {
|
||||
if uat == nil {
|
||||
return fmt.Errorf("needs pointer to userAuthToken struct")
|
||||
}
|
||||
|
||||
uat.Id = ut.Id
|
||||
uat.UserId = ut.UserId
|
||||
uat.AuthToken = ut.AuthToken
|
||||
uat.PrevAuthToken = ut.PrevAuthToken
|
||||
uat.UserAgent = ut.UserAgent
|
||||
uat.ClientIp = ut.ClientIp
|
||||
uat.AuthTokenSeen = ut.AuthTokenSeen
|
||||
uat.SeenAt = ut.SeenAt
|
||||
uat.RotatedAt = ut.RotatedAt
|
||||
uat.CreatedAt = ut.CreatedAt
|
||||
uat.UpdatedAt = ut.UpdatedAt
|
||||
uat.UnhashedToken = ut.UnhashedToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uat *userAuthToken) toUserToken(ut *models.UserToken) error {
|
||||
if uat == nil {
|
||||
return fmt.Errorf("needs pointer to userAuthToken struct")
|
||||
}
|
||||
|
||||
ut.Id = uat.Id
|
||||
ut.UserId = uat.UserId
|
||||
ut.AuthToken = uat.AuthToken
|
||||
ut.PrevAuthToken = uat.PrevAuthToken
|
||||
ut.UserAgent = uat.UserAgent
|
||||
ut.ClientIp = uat.ClientIp
|
||||
ut.AuthTokenSeen = uat.AuthTokenSeen
|
||||
ut.SeenAt = uat.SeenAt
|
||||
ut.RotatedAt = uat.RotatedAt
|
||||
ut.CreatedAt = uat.CreatedAt
|
||||
ut.UpdatedAt = uat.UpdatedAt
|
||||
ut.UnhashedToken = uat.UnhashedToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(time.Hour * 12)
|
||||
deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.LoginDeleteExpiredTokensAfterDays)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
srv.ServerLockService.LockAndExecute(ctx, "delete old sessions", time.Hour*12, func() {
|
||||
srv.deleteOldSession(deleteSessionAfter)
|
||||
})
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *UserAuthTokenServiceImpl) deleteOldSession(deleteSessionAfter time.Duration) (int64, error) {
|
||||
sql := `DELETE from user_auth_token WHERE rotated_at < ?`
|
||||
|
||||
deleteBefore := getTime().Add(-deleteSessionAfter)
|
||||
res, err := srv.SQLStore.NewSession().Exec(sql, deleteBefore.Unix())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
srv.log.Info("deleted old sessions", "count", affected)
|
||||
|
||||
return affected, err
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUserAuthTokenCleanup(t *testing.T) {
|
||||
|
||||
Convey("Test user auth token cleanup", t, func() {
|
||||
ctx := createTestContext(t)
|
||||
|
||||
insertToken := func(token string, prev string, rotatedAt int64) {
|
||||
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
|
||||
_, err := ctx.sqlstore.NewSession().Insert(&ut)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
// insert three old tokens that should be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), int64(i))
|
||||
}
|
||||
|
||||
// insert three active tokens that should not be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), getTime().Unix())
|
||||
}
|
||||
|
||||
affected, err := ctx.tokenService.deleteOldSession(time.Hour)
|
||||
So(err, ShouldBeNil)
|
||||
So(affected, ShouldEqual, 3)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (srv *UserAuthTokenService) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
maxInactiveLifetime := time.Duration(srv.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
|
||||
maxLifetime := time.Duration(srv.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||
|
||||
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
|
||||
srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime)
|
||||
})
|
||||
if err != nil {
|
||||
srv.log.Error("failed to lock and execite cleanup of expired auth token", "erro", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
|
||||
srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
srv.log.Error("failed to lock and execite cleanup of expired auth token", "erro", err)
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *UserAuthTokenService) deleteExpiredTokens(maxInactiveLifetime, maxLifetime time.Duration) (int64, error) {
|
||||
createdBefore := getTime().Add(-maxLifetime)
|
||||
rotatedBefore := getTime().Add(-maxInactiveLifetime)
|
||||
|
||||
srv.log.Debug("starting cleanup of expired auth tokens", "createdBefore", createdBefore, "rotatedBefore", rotatedBefore)
|
||||
|
||||
sql := `DELETE from user_auth_token WHERE created_at <= ? OR rotated_at <= ?`
|
||||
res, err := srv.SQLStore.NewSession().Exec(sql, createdBefore.Unix(), rotatedBefore.Unix())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
srv.log.Error("failed to cleanup expired auth tokens", "error", err)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
srv.log.Debug("cleanup of expired auth tokens done", "count", affected)
|
||||
return affected, err
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUserAuthTokenCleanup(t *testing.T) {
|
||||
|
||||
Convey("Test user auth token cleanup", t, func() {
|
||||
ctx := createTestContext(t)
|
||||
ctx.tokenService.Cfg.LoginMaxInactiveLifetimeDays = 7
|
||||
ctx.tokenService.Cfg.LoginMaxLifetimeDays = 30
|
||||
|
||||
insertToken := func(token string, prev string, createdAt, rotatedAt int64) {
|
||||
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
|
||||
_, err := ctx.sqlstore.NewSession().Insert(&ut)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC)
|
||||
getTime = func() time.Time {
|
||||
return t
|
||||
}
|
||||
|
||||
Convey("should delete tokens where token rotation age is older than or equal 7 days", func() {
|
||||
from := t.Add(-7 * 24 * time.Hour)
|
||||
|
||||
// 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), from.Unix(), from.Unix())
|
||||
}
|
||||
|
||||
// insert three active tokens that should not be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
from = from.Add(time.Second)
|
||||
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix())
|
||||
}
|
||||
|
||||
affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour)
|
||||
So(err, ShouldBeNil)
|
||||
So(affected, ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("should delete tokens where token age is older than or equal 30 days", func() {
|
||||
from := t.Add(-30 * 24 * time.Hour)
|
||||
fromRotate := t.Add(-time.Second)
|
||||
|
||||
// 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), from.Unix(), fromRotate.Unix())
|
||||
}
|
||||
|
||||
// insert three active tokens that should not be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
from = from.Add(time.Second)
|
||||
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), fromRotate.Unix())
|
||||
}
|
||||
|
||||
affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour)
|
||||
So(err, ShouldBeNil)
|
||||
So(affected, ShouldEqual, 3)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
|
||||
files, err := ioutil.ReadDir(cr.path)
|
||||
if err != nil {
|
||||
cr.log.Error("can't read dashboard provisioning files from directory", "path", cr.path)
|
||||
cr.log.Error("can't read dashboard provisioning files from directory", "path", cr.path, "error", err)
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error)
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
cr.log.Error("can't read datasource provisioning files from directory", "path", path)
|
||||
cr.log.Error("can't read datasource provisioning files from directory", "path", path, "error", err)
|
||||
return datasources, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (cr *configReader) readConfig(path string) ([]*notificationsAsConfig, error
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
cr.log.Error("Can't read alert notification provisioning files from directory", "path", path)
|
||||
cr.log.Error("Can't read alert notification provisioning files from directory", "path", path, "error", err)
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,23 @@ package quota
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func QuotaReached(c *m.ReqContext, target string) (bool, error) {
|
||||
func init() {
|
||||
registry.RegisterService(&QuotaService{})
|
||||
}
|
||||
|
||||
type QuotaService struct {
|
||||
AuthTokenService m.UserTokenService `inject:""`
|
||||
}
|
||||
|
||||
func (qs *QuotaService) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, error) {
|
||||
if !setting.Quota.Enabled {
|
||||
return false, nil
|
||||
}
|
||||
@@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
if target == "session" {
|
||||
usedSessions := session.GetSessionCount()
|
||||
|
||||
usedSessions, err := qs.AuthTokenService.ActiveTokenCount()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if int64(usedSessions) > scope.DefaultLimit {
|
||||
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
|
||||
return true, nil
|
||||
|
||||
@@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
||||
defer middleware.RemoveRenderAuthKey(renderKey)
|
||||
|
||||
phantomDebugArg := "--debug=false"
|
||||
if log.GetLogLevelFor("renderer") >= log.LvlDebug {
|
||||
if log.GetLogLevelFor("rendering") >= log.LvlDebug {
|
||||
phantomDebugArg = "--debug=true"
|
||||
}
|
||||
|
||||
@@ -64,13 +64,26 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
||||
cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
timezone := ""
|
||||
|
||||
if opts.Timezone != "" {
|
||||
timezone = isoTimeOffsetToPosixTz(opts.Timezone)
|
||||
baseEnviron := os.Environ()
|
||||
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(opts.Timezone))
|
||||
cmd.Env = appendEnviron(baseEnviron, "TZ", timezone)
|
||||
}
|
||||
|
||||
rs.log.Debug("executing Phantomjs", "binPath", binPath, "cmdArgs", cmdArgs, "timezone", timezone)
|
||||
|
||||
out, err := cmd.Output()
|
||||
|
||||
if out != nil {
|
||||
rs.log.Debug("Phantomjs output", "out", string(out))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rs.log.Debug("Phantomjs error", "error", err)
|
||||
}
|
||||
|
||||
// check for timeout first
|
||||
if commandCtx.Err() == context.DeadlineExceeded {
|
||||
rs.log.Info("Rendering timed out")
|
||||
@@ -82,8 +95,6 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs.log.Debug("Phantomjs output", "out", string(out))
|
||||
|
||||
rs.log.Debug("Image rendered", "path", pngPath)
|
||||
return &RenderResult{FilePath: pngPath}, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type Hit struct {
|
||||
Title string `json:"title"`
|
||||
Uri string `json:"uri"`
|
||||
Url string `json:"url"`
|
||||
Slug string `json:"slug"`
|
||||
Type HitType `json:"type"`
|
||||
Tags []string `json:"tags"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
|
||||
var sessionManager *ms.Manager
|
||||
var sessionOptions *ms.Options
|
||||
var StartSessionGC func()
|
||||
var StartSessionGC func() = func() {}
|
||||
var GetSessionCount func() int
|
||||
var sessionLogger = log.New("session")
|
||||
var sessionConnMaxLifetime int64
|
||||
|
||||
@@ -309,7 +309,9 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
res, err := sess.Exec(buffer.String(), params...)
|
||||
sqlOrArgs := append([]interface{}{buffer.String()}, params...)
|
||||
|
||||
res, err := sess.Exec(sqlOrArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -258,11 +258,15 @@ func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
|
||||
queryParams = []interface{}{params.DashboardId, params.PanelId, params.OrgId}
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
|
||||
sqlOrArgs := append([]interface{}{annoTagSql}, queryParams...)
|
||||
|
||||
if _, err := sess.Exec(sqlOrArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(sql, queryParams...); err != nil {
|
||||
sqlOrArgs = append([]interface{}{sql}, queryParams...)
|
||||
|
||||
if _, err := sess.Exec(sqlOrArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
@@ -51,7 +51,7 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
|
||||
dashboard_version.message,
|
||||
dashboard_version.data,`+
|
||||
dialect.Quote("user")+`.login as created_by`).
|
||||
Join("LEFT", "user", `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
|
||||
Join("LEFT", dialect.Quote("user"), `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
|
||||
Join("LEFT", "dashboard", `dashboard.id = dashboard_version.dashboard_id`).
|
||||
Where("dashboard_version.dashboard_id=? AND dashboard.org_id=?", query.DashboardId, query.OrgId).
|
||||
OrderBy("dashboard_version.version DESC").
|
||||
@@ -102,7 +102,8 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
|
||||
|
||||
if len(versionIdsToDelete) > 0 {
|
||||
deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)`
|
||||
expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...)
|
||||
sqlOrArgs := append([]interface{}{deleteExpiredSql}, versionIdsToDelete...)
|
||||
expiredResponse, err := sess.Exec(sqlOrArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package sqlstore
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
@@ -95,6 +97,10 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error {
|
||||
return m.ErrDataSourceNameExists
|
||||
}
|
||||
|
||||
if cmd.JsonData == nil {
|
||||
cmd.JsonData = simplejson.New()
|
||||
}
|
||||
|
||||
ds := &m.DataSource{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
@@ -142,6 +148,10 @@ func updateIsDefaultFlag(ds *m.DataSource, sess *DBSession) error {
|
||||
|
||||
func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
if cmd.JsonData == nil {
|
||||
cmd.JsonData = simplejson.New()
|
||||
}
|
||||
|
||||
ds := &m.DataSource{
|
||||
Id: cmd.Id,
|
||||
OrgId: cmd.OrgId,
|
||||
|
||||
@@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if result == nil || len(result) == 0 || result[0] == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxId = toInt64(result[0]["id"])
|
||||
|
||||
if maxId == 0 {
|
||||
|
||||
@@ -130,4 +130,7 @@ func addDataSourceMigration(mg *Migrator) {
|
||||
|
||||
const migrateLoggingToLoki = `UPDATE data_source SET type = 'loki' WHERE type = 'logging'`
|
||||
mg.AddMigration("Migrate logging ds to loki ds", NewRawSqlMigration(migrateLoggingToLoki))
|
||||
|
||||
const setEmptyJSONWhereNullJSON = `UPDATE data_source SET json_data = '{}' WHERE json_data is null`
|
||||
mg.AddMigration("Update json_data with nulls", NewRawSqlMigration(setEmptyJSONWhereNullJSON))
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
|
||||
query.Result = make([]*m.OrgUserDTO, 0)
|
||||
|
||||
sess := x.Table("org_user")
|
||||
sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
|
||||
whereConditions := make([]string, 0)
|
||||
whereParams := make([]interface{}, 0)
|
||||
|
||||
@@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
|
||||
|
||||
cnnstr += ss.buildExtraConnectionString('&')
|
||||
case migrator.POSTGRES:
|
||||
host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432")
|
||||
if ss.dbCfg.Pwd == "" {
|
||||
ss.dbCfg.Pwd = "''"
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
|
||||
|
||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`)
|
||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`)
|
||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams`)
|
||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`)
|
||||
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS auth_tokens`)
|
||||
|
||||
var stats m.SystemStats
|
||||
_, err := x.SQL(sb.GetSqlString(), sb.params...).Get(&stats)
|
||||
|
||||
@@ -280,7 +280,7 @@ func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
|
||||
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
|
||||
query.Result = make([]*m.TeamMemberDTO, 0)
|
||||
sess := x.Table("team_member")
|
||||
sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
if query.OrgId != 0 {
|
||||
sess.Where("team_member.org_id=?", query.OrgId)
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ func TestUserDataAccess(t *testing.T) {
|
||||
func GetOrgUsersForTest(query *m.GetOrgUsersQuery) error {
|
||||
query.Result = make([]*m.OrgUserDTO, 0)
|
||||
sess := x.Table("org_user")
|
||||
sess.Join("LEFT ", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Join("LEFT ", x.Dialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Where("org_user.org_id=?", query.OrgId)
|
||||
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user