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
## [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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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

View File

@@ -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"
@@ -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']
) =>

View File

@@ -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',
}