dashboard_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. package api
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/grafana/grafana/pkg/api/dtos"
  6. "github.com/grafana/grafana/pkg/bus"
  7. "github.com/grafana/grafana/pkg/components/simplejson"
  8. "github.com/grafana/grafana/pkg/middleware"
  9. m "github.com/grafana/grafana/pkg/models"
  10. "github.com/grafana/grafana/pkg/services/alerting"
  11. "github.com/grafana/grafana/pkg/services/dashboards"
  12. "github.com/grafana/grafana/pkg/setting"
  13. . "github.com/smartystreets/goconvey/convey"
  14. )
  15. type fakeDashboardRepo struct {
  16. inserted []*dashboards.SaveDashboardItem
  17. getDashboard []*m.Dashboard
  18. }
  19. func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
  20. repo.inserted = append(repo.inserted, json)
  21. return json.Dashboard, nil
  22. }
  23. var fakeRepo *fakeDashboardRepo
  24. // This tests two main scenarios. If a user has access to execute an action on a dashboard:
  25. // 1. and the dashboard is in a folder which does not have an acl
  26. // 2. and the dashboard is in a folder which does have an acl
  27. func TestDashboardApiEndpoint(t *testing.T) {
  28. Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
  29. fakeDash := m.NewDashboard("Child dash")
  30. fakeDash.Id = 1
  31. fakeDash.FolderId = 1
  32. fakeDash.HasAcl = false
  33. bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
  34. query.Result = fakeDash
  35. return nil
  36. })
  37. viewerRole := m.ROLE_VIEWER
  38. editorRole := m.ROLE_EDITOR
  39. aclMockResp := []*m.DashboardAclInfoDTO{
  40. {Role: &viewerRole, Permission: m.PERMISSION_VIEW},
  41. {Role: &editorRole, Permission: m.PERMISSION_EDIT},
  42. }
  43. bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
  44. query.Result = aclMockResp
  45. return nil
  46. })
  47. bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
  48. query.Result = []*m.Team{}
  49. return nil
  50. })
  51. cmd := m.SaveDashboardCommand{
  52. Dashboard: simplejson.NewFromAny(map[string]interface{}{
  53. "folderId": fakeDash.FolderId,
  54. "title": fakeDash.Title,
  55. "id": fakeDash.Id,
  56. }),
  57. }
  58. // This tests two scenarios:
  59. // 1. user is an org viewer
  60. // 2. user is an org editor
  61. Convey("When user is an Org Viewer", func() {
  62. role := m.ROLE_VIEWER
  63. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  64. dash := GetDashboardShouldReturn200(sc)
  65. Convey("Should not be able to edit or save dashboard", func() {
  66. So(dash.Meta.CanEdit, ShouldBeFalse)
  67. So(dash.Meta.CanSave, ShouldBeFalse)
  68. So(dash.Meta.CanAdmin, ShouldBeFalse)
  69. })
  70. })
  71. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  72. CallDeleteDashboard(sc)
  73. So(sc.resp.Code, ShouldEqual, 403)
  74. })
  75. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  76. CallGetDashboardVersion(sc)
  77. So(sc.resp.Code, ShouldEqual, 403)
  78. })
  79. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  80. CallGetDashboardVersions(sc)
  81. So(sc.resp.Code, ShouldEqual, 403)
  82. })
  83. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  84. CallPostDashboard(sc)
  85. So(sc.resp.Code, ShouldEqual, 403)
  86. })
  87. })
  88. Convey("When user is an Org Editor", func() {
  89. role := m.ROLE_EDITOR
  90. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  91. dash := GetDashboardShouldReturn200(sc)
  92. Convey("Should be able to edit or save dashboard", func() {
  93. So(dash.Meta.CanEdit, ShouldBeTrue)
  94. So(dash.Meta.CanSave, ShouldBeTrue)
  95. So(dash.Meta.CanAdmin, ShouldBeFalse)
  96. })
  97. })
  98. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  99. CallDeleteDashboard(sc)
  100. So(sc.resp.Code, ShouldEqual, 200)
  101. })
  102. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  103. CallGetDashboardVersion(sc)
  104. So(sc.resp.Code, ShouldEqual, 200)
  105. })
  106. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  107. CallGetDashboardVersions(sc)
  108. So(sc.resp.Code, ShouldEqual, 200)
  109. })
  110. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  111. CallPostDashboard(sc)
  112. So(sc.resp.Code, ShouldEqual, 200)
  113. })
  114. Convey("When saving a dashboard folder in another folder", func() {
  115. bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
  116. query.Result = fakeDash
  117. query.Result.IsFolder = true
  118. return nil
  119. })
  120. invalidCmd := m.SaveDashboardCommand{
  121. FolderId: fakeDash.FolderId,
  122. IsFolder: true,
  123. Dashboard: simplejson.NewFromAny(map[string]interface{}{
  124. "folderId": fakeDash.FolderId,
  125. "title": fakeDash.Title,
  126. }),
  127. }
  128. Convey("Should return an error", func() {
  129. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
  130. CallPostDashboard(sc)
  131. So(sc.resp.Code, ShouldEqual, 400)
  132. })
  133. })
  134. })
  135. })
  136. })
  137. Convey("Given a dashboard with a parent folder which has an acl", t, func() {
  138. fakeDash := m.NewDashboard("Child dash")
  139. fakeDash.Id = 1
  140. fakeDash.FolderId = 1
  141. fakeDash.HasAcl = true
  142. setting.ViewersCanEdit = false
  143. aclMockResp := []*m.DashboardAclInfoDTO{
  144. {
  145. DashboardId: 1,
  146. Permission: m.PERMISSION_EDIT,
  147. UserId: 200,
  148. },
  149. }
  150. bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
  151. query.Result = aclMockResp
  152. return nil
  153. })
  154. bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
  155. query.Result = fakeDash
  156. return nil
  157. })
  158. bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
  159. query.Result = []*m.Team{}
  160. return nil
  161. })
  162. cmd := m.SaveDashboardCommand{
  163. FolderId: fakeDash.FolderId,
  164. Dashboard: simplejson.NewFromAny(map[string]interface{}{
  165. "id": fakeDash.Id,
  166. "folderId": fakeDash.FolderId,
  167. "title": fakeDash.Title,
  168. }),
  169. }
  170. // This tests six scenarios:
  171. // 1. user is an org viewer AND has no permissions for this dashboard
  172. // 2. user is an org editor AND has no permissions for this dashboard
  173. // 3. user is an org viewer AND has been granted edit permission for the dashboard
  174. // 4. user is an org viewer AND all viewers have edit permission for this dashboard
  175. // 5. user is an org viewer AND has been granted an admin permission
  176. // 6. user is an org editor AND has been granted a view permission
  177. Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
  178. role := m.ROLE_VIEWER
  179. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  180. sc.handlerFunc = GetDashboard
  181. sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
  182. Convey("Should be denied access", func() {
  183. So(sc.resp.Code, ShouldEqual, 403)
  184. })
  185. })
  186. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  187. CallDeleteDashboard(sc)
  188. So(sc.resp.Code, ShouldEqual, 403)
  189. })
  190. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  191. CallGetDashboardVersion(sc)
  192. So(sc.resp.Code, ShouldEqual, 403)
  193. })
  194. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  195. CallGetDashboardVersions(sc)
  196. So(sc.resp.Code, ShouldEqual, 403)
  197. })
  198. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  199. CallPostDashboard(sc)
  200. So(sc.resp.Code, ShouldEqual, 403)
  201. })
  202. })
  203. Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
  204. role := m.ROLE_EDITOR
  205. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  206. sc.handlerFunc = GetDashboard
  207. sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
  208. Convey("Should be denied access", func() {
  209. So(sc.resp.Code, ShouldEqual, 403)
  210. })
  211. })
  212. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  213. CallDeleteDashboard(sc)
  214. So(sc.resp.Code, ShouldEqual, 403)
  215. })
  216. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  217. CallGetDashboardVersion(sc)
  218. So(sc.resp.Code, ShouldEqual, 403)
  219. })
  220. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  221. CallGetDashboardVersions(sc)
  222. So(sc.resp.Code, ShouldEqual, 403)
  223. })
  224. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  225. CallPostDashboard(sc)
  226. So(sc.resp.Code, ShouldEqual, 403)
  227. })
  228. })
  229. Convey("When user is an Org Viewer but has an edit permission", func() {
  230. role := m.ROLE_VIEWER
  231. mockResult := []*m.DashboardAclInfoDTO{
  232. {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
  233. }
  234. bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
  235. query.Result = mockResult
  236. return nil
  237. })
  238. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  239. dash := GetDashboardShouldReturn200(sc)
  240. Convey("Should be able to get dashboard with edit rights", func() {
  241. So(dash.Meta.CanEdit, ShouldBeTrue)
  242. So(dash.Meta.CanSave, ShouldBeTrue)
  243. So(dash.Meta.CanAdmin, ShouldBeFalse)
  244. })
  245. })
  246. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  247. CallDeleteDashboard(sc)
  248. So(sc.resp.Code, ShouldEqual, 200)
  249. })
  250. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  251. CallGetDashboardVersion(sc)
  252. So(sc.resp.Code, ShouldEqual, 200)
  253. })
  254. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  255. CallGetDashboardVersions(sc)
  256. So(sc.resp.Code, ShouldEqual, 200)
  257. })
  258. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  259. CallPostDashboard(sc)
  260. So(sc.resp.Code, ShouldEqual, 200)
  261. })
  262. })
  263. Convey("When user is an Org Viewer and viewers can edit", func() {
  264. role := m.ROLE_VIEWER
  265. setting.ViewersCanEdit = true
  266. mockResult := []*m.DashboardAclInfoDTO{
  267. {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
  268. }
  269. bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
  270. query.Result = mockResult
  271. return nil
  272. })
  273. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  274. dash := GetDashboardShouldReturn200(sc)
  275. Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
  276. So(dash.Meta.CanEdit, ShouldBeTrue)
  277. So(dash.Meta.CanSave, ShouldBeFalse)
  278. So(dash.Meta.CanAdmin, ShouldBeFalse)
  279. })
  280. })
  281. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  282. CallDeleteDashboard(sc)
  283. So(sc.resp.Code, ShouldEqual, 403)
  284. })
  285. })
  286. Convey("When user is an Org Viewer but has an admin permission", func() {
  287. role := m.ROLE_VIEWER
  288. mockResult := []*m.DashboardAclInfoDTO{
  289. {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
  290. }
  291. bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
  292. query.Result = mockResult
  293. return nil
  294. })
  295. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  296. dash := GetDashboardShouldReturn200(sc)
  297. Convey("Should be able to get dashboard with edit rights", func() {
  298. So(dash.Meta.CanEdit, ShouldBeTrue)
  299. So(dash.Meta.CanSave, ShouldBeTrue)
  300. So(dash.Meta.CanAdmin, ShouldBeTrue)
  301. })
  302. })
  303. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  304. CallDeleteDashboard(sc)
  305. So(sc.resp.Code, ShouldEqual, 200)
  306. })
  307. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  308. CallGetDashboardVersion(sc)
  309. So(sc.resp.Code, ShouldEqual, 200)
  310. })
  311. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  312. CallGetDashboardVersions(sc)
  313. So(sc.resp.Code, ShouldEqual, 200)
  314. })
  315. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  316. CallPostDashboard(sc)
  317. So(sc.resp.Code, ShouldEqual, 200)
  318. })
  319. })
  320. Convey("When user is an Org Editor but has a view permission", func() {
  321. role := m.ROLE_EDITOR
  322. mockResult := []*m.DashboardAclInfoDTO{
  323. {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
  324. }
  325. bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
  326. query.Result = mockResult
  327. return nil
  328. })
  329. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  330. dash := GetDashboardShouldReturn200(sc)
  331. Convey("Should not be able to edit or save dashboard", func() {
  332. So(dash.Meta.CanEdit, ShouldBeFalse)
  333. So(dash.Meta.CanSave, ShouldBeFalse)
  334. })
  335. })
  336. loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
  337. CallDeleteDashboard(sc)
  338. So(sc.resp.Code, ShouldEqual, 403)
  339. })
  340. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
  341. CallGetDashboardVersion(sc)
  342. So(sc.resp.Code, ShouldEqual, 403)
  343. })
  344. loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
  345. CallGetDashboardVersions(sc)
  346. So(sc.resp.Code, ShouldEqual, 403)
  347. })
  348. postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
  349. CallPostDashboard(sc)
  350. So(sc.resp.Code, ShouldEqual, 403)
  351. })
  352. })
  353. })
  354. }
  355. func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
  356. sc.handlerFunc = GetDashboard
  357. sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
  358. So(sc.resp.Code, ShouldEqual, 200)
  359. dash := dtos.DashboardFullWithMeta{}
  360. err := json.NewDecoder(sc.resp.Body).Decode(&dash)
  361. So(err, ShouldBeNil)
  362. return dash
  363. }
  364. func CallGetDashboardVersion(sc *scenarioContext) {
  365. bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
  366. query.Result = &m.DashboardVersion{}
  367. return nil
  368. })
  369. sc.handlerFunc = GetDashboardVersion
  370. sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
  371. }
  372. func CallGetDashboardVersions(sc *scenarioContext) {
  373. bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error {
  374. query.Result = []*m.DashboardVersionDTO{}
  375. return nil
  376. })
  377. sc.handlerFunc = GetDashboardVersions
  378. sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
  379. }
  380. func CallDeleteDashboard(sc *scenarioContext) {
  381. bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
  382. return nil
  383. })
  384. sc.handlerFunc = DeleteDashboard
  385. sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
  386. }
  387. func CallPostDashboard(sc *scenarioContext) {
  388. bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
  389. return nil
  390. })
  391. bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
  392. cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
  393. return nil
  394. })
  395. bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
  396. return nil
  397. })
  398. sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
  399. }
  400. func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
  401. Convey(desc+" "+url, func() {
  402. defer bus.ClearBusHandlers()
  403. sc := setupScenarioContext(url)
  404. sc.defaultHandler = wrap(func(c *middleware.Context) Response {
  405. sc.context = c
  406. sc.context.UserId = TestUserID
  407. sc.context.OrgId = TestOrgID
  408. sc.context.OrgRole = role
  409. return PostDashboard(c, cmd)
  410. })
  411. fakeRepo = &fakeDashboardRepo{}
  412. dashboards.SetRepository(fakeRepo)
  413. sc.m.Post(routePattern, sc.defaultHandler)
  414. fn(sc)
  415. })
  416. }