Skip to content

Commit

Permalink
feat: SSAPI Auth Token (BPS-281) (#1990)
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb-Hurshman authored Dec 2, 2024
2 parents 44f73b2 + de5f00c commit b2f0bbf
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 24 deletions.
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

0 comments on commit b2f0bbf

Please sign in to comment.