middleware_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. package middleware
  2. import (
  3. "context"
  4. "encoding/base32"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "path/filepath"
  9. "testing"
  10. "time"
  11. . "github.com/smartystreets/goconvey/convey"
  12. "github.com/stretchr/testify/assert"
  13. "gopkg.in/macaron.v1"
  14. "github.com/grafana/grafana/pkg/api/dtos"
  15. "github.com/grafana/grafana/pkg/bus"
  16. "github.com/grafana/grafana/pkg/infra/remotecache"
  17. "github.com/grafana/grafana/pkg/models"
  18. "github.com/grafana/grafana/pkg/services/auth"
  19. "github.com/grafana/grafana/pkg/services/login"
  20. "github.com/grafana/grafana/pkg/setting"
  21. "github.com/grafana/grafana/pkg/util"
  22. )
  23. const errorTemplate = "error-template"
  24. func mockGetTime() {
  25. var timeSeed int64
  26. getTime = func() time.Time {
  27. fakeNow := time.Unix(timeSeed, 0)
  28. timeSeed++
  29. return fakeNow
  30. }
  31. }
  32. func resetGetTime() {
  33. getTime = time.Now
  34. }
  35. func TestMiddleWareSecurityHeaders(t *testing.T) {
  36. setting.ERR_TEMPLATE_NAME = errorTemplate
  37. Convey("Given the grafana middleware", t, func() {
  38. middlewareScenario(t, "middleware should get correct x-xss-protection header", func(sc *scenarioContext) {
  39. setting.XSSProtectionHeader = true
  40. sc.fakeReq("GET", "/api/").exec()
  41. So(sc.resp.Header().Get("X-XSS-Protection"), ShouldEqual, "1; mode=block")
  42. })
  43. middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(sc *scenarioContext) {
  44. setting.XSSProtectionHeader = false
  45. sc.fakeReq("GET", "/api/").exec()
  46. So(sc.resp.Header().Get("X-XSS-Protection"), ShouldBeEmpty)
  47. })
  48. middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(sc *scenarioContext) {
  49. setting.StrictTransportSecurity = true
  50. setting.Protocol = setting.HTTPS
  51. setting.StrictTransportSecurityMaxAge = 64000
  52. sc.fakeReq("GET", "/api/").exec()
  53. So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000")
  54. setting.StrictTransportSecurityPreload = true
  55. sc.fakeReq("GET", "/api/").exec()
  56. So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload")
  57. setting.StrictTransportSecuritySubDomains = true
  58. sc.fakeReq("GET", "/api/").exec()
  59. So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload; includeSubDomains")
  60. })
  61. })
  62. }
  63. func TestMiddlewareContext(t *testing.T) {
  64. setting.ERR_TEMPLATE_NAME = errorTemplate
  65. Convey("Given the grafana middleware", t, func() {
  66. middlewareScenario(t, "middleware should add context to injector", func(sc *scenarioContext) {
  67. sc.fakeReq("GET", "/").exec()
  68. So(sc.context, ShouldNotBeNil)
  69. })
  70. middlewareScenario(t, "Default middleware should allow get request", func(sc *scenarioContext) {
  71. sc.fakeReq("GET", "/").exec()
  72. So(sc.resp.Code, ShouldEqual, 200)
  73. })
  74. middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(sc *scenarioContext) {
  75. sc.fakeReq("GET", "/api/search").exec()
  76. So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache")
  77. So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache")
  78. So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1")
  79. })
  80. middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func(sc *scenarioContext) {
  81. sc.fakeReq("GET", "/api/datasources/proxy/1/test").exec()
  82. So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
  83. So(sc.resp.Header().Get("Pragma"), ShouldBeEmpty)
  84. So(sc.resp.Header().Get("Expires"), ShouldBeEmpty)
  85. })
  86. middlewareScenario(t, "middleware should add Cache-Control header for requests with html response", func(sc *scenarioContext) {
  87. sc.handler(func(c *models.ReqContext) {
  88. data := &dtos.IndexViewData{
  89. User: &dtos.CurrentUser{},
  90. Settings: map[string]interface{}{},
  91. NavTree: []*dtos.NavLink{},
  92. }
  93. c.HTML(200, "index-template", data)
  94. })
  95. sc.fakeReq("GET", "/").exec()
  96. So(sc.resp.Code, ShouldEqual, 200)
  97. So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache")
  98. So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache")
  99. So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1")
  100. })
  101. middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func(sc *scenarioContext) {
  102. sc.fakeReq("GET", "/api/search").exec()
  103. So(sc.resp.Header().Get("X-Frame-Options"), ShouldEqual, "deny")
  104. })
  105. middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func(sc *scenarioContext) {
  106. setting.AllowEmbedding = true
  107. sc.fakeReq("GET", "/api/search").exec()
  108. So(sc.resp.Header().Get("X-Frame-Options"), ShouldBeEmpty)
  109. })
  110. middlewareScenario(t, "Invalid api key", func(sc *scenarioContext) {
  111. sc.apiKey = "invalid_key_test"
  112. sc.fakeReq("GET", "/").exec()
  113. Convey("Should not init session", func() {
  114. So(sc.resp.Header().Get("Set-Cookie"), ShouldBeEmpty)
  115. })
  116. Convey("Should return 401", func() {
  117. So(sc.resp.Code, ShouldEqual, 401)
  118. So(sc.respJson["message"], ShouldEqual, errStringInvalidAPIKey)
  119. })
  120. })
  121. middlewareScenario(t, "Valid api key", func(sc *scenarioContext) {
  122. keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
  123. bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
  124. query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
  125. return nil
  126. })
  127. sc.fakeReq("GET", "/").withValidApiKey().exec()
  128. Convey("Should return 200", func() {
  129. So(sc.resp.Code, ShouldEqual, 200)
  130. })
  131. Convey("Should init middleware context", func() {
  132. So(sc.context.IsSignedIn, ShouldEqual, true)
  133. So(sc.context.OrgId, ShouldEqual, 12)
  134. So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
  135. })
  136. })
  137. middlewareScenario(t, "Valid api key, but does not match db hash", func(sc *scenarioContext) {
  138. keyhash := "Something_not_matching"
  139. bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
  140. query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
  141. return nil
  142. })
  143. sc.fakeReq("GET", "/").withValidApiKey().exec()
  144. Convey("Should return api key invalid", func() {
  145. So(sc.resp.Code, ShouldEqual, 401)
  146. So(sc.respJson["message"], ShouldEqual, errStringInvalidAPIKey)
  147. })
  148. })
  149. middlewareScenario(t, "Valid api key, but expired", func(sc *scenarioContext) {
  150. mockGetTime()
  151. defer resetGetTime()
  152. keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
  153. bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
  154. // api key expired one second before
  155. expires := getTime().Add(-1 * time.Second).Unix()
  156. query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash,
  157. Expires: &expires}
  158. return nil
  159. })
  160. sc.fakeReq("GET", "/").withValidApiKey().exec()
  161. Convey("Should return 401", func() {
  162. So(sc.resp.Code, ShouldEqual, 401)
  163. So(sc.respJson["message"], ShouldEqual, "Expired API key")
  164. })
  165. })
  166. middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) {
  167. sc.withTokenSessionCookie("token")
  168. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  169. query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
  170. return nil
  171. })
  172. sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
  173. return &models.UserToken{
  174. UserId: 12,
  175. UnhashedToken: unhashedToken,
  176. }, nil
  177. }
  178. sc.fakeReq("GET", "/").exec()
  179. Convey("Should init context with user info", func() {
  180. So(sc.context.IsSignedIn, ShouldBeTrue)
  181. So(sc.context.UserId, ShouldEqual, 12)
  182. So(sc.context.UserToken.UserId, ShouldEqual, 12)
  183. So(sc.context.UserToken.UnhashedToken, ShouldEqual, "token")
  184. })
  185. Convey("Should not set cookie", func() {
  186. So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, "")
  187. })
  188. })
  189. middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) {
  190. sc.withTokenSessionCookie("token")
  191. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  192. query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
  193. return nil
  194. })
  195. sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
  196. return &models.UserToken{
  197. UserId: 12,
  198. UnhashedToken: "",
  199. }, nil
  200. }
  201. sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken, clientIP, userAgent string) (bool, error) {
  202. userToken.UnhashedToken = "rotated"
  203. return true, nil
  204. }
  205. maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour)
  206. maxAge := (maxAgeHours + time.Hour).Seconds()
  207. sameSitePolicies := []http.SameSite{
  208. http.SameSiteDefaultMode,
  209. http.SameSiteLaxMode,
  210. http.SameSiteStrictMode,
  211. }
  212. for _, sameSitePolicy := range sameSitePolicies {
  213. setting.CookieSameSite = sameSitePolicy
  214. expectedCookie := &http.Cookie{
  215. Name: setting.LoginCookieName,
  216. Value: "rotated",
  217. Path: setting.AppSubUrl + "/",
  218. HttpOnly: true,
  219. MaxAge: int(maxAge),
  220. Secure: setting.CookieSecure,
  221. }
  222. if sameSitePolicy != http.SameSiteDefaultMode {
  223. expectedCookie.SameSite = sameSitePolicy
  224. }
  225. sc.fakeReq("GET", "/").exec()
  226. Convey(fmt.Sprintf("Should init context with user info and setting.SameSite=%v", sameSitePolicy), func() {
  227. So(sc.context.IsSignedIn, ShouldBeTrue)
  228. So(sc.context.UserId, ShouldEqual, 12)
  229. So(sc.context.UserToken.UserId, ShouldEqual, 12)
  230. So(sc.context.UserToken.UnhashedToken, ShouldEqual, "rotated")
  231. })
  232. Convey(fmt.Sprintf("Should set cookie with setting.SameSite=%v", sameSitePolicy), func() {
  233. So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String())
  234. })
  235. }
  236. })
  237. middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) {
  238. sc.withTokenSessionCookie("token")
  239. sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
  240. return nil, models.ErrUserTokenNotFound
  241. }
  242. sc.fakeReq("GET", "/").exec()
  243. Convey("Should not init context with user info", func() {
  244. So(sc.context.IsSignedIn, ShouldBeFalse)
  245. So(sc.context.UserId, ShouldEqual, 0)
  246. So(sc.context.UserToken, ShouldBeNil)
  247. })
  248. })
  249. middlewareScenario(t, "When anonymous access is enabled", func(sc *scenarioContext) {
  250. setting.AnonymousEnabled = true
  251. setting.AnonymousOrgName = "test"
  252. setting.AnonymousOrgRole = string(models.ROLE_EDITOR)
  253. bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
  254. So(query.Name, ShouldEqual, "test")
  255. query.Result = &models.Org{Id: 2, Name: "test"}
  256. return nil
  257. })
  258. sc.fakeReq("GET", "/").exec()
  259. Convey("Should init context with org info", func() {
  260. So(sc.context.UserId, ShouldEqual, 0)
  261. So(sc.context.OrgId, ShouldEqual, 2)
  262. So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
  263. })
  264. Convey("context signed in should be false", func() {
  265. So(sc.context.IsSignedIn, ShouldBeFalse)
  266. })
  267. })
  268. Convey("auth_proxy", func() {
  269. setting.AuthProxyEnabled = true
  270. setting.AuthProxyWhitelist = ""
  271. setting.AuthProxyAutoSignUp = true
  272. setting.LDAPEnabled = true
  273. setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
  274. setting.AuthProxyHeaderProperty = "username"
  275. setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
  276. name := "markelog"
  277. group := "grafana-core-team"
  278. middlewareScenario(t, "Should not sync the user if it's in the cache", func(sc *scenarioContext) {
  279. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  280. query.Result = &models.SignedInUser{OrgId: 4, UserId: query.UserId}
  281. return nil
  282. })
  283. key := fmt.Sprintf(cachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group)))
  284. sc.remoteCacheService.Set(key, int64(33), 0)
  285. sc.fakeReq("GET", "/")
  286. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  287. sc.req.Header.Add("X-WEBAUTH-GROUPS", group)
  288. sc.exec()
  289. Convey("Should init user via cache", func() {
  290. So(sc.context.IsSignedIn, ShouldBeTrue)
  291. So(sc.context.UserId, ShouldEqual, 33)
  292. So(sc.context.OrgId, ShouldEqual, 4)
  293. })
  294. })
  295. middlewareScenario(t, "Should respect auto signup option", func(sc *scenarioContext) {
  296. setting.LDAPEnabled = false
  297. setting.AuthProxyAutoSignUp = false
  298. var actualAuthProxyAutoSignUp *bool = nil
  299. bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
  300. actualAuthProxyAutoSignUp = &cmd.SignupAllowed
  301. return login.ErrInvalidCredentials
  302. })
  303. sc.fakeReq("GET", "/")
  304. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  305. sc.exec()
  306. assert.False(t, *actualAuthProxyAutoSignUp)
  307. assert.Equal(t, sc.resp.Code, 407)
  308. assert.Nil(t, sc.context)
  309. })
  310. middlewareScenario(t, "Should create an user from a header", func(sc *scenarioContext) {
  311. setting.LDAPEnabled = false
  312. setting.AuthProxyAutoSignUp = true
  313. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  314. if query.UserId > 0 {
  315. query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
  316. return nil
  317. }
  318. return models.ErrUserNotFound
  319. })
  320. bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
  321. cmd.Result = &models.User{Id: 33}
  322. return nil
  323. })
  324. sc.fakeReq("GET", "/")
  325. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  326. sc.exec()
  327. Convey("Should create user from header info", func() {
  328. So(sc.context.IsSignedIn, ShouldBeTrue)
  329. So(sc.context.UserId, ShouldEqual, 33)
  330. So(sc.context.OrgId, ShouldEqual, 4)
  331. })
  332. })
  333. middlewareScenario(t, "Should get an existing user from header", func(sc *scenarioContext) {
  334. setting.LDAPEnabled = false
  335. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  336. query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
  337. return nil
  338. })
  339. bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
  340. cmd.Result = &models.User{Id: 12}
  341. return nil
  342. })
  343. sc.fakeReq("GET", "/")
  344. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  345. sc.exec()
  346. Convey("Should init context with user info", func() {
  347. So(sc.context.IsSignedIn, ShouldBeTrue)
  348. So(sc.context.UserId, ShouldEqual, 12)
  349. So(sc.context.OrgId, ShouldEqual, 2)
  350. })
  351. })
  352. middlewareScenario(t, "Should allow the request from whitelist IP", func(sc *scenarioContext) {
  353. setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
  354. setting.LDAPEnabled = false
  355. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  356. query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
  357. return nil
  358. })
  359. bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
  360. cmd.Result = &models.User{Id: 33}
  361. return nil
  362. })
  363. sc.fakeReq("GET", "/")
  364. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  365. sc.req.RemoteAddr = "[2001::23]:12345"
  366. sc.exec()
  367. Convey("Should init context with user info", func() {
  368. So(sc.context.IsSignedIn, ShouldBeTrue)
  369. So(sc.context.UserId, ShouldEqual, 33)
  370. So(sc.context.OrgId, ShouldEqual, 4)
  371. })
  372. })
  373. middlewareScenario(t, "Should not allow the request from whitelist IP", func(sc *scenarioContext) {
  374. setting.AuthProxyWhitelist = "8.8.8.8"
  375. setting.LDAPEnabled = false
  376. bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
  377. query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
  378. return nil
  379. })
  380. bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
  381. cmd.Result = &models.User{Id: 33}
  382. return nil
  383. })
  384. sc.fakeReq("GET", "/")
  385. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  386. sc.req.RemoteAddr = "[2001::23]:12345"
  387. sc.exec()
  388. Convey("Should return 407 status code", func() {
  389. So(sc.resp.Code, ShouldEqual, 407)
  390. So(sc.context, ShouldBeNil)
  391. })
  392. })
  393. middlewareScenario(t, "Should return 407 status code if LDAP says no", func(sc *scenarioContext) {
  394. bus.AddHandler("LDAP", func(cmd *models.UpsertUserCommand) error {
  395. return errors.New("Do not add user")
  396. })
  397. sc.fakeReq("GET", "/")
  398. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  399. sc.exec()
  400. Convey("Should return 407 status code", func() {
  401. So(sc.resp.Code, ShouldEqual, 407)
  402. So(sc.context, ShouldBeNil)
  403. })
  404. })
  405. middlewareScenario(t, "Should return 407 status code if there is cache mishap", func(sc *scenarioContext) {
  406. bus.AddHandler("Do not have the user", func(query *models.GetSignedInUserQuery) error {
  407. return errors.New("Do not add user")
  408. })
  409. sc.fakeReq("GET", "/")
  410. sc.req.Header.Add(setting.AuthProxyHeaderName, name)
  411. sc.exec()
  412. Convey("Should return 407 status code", func() {
  413. So(sc.resp.Code, ShouldEqual, 407)
  414. So(sc.context, ShouldBeNil)
  415. })
  416. })
  417. })
  418. })
  419. }
  420. func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
  421. Convey(desc, func() {
  422. defer bus.ClearBusHandlers()
  423. setting.LoginCookieName = "grafana_session"
  424. setting.LoginMaxLifetimeDays = 30
  425. sc := &scenarioContext{}
  426. viewsPath, _ := filepath.Abs("../../public/views")
  427. sc.m = macaron.New()
  428. sc.m.Use(AddDefaultResponseHeaders())
  429. sc.m.Use(macaron.Renderer(macaron.RenderOptions{
  430. Directory: viewsPath,
  431. Delims: macaron.Delims{Left: "[[", Right: "]]"},
  432. }))
  433. sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
  434. sc.remoteCacheService = remotecache.NewFakeStore(t)
  435. sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService))
  436. sc.m.Use(OrgRedirect())
  437. sc.defaultHandler = func(c *models.ReqContext) {
  438. sc.context = c
  439. if sc.handlerFunc != nil {
  440. sc.handlerFunc(sc.context)
  441. }
  442. }
  443. sc.m.Get("/", sc.defaultHandler)
  444. fn(sc)
  445. })
  446. }