1
0
Fork 0
telegraf/plugins/inputs/tacacs/tacacs_test.go

390 lines
11 KiB
Go
Raw Permalink Normal View History

package tacacs
import (
"context"
"net"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/nwaples/tacplus"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
)
type testRequestHandler map[string]struct {
password string
args []string
}
func (t testRequestHandler) HandleAuthenStart(_ context.Context, a *tacplus.AuthenStart, s *tacplus.ServerSession) *tacplus.AuthenReply {
user := a.User
for user == "" {
c, err := s.GetUser(context.Background(), "Username:")
if err != nil || c.Abort {
return nil
}
user = c.Message
}
pass := ""
for pass == "" {
c, err := s.GetPass(context.Background(), "Password:")
if err != nil || c.Abort {
return nil
}
pass = c.Message
}
if u, ok := t[user]; ok && u.password == pass {
return &tacplus.AuthenReply{Status: tacplus.AuthenStatusPass}
}
return &tacplus.AuthenReply{Status: tacplus.AuthenStatusFail}
}
func (t testRequestHandler) HandleAuthorRequest(_ context.Context, a *tacplus.AuthorRequest, _ *tacplus.ServerSession) *tacplus.AuthorResponse {
if u, ok := t[a.User]; ok {
return &tacplus.AuthorResponse{Status: tacplus.AuthorStatusPassAdd, Arg: u.args}
}
return &tacplus.AuthorResponse{Status: tacplus.AuthorStatusFail}
}
func (testRequestHandler) HandleAcctRequest(context.Context, *tacplus.AcctRequest, *tacplus.ServerSession) *tacplus.AcctReply {
return &tacplus.AcctReply{Status: tacplus.AcctStatusSuccess}
}
func TestTacacsInit(t *testing.T) {
var testset = []struct {
name string
testingTimeout config.Duration
serversToTest []string
usedUsername config.Secret
usedPassword config.Secret
usedSecret config.Secret
requestAddr string
errContains string
}{
{
name: "empty_creds",
testingTimeout: config.Duration(time.Second * 5),
serversToTest: []string{"foo.bar:80"},
usedUsername: config.NewSecret([]byte(``)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
errContains: "empty credentials were provided (username, password or secret)",
},
{
name: "wrong_reqaddress",
testingTimeout: config.Duration(time.Second * 5),
serversToTest: []string{"foo.bar:80"},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
requestAddr: "257.257.257.257",
errContains: "invalid ip address provided for request_ip",
},
{
name: "no_reqaddress_no_servers",
testingTimeout: config.Duration(time.Second * 5),
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
},
}
for _, tt := range testset {
t.Run(tt.name, func(t *testing.T) {
plugin := &Tacacs{
ResponseTimeout: tt.testingTimeout,
Servers: tt.serversToTest,
Username: tt.usedUsername,
Password: tt.usedPassword,
Secret: tt.usedSecret,
RequestAddr: tt.requestAddr,
Log: testutil.Logger{},
}
err := plugin.Init()
if tt.errContains == "" {
require.NoError(t, err)
if tt.requestAddr == "" {
require.Equal(t, "127.0.0.1", plugin.RequestAddr)
}
if len(tt.serversToTest) == 0 {
require.Equal(t, []string{"127.0.0.1:49"}, plugin.Servers)
}
} else {
require.ErrorContains(t, err, tt.errContains)
}
})
}
}
func TestTacacsLocal(t *testing.T) {
testHandler := tacplus.ServerConnHandler{
Handler: &testRequestHandler{
"testusername": {
password: "testpassword",
},
},
ConnConfig: tacplus.ConnConfig{
Secret: []byte(`testsecret`),
Mux: true,
},
}
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err, "local net listen failed to start listening")
srvLocal := l.Addr().String()
srv := &tacplus.Server{
ServeConn: func(nc net.Conn) {
testHandler.Serve(nc)
},
}
go func() {
if err := srv.Serve(l); err != nil {
t.Logf("local srv.Serve failed to start serving on %s", srvLocal)
}
}()
var testset = []struct {
name string
testingTimeout config.Duration
serverToTest []string
usedUsername config.Secret
usedPassword config.Secret
usedSecret config.Secret
requestAddr string
errContains string
reqRespStatus string
}{
{
name: "success_timeout_0s",
testingTimeout: config.Duration(0),
serverToTest: []string{srvLocal},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
requestAddr: "127.0.0.1",
reqRespStatus: "AuthenStatusPass",
},
{
name: "wrongpw",
testingTimeout: config.Duration(time.Second * 5),
serverToTest: []string{srvLocal},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`WRONGPASSWORD`)),
usedSecret: config.NewSecret([]byte(`testsecret`)),
requestAddr: "127.0.0.1",
reqRespStatus: "AuthenStatusFail",
},
{
name: "wrongsecret",
testingTimeout: config.Duration(time.Second * 5),
serverToTest: []string{srvLocal},
usedUsername: config.NewSecret([]byte(`testusername`)),
usedPassword: config.NewSecret([]byte(`testpassword`)),
usedSecret: config.NewSecret([]byte(`WRONGSECRET`)),
requestAddr: "127.0.0.1",
errContains: "error on new tacacs authentication start request to " + srvLocal + " : bad secret or packet",
},
}
for _, tt := range testset {
t.Run(tt.name, func(t *testing.T) {
plugin := &Tacacs{
ResponseTimeout: tt.testingTimeout,
Servers: tt.serverToTest,
Username: tt.usedUsername,
Password: tt.usedPassword,
Secret: tt.usedSecret,
RequestAddr: tt.requestAddr,
Log: testutil.Logger{},
}
var acc testutil.Accumulator
require.NoError(t, plugin.Init())
require.NoError(t, plugin.Gather(&acc))
if tt.errContains == "" {
require.Empty(t, acc.Errors)
expected := []telegraf.Metric{
metric.New(
"tacacs",
map[string]string{"source": srvLocal},
map[string]interface{}{
"responsetime_ms": int64(0),
"response_status": tt.reqRespStatus,
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.IgnoreFields("responsetime_ms"),
}
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), options...)
} else {
require.Len(t, acc.Errors, 1)
require.ErrorContains(t, acc.FirstError(), tt.errContains)
require.Empty(t, acc.GetTelegrafMetrics())
}
})
}
}
func TestTacacsLocalTimeout(t *testing.T) {
testHandler := tacplus.ServerConnHandler{
Handler: &testRequestHandler{
"testusername": {
password: "testpassword",
},
},
ConnConfig: tacplus.ConnConfig{
Secret: []byte(`testsecret`),
Mux: true,
},
}
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err, "local net listen failed to start listening")
srvLocal := l.Addr().String()
srv := &tacplus.Server{
ServeConn: func(nc net.Conn) {
testHandler.Serve(nc)
},
}
go func() {
if err := srv.Serve(l); err != nil {
t.Logf("local srv.Serve failed to start serving on %s", srvLocal)
}
}()
// Initialize the plugin
plugin := &Tacacs{
ResponseTimeout: config.Duration(time.Microsecond),
Servers: []string{"unreachable.test:49"},
Username: config.NewSecret([]byte(`testusername`)),
Password: config.NewSecret([]byte(`testpassword`)),
Secret: config.NewSecret([]byte(`testsecret`)),
RequestAddr: "127.0.0.1",
Log: &testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Try to connect, this will return a metric with the timeout...
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
expected := []telegraf.Metric{
metric.New(
"tacacs",
map[string]string{"source": "unreachable.test:49"},
map[string]interface{}{
"response_status": string("Timeout"),
"responsetime_ms": int64(0),
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.IgnoreFields("responsetime_ms"),
}
require.Empty(t, acc.Errors)
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), options...)
}
func TestTacacsIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
container := testutil.Container{
Image: "dchidell/docker-tacacs",
ExposedPorts: []string{"49/tcp"},
WaitingFor: wait.ForAll(
wait.ForLog("Starting server..."),
),
}
require.NoError(t, container.Start(), "failed to start container")
defer container.Terminate()
port := container.Ports["49"]
// Define the testset
var testset = []struct {
name string
testingTimeout config.Duration
usedPassword string
reqRespStatus string
}{
{
name: "timeout_3s",
testingTimeout: config.Duration(time.Second * 3),
usedPassword: "cisco",
reqRespStatus: "AuthenStatusPass",
},
{
name: "timeout_0s",
testingTimeout: config.Duration(0),
usedPassword: "cisco",
reqRespStatus: "AuthenStatusPass",
},
{
name: "wrong_pw",
testingTimeout: config.Duration(time.Second * 5),
usedPassword: "wrongpass",
reqRespStatus: "AuthenStatusFail",
},
}
for _, tt := range testset {
t.Run(tt.name, func(t *testing.T) {
// Setup the plugin-under-test
plugin := &Tacacs{
ResponseTimeout: tt.testingTimeout,
Servers: []string{container.Address + ":" + port},
Username: config.NewSecret([]byte(`iosadmin`)),
Password: config.NewSecret([]byte(tt.usedPassword)),
Secret: config.NewSecret([]byte(`ciscotacacskey`)),
RequestAddr: "127.0.0.1",
Log: testutil.Logger{},
}
var acc testutil.Accumulator
// Startup the plugin & Gather
require.NoError(t, plugin.Init())
require.NoError(t, plugin.Gather(&acc))
require.NoError(t, acc.FirstError())
expected := []telegraf.Metric{
metric.New(
"tacacs",
map[string]string{"source": container.Address + ":" + port},
map[string]interface{}{
"responsetime_ms": int64(0),
"response_status": tt.reqRespStatus,
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.IgnoreFields("responsetime_ms"),
}
testutil.RequireMetricsStructureEqual(t, expected, acc.GetTelegrafMetrics(), options...)
})
}
}