From 83df71caf79b039567cecf9a60020daa69e8d880 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Wed, 14 Jan 2026 21:12:18 +0100 Subject: [PATCH] Data Source: Proxy fallback routes must match all inputs (#116274) (cherry picked from commit 9e399e0b19a713c665baa7e06bcfb5af16774ebb) --- pkg/api/pluginproxy/ds_proxy.go | 13 +++- pkg/api/pluginproxy/ds_proxy_test.go | 88 ++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 00ccb0a665c..391dae341b5 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -32,6 +32,8 @@ import ( var ( logger = glog.New("data-proxy-log") client = newHTTPClient() + + errPluginProxyRouteAccessDenied = errors.New("plugin proxy route access denied") ) type DataSourceProxy struct { @@ -308,12 +310,21 @@ func (proxy *DataSourceProxy) validateRequest() error { if err != nil { return err } + // issues/116273: When we have an empty input route (or input that becomes relative to "."), we do not want it + // to be ".". This is because the `CleanRelativePath` function will never return "./" prefixes, and as such, + // the common prefix we need is an empty string. + if r1 == "." && proxy.proxyPath != "." { + r1 = "" + } + if r2 == "." && route.Path != "." { + r2 = "" + } if !strings.HasPrefix(r1, r2) { continue } if !proxy.hasAccessToRoute(route) { - return errors.New("plugin proxy route access denied") + return errPluginProxyRouteAccessDenied } proxy.matchedRoute = route diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index e22b8707834..8725268e1c9 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -673,6 +673,94 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { runDatasourceAuthTest(t, secretsService, secretsStore, cfg, test) } }) + + t.Run("Regression of 116273: Fallback routes should apply fallback route roles", func(t *testing.T) { + for _, tc := range []struct { + InputPath string + ConfigurationPath string + ExpectError bool + }{ + { + InputPath: "api/v2/leak-ur-secrets", + ConfigurationPath: "", + ExpectError: true, + }, + { + InputPath: "", + ConfigurationPath: "", + ExpectError: true, + }, + { + InputPath: ".", + ConfigurationPath: ".", + ExpectError: true, + }, + { + InputPath: "", + ConfigurationPath: ".", + ExpectError: false, + }, + { + InputPath: "api", + ConfigurationPath: ".", + ExpectError: false, + }, + } { + orEmptyStr := func(s string) string { + if s == "" { + return "" + } + return s + } + t.Run( + fmt.Sprintf("with inputPath=%s, configurationPath=%s, expectError=%v", + orEmptyStr(tc.InputPath), orEmptyStr(tc.ConfigurationPath), tc.ExpectError), + func(t *testing.T) { + ds := &datasources.DataSource{ + UID: "dsUID", + JsonData: simplejson.New(), + } + routes := []*plugins.Route{ + { + Path: tc.ConfigurationPath, + ReqRole: org.RoleAdmin, + Method: "GET", + }, + { + Path: tc.ConfigurationPath, + ReqRole: org.RoleAdmin, + Method: "POST", + }, + { + Path: tc.ConfigurationPath, + ReqRole: org.RoleAdmin, + Method: "PUT", + }, + { + Path: tc.ConfigurationPath, + ReqRole: org.RoleAdmin, + Method: "DELETE", + }, + } + + req, err := http.NewRequestWithContext(t.Context(), "GET", "http://localhost/"+tc.InputPath, nil) + require.NoError(t, err, "failed to create HTTP request") + ctx := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{OrgRole: org.RoleViewer}, + } + proxy, err := setupDSProxyTest(t, ctx, ds, routes, tc.InputPath) + require.NoError(t, err, "failed to setup proxy test") + err = proxy.validateRequest() + if tc.ExpectError { + require.ErrorIs(t, err, errPluginProxyRouteAccessDenied, "request was not denied due to access denied?") + } else { + require.NoError(t, err, "request was unexpectedly denied access") + } + }, + ) + } + }) } // test DataSourceProxy request handling.