diff --git a/CHANGELOG.md b/CHANGELOG.md index 6963e5c..9b6afae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pkg/datasource/zabbix_test.go b/pkg/datasource/zabbix_test.go index bffa375..02a293f 100644 --- a/pkg/datasource/zabbix_test.go +++ b/pkg/datasource/zabbix_test.go @@ -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 { diff --git a/pkg/settings/models.go b/pkg/settings/models.go index 2566bae..6067961 100644 --- a/pkg/settings/models.go +++ b/pkg/settings/models.go @@ -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 diff --git a/pkg/settings/settings.go b/pkg/settings/settings.go index 5d18efa..ac05c5d 100644 --- a/pkg/settings/settings.go +++ b/pkg/settings/settings.go @@ -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, diff --git a/pkg/zabbix/zabbix.go b/pkg/zabbix/zabbix.go index 01632f4..3cebbc0 100644 --- a/pkg/zabbix/zabbix.go +++ b/pkg/zabbix/zabbix.go @@ -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 { diff --git a/pkg/zabbix/zabbix_test.go b/pkg/zabbix/zabbix_test.go index 27849c9..55a9f20 100644 --- a/pkg/zabbix/zabbix_test.go +++ b/pkg/zabbix/zabbix_test.go @@ -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()) diff --git a/pkg/zabbixapi/zabbix_api.go b/pkg/zabbixapi/zabbix_api.go index dc6490d..2e99957 100644 --- a/pkg/zabbixapi/zabbix_api.go +++ b/pkg/zabbixapi/zabbix_api.go @@ -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 diff --git a/src/datasource/components/ConfigEditor.tsx b/src/datasource/components/ConfigEditor.tsx index 925d5e6..494c330 100644 --- a/src/datasource/components/ConfigEditor.tsx +++ b/src/datasource/components/ConfigEditor.tsx @@ -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> = [ + { label: 'User and password', value: ZabbixAuthType.UserLogin }, + { label: 'API token', value: ZabbixAuthType.Token }, +]; + export type Props = DataSourcePluginOptionsEditorProps; 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) => {

Zabbix API details

-
- + + Auth type + +