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:
Alexander Zobnin
2023-07-26 18:23:44 +03:00
committed by GitHub
parent 8205f7aaf8
commit ac976945a5
9 changed files with 174 additions and 55 deletions

View File

@@ -1,10 +1,11 @@
# Change Log # Change Log
## [4.4.0] - 2023-06-06 ## [4.4.0] - Unreleased
### Added ### 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 ## [4.3.1] - 2023-03-23

View File

@@ -3,6 +3,7 @@ package datasource
import ( import (
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings" "github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/log"
) )
@@ -13,7 +14,7 @@ var basicDatasourceInfo = &backend.DataSourceInstanceSettings{
ID: 1, ID: 1,
Name: "TestDatasource", Name: "TestDatasource",
URL: "http://zabbix.org/zabbix", 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 { func mockZabbixQuery(method string, params zabbix.ZabbixAPIParams) *zabbix.ZabbixAPIRequest {

View File

@@ -2,8 +2,14 @@ package settings
import "time" import "time"
const (
AuthTypeUserLogin = "userLogin"
AuthTypeToken = "token"
)
// ZabbixDatasourceSettingsDTO model // ZabbixDatasourceSettingsDTO model
type ZabbixDatasourceSettingsDTO struct { type ZabbixDatasourceSettingsDTO struct {
AuthType string `json:"authType"`
Trends bool `json:"trends"` Trends bool `json:"trends"`
TrendsFrom string `json:"trendsFrom"` TrendsFrom string `json:"trendsFrom"`
TrendsRange string `json:"trendsRange"` TrendsRange string `json:"trendsRange"`
@@ -16,6 +22,7 @@ type ZabbixDatasourceSettingsDTO struct {
// ZabbixDatasourceSettings model // ZabbixDatasourceSettings model
type ZabbixDatasourceSettings struct { type ZabbixDatasourceSettings struct {
AuthType string
Trends bool Trends bool
TrendsFrom time.Duration TrendsFrom time.Duration
TrendsRange time.Duration TrendsRange time.Duration

View File

@@ -3,10 +3,12 @@ package settings
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"strconv" "strconv"
"time" "time"
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
"github.com/grafana/grafana-plugin-sdk-go/backend"
) )
func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) { func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) {
@@ -17,6 +19,10 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
return nil, err return nil, err
} }
if zabbixSettingsDTO.AuthType == "" {
zabbixSettingsDTO.AuthType = AuthTypeUserLogin
}
if zabbixSettingsDTO.TrendsFrom == "" { if zabbixSettingsDTO.TrendsFrom == "" {
zabbixSettingsDTO.TrendsFrom = "7d" zabbixSettingsDTO.TrendsFrom = "7d"
} }
@@ -65,6 +71,7 @@ func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
} }
zabbixSettings := &ZabbixDatasourceSettings{ zabbixSettings := &ZabbixDatasourceSettings{
AuthType: zabbixSettingsDTO.AuthType,
Trends: zabbixSettingsDTO.Trends, Trends: zabbixSettingsDTO.Trends,
TrendsFrom: trendsFrom, TrendsFrom: trendsFrom,
TrendsRange: trendsRange, TrendsRange: trendsRange,

View File

@@ -2,6 +2,7 @@ package zabbix
import ( import (
"context" "context"
"errors"
"strings" "strings"
"time" "time"
@@ -9,6 +10,7 @@ import (
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings" "github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi"
"github.com/bitly/go-simplejson" "github.com/bitly/go-simplejson"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log" "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, // Zabbix is a wrapper for Zabbix API. It wraps Zabbix API queries and performs authentication, adds caching,
// deduplication and other performance optimizations. // deduplication and other performance optimizations.
type Zabbix struct { type Zabbix struct {
api *zabbixapi.ZabbixAPI api *zabbixapi.ZabbixAPI
dsInfo *backend.DataSourceInstanceSettings dsInfo *backend.DataSourceInstanceSettings
cache *ZabbixCache settings *settings.ZabbixDatasourceSettings
version int cache *ZabbixCache
logger log.Logger version int
logger log.Logger
} }
// New returns new instance of Zabbix client. // 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) zabbixCache := NewZabbixCache(zabbixSettings.CacheTTL, 10*time.Minute)
return &Zabbix{ return &Zabbix{
api: zabbixAPI, api: zabbixAPI,
dsInfo: dsInfo, dsInfo: dsInfo,
cache: zabbixCache, settings: zabbixSettings,
logger: logger, cache: zabbixCache,
logger: logger,
}, nil }, nil
} }
@@ -90,11 +94,12 @@ func (zabbix *Zabbix) request(ctx context.Context, method string, params ZabbixA
result, err := zabbix.api.Request(ctx, method, params) result, err := zabbix.api.Request(ctx, method, params)
notAuthorized := isNotAuthorized(err) notAuthorized := isNotAuthorized(err)
if err == zabbixapi.ErrNotAuthenticated || notAuthorized { isTokenAuth := zabbix.settings.AuthType == settings.AuthTypeToken
if err == zabbixapi.ErrNotAuthenticated || (notAuthorized && !isTokenAuth) {
if notAuthorized { if notAuthorized {
zabbix.logger.Debug("Authentication token expired, performing re-login") zabbix.logger.Debug("Authentication token expired, performing re-login")
} }
err = zabbix.Login(ctx) err = zabbix.Authenticate(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -106,12 +111,27 @@ func (zabbix *Zabbix) request(ctx context.Context, method string, params ZabbixA
return result, err 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) jsonData, err := simplejson.NewJson(zabbix.dsInfo.JSONData)
if err != nil { if err != nil {
return err 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() zabbixLogin := jsonData.Get("username").MustString()
var zabbixPassword string var zabbixPassword string
if securePassword, exists := zabbix.dsInfo.DecryptedSecureJSONData["password"]; exists { if securePassword, exists := zabbix.dsInfo.DecryptedSecureJSONData["password"]; exists {

View File

@@ -4,8 +4,9 @@ import (
"context" "context"
"testing" "testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/grafana/grafana-plugin-sdk-go/backend"
) )
var basicDatasourceInfo = &backend.DataSourceInstanceSettings{ var basicDatasourceInfo = &backend.DataSourceInstanceSettings{
@@ -19,7 +20,7 @@ var emptyParams = map[string]interface{}{}
func TestLogin(t *testing.T) { func TestLogin(t *testing.T) {
zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"secretauth"}`, 200) zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":"secretauth"}`, 200)
err := zabbixClient.Login(context.Background()) err := zabbixClient.Authenticate(context.Background())
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "secretauth", zabbixClient.api.GetAuth()) assert.Equal(t, "secretauth", zabbixClient.api.GetAuth())
@@ -27,7 +28,7 @@ func TestLogin(t *testing.T) {
func TestLoginError(t *testing.T) { func TestLoginError(t *testing.T) {
zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":""}`, 500) zabbixClient, _ := MockZabbixClient(basicDatasourceInfo, `{"result":""}`, 500)
err := zabbixClient.Login(context.Background()) err := zabbixClient.Authenticate(context.Background())
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, "", zabbixClient.api.GetAuth()) assert.Equal(t, "", zabbixClient.api.GetAuth())

View File

@@ -13,8 +13,9 @@ import (
"github.com/alexanderzobnin/grafana-zabbix/pkg/metrics" "github.com/alexanderzobnin/grafana-zabbix/pkg/metrics"
"github.com/bitly/go-simplejson" "github.com/bitly/go-simplejson"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
) )
var ( var (
@@ -168,6 +169,15 @@ func (api *ZabbixAPI) Authenticate(ctx context.Context, username string, passwor
return nil 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 { func isDeprecatedUserParamError(err error) bool {
if err == nil { if err == nil {
return false return false

View File

@@ -2,13 +2,18 @@ import React, { useEffect, useState } from 'react';
import { getDataSourceSrv, config } from '@grafana/runtime'; import { getDataSourceSrv, config } from '@grafana/runtime';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SelectableValue } from '@grafana/data'; import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SelectableValue } from '@grafana/data';
import { Button, DataSourceHttpSettings, InlineFormLabel, LegacyForms, Select } from '@grafana/ui'; import { Button, DataSourceHttpSettings, InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
import { ZabbixDSOptions, ZabbixSecureJSONData } from '../types'; import { ZabbixAuthType, ZabbixDSOptions, ZabbixSecureJSONData } from '../types';
import { gte } from 'semver'; import { gte } from 'semver';
const { FormField, Switch } = LegacyForms; const { FormField, Switch } = LegacyForms;
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb']; 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 type Props = DataSourcePluginOptionsEditorProps<ZabbixDSOptions, ZabbixSecureJSONData>;
export const ConfigEditor = (props: Props) => { export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props; const { options, onOptionsChange } = props;
@@ -32,6 +37,7 @@ export const ConfigEditor = (props: Props) => {
onOptionsChange({ onOptionsChange({
...options, ...options,
jsonData: { jsonData: {
authType: ZabbixAuthType.UserLogin,
trends: true, trends: true,
trendsFrom: '', trendsFrom: '',
trendsRange: '', trendsRange: '',
@@ -82,41 +88,84 @@ export const ConfigEditor = (props: Props) => {
<div className="gf-form-group"> <div className="gf-form-group">
<h3 className="page-heading">Zabbix API details</h3> <h3 className="page-heading">Zabbix API details</h3>
<div className="gf-form max-width-25"> <div className="gf-form">
<FormField <InlineFormLabel width={7} tooltip="Token authentication available in Zabbix version 5.4 and higher.">
labelWidth={7} Auth type
inputWidth={15} </InlineFormLabel>
label="Username" <Select
value={options.jsonData.username || ''} width={30}
onChange={jsonDataChangeHandler('username', options, onOptionsChange)} options={authOptions}
required value={options.jsonData.authType}
onChange={jsonDataSelectHandler('authType', options, onOptionsChange)}
/> />
</div> </div>
<div className="gf-form max-width-25"> {options.jsonData?.authType === ZabbixAuthType.Token ? (
{options.secureJsonFields?.password ? ( <>
<> <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 <FormField
labelWidth={7} labelWidth={7}
inputWidth={15} inputWidth={15}
label="Password" label="Username"
disabled={true} value={options.jsonData.username || ''}
value="" onChange={jsonDataChangeHandler('username', options, onOptionsChange)}
placeholder="Configured" required
/> />
<Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button> </div>
</> <div className="gf-form max-width-25">
) : ( {options.secureJsonFields?.password ? (
<FormField <>
labelWidth={7} <FormField
inputWidth={15} labelWidth={7}
label="Password" inputWidth={15}
type="password" label="Password"
value={options.secureJsonData?.password || options.jsonData.password || ''} disabled={true}
onChange={secureJsonDataChangeHandler('password', options, onOptionsChange)} value=""
required placeholder="Configured"
/> />
)} <Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button>
</div> </>
) : (
<FormField
labelWidth={7}
inputWidth={15}
label="Password"
type="password"
value={options.secureJsonData?.password || options.jsonData.password || ''}
onChange={secureJsonDataChangeHandler('password', options, onOptionsChange)}
required
/>
)}
</div>
</>
)}
<Switch <Switch
label="Trends" label="Trends"
labelClass="width-7" labelClass="width-7"
@@ -222,7 +271,7 @@ export const ConfigEditor = (props: Props) => {
)} )}
</div> </div>
<div className="gf-form-group"> <div className="gf-form-group">
<h3 className="page-heading">Other</h3> <h3 className="page-heading">Other</h3>
<Switch <Switch
label="Disable acknowledges for read-only users" label="Disable acknowledges for read-only users"
@@ -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 = const jsonDataSwitchHandler =
( (
key: keyof ZabbixDSOptions, key: keyof ZabbixDSOptions,
@@ -291,7 +356,7 @@ const jsonDataSwitchHandler =
const secureJsonDataChangeHandler = const secureJsonDataChangeHandler =
( (
key: keyof ZabbixDSOptions, key: keyof ZabbixSecureJSONData,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>, value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange'] onChange: Props['onOptionsChange']
) => ) =>
@@ -307,7 +372,7 @@ const secureJsonDataChangeHandler =
const resetSecureJsonField = const resetSecureJsonField =
( (
key: keyof ZabbixDSOptions, key: keyof ZabbixSecureJSONData,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>, value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange'] onChange: Props['onOptionsChange']
) => ) =>

View File

@@ -1,6 +1,7 @@
import { BusEventWithPayload, DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data'; import { BusEventWithPayload, DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data';
export interface ZabbixDSOptions extends DataSourceJsonData { export interface ZabbixDSOptions extends DataSourceJsonData {
authType?: ZabbixAuthType;
username: string; username: string;
password?: string; password?: string;
trends: boolean; trends: boolean;
@@ -19,6 +20,7 @@ export interface ZabbixDSOptions extends DataSourceJsonData {
export interface ZabbixSecureJSONData { export interface ZabbixSecureJSONData {
password?: string; password?: string;
apiToken?: string;
} }
export interface ZabbixConnectionInfo { export interface ZabbixConnectionInfo {
@@ -408,3 +410,8 @@ export interface ZBXAlert {
export class ZBXQueryUpdatedEvent extends BusEventWithPayload<any> { export class ZBXQueryUpdatedEvent extends BusEventWithPayload<any> {
static type = 'zbx-query-updated'; static type = 'zbx-query-updated';
} }
export enum ZabbixAuthType {
UserLogin = 'userLogin',
Token = 'token',
}