Auth: Able to use API tokens for authentication (#1662)
* Auth: Able to use API tokens for authentication * Update change log
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
# Change Log
|
||||
|
||||
## [4.4.0] - 2023-06-06
|
||||
## [4.4.0] - Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Enables PDC for zabbix datasource, [#1653](https://github.com/alexanderzobnin/grafana-zabbix/issues/1653)
|
||||
- Support for secure socks proxy, [#1653](https://github.com/alexanderzobnin/grafana-zabbix/issues/1653)
|
||||
- Able to use API tokens for authentication, [#1513](https://github.com/alexanderzobnin/grafana-zabbix/issues/1513)
|
||||
|
||||
## [4.3.1] - 2023-03-23
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package datasource
|
||||
import (
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
@@ -13,7 +14,7 @@ var basicDatasourceInfo = &backend.DataSourceInstanceSettings{
|
||||
ID: 1,
|
||||
Name: "TestDatasource",
|
||||
URL: "http://zabbix.org/zabbix",
|
||||
JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m"}`),
|
||||
JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m", "authType":"token"}`),
|
||||
}
|
||||
|
||||
func mockZabbixQuery(method string, params zabbix.ZabbixAPIParams) *zabbix.ZabbixAPIRequest {
|
||||
|
||||
@@ -2,8 +2,14 @@ package settings
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
AuthTypeUserLogin = "userLogin"
|
||||
AuthTypeToken = "token"
|
||||
)
|
||||
|
||||
// ZabbixDatasourceSettingsDTO model
|
||||
type ZabbixDatasourceSettingsDTO struct {
|
||||
AuthType string `json:"authType"`
|
||||
Trends bool `json:"trends"`
|
||||
TrendsFrom string `json:"trendsFrom"`
|
||||
TrendsRange string `json:"trendsRange"`
|
||||
@@ -16,6 +22,7 @@ type ZabbixDatasourceSettingsDTO struct {
|
||||
|
||||
// ZabbixDatasourceSettings model
|
||||
type ZabbixDatasourceSettings struct {
|
||||
AuthType string
|
||||
Trends bool
|
||||
TrendsFrom time.Duration
|
||||
TrendsRange time.Duration
|
||||
|
||||
@@ -3,10 +3,12 @@ package settings
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
)
|
||||
|
||||
func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) {
|
||||
@@ -17,6 +19,10 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if zabbixSettingsDTO.AuthType == "" {
|
||||
zabbixSettingsDTO.AuthType = AuthTypeUserLogin
|
||||
}
|
||||
|
||||
if zabbixSettingsDTO.TrendsFrom == "" {
|
||||
zabbixSettingsDTO.TrendsFrom = "7d"
|
||||
}
|
||||
@@ -65,6 +71,7 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
|
||||
}
|
||||
|
||||
zabbixSettings := &ZabbixDatasourceSettings{
|
||||
AuthType: zabbixSettingsDTO.AuthType,
|
||||
Trends: zabbixSettingsDTO.Trends,
|
||||
TrendsFrom: trendsFrom,
|
||||
TrendsRange: trendsRange,
|
||||
|
||||
@@ -2,6 +2,7 @@ package zabbix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi"
|
||||
"github.com/bitly/go-simplejson"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
@@ -16,11 +18,12 @@ import (
|
||||
// Zabbix is a wrapper for Zabbix API. It wraps Zabbix API queries and performs authentication, adds caching,
|
||||
// deduplication and other performance optimizations.
|
||||
type Zabbix struct {
|
||||
api *zabbixapi.ZabbixAPI
|
||||
dsInfo *backend.DataSourceInstanceSettings
|
||||
cache *ZabbixCache
|
||||
version int
|
||||
logger log.Logger
|
||||
api *zabbixapi.ZabbixAPI
|
||||
dsInfo *backend.DataSourceInstanceSettings
|
||||
settings *settings.ZabbixDatasourceSettings
|
||||
cache *ZabbixCache
|
||||
version int
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// New returns new instance of Zabbix client.
|
||||
@@ -29,10 +32,11 @@ func New(dsInfo *backend.DataSourceInstanceSettings, zabbixSettings *settings.Za
|
||||
zabbixCache := NewZabbixCache(zabbixSettings.CacheTTL, 10*time.Minute)
|
||||
|
||||
return &Zabbix{
|
||||
api: zabbixAPI,
|
||||
dsInfo: dsInfo,
|
||||
cache: zabbixCache,
|
||||
logger: logger,
|
||||
api: zabbixAPI,
|
||||
dsInfo: dsInfo,
|
||||
settings: zabbixSettings,
|
||||
cache: zabbixCache,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -90,11 +94,12 @@ func (zabbix *Zabbix) request(ctx context.Context, method string, params ZabbixA
|
||||
|
||||
result, err := zabbix.api.Request(ctx, method, params)
|
||||
notAuthorized := isNotAuthorized(err)
|
||||
if err == zabbixapi.ErrNotAuthenticated || notAuthorized {
|
||||
isTokenAuth := zabbix.settings.AuthType == settings.AuthTypeToken
|
||||
if err == zabbixapi.ErrNotAuthenticated || (notAuthorized && !isTokenAuth) {
|
||||
if notAuthorized {
|
||||
zabbix.logger.Debug("Authentication token expired, performing re-login")
|
||||
}
|
||||
err = zabbix.Login(ctx)
|
||||
err = zabbix.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -106,12 +111,27 @@ func (zabbix *Zabbix) request(ctx context.Context, method string, params ZabbixA
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (zabbix *Zabbix) Login(ctx context.Context) error {
|
||||
func (zabbix *Zabbix) Authenticate(ctx context.Context) error {
|
||||
jsonData, err := simplejson.NewJson(zabbix.dsInfo.JSONData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authType := zabbix.settings.AuthType
|
||||
if authType == settings.AuthTypeToken {
|
||||
token, exists := zabbix.dsInfo.DecryptedSecureJSONData["apiToken"]
|
||||
if !exists {
|
||||
return errors.New("cannot find Zabbix API token")
|
||||
}
|
||||
err = zabbix.api.AuthenticateWithToken(ctx, token)
|
||||
if err != nil {
|
||||
zabbix.logger.Error("Zabbix authentication error", "error", err)
|
||||
return err
|
||||
}
|
||||
zabbix.logger.Debug("Using API token for authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
zabbixLogin := jsonData.Get("username").MustString()
|
||||
var zabbixPassword string
|
||||
if securePassword, exists := zabbix.dsInfo.DecryptedSecureJSONData["password"]; exists {
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
)
|
||||
|
||||
var basicDatasourceInfo = &backend.DataSourceInstanceSettings{
|
||||
@@ -19,7 +20,7 @@ var emptyParams = map[string]interface{}{}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"secretauth"}`, 200)
|
||||
err := zabbixClient.Login(context.Background())
|
||||
err := zabbixClient.Authenticate(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "secretauth", zabbixClient.api.GetAuth())
|
||||
@@ -27,7 +28,7 @@ func TestLogin(t *testing.T) {
|
||||
|
||||
func TestLoginError(t *testing.T) {
|
||||
zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":""}`, 500)
|
||||
err := zabbixClient.Login(context.Background())
|
||||
err := zabbixClient.Authenticate(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", zabbixClient.api.GetAuth())
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
|
||||
"github.com/alexanderzobnin/grafana-zabbix/pkg/metrics"
|
||||
"github.com/bitly/go-simplejson"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -168,6 +169,15 @@ func (api *ZabbixAPI) Authenticate(ctx context.Context, username string, passwor
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthenticateWithToken performs authentication with API token.
|
||||
func (api *ZabbixAPI) AuthenticateWithToken(ctx context.Context, token string) error {
|
||||
if token == "" {
|
||||
return errors.New("API token is empty")
|
||||
}
|
||||
api.SetAuth(token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDeprecatedUserParamError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
@@ -2,13 +2,18 @@ import React, { useEffect, useState } from 'react';
|
||||
import { getDataSourceSrv, config } from '@grafana/runtime';
|
||||
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SelectableValue } from '@grafana/data';
|
||||
import { Button, DataSourceHttpSettings, InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
|
||||
import { ZabbixDSOptions, ZabbixSecureJSONData } from '../types';
|
||||
import { ZabbixAuthType, ZabbixDSOptions, ZabbixSecureJSONData } from '../types';
|
||||
import { gte } from 'semver';
|
||||
|
||||
const { FormField, Switch } = LegacyForms;
|
||||
|
||||
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
|
||||
|
||||
const authOptions: Array<SelectableValue<ZabbixAuthType>> = [
|
||||
{ label: 'User and password', value: ZabbixAuthType.UserLogin },
|
||||
{ label: 'API token', value: ZabbixAuthType.Token },
|
||||
];
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<ZabbixDSOptions, ZabbixSecureJSONData>;
|
||||
export const ConfigEditor = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
@@ -32,6 +37,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
jsonData: {
|
||||
authType: ZabbixAuthType.UserLogin,
|
||||
trends: true,
|
||||
trendsFrom: '',
|
||||
trendsRange: '',
|
||||
@@ -82,41 +88,84 @@ export const ConfigEditor = (props: Props) => {
|
||||
|
||||
<div className="gf-form-group">
|
||||
<h3 className="page-heading">Zabbix API details</h3>
|
||||
<div className="gf-form max-width-25">
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="Username"
|
||||
value={options.jsonData.username || ''}
|
||||
onChange={jsonDataChangeHandler('username', options, onOptionsChange)}
|
||||
required
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel width={7} tooltip="Token authentication available in Zabbix version 5.4 and higher.">
|
||||
Auth type
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
width={30}
|
||||
options={authOptions}
|
||||
value={options.jsonData.authType}
|
||||
onChange={jsonDataSelectHandler('authType', options, onOptionsChange)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form max-width-25">
|
||||
{options.secureJsonFields?.password ? (
|
||||
<>
|
||||
{options.jsonData?.authType === ZabbixAuthType.Token ? (
|
||||
<>
|
||||
<div className="gf-form max-width-25">
|
||||
{options.secureJsonFields?.apiToken ? (
|
||||
<>
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="API token"
|
||||
disabled={true}
|
||||
value=""
|
||||
placeholder="Configured"
|
||||
/>
|
||||
<Button onClick={resetSecureJsonField('apiToken', options, onOptionsChange)}>Reset</Button>
|
||||
</>
|
||||
) : (
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="API token"
|
||||
type="password"
|
||||
value={options.secureJsonData?.apiToken || ''}
|
||||
onChange={secureJsonDataChangeHandler('apiToken', options, onOptionsChange)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="gf-form max-width-25">
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="Password"
|
||||
disabled={true}
|
||||
value=""
|
||||
placeholder="Configured"
|
||||
label="Username"
|
||||
value={options.jsonData.username || ''}
|
||||
onChange={jsonDataChangeHandler('username', options, onOptionsChange)}
|
||||
required
|
||||
/>
|
||||
<Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button>
|
||||
</>
|
||||
) : (
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="Password"
|
||||
type="password"
|
||||
value={options.secureJsonData?.password || options.jsonData.password || ''}
|
||||
onChange={secureJsonDataChangeHandler('password', options, onOptionsChange)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form max-width-25">
|
||||
{options.secureJsonFields?.password ? (
|
||||
<>
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="Password"
|
||||
disabled={true}
|
||||
value=""
|
||||
placeholder="Configured"
|
||||
/>
|
||||
<Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button>
|
||||
</>
|
||||
) : (
|
||||
<FormField
|
||||
labelWidth={7}
|
||||
inputWidth={15}
|
||||
label="Password"
|
||||
type="password"
|
||||
value={options.secureJsonData?.password || options.jsonData.password || ''}
|
||||
onChange={secureJsonDataChangeHandler('password', options, onOptionsChange)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Switch
|
||||
label="Trends"
|
||||
labelClass="width-7"
|
||||
@@ -222,7 +271,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form-group">
|
||||
<h3 className="page-heading">Other</h3>
|
||||
<Switch
|
||||
label="Disable acknowledges for read-only users"
|
||||
@@ -251,7 +300,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
tooltip="Enable proxying the datasource connection through the secure socks proxy to a different network."
|
||||
onChange={jsonDataSwitchHandler('enableSecureSocksProxy', options, onOptionsChange)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -273,6 +322,22 @@ const jsonDataChangeHandler =
|
||||
});
|
||||
};
|
||||
|
||||
const jsonDataSelectHandler =
|
||||
(
|
||||
key: keyof ZabbixDSOptions,
|
||||
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
|
||||
onChange: Props['onOptionsChange']
|
||||
) =>
|
||||
(option: SelectableValue) => {
|
||||
onChange({
|
||||
...value,
|
||||
jsonData: {
|
||||
...value.jsonData,
|
||||
[key]: option.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const jsonDataSwitchHandler =
|
||||
(
|
||||
key: keyof ZabbixDSOptions,
|
||||
@@ -291,7 +356,7 @@ const jsonDataSwitchHandler =
|
||||
|
||||
const secureJsonDataChangeHandler =
|
||||
(
|
||||
key: keyof ZabbixDSOptions,
|
||||
key: keyof ZabbixSecureJSONData,
|
||||
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
|
||||
onChange: Props['onOptionsChange']
|
||||
) =>
|
||||
@@ -307,7 +372,7 @@ const secureJsonDataChangeHandler =
|
||||
|
||||
const resetSecureJsonField =
|
||||
(
|
||||
key: keyof ZabbixDSOptions,
|
||||
key: keyof ZabbixSecureJSONData,
|
||||
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
|
||||
onChange: Props['onOptionsChange']
|
||||
) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BusEventWithPayload, DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface ZabbixDSOptions extends DataSourceJsonData {
|
||||
authType?: ZabbixAuthType;
|
||||
username: string;
|
||||
password?: string;
|
||||
trends: boolean;
|
||||
@@ -19,6 +20,7 @@ export interface ZabbixDSOptions extends DataSourceJsonData {
|
||||
|
||||
export interface ZabbixSecureJSONData {
|
||||
password?: string;
|
||||
apiToken?: string;
|
||||
}
|
||||
|
||||
export interface ZabbixConnectionInfo {
|
||||
@@ -408,3 +410,8 @@ export interface ZBXAlert {
|
||||
export class ZBXQueryUpdatedEvent extends BusEventWithPayload<any> {
|
||||
static type = 'zbx-query-updated';
|
||||
}
|
||||
|
||||
export enum ZabbixAuthType {
|
||||
UserLogin = 'userLogin',
|
||||
Token = 'token',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user