ds_proxy_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. package pluginproxy
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io/ioutil"
  6. "net/http"
  7. "net/url"
  8. "testing"
  9. "time"
  10. "golang.org/x/oauth2"
  11. macaron "gopkg.in/macaron.v1"
  12. "github.com/grafana/grafana/pkg/bus"
  13. "github.com/grafana/grafana/pkg/components/simplejson"
  14. "github.com/grafana/grafana/pkg/log"
  15. "github.com/grafana/grafana/pkg/login/social"
  16. m "github.com/grafana/grafana/pkg/models"
  17. "github.com/grafana/grafana/pkg/plugins"
  18. "github.com/grafana/grafana/pkg/setting"
  19. "github.com/grafana/grafana/pkg/util"
  20. . "github.com/smartystreets/goconvey/convey"
  21. )
  22. func TestDSRouteRule(t *testing.T) {
  23. Convey("DataSourceProxy", t, func() {
  24. Convey("Plugin with routes", func() {
  25. plugin := &plugins.DataSourcePlugin{
  26. Routes: []*plugins.AppPluginRoute{
  27. {
  28. Path: "api/v4/",
  29. Url: "https://www.google.com",
  30. ReqRole: m.ROLE_EDITOR,
  31. Headers: []plugins.AppPluginRouteHeader{
  32. {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
  33. },
  34. },
  35. {
  36. Path: "api/admin",
  37. Url: "https://www.google.com",
  38. ReqRole: m.ROLE_ADMIN,
  39. Headers: []plugins.AppPluginRouteHeader{
  40. {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
  41. },
  42. },
  43. {
  44. Path: "api/anon",
  45. Url: "https://www.google.com",
  46. Headers: []plugins.AppPluginRouteHeader{
  47. {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
  48. },
  49. },
  50. {
  51. Path: "api/common",
  52. Url: "{{.JsonData.dynamicUrl}}",
  53. Headers: []plugins.AppPluginRouteHeader{
  54. {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
  55. },
  56. },
  57. },
  58. }
  59. setting.SecretKey = "password"
  60. key, _ := util.Encrypt([]byte("123"), "password")
  61. ds := &m.DataSource{
  62. JsonData: simplejson.NewFromAny(map[string]interface{}{
  63. "clientId": "asd",
  64. "dynamicUrl": "https://dynamic.grafana.com",
  65. }),
  66. SecureJsonData: map[string][]byte{
  67. "key": key,
  68. },
  69. }
  70. req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
  71. ctx := &m.ReqContext{
  72. Context: &macaron.Context{
  73. Req: macaron.Request{Request: req},
  74. },
  75. SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
  76. }
  77. Convey("When matching route path", func() {
  78. proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{})
  79. proxy.route = plugin.Routes[0]
  80. ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
  81. Convey("should add headers and update url", func() {
  82. So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
  83. So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
  84. })
  85. })
  86. Convey("When matching route path and has dynamic url", func() {
  87. proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", &setting.Cfg{})
  88. proxy.route = plugin.Routes[3]
  89. ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
  90. Convey("should add headers and interpolate the url", func() {
  91. So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
  92. So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
  93. })
  94. })
  95. Convey("Validating request", func() {
  96. Convey("plugin route with valid role", func() {
  97. proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{})
  98. err := proxy.validateRequest()
  99. So(err, ShouldBeNil)
  100. })
  101. Convey("plugin route with admin role and user is editor", func() {
  102. proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{})
  103. err := proxy.validateRequest()
  104. So(err, ShouldNotBeNil)
  105. })
  106. Convey("plugin route with admin role and user is admin", func() {
  107. ctx.SignedInUser.OrgRole = m.ROLE_ADMIN
  108. proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{})
  109. err := proxy.validateRequest()
  110. So(err, ShouldBeNil)
  111. })
  112. })
  113. })
  114. Convey("Plugin with multiple routes for token auth", func() {
  115. plugin := &plugins.DataSourcePlugin{
  116. Routes: []*plugins.AppPluginRoute{
  117. {
  118. Path: "pathwithtoken1",
  119. Url: "https://api.nr1.io/some/path",
  120. TokenAuth: &plugins.JwtTokenAuth{
  121. Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
  122. Params: map[string]string{
  123. "grant_type": "client_credentials",
  124. "client_id": "{{.JsonData.clientId}}",
  125. "client_secret": "{{.SecureJsonData.clientSecret}}",
  126. "resource": "https://api.nr1.io",
  127. },
  128. },
  129. },
  130. {
  131. Path: "pathwithtoken2",
  132. Url: "https://api.nr2.io/some/path",
  133. TokenAuth: &plugins.JwtTokenAuth{
  134. Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
  135. Params: map[string]string{
  136. "grant_type": "client_credentials",
  137. "client_id": "{{.JsonData.clientId}}",
  138. "client_secret": "{{.SecureJsonData.clientSecret}}",
  139. "resource": "https://api.nr2.io",
  140. },
  141. },
  142. },
  143. },
  144. }
  145. setting.SecretKey = "password"
  146. key, _ := util.Encrypt([]byte("123"), "password")
  147. ds := &m.DataSource{
  148. JsonData: simplejson.NewFromAny(map[string]interface{}{
  149. "clientId": "asd",
  150. "tenantId": "mytenantId",
  151. }),
  152. SecureJsonData: map[string][]byte{
  153. "clientSecret": key,
  154. },
  155. }
  156. req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
  157. ctx := &m.ReqContext{
  158. Context: &macaron.Context{
  159. Req: macaron.Request{Request: req},
  160. },
  161. SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
  162. }
  163. Convey("When creating and caching access tokens", func() {
  164. var authorizationHeaderCall1 string
  165. var authorizationHeaderCall2 string
  166. Convey("first call should add authorization header with access token", func() {
  167. json, err := ioutil.ReadFile("./test-data/access-token-1.json")
  168. So(err, ShouldBeNil)
  169. client = newFakeHTTPClient(json)
  170. proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{})
  171. proxy1.route = plugin.Routes[0]
  172. ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds)
  173. authorizationHeaderCall1 = req.Header.Get("Authorization")
  174. So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
  175. So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
  176. Convey("second call to another route should add a different access token", func() {
  177. json2, err := ioutil.ReadFile("./test-data/access-token-2.json")
  178. So(err, ShouldBeNil)
  179. req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
  180. client = newFakeHTTPClient(json2)
  181. proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", &setting.Cfg{})
  182. proxy2.route = plugin.Routes[1]
  183. ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds)
  184. authorizationHeaderCall2 = req.Header.Get("Authorization")
  185. So(req.URL.String(), ShouldEqual, "https://api.nr2.io/some/path")
  186. So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
  187. So(authorizationHeaderCall2, ShouldStartWith, "Bearer eyJ0e")
  188. So(authorizationHeaderCall2, ShouldNotEqual, authorizationHeaderCall1)
  189. Convey("third call to first route should add cached access token", func() {
  190. req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
  191. client = newFakeHTTPClient([]byte{})
  192. proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{})
  193. proxy3.route = plugin.Routes[0]
  194. ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds)
  195. authorizationHeaderCall3 := req.Header.Get("Authorization")
  196. So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
  197. So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
  198. So(authorizationHeaderCall3, ShouldStartWith, "Bearer eyJ0e")
  199. So(authorizationHeaderCall3, ShouldEqual, authorizationHeaderCall1)
  200. })
  201. })
  202. })
  203. })
  204. })
  205. Convey("When proxying graphite", func() {
  206. setting.BuildVersion = "5.3.0"
  207. plugin := &plugins.DataSourcePlugin{}
  208. ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
  209. ctx := &m.ReqContext{}
  210. proxy := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{})
  211. req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
  212. So(err, ShouldBeNil)
  213. proxy.getDirector()(req)
  214. Convey("Can translate request url and path", func() {
  215. So(req.URL.Host, ShouldEqual, "graphite:8080")
  216. So(req.URL.Path, ShouldEqual, "/render")
  217. So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0")
  218. })
  219. })
  220. Convey("When proxying InfluxDB", func() {
  221. plugin := &plugins.DataSourcePlugin{}
  222. ds := &m.DataSource{
  223. Type: m.DS_INFLUXDB_08,
  224. Url: "http://influxdb:8083",
  225. Database: "site",
  226. User: "user",
  227. Password: "password",
  228. }
  229. ctx := &m.ReqContext{}
  230. proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
  231. req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
  232. So(err, ShouldBeNil)
  233. proxy.getDirector()(req)
  234. Convey("Should add db to url", func() {
  235. So(req.URL.Path, ShouldEqual, "/db/site/")
  236. })
  237. Convey("Should add username and password", func() {
  238. queryVals := req.URL.Query()
  239. So(queryVals["u"][0], ShouldEqual, "user")
  240. So(queryVals["p"][0], ShouldEqual, "password")
  241. })
  242. })
  243. Convey("When proxying a data source with no keepCookies specified", func() {
  244. plugin := &plugins.DataSourcePlugin{}
  245. json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`))
  246. ds := &m.DataSource{
  247. Type: m.DS_GRAPHITE,
  248. Url: "http://graphite:8086",
  249. JsonData: json,
  250. }
  251. ctx := &m.ReqContext{}
  252. proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
  253. requestURL, _ := url.Parse("http://grafana.com/sub")
  254. req := http.Request{URL: requestURL, Header: make(http.Header)}
  255. cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
  256. req.Header.Set("Cookie", cookies)
  257. proxy.getDirector()(&req)
  258. Convey("Should clear all cookies", func() {
  259. So(req.Header.Get("Cookie"), ShouldEqual, "")
  260. })
  261. })
  262. Convey("When proxying a data source with keep cookies specified", func() {
  263. plugin := &plugins.DataSourcePlugin{}
  264. json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`))
  265. ds := &m.DataSource{
  266. Type: m.DS_GRAPHITE,
  267. Url: "http://graphite:8086",
  268. JsonData: json,
  269. }
  270. ctx := &m.ReqContext{}
  271. proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
  272. requestURL, _ := url.Parse("http://grafana.com/sub")
  273. req := http.Request{URL: requestURL, Header: make(http.Header)}
  274. cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
  275. req.Header.Set("Cookie", cookies)
  276. proxy.getDirector()(&req)
  277. Convey("Should keep named cookies", func() {
  278. So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test")
  279. })
  280. })
  281. Convey("When proxying a data source with custom headers specified", func() {
  282. plugin := &plugins.DataSourcePlugin{}
  283. encryptedData, err := util.Encrypt([]byte(`Bearer xf5yhfkpsnmgo`), setting.SecretKey)
  284. ds := &m.DataSource{
  285. Type: m.DS_PROMETHEUS,
  286. Url: "http://prometheus:9090",
  287. JsonData: simplejson.NewFromAny(map[string]interface{}{
  288. "httpHeaderName1": "Authorization",
  289. }),
  290. SecureJsonData: map[string][]byte{
  291. "httpHeaderValue1": encryptedData,
  292. },
  293. }
  294. ctx := &m.ReqContext{}
  295. proxy := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
  296. requestURL, _ := url.Parse("http://grafana.com/sub")
  297. req := http.Request{URL: requestURL, Header: make(http.Header)}
  298. proxy.getDirector()(&req)
  299. if err != nil {
  300. log.Fatal(4, err.Error())
  301. }
  302. Convey("Match header value after decryption", func() {
  303. So(req.Header.Get("Authorization"), ShouldEqual, "Bearer xf5yhfkpsnmgo")
  304. })
  305. })
  306. Convey("When proxying a custom datasource", func() {
  307. plugin := &plugins.DataSourcePlugin{}
  308. ds := &m.DataSource{
  309. Type: "custom-datasource",
  310. Url: "http://host/root/",
  311. }
  312. ctx := &m.ReqContext{}
  313. proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{})
  314. req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
  315. req.Header.Add("Origin", "grafana.com")
  316. req.Header.Add("Referer", "grafana.com")
  317. req.Header.Add("X-Canary", "stillthere")
  318. So(err, ShouldBeNil)
  319. proxy.getDirector()(req)
  320. Convey("Should keep user request (including trailing slash)", func() {
  321. So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/")
  322. })
  323. Convey("Origin and Referer headers should be dropped", func() {
  324. So(req.Header.Get("Origin"), ShouldEqual, "")
  325. So(req.Header.Get("Referer"), ShouldEqual, "")
  326. So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere")
  327. })
  328. })
  329. Convey("When proxying a datasource that has oauth token pass-thru enabled", func() {
  330. social.SocialMap["generic_oauth"] = &social.SocialGenericOAuth{
  331. SocialBase: &social.SocialBase{
  332. Config: &oauth2.Config{},
  333. },
  334. }
  335. bus.AddHandler("test", func(query *m.GetAuthInfoQuery) error {
  336. query.Result = &m.UserAuth{
  337. Id: 1,
  338. UserId: 1,
  339. AuthModule: "generic_oauth",
  340. OAuthAccessToken: "testtoken",
  341. OAuthRefreshToken: "testrefreshtoken",
  342. OAuthTokenType: "Bearer",
  343. OAuthExpiry: time.Now().AddDate(0, 0, 1),
  344. }
  345. return nil
  346. })
  347. plugin := &plugins.DataSourcePlugin{}
  348. ds := &m.DataSource{
  349. Type: "custom-datasource",
  350. Url: "http://host/root/",
  351. JsonData: simplejson.NewFromAny(map[string]interface{}{
  352. "oauthPassThru": true,
  353. }),
  354. }
  355. req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
  356. ctx := &m.ReqContext{
  357. SignedInUser: &m.SignedInUser{UserId: 1},
  358. Context: &macaron.Context{
  359. Req: macaron.Request{Request: req},
  360. },
  361. }
  362. proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{})
  363. req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
  364. So(err, ShouldBeNil)
  365. proxy.getDirector()(req)
  366. Convey("Should have access token in header", func() {
  367. So(req.Header.Get("Authorization"), ShouldEqual, fmt.Sprintf("%s %s", "Bearer", "testtoken"))
  368. })
  369. })
  370. Convey("When SendUserHeader config is enabled", func() {
  371. req := getDatasourceProxiedRequest(
  372. &m.ReqContext{
  373. SignedInUser: &m.SignedInUser{
  374. Login: "test_user",
  375. },
  376. },
  377. &setting.Cfg{SendUserHeader: true},
  378. )
  379. Convey("Should add header with username", func() {
  380. So(req.Header.Get("X-Grafana-User"), ShouldEqual, "test_user")
  381. })
  382. })
  383. Convey("When SendUserHeader config is disabled", func() {
  384. req := getDatasourceProxiedRequest(
  385. &m.ReqContext{
  386. SignedInUser: &m.SignedInUser{
  387. Login: "test_user",
  388. },
  389. },
  390. &setting.Cfg{SendUserHeader: false},
  391. )
  392. Convey("Should not add header with username", func() {
  393. // Get will return empty string even if header is not set
  394. So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
  395. })
  396. })
  397. Convey("When SendUserHeader config is enabled but user is anonymous", func() {
  398. req := getDatasourceProxiedRequest(
  399. &m.ReqContext{
  400. SignedInUser: &m.SignedInUser{IsAnonymous: true},
  401. },
  402. &setting.Cfg{SendUserHeader: true},
  403. )
  404. Convey("Should not add header with username", func() {
  405. // Get will return empty string even if header is not set
  406. So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
  407. })
  408. })
  409. })
  410. }
  411. // getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
  412. func getDatasourceProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request {
  413. plugin := &plugins.DataSourcePlugin{}
  414. ds := &m.DataSource{
  415. Type: "custom",
  416. Url: "http://host/root/",
  417. }
  418. proxy := NewDataSourceProxy(ds, plugin, ctx, "", cfg)
  419. req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
  420. So(err, ShouldBeNil)
  421. proxy.getDirector()(req)
  422. return req
  423. }
  424. type httpClientStub struct {
  425. fakeBody []byte
  426. }
  427. func (c *httpClientStub) Do(req *http.Request) (*http.Response, error) {
  428. bodyJSON, _ := simplejson.NewJson(c.fakeBody)
  429. _, passedTokenCacheTest := bodyJSON.CheckGet("expires_on")
  430. So(passedTokenCacheTest, ShouldBeTrue)
  431. bodyJSON.Set("expires_on", fmt.Sprint(time.Now().Add(time.Second*60).Unix()))
  432. body, _ := bodyJSON.MarshalJSON()
  433. resp := &http.Response{
  434. Body: ioutil.NopCloser(bytes.NewReader(body)),
  435. }
  436. return resp, nil
  437. }
  438. func newFakeHTTPClient(fakeBody []byte) httpClient {
  439. return &httpClientStub{
  440. fakeBody: fakeBody,
  441. }
  442. }