Selaa lähdekoodia

Datasource proxy & session timeout fix (casued 401 Unauthorized error after a while), Fixes #1667

Torkel Ödegaard 10 vuotta sitten
vanhempi
commit
22adf0d06e

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@
 - [Issue #1660](https://github.com/grafana/grafana/issues/1660). OAuth: Specify allowed email address domains for google or and github oauth logins
 
 **Fixes**
+- [Issue #1667](https://github.com/grafana/grafana/issues/1667). Datasource proxy & session timeout fix (casued 401 Unauthorized error after a while)
 - [Issue #1707](https://github.com/grafana/grafana/issues/1707). Unsaved changes: Do not show for snapshots, scripted and file based dashboards
 - [Issue #1703](https://github.com/grafana/grafana/issues/1703). Unsaved changes: Do not show for users with role `Viewer`
 - [Issue #1675](https://github.com/grafana/grafana/issues/1675). Data source proxy: Fixed issue with Gzip enabled and data source proxy

+ 3 - 0
pkg/api/api.go

@@ -48,6 +48,9 @@ func Register(r *macaron.Macaron) {
 	r.Get("/api/snapshots/:key", GetDashboardSnapshot)
 	r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
 
+	// api renew session based on remember cookie
+	r.Get("/api/login/ping", LoginApiPing)
+
 	// authed api
 	r.Group("/api", func() {
 		// user

+ 25 - 14
pkg/api/login.go

@@ -28,11 +28,25 @@ func LoginView(c *middleware.Context) {
 	settings["githubAuthEnabled"] = setting.OAuthService.GitHub
 	settings["disableUserSignUp"] = !setting.AllowUserSignUp
 
+	if !tryLoginUsingRememberCookie(c) {
+		c.HTML(200, VIEW_INDEX)
+		return
+	}
+
+	if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 {
+		c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
+		c.Redirect(redirectTo)
+		return
+	}
+
+	c.Redirect(setting.AppSubUrl + "/")
+}
+
+func tryLoginUsingRememberCookie(c *middleware.Context) bool {
 	// Check auto-login.
 	uname := c.GetCookie(setting.CookieUserName)
 	if len(uname) == 0 {
-		c.HTML(200, VIEW_INDEX)
-		return
+		return false
 	}
 
 	isSucceed := false
@@ -47,32 +61,29 @@ func LoginView(c *middleware.Context) {
 
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: uname}
 	if err := bus.Dispatch(&userQuery); err != nil {
-		if err != m.ErrUserNotFound {
-			c.Handle(500, "GetUserByLoginQuery", err)
-		} else {
-			c.HTML(200, VIEW_INDEX)
-		}
-		return
+		return false
 	}
 
 	user := userQuery.Result
 
+	// validate remember me cookie
 	if val, _ := c.GetSuperSecureCookie(
 		util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName); val != user.Login {
-		c.HTML(200, VIEW_INDEX)
-		return
+		return false
 	}
 
 	isSucceed = true
 	loginUserWithUser(user, c)
+	return true
+}
 
-	if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 {
-		c.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
-		c.Redirect(redirectTo)
+func LoginApiPing(c *middleware.Context) {
+	if !tryLoginUsingRememberCookie(c) {
+		c.JsonApiErr(401, "Unauthorized", nil)
 		return
 	}
 
-	c.Redirect(setting.AppSubUrl + "/")
+	c.JsonOK("Logged in")
 }
 
 func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) {

+ 2 - 2
public/app/plugins/datasource/graphite/datasource.js

@@ -14,7 +14,7 @@ function (angular, _, $, config, kbn, moment) {
 
   var module = angular.module('grafana.services');
 
-  module.factory('GraphiteDatasource', function($q, $http, templateSrv) {
+  module.factory('GraphiteDatasource', function($q, backendSrv, templateSrv) {
 
     function GraphiteDatasource(datasource) {
       this.basicAuth = datasource.basicAuth;
@@ -218,7 +218,7 @@ function (angular, _, $, config, kbn, moment) {
       options.url = this.url + options.url;
       options.inspect = { type: 'graphite' };
 
-      return $http(options);
+      return backendSrv.datasourceRequest(options);
     };
 
     GraphiteDatasource.prototype._seriesRefLetters = [

+ 62 - 29
public/app/services/backendSrv.js

@@ -28,43 +28,46 @@ function (angular, _, config) {
     };
 
     this._handleError = function(err) {
-      if (err.status === 422) {
-        alertSrv.set("Validation failed", "", "warning", 4000);
-        throw err.data;
-      }
+      return function() {
+        if (err.isHandled) {
+          return;
+        }
 
-      var data = err.data || { message: 'Unexpected error' };
+        if (err.status === 422) {
+          alertSrv.set("Validation failed", "", "warning", 4000);
+          throw err.data;
+        }
 
-      if (_.isString(data)) {
-        data = { message: data };
-      }
+        var data = err.data || { message: 'Unexpected error' };
 
-      data.severity = 'error';
+        if (_.isString(data)) {
+          data = { message: data };
+        }
 
-      if (err.status < 500) {
-        data.severity = "warning";
-      }
+        data.severity = 'error';
 
-      if (data.message) {
-        alertSrv.set("Problem!", data.message, data.severity, 10000);
-      }
+        if (err.status < 500) {
+          data.severity = "warning";
+        }
+
+        if (data.message) {
+          alertSrv.set("Problem!", data.message, data.severity, 10000);
+        }
 
-      throw data;
+        throw data;
+      };
     };
 
     this.request = function(options) {
-      var httpOptions = {
-        url: options.url,
-        method: options.method,
-        data: options.data,
-        params: options.params,
-      };
+      options.retry = options.retry || 0;
+      var requestIsLocal = options.url.indexOf('/') === 0;
+      var firstAttempt = options.retry === 0;
 
-      if (httpOptions.url.indexOf('/') === 0) {
-        httpOptions.url = config.appSubUrl + httpOptions.url;
+      if (requestIsLocal && firstAttempt) {
+        options.url = config.appSubUrl + options.url;
       }
 
-      return $http(httpOptions).then(function(results) {
+      return $http(options).then(function(results) {
         if (options.method !== 'GET') {
           if (results && results.data.message) {
             alertSrv.set(results.data.message, '', 'success', 3000);
@@ -72,15 +75,45 @@ function (angular, _, config) {
         }
         return results.data;
       }, function(err) {
-        $timeout(function() {
-          if (err.isHandled) { return; }
-          self._handleError(err);
-        }, 50);
+        // handle unauthorized
+        if (err.status === 401 && firstAttempt) {
+          return self.loginPing().then(function() {
+            options.retry = 1;
+            return self.request(options);
+          });
+        }
+
+        $timeout(self._handleError(err), 50);
+        throw err;
+      });
+    };
+
+    this.datasourceRequest = function(options) {
+      options.retry = options.retry || 0;
+      var requestIsLocal = options.url.indexOf('/') === 0;
+      var firstAttempt = options.retry === 0;
+
+      if (requestIsLocal && firstAttempt) {
+        options.url = config.appSubUrl + options.url;
+      }
+
+      return $http(options).then(null, function(err) {
+        // handle unauthorized for backend requests
+        if (requestIsLocal && firstAttempt  && err.status === 401) {
+          return self.loginPing().then(function() {
+            options.retry = 1;
+            return self.datasourceRequest(options);
+          });
+        }
 
         throw err;
       });
     };
 
+    this.loginPing = function() {
+      return this.request({url: '/api/login/ping', method: 'GET', retry: 1 });
+    };
+
     this.search = function(query) {
       return this.get('/api/search', query);
     };

+ 15 - 10
public/test/specs/graphiteDatasource-specs.js

@@ -8,7 +8,8 @@ define([
     var ctx = new helpers.ServiceTestContext();
 
     beforeEach(module('grafana.services'));
-    beforeEach(ctx.providePhase());
+    beforeEach(ctx.providePhase(['backendSrv']));
+
     beforeEach(ctx.createService('GraphiteDatasource'));
     beforeEach(function() {
       ctx.ds = new ctx.service({ url: [''] });
@@ -21,25 +22,25 @@ define([
         maxDataPoints: 500,
       };
 
-      var response = [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]], }];
       var results;
-      var request;
+      var requestOptions;
 
       beforeEach(function() {
-
-        ctx.$httpBackend.expectPOST('/render', function(body) { request = body; return true; })
-          .respond(response);
+        ctx.backendSrv.datasourceRequest = function(options) {
+          requestOptions = options;
+          return ctx.$q.when({data: [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]] }]});
+        };
 
         ctx.ds.query(query).then(function(data) { results = data; });
-        ctx.$httpBackend.flush();
+        ctx.$rootScope.$apply();
       });
 
       it('should generate the correct query', function() {
-        ctx.$httpBackend.verifyNoOutstandingExpectation();
+        expect(requestOptions.url).to.be('/render');
       });
 
       it('should query correctly', function() {
-        var params = request.split('&');
+        var params = requestOptions.data.split('&');
         expect(params).to.contain('target=prod1.count');
         expect(params).to.contain('target=prod2.count');
         expect(params).to.contain('from=-1h');
@@ -47,7 +48,7 @@ define([
       });
 
       it('should exclude undefined params', function() {
-        var params = request.split('&');
+        var params = requestOptions.data.split('&');
         expect(params).to.not.contain('cacheTimeout=undefined');
       });
 
@@ -56,6 +57,10 @@ define([
         expect(results.data[0].target).to.be('prod1.count');
       });
 
+      it('should convert to millisecond resolution', function() {
+        expect(results.data[0].datapoints[0][0]).to.be(10);
+      });
+
     });
 
     describe('building graphite params', function() {

+ 1 - 0
public/test/specs/helpers.js

@@ -68,6 +68,7 @@ define([
     self.templateSrv = new TemplateSrvStub();
     self.timeSrv = new TimeSrvStub();
     self.datasourceSrv = {};
+    self.backendSrv = {};
     self.$routeParams = {};
 
     this.providePhase = function(mocks) {