Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SSAPI Auth Token (BPS-281) #1990

Merged
merged 6 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 45 additions & 13 deletions receiver/splunksearchapireceiver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,29 @@ type splunkSearchAPIClient interface {
}

type defaultSplunkSearchAPIClient struct {
client *http.Client
endpoint string
logger *zap.Logger
username string
password string
client *http.Client
endpoint string
logger *zap.Logger
username string
password string
authToken string
tokenType string
}

func newSplunkSearchAPIClient(ctx context.Context, settings component.TelemetrySettings, conf Config, host component.Host) (*defaultSplunkSearchAPIClient, error) {
client, err := conf.ClientConfig.ToClient(ctx, host, settings)
if err != nil {
return nil, err
}

return &defaultSplunkSearchAPIClient{
client: client,
endpoint: conf.Endpoint,
logger: settings.Logger,
username: conf.Username,
password: conf.Password,
client: client,
endpoint: conf.Endpoint,
logger: settings.Logger,
username: conf.Username,
password: conf.Password,
authToken: conf.AuthToken,
tokenType: conf.TokenType,
}, nil
}

Expand All @@ -70,7 +75,11 @@ func (c defaultSplunkSearchAPIClient) CreateSearchJob(search string) (CreateJobR
if err != nil {
return CreateJobResponse{}, err
}
req.SetBasicAuth(c.username, c.password)

err = c.SetSplunkRequestAuth(req)
if err != nil {
return CreateJobResponse{}, err
}

resp, err := c.client.Do(req)
if err != nil {
Expand Down Expand Up @@ -102,7 +111,11 @@ func (c defaultSplunkSearchAPIClient) GetJobStatus(sid string) (SearchJobStatusR
if err != nil {
return SearchJobStatusResponse{}, err
}
req.SetBasicAuth(c.username, c.password)

err = c.SetSplunkRequestAuth(req)
if err != nil {
return SearchJobStatusResponse{}, err
}

resp, err := c.client.Do(req)
if err != nil {
Expand Down Expand Up @@ -133,7 +146,11 @@ func (c defaultSplunkSearchAPIClient) GetSearchResults(sid string, offset int, b
if err != nil {
return SearchResultsResponse{}, err
}
req.SetBasicAuth(c.username, c.password)

err = c.SetSplunkRequestAuth(req)
if err != nil {
return SearchResultsResponse{}, err
}

resp, err := c.client.Do(req)
if err != nil {
Expand All @@ -157,3 +174,18 @@ func (c defaultSplunkSearchAPIClient) GetSearchResults(sid string, offset int, b

return searchResults, nil
}

func (c defaultSplunkSearchAPIClient) SetSplunkRequestAuth(req *http.Request) error {
if c.authToken != "" {
if strings.EqualFold(c.tokenType, TokenTypeBearer) {
req.Header.Set("Authorization", "Bearer "+string(c.authToken))
} else if strings.EqualFold(c.tokenType, TokenTypeSplunk) {
req.Header.Set("Authorization", "Splunk "+string(c.authToken))
} else {
return fmt.Errorf("auth_token provided without a correct token type, valid token types are %v", []string{TokenTypeBearer, TokenTypeSplunk})
}
} else {
req.SetBasicAuth(c.username, c.password)
}
return nil
}
24 changes: 24 additions & 0 deletions receiver/splunksearchapireceiver/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ func TestGetSearchResults(t *testing.T) {
require.Empty(t, resp)
}

func TestSetSplunkRequestAuth(t *testing.T) {
client := defaultSplunkSearchAPIClient{
username: "user",
password: "password",
}
req := httptest.NewRequest("GET", "http://localhost:8089", nil)
client.SetSplunkRequestAuth(req)
require.Equal(t, req.Header.Get("Authorization"), "Basic dXNlcjpwYXNzd29yZA==")

client = defaultSplunkSearchAPIClient{
authToken: "token",
tokenType: TokenTypeBearer,
}
client.SetSplunkRequestAuth(req)
require.Equal(t, req.Header.Get("Authorization"), "Bearer token")

client = defaultSplunkSearchAPIClient{
authToken: "token",
tokenType: TokenTypeSplunk,
}
client.SetSplunkRequestAuth(req)
require.Equal(t, req.Header.Get("Authorization"), "Splunk token")
}

// mock Splunk servers
func newMockServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
Expand Down
34 changes: 28 additions & 6 deletions receiver/splunksearchapireceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package splunksearchapireceiver

import (
"errors"
"fmt"
"strings"
"time"

Expand All @@ -25,13 +26,19 @@ import (

var (
errNonStandaloneSearchQuery = errors.New("only standalone search commands can be used for scraping data")
// TokenTypeBearer is the token type for Bearer tokens
TokenTypeBearer = "Bearer"
// TokenTypeSplunk is the token type for Splunk tokens
TokenTypeSplunk = "Splunk"
)

// Config struct to represent the configuration for the Splunk Search API receiver
type Config struct {
confighttp.ClientConfig `mapstructure:",squash"`
Username string `mapstructure:"splunk_username"`
Password string `mapstructure:"splunk_password"`
Username string `mapstructure:"splunk_username,omitempty"`
Password string `mapstructure:"splunk_password,omitempty"`
AuthToken string `mapstructure:"auth_token,omitempty"`
TokenType string `mapstructure:"token_type,omitempty"`
Searches []Search `mapstructure:"searches"`
JobPollInterval time.Duration `mapstructure:"job_poll_interval"`
StorageID *component.ID `mapstructure:"storage"`
Expand All @@ -51,12 +58,27 @@ func (cfg *Config) Validate() error {
if cfg.Endpoint == "" {
return errors.New("missing Splunk server endpoint")
}
if cfg.Username == "" {
return errors.New("missing Splunk username")

if cfg.Username == "" && cfg.AuthToken == "" {
return errors.New("missing Splunk username or auth token")
}

if cfg.Password == "" && cfg.AuthToken == "" {
return errors.New("missing Splunk password or auth token")
}
if cfg.Password == "" {
return errors.New("missing Splunk password")

if cfg.AuthToken != "" {
if cfg.TokenType == "" {
return errors.New("auth_token provided without a token type")
}
if !strings.EqualFold(cfg.TokenType, TokenTypeBearer) && !strings.EqualFold(cfg.TokenType, TokenTypeSplunk) {
return fmt.Errorf("auth_token provided without a correct token type, valid token types are %v", []string{TokenTypeBearer, TokenTypeSplunk})
}
if cfg.Username != "" || cfg.Password != "" {
return errors.New("auth_token and username/password were both provided, only one can be provided to authenticate with Splunk")
}
}

if len(cfg.Searches) == 0 {
return errors.New("at least one search must be provided")
}
Expand Down
76 changes: 72 additions & 4 deletions receiver/splunksearchapireceiver/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func TestValidate(t *testing.T) {
endpoint string
username string
password string
authToken string
tokenType string
storage string
searches []Search
errExpected bool
Expand All @@ -48,7 +50,7 @@ func TestValidate(t *testing.T) {
errText: "missing Splunk server endpoint",
},
{
desc: "Missing username",
desc: "Missing username, no auth token",
endpoint: "http://localhost:8089",
password: "password",
storage: "file_storage",
Expand All @@ -60,10 +62,10 @@ func TestValidate(t *testing.T) {
},
},
errExpected: true,
errText: "missing Splunk username",
errText: "missing Splunk username or auth token",
},
{
desc: "Missing password",
desc: "Missing password, no auth token",
endpoint: "http://localhost:8089",
username: "user",
storage: "file_storage",
Expand All @@ -75,7 +77,56 @@ func TestValidate(t *testing.T) {
},
},
errExpected: true,
errText: "missing Splunk password",
errText: "missing Splunk password or auth token",
},
{
desc: "Auth token without token type",
endpoint: "http://localhost:8089",
authToken: "token",
storage: "file_storage",
searches: []Search{
{
Query: "search index=_internal",
EarliestTime: "2024-10-30T04:00:00.000Z",
LatestTime: "2024-10-30T14:00:00.000Z",
},
},
errExpected: true,
errText: "auth_token provided without a token type",
},
{
desc: "Auth token with invalid token type",
endpoint: "http://localhost:8089",
authToken: "token",
tokenType: "invalid",
storage: "file_storage",
searches: []Search{
{
Query: "search index=_internal",
EarliestTime: "2024-10-30T04:00:00.000Z",
LatestTime: "2024-10-30T14:00:00.000Z",
},
},
errExpected: true,
errText: "auth_token provided without a correct token type, valid token types are [Bearer Splunk]",
},
{
desc: "Auth token and username/password provided",
endpoint: "http://localhost:8089",
username: "user",
password: "password",
authToken: "token",
tokenType: "Bearer",
storage: "file_storage",
searches: []Search{
{
Query: "search index=_internal",
EarliestTime: "2024-10-30T04:00:00.000Z",
LatestTime: "2024-10-30T14:00:00.000Z",
},
},
errExpected: true,
errText: "auth_token and username/password were both provided, only one can be provided to authenticate with Splunk",
},
{
desc: "Missing storage",
Expand Down Expand Up @@ -209,6 +260,21 @@ func TestValidate(t *testing.T) {
},
errExpected: false,
},
{
desc: "Valid config with auth token",
endpoint: "http://localhost:8089",
authToken: "token",
tokenType: "Bearer",
storage: "file_storage",
searches: []Search{
{
Query: "search index=_internal",
EarliestTime: "2024-10-30T04:00:00.000Z",
LatestTime: "2024-10-30T14:00:00.000Z",
},
},
errExpected: false,
},
{
desc: "Valid config with multiple searches",
endpoint: "http://localhost:8089",
Expand Down Expand Up @@ -268,6 +334,8 @@ func TestValidate(t *testing.T) {
cfg.Endpoint = tc.endpoint
cfg.Username = tc.username
cfg.Password = tc.password
cfg.AuthToken = tc.authToken
cfg.TokenType = tc.tokenType
cfg.Searches = tc.searches
if tc.storage != "" {
cfg.StorageID = &component.ID{}
Expand Down
11 changes: 10 additions & 1 deletion receiver/splunksearchapireceiver/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,22 +111,30 @@ func (ssapir *splunksearchapireceiver) Shutdown(ctx context.Context) error {

func (ssapir *splunksearchapireceiver) runQueries(ctx context.Context) error {
for _, search := range ssapir.config.Searches {
// set default event batch size
if search.EventBatchSize == 0 {
search.EventBatchSize = 100
}

// create search in Splunk
searchID, err := ssapir.createSplunkSearch(search)
if err != nil {
ssapir.logger.Error("error creating search", zap.Error(err))
return err
}

// wait for search to complete
if err = ssapir.pollSearchCompletion(ctx, searchID); err != nil {
ssapir.logger.Error("error polling for search completion", zap.Error(err))
return err
}

for {
ssapir.logger.Info("fetching search results")
results, err := ssapir.getSplunkSearchResults(searchID, offset, search.EventBatchSize)
if err != nil {
ssapir.logger.Error("error fetching search results", zap.Error(err))
return err
}
ssapir.logger.Info("search results fetched", zap.Int("num_results", len(results.Results)))

Expand Down Expand Up @@ -198,10 +206,11 @@ func (ssapir *splunksearchapireceiver) runQueries(ctx context.Context) error {
}
// if the number of results is less than the results per request, we have queried all pages for the search
if len(results.Results) < search.EventBatchSize {
ssapir.logger.Debug("results less than batch size, stopping search result export")
break
}

}

ssapir.logger.Debug("all search results exported", zap.String("query", search.Query), zap.Int("total results", exportedEvents))
}
return nil
Expand Down