Datasource Plugin Development. Securing Credentials on a self-hosted Grafana instance

Hey,
I’m developing a datasource plugin at the moment, where my datasources are created exclusively through the UI. Calls to each data source are supposed to be proxied through Grafana’s backend.

The plugin is now functioning, but it has come to my attention that the API key associated with a specific data source instance is stored in plain text and can be accessed from the frontend via the regular browser developer tools.

I’ve attempted to use the ‘secureJsonData’ feature by adding routes to my plugin.json file as described in https://grafana.com/docs/grafana/latest/plugins/developing/auth-for-datasources/. However, when I try to make a web call over these routes, I get a status code of 200 and the following response embedded in a html error page:

“If you’re seeing this Grafana has failed to load its application files”

Now as I’m currently developing the plugin, I am hosting my Grafana instance on my development machine. From the few snippets of discussion I’ve found on the subject, it seems that using routes on a self hosted Grafana instance is not possible?

So my question is, how do I use the ‘secretJsonData’ feature to encrypt credentials in the frontend, when I’m hosting Grafana on the same machine that I’m developing my plugin on?

Oh yeah, I’m running Grafana 6.7.2 on a Windows 10 machine.

First thing to check is the URL that you are passing to the backendSrv(). Suppose the “path” of the route is myservice, the URL has to be instanceSettings.url + “/myservice”. This URL is translated by Grafana to the actual URL that is configured in the “url” field of the route.

How do you pass the api key? Grafana supports OAuth and HTTP Header. Does the auth of the target service work with OAuth or the api key in the HTTP Header? If you are trying to pass the api key as a part of query string, it won’t work. It’s a known issue.

Hey, thanks for the quick response :- )
Double checking my path it seems to be accurate, but I still get the same error. As for authentication I use the HTTP Authorization header so that should be a case that Grafana covers from what I understand.

Alright, I’ve tried to simplify things, and I’m now just attempting to make requests that do not require authentication. As before I get a 200 status code and the following message when looking at the response:

If you’re seeing this Grafana has failed to load its application files

This could be caused by your reverse proxy settings.

If you host grafana under subpath make sure your grafana.ini root_path setting includes subpath

If you have a local dev build make sure you build frontend using: npm run dev, npm run watch, or npm run build

Sometimes restarting grafana-server can help

This is a bit weird to me, as I’m not running Grafana behind a reverse proxy. I’m running it locally on my development machine on port 4000.

This is how my routes are configured in plugin.json:

“routes”: [{
“path”: “azuremonitor”,
“method”: “GET”,
“url”: “http://management.azure.com
}]

And this is how I try to use it in my datasource.ts file for testing connection to a datasource. Note that the endpoint called is really random and does not prove data source connection, it is just an easy way for me to wire this up and test my routing:

testDatasource() {
    return this.datasourceAttrs.backendSrv
      .datasourceRequest({url: "azuremonitor", method: "GET"})
        .then(response => {
          // Catch response
        },
        err => { 
          // Catch error
        });
  }

Testing this I can tell that it makes a request to localhost:4000/azuremonitor.

As a sidenote I have not changed anything in my Grafana.ini file other than switching the hosting port from 3000 to 4000.

Hope this info makes my problem a bit more clear :- )

ok, so the reason hitting Grafana is you haven’t prepended the “instance url”. Grafana assigns a URL for each configured datasource. The syntax is

http://localhost:4000/api/datasources/proxy/<numeric instance id>/<your route>/<actual path for your service>

The .datasourceRequest takes the fragment of the url beginning with /api/....... The instanceSettings.url holds the value including the instance id e.g /api/datasources/proxy/123

Here is how I do usually. At the beginning of the constructor, copy the instanceSettings.url to this.url. Then prepend the this.url to the route.

url?: string;
constructor(instanceSettings: DataSourceInstanceSettings<...>, .... ) {
    super(instanceSettings);
   this.url = instanceSettings.url;
}

testDatasource() {
    return this.datasourceAttrs.backendSrv
      .datasourceRequest({url: this.url + "/" + "azuremonitor" + "/path-for-the-actual-service", method: "GET"})
        .then(response => {
          // Catch response
        },
        err => { 
          // Catch error
        });
  }

The source of confusion is the same word url is used everywhere for different meanings. To be fair, Grafana has to support the datasource that works for the proxied access and the direct access. Besides, the “route” is Grafana specific concept. I think it deserves an own parameter than a part of URL path.

That’s exactly what I was missing. Was able to complete my task after prepending the datasource url as you described. Thank you very much!