390 lines
11 KiB
Go
390 lines
11 KiB
Go
|
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...)
|
||
|
})
|
||
|
}
|
||
|
}
|