1package api
2
3import (
4	"context"
5	"net/http"
6	"net/http/httptest"
7	"testing"
8
9	"github.com/grafana/grafana/pkg/services/accesscontrol"
10	"github.com/grafana/grafana/pkg/services/provisioning"
11	"github.com/grafana/grafana/pkg/setting"
12	"github.com/stretchr/testify/assert"
13)
14
15type reloadProvisioningTestCase struct {
16	desc         string
17	url          string
18	expectedCode int
19	expectedBody string
20	permissions  []*accesscontrol.Permission
21	exit         bool
22	checkCall    func(mock provisioning.ProvisioningServiceMock)
23}
24
25func TestAPI_AdminProvisioningReload_AccessControl(t *testing.T) {
26	tests := []reloadProvisioningTestCase{
27		{
28			desc:         "should work for dashboards with specific scope",
29			expectedCode: http.StatusOK,
30			expectedBody: `{"message":"Dashboards config reloaded"}`,
31			permissions: []*accesscontrol.Permission{
32				{
33					Action: ActionProvisioningReload,
34					Scope:  ScopeProvisionersDashboards,
35				},
36			},
37			url: "/api/admin/provisioning/dashboards/reload",
38			checkCall: func(mock provisioning.ProvisioningServiceMock) {
39				assert.Len(t, mock.Calls.ProvisionDashboards, 1)
40			},
41		},
42		{
43			desc:         "should work for dashboards with broader scope",
44			expectedCode: http.StatusOK,
45			expectedBody: `{"message":"Dashboards config reloaded"}`,
46			permissions: []*accesscontrol.Permission{
47				{
48					Action: ActionProvisioningReload,
49					Scope:  ScopeProvisionersAll,
50				},
51			},
52			url: "/api/admin/provisioning/dashboards/reload",
53			checkCall: func(mock provisioning.ProvisioningServiceMock) {
54				assert.Len(t, mock.Calls.ProvisionDashboards, 1)
55			},
56		},
57		{
58			desc:         "should fail for dashboard with wrong scope",
59			expectedCode: http.StatusForbidden,
60			permissions: []*accesscontrol.Permission{
61				{
62					Action: ActionProvisioningReload,
63					Scope:  "services:noservice",
64				},
65			},
66			url:  "/api/admin/provisioning/dashboards/reload",
67			exit: true,
68		},
69		{
70			desc:         "should fail for dashboard with no permission",
71			expectedCode: http.StatusForbidden,
72			url:          "/api/admin/provisioning/dashboards/reload",
73			exit:         true,
74		},
75		{
76			desc:         "should work for notifications with specific scope",
77			expectedCode: http.StatusOK,
78			expectedBody: `{"message":"Notifications config reloaded"}`,
79			permissions: []*accesscontrol.Permission{
80				{
81					Action: ActionProvisioningReload,
82					Scope:  ScopeProvisionersNotifications,
83				},
84			},
85			url: "/api/admin/provisioning/notifications/reload",
86			checkCall: func(mock provisioning.ProvisioningServiceMock) {
87				assert.Len(t, mock.Calls.ProvisionNotifications, 1)
88			},
89		},
90		{
91			desc:         "should fail for notifications with no permission",
92			expectedCode: http.StatusForbidden,
93			url:          "/api/admin/provisioning/notifications/reload",
94			exit:         true,
95		},
96		{
97			desc:         "should work for datasources with specific scope",
98			expectedCode: http.StatusOK,
99			expectedBody: `{"message":"Datasources config reloaded"}`,
100			permissions: []*accesscontrol.Permission{
101				{
102					Action: ActionProvisioningReload,
103					Scope:  ScopeProvisionersDatasources,
104				},
105			},
106			url: "/api/admin/provisioning/datasources/reload",
107			checkCall: func(mock provisioning.ProvisioningServiceMock) {
108				assert.Len(t, mock.Calls.ProvisionDatasources, 1)
109			},
110		},
111		{
112			desc:         "should fail for datasources with no permission",
113			expectedCode: http.StatusForbidden,
114			url:          "/api/admin/provisioning/datasources/reload",
115			exit:         true,
116		},
117		{
118			desc:         "should work for plugins with specific scope",
119			expectedCode: http.StatusOK,
120			expectedBody: `{"message":"Plugins config reloaded"}`,
121			permissions: []*accesscontrol.Permission{
122				{
123					Action: ActionProvisioningReload,
124					Scope:  ScopeProvisionersPlugins,
125				},
126			},
127			url: "/api/admin/provisioning/plugins/reload",
128			checkCall: func(mock provisioning.ProvisioningServiceMock) {
129				assert.Len(t, mock.Calls.ProvisionPlugins, 1)
130			},
131		},
132		{
133			desc:         "should fail for plugins with no permission",
134			expectedCode: http.StatusForbidden,
135			url:          "/api/admin/provisioning/plugins/reload",
136			exit:         true,
137		},
138	}
139
140	cfg := setting.NewCfg()
141
142	for _, test := range tests {
143		t.Run(test.desc, func(t *testing.T) {
144			sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
145
146			// Setup the mock
147			provisioningMock := provisioning.NewProvisioningServiceMock(context.Background())
148			hs.ProvisioningService = provisioningMock
149
150			sc.resp = httptest.NewRecorder()
151			var err error
152			sc.req, err = http.NewRequest(http.MethodPost, test.url, nil)
153			assert.NoError(t, err)
154
155			sc.exec()
156
157			// Check return code
158			assert.Equal(t, test.expectedCode, sc.resp.Code)
159			if test.exit {
160				return
161			}
162
163			// Check body
164			assert.Equal(t, test.expectedBody, sc.resp.Body.String())
165
166			// Check we actually called the provisioning service
167			test.checkCall(*provisioningMock)
168		})
169	}
170}
171