Skip to content

Commit

Permalink
Add support for API Keys (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusoe authored Aug 19, 2020
1 parent 55311ba commit 2638348
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 36 deletions.
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ Grab the URL for [the latest release zip file][2].

```
$ sudo grafana-cli --pluginUrl https://github.com/sensu/grafana-sensu-go-datasource/releases/download/1.0.1/sensu-sensugo-datasource-1.0.1.zip plugins install sensu-sensugo-datasource
installing sensu-sensugo-datasource @
installing sensu-sensugo-datasource @
from url: https://github.com/sensu/grafana-sensu-go-datasource/releases/download/1.0.1/sensu-sensugo-datasource-1.0.1.zip
into: /var/lib/grafana/plugins
✔ Installed sensu-sensugo-datasource successfully
✔ Installed sensu-sensugo-datasource successfully
Restart grafana after installing plugins . <service grafana-server restart>
```
Expand All @@ -50,18 +50,23 @@ Select Add data source, and choose Sensu Go.

To configure the Sensu Go Data Source:

- **Add your Sensu backend API URL** (default: `http://localhost:8080`). When connecting to a Sensu cluster, connect to any single backend in the cluster. For more information about configuring the Sensu API URL, see the [Sensu docs][4].
- **Check the option for Basic Auth**.
- **Add a Sensu username and password** with get and list permissions for entities, events, and namespaces (default admin user: username `admin`, password `P@ssw0rd!`). For more information about creating a Sensu cluster role, see the [Sensu docs][5].
1. **Add your Sensu backend API URL** (default: `http://localhost:8080`). When connecting to a Sensu cluster, connect to any single backend in the cluster. For more information about configuring the Sensu API URL, see the [Sensu docs][4].
2. **Configure the authentification mechanism**. Since version 1.1.0 of the data source it is possible to choose between **API key** and **Basic Auth** authentication.

<img alt="Grafana user interface showing the configuration settings for the Sensu Go Data Source"
* **Basic Auth**
* **Check the option for Basic Auth**.
* **Add a Sensu username and password** with get and list permissions for entities, events, and namespaces (default admin user: username `admin`, password `P@ssw0rd!`). For more information about creating a Sensu cluster role, see the [Sensu docs][5].
<img alt="Grafana user interface showing the configuration settings for the Sensu Go Data Source"
src="/images/configure-data-source.png"
width="750"
/>
* **API Key Auth**
* **Enable the option for the usage of an API key**.
* **Enter the API key which you want to use**. See the Sensu Go documentation for information on [how to create an API key][api_key_doc].
![API key configuration in the Sensu So Data Source](/images/configure-api-key.png)

Select Save & Test. You should see a banner confirming that Grafana is connected to Sensu Go.

<img alt="Confirmation banner with the message: Successfully connected against the Sensu Go API"
3. Select Save & Test. You should see a banner confirming that Grafana is connected to Sensu Go.
<img alt="Confirmation banner with the message: Successfully connected against the Sensu Go API"
src="/images/configure-success.png"
width="750"
/>
Expand Down Expand Up @@ -154,3 +159,4 @@ Running `release-it` creates a `releases` directory containing the built zip arc
[9]: https://grafana.com/docs/guides/basic_concepts/
[10]: https://www.npmjs.com/package/release-it
[npm]: https://www.npmjs.com/get-npm
[api_key_doc]: https://docs.sensu.io/sensu-go/latest/reference/apikeys/
Binary file added images/configure-api-key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/configure-data-source.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"author": "Novatec Consulting GmbH",
"license": "",
"lint-staged": {
"*.{js,ts,json,css,md}": ["prettier --write", "git add"]
"*.{js,ts,json,css}": ["prettier --write", "git add"]
},
"prettier": {
"singleQuote": true,
Expand Down
38 changes: 37 additions & 1 deletion src/config_ctrl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import InstanceSettings from './model/InstanceSettings';

/**
* Controller responsible for the configuration ui.
*/
export class SensuConfigCtrl {
static templateUrl = 'partials/config.html';
}

// the current datasource settings
current: InstanceSettings;

/** @ngInject **/
constructor($scope) {
const that = this;
$scope.$watch(() => that.current.url, (value) => that.current.jsonData.currentUrl = value);
$scope.$watch(() => that.current.basicAuth, (value) => {
if (value) {
that.current.jsonData.useApiKey = false;
}
});
}

/**
* When the "Use API Key" option is toggled.
*/
onUseApiKeyToggle = () => {
const current = this.current;
if (current.jsonData.useApiKey) {
current.basicAuth = false;
this.resetApiKey();
}
}

/**
* Resets the currely set API key.
*/
resetApiKey = () => {
this.current.secureJsonFields.apiKey = false;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.apiKey = '';
}
}
18 changes: 14 additions & 4 deletions src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import ColumnMapping from './model/ColumnMapping';
import DataPoint from './model/DataPoint';
import Filter from './model/Filter';
import QueryComponents from './model/QueryComponents';
import InstanceSettings from './model/InstanceSettings';

export default class SensuDatasource {

url: string;

/** @ngInject */
constructor(public instanceSettings, public backendSrv, private templateSrv) {
constructor(public instanceSettings: InstanceSettings, public backendSrv, private templateSrv) {
this.url = instanceSettings.url.trim();
}

Expand Down Expand Up @@ -487,16 +489,24 @@ export default class SensuDatasource {
* Used by the config UI to test a datasource.
*/
testDatasource() {
const { useApiKey } = this.instanceSettings.jsonData;

// the /auth/test endpoint is only available for testing basic auth credentials
const testUrl = useApiKey ? '/api/core/v2/namespaces' : '/auth/test';

return sensu
._request(this, 'GET', '/auth/test')
._request(this, 'GET', testUrl)
.then(() => {
return {
status: 'success',
message: 'Successfully connected against the Sensu Go API',
};
})
.catch(err => {
return { status: 'error', message: err.message };
.catch(error => {
if (useApiKey && error.data === 'access_error') {
return { status: 'error', message: 'API Key Invalid: Could not logged in using API key' };
}
return { status: 'error', message: error.message };
});
}
}
34 changes: 34 additions & 0 deletions src/model/InstanceSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default interface ConfigSettings {
// the datasource url
url: string;

// whether basic auth is used
basicAuth: boolean;

// additional data
jsonData: JsonData;

// additional secured data
secureJsonData: SecureJsonData;

// map defining which secured data element is set
secureJsonFields: SecureJsonFields;
};

export interface JsonData {
// copy of the current datasource url - used for dynamic routing
currentUrl: string;

// whether an API key should be used
useApiKey: boolean;
};

export interface SecureJsonData {
// the specified API key
apiKey?: string;
};

export interface SecureJsonFields {
// whether an API key has been stored by Grafana
apiKey?: boolean;
};
30 changes: 29 additions & 1 deletion src/partials/config.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
<datasource-http-settings current="ctrl.current" no-direct-access="true" suggest-url="http://localhost:8080">
</datasource-http-settings>
</datasource-http-settings>

<h3 class="page-heading">API Key Auth</h3>

<div class="gf-form-group">
<gf-form-switch class="gf-form" label="Use API key authentication" label-class="width-19" switch-class="max-width-6"
checked="ctrl.current.jsonData.useApiKey" on-change="ctrl.onUseApiKeyToggle()">
</gf-form-switch>

<div class="gf-form-inline" ng-if="ctrl.current.jsonData.useApiKey && !ctrl.current.secureJsonFields.apiKey">
<div class="gf-form">
<span class="gf-form-label width-9">API Key</span>
<input class="gf-form-input width-30" type="text" ng-model="ctrl.current.secureJsonData.apiKey"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
<info-popover mode="right-absolute">
<p>
Please see the Sensu Go
<a target="_blank" href="https://docs.sensu.io/sensu-go/latest/reference/apikeys/">documentation</a>
for information on how to create an API key.
</p>
</info-popover>
</div>
</div>
<div class="gf-form" ng-if="ctrl.current.jsonData.useApiKey && ctrl.current.secureJsonFields.apiKey">
<span class="gf-form-label width-9">API Key</span>
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" />
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.resetApiKey()">reset</a>
</div>
</div>
12 changes: 10 additions & 2 deletions src/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
"updated": "%TODAY%"
},
"dependencies": {
"grafanaVersion": "5.3.x"
}
"grafanaVersion": "6.0.0"
},
"routes": [
{
"path": "api_key_auth",
"method": "GET",
"url": "{{.JsonData.currentUrl}}",
"headers": [{ "name": "Authorization", "content": "Key {{.SecureJsonData.apiKey}}" }]
}
]
}
62 changes: 44 additions & 18 deletions src/sensu/sensu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ export default class Sensu {
*/
static readonly apiBaseUrl = '/api/core/v2';

static readonly apiKeyUrlPrefix = '/api_key_auth';

/**
* Executes a query against the given datasource. An access token will be gathered if needed.
*
* @param datasource the datasource to use
* @param options the options specifying the query's request
*/
static query(datasource: any, options: QueryOptions) {
const {method, url, namespace, limit, forceAccessTokenRefresh} = options;
const { method, url, namespace, limit, forceAccessTokenRefresh } = options;
const { useApiKey } = datasource.instanceSettings.jsonData;

if (forceAccessTokenRefresh) {
delete datasource.instanceSettings.tokens;
}
Expand All @@ -42,22 +46,35 @@ export default class Sensu {
fullUrl += '?limit=' + limit;
}

return this._authenticate(datasource)
.then(() => this._request(datasource, method, fullUrl))
.catch(() => this.query(datasource, {...options, forceAccessTokenRefresh: true}));
return Sensu._authenticate(datasource)
.then(() => Sensu._request(datasource, method, fullUrl))
.catch((error) => {
if (!useApiKey && !forceAccessTokenRefresh) {
// in case api tokens (not api key) are used, try to refresh the token
Sensu.query(datasource, { ...options, forceAccessTokenRefresh: true });
} else {
throw error;
}
});
}

/**
* Checks whether an access token exist. If none exists or it is expired a new one will be fetched.
* In case an api key auth is used, this method will never fetch a token.
*
* @param datasource the datasource to use
*/
static _authenticate(datasource: any) {
const instanceSettings = datasource.instanceSettings;
let acquireToken =
!instanceSettings.tokens || this._isTokenExpired(instanceSettings.tokens);
const { tokens, jsonData: { useApiKey } } = datasource.instanceSettings;

// never aquire token in case of api key auth
if (useApiKey) {
return Promise.resolve(true);
}

let acquireToken = !tokens || Sensu._isTokenExpired(tokens);
if (acquireToken) {
return this._acquireAccessToken(datasource);
return Sensu._acquireAccessToken(datasource);
} else {
return Promise.resolve(true);
}
Expand All @@ -73,7 +90,7 @@ export default class Sensu {
let expiresAt: number = token.expires_at;

if (token.expires_offset) {
expiresAt = expiresAt - token.expires_offset - this.tokenExpireOffset_s;
expiresAt = expiresAt - token.expires_offset - Sensu.tokenExpireOffset_s;
}

return expiresAt < timestampNow;
Expand All @@ -85,11 +102,11 @@ export default class Sensu {
* @param datasource the datasource to use
*/
static _acquireAccessToken(datasource: any) {
return this._request(datasource, 'GET', '/auth').then(result => {
return Sensu._request(datasource, 'GET', '/auth').then(result => {
let tokens: AccessToken = result.data;

let timestampNow: number = Math.floor(Date.now() / 1000);
let expiresOffset: number = tokens.expires_at - timestampNow - this.tokenTimeout_s;
let expiresOffset: number = tokens.expires_at - timestampNow - Sensu.tokenTimeout_s;

tokens.expires_offset = expiresOffset;

Expand All @@ -105,23 +122,32 @@ export default class Sensu {
* @param url the url to send the request to
*/
static _request(datasource: any, method: string, url: string) {
let req: any = {
method: method,
url: datasource.url + url,
const { useApiKey } = datasource.instanceSettings.jsonData;

const req: any = {
method: method
};

req.headers = {
'Content-Type': 'application/json',
};

if (_.has(datasource.instanceSettings, 'tokens')) {
req.headers.Authorization =
'Bearer ' + datasource.instanceSettings.tokens.access_token;
if (useApiKey) {
// authentication via api key using authentication route
req.url = datasource.url + Sensu.apiKeyUrlPrefix + url;
} else {
// authetnication via bearer token
req.url = datasource.url + url;

if (_.has(datasource.instanceSettings, 'tokens')) {
req.headers.Authorization =
'Bearer ' + datasource.instanceSettings.tokens.access_token;
}
}

return datasource.backendSrv
.datasourceRequest(req)
.then(this._handleRequestResult, this._handleRequestError);
.then(Sensu._handleRequestResult, Sensu._handleRequestError);
}

/**
Expand Down

0 comments on commit 2638348

Please sign in to comment.