Skip to content

Commit f2ec169

Browse files
author
Eric Lee
authored
[cortex] Authentication Implementation and Timestamp Fix (#246)
* Create auth.go, auth_test.go and add copyright and license * Add helper function to create files for testing * Setup authentication tests and add first test for BasicAuth * Add BasicAuth and error definitions * Add additional tests for BasicAuth * Add file creation to pass basic auth tests * Add bearer token tests * Add bearer token authentication * Adjust timestamp to milliseconds and remove debugging print statement * Run make precommit and fix lint issues * Changed error strings to start with lowercase letter * Add explicit base time unit to timestamp * Moved basic auth validation and validation tests to config.go * Update comments for clarity
1 parent a21b1d8 commit f2ec169

File tree

7 files changed

+333
-20
lines changed

7 files changed

+333
-20
lines changed

exporters/metric/cortex/auth.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cortex
16+
17+
import (
18+
"fmt"
19+
"io/ioutil"
20+
"net/http"
21+
)
22+
23+
// ErrFailedToReadFile occurs when a password / bearer token file exists, but could
24+
// not be read.
25+
var ErrFailedToReadFile = fmt.Errorf("failed to read password / bearer token file")
26+
27+
// addBasicAuth sets the Authorization header for basic authentication using a username
28+
// and a password / password file. The header value is not changed if an Authorization
29+
// header already exists and no action is taken if the Exporter is not configured with
30+
// basic authorization credentials.
31+
func (e *Exporter) addBasicAuth(req *http.Request) error {
32+
// No need to add basic auth if it isn't provided or if the Authorization header is
33+
// already set.
34+
if _, exists := e.config.Headers["Authorization"]; exists {
35+
return nil
36+
}
37+
if e.config.BasicAuth == nil {
38+
return nil
39+
}
40+
41+
username := e.config.BasicAuth["username"]
42+
43+
// Use password from password file if it exists.
44+
passwordFile := e.config.BasicAuth["password_file"]
45+
if passwordFile != "" {
46+
file, err := ioutil.ReadFile(passwordFile)
47+
if err != nil {
48+
return ErrFailedToReadFile
49+
}
50+
password := string(file)
51+
req.SetBasicAuth(username, password)
52+
return nil
53+
}
54+
55+
// Use provided password.
56+
password := e.config.BasicAuth["password"]
57+
req.SetBasicAuth(username, password)
58+
59+
return nil
60+
}
61+
62+
// addBearerTokenAuth sets the Authorization header for bearer tokens using a bearer token
63+
// string or a bearer token file. The header value is not changed if an Authorization
64+
// header already exists and no action is taken if the Exporter is not configured with
65+
// bearer token credentials.
66+
func (e *Exporter) addBearerTokenAuth(req *http.Request) error {
67+
// No need to add bearer token auth if the Authorization header is already set.
68+
if _, exists := e.config.Headers["Authorization"]; exists {
69+
return nil
70+
}
71+
72+
// Use bearer token from bearer token file if it exists.
73+
if e.config.BearerTokenFile != "" {
74+
file, err := ioutil.ReadFile(e.config.BearerTokenFile)
75+
if err != nil {
76+
return ErrFailedToReadFile
77+
}
78+
bearerTokenString := "Bearer " + string(file)
79+
req.Header.Set("Authorization", bearerTokenString)
80+
return nil
81+
}
82+
83+
// Otherwise, use bearer token field.
84+
if e.config.BearerToken != "" {
85+
bearerTokenString := "Bearer " + e.config.BearerToken
86+
req.Header.Set("Authorization", bearerTokenString)
87+
}
88+
89+
return nil
90+
}

exporters/metric/cortex/auth_test.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cortex
16+
17+
import (
18+
"encoding/base64"
19+
"io/ioutil"
20+
"net/http"
21+
"net/http/httptest"
22+
"os"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
// TestAuthentication checks whether http requests are properly authenticated with either
29+
// bearer tokens or basic authentication in the addHeaders method.
30+
func TestAuthentication(t *testing.T) {
31+
tests := []struct {
32+
testName string
33+
basicAuth map[string]string
34+
basicAuthPasswordFileContents []byte
35+
bearerToken string
36+
bearerTokenFile string
37+
bearerTokenFileContents []byte
38+
expectedAuthHeaderValue string
39+
expectedError error
40+
}{
41+
{
42+
testName: "Basic Auth with password",
43+
basicAuth: map[string]string{
44+
"username": "TestUser",
45+
"password": "TestPassword",
46+
},
47+
expectedAuthHeaderValue: "Basic " + base64.StdEncoding.EncodeToString(
48+
[]byte("TestUser:TestPassword"),
49+
),
50+
expectedError: nil,
51+
},
52+
{
53+
testName: "Basic Auth with password file",
54+
basicAuth: map[string]string{
55+
"username": "TestUser",
56+
"password_file": "passwordFile",
57+
},
58+
basicAuthPasswordFileContents: []byte("TestPassword"),
59+
expectedAuthHeaderValue: "Basic " + base64.StdEncoding.EncodeToString(
60+
[]byte("TestUser:TestPassword"),
61+
),
62+
expectedError: nil,
63+
},
64+
{
65+
testName: "Basic Auth with bad password file",
66+
basicAuth: map[string]string{
67+
"username": "TestUser",
68+
"password_file": "missingPasswordFile",
69+
},
70+
expectedAuthHeaderValue: "",
71+
expectedError: ErrFailedToReadFile,
72+
},
73+
{
74+
testName: "Bearer Token",
75+
bearerToken: "testToken",
76+
expectedAuthHeaderValue: "Bearer testToken",
77+
expectedError: nil,
78+
},
79+
{
80+
testName: "Bearer Token with bad bearer token file",
81+
bearerTokenFile: "missingBearerTokenFile",
82+
expectedAuthHeaderValue: "",
83+
expectedError: ErrFailedToReadFile,
84+
},
85+
{
86+
testName: "Bearer Token with bearer token file",
87+
bearerTokenFile: "bearerTokenFile",
88+
expectedAuthHeaderValue: "Bearer testToken",
89+
bearerTokenFileContents: []byte("testToken"),
90+
expectedError: nil,
91+
},
92+
}
93+
for _, test := range tests {
94+
t.Run(test.testName, func(t *testing.T) {
95+
// Set up a test server that runs a handler function when it receives a http
96+
// request. The server writes the request's Authorization header to the
97+
// response body.
98+
handler := func(rw http.ResponseWriter, req *http.Request) {
99+
authHeaderValue := req.Header.Get("Authorization")
100+
_, err := rw.Write([]byte(authHeaderValue))
101+
require.Nil(t, err)
102+
}
103+
server := httptest.NewServer(http.HandlerFunc(handler))
104+
defer server.Close()
105+
106+
// Create the necessary files for tests.
107+
if test.basicAuth != nil {
108+
passwordFile := test.basicAuth["password_file"]
109+
if passwordFile != "" && test.basicAuthPasswordFileContents != nil {
110+
filepath := "./" + test.basicAuth["password_file"]
111+
err := createFile(test.basicAuthPasswordFileContents, filepath)
112+
require.Nil(t, err)
113+
defer os.Remove(filepath)
114+
}
115+
}
116+
if test.bearerTokenFile != "" && test.bearerTokenFileContents != nil {
117+
filepath := "./" + test.bearerTokenFile
118+
err := createFile(test.bearerTokenFileContents, filepath)
119+
require.Nil(t, err)
120+
defer os.Remove(filepath)
121+
}
122+
123+
// Create a HTTP request and add headers to it through an Exporter. Since the
124+
// Exporter has an empty Headers map, authentication methods will be called.
125+
exporter := Exporter{
126+
Config{
127+
BasicAuth: test.basicAuth,
128+
BearerToken: test.bearerToken,
129+
BearerTokenFile: test.bearerTokenFile,
130+
},
131+
}
132+
req, err := http.NewRequest(http.MethodPost, server.URL, nil)
133+
require.Nil(t, err)
134+
err = exporter.addHeaders(req)
135+
136+
// Verify the error and if the Authorization header was correctly set.
137+
if err != nil {
138+
require.Equal(t, err.Error(), test.expectedError.Error())
139+
} else {
140+
require.Nil(t, test.expectedError)
141+
authHeaderValue := req.Header.Get("Authorization")
142+
require.Equal(t, authHeaderValue, test.expectedAuthHeaderValue)
143+
}
144+
})
145+
}
146+
}
147+
148+
// createFile writes a file with a slice of bytes at a specified filepath.
149+
func createFile(bytes []byte, filepath string) error {
150+
err := ioutil.WriteFile(filepath, bytes, 0644)
151+
if err != nil {
152+
return err
153+
}
154+
return nil
155+
}

exporters/metric/cortex/config.go

+25-7
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,23 @@ import (
2323
var (
2424
// ErrTwoPasswords occurs when the YAML file contains both `password` and
2525
// `password_file`.
26-
ErrTwoPasswords = fmt.Errorf("Cannot have two passwords in the YAML file")
26+
ErrTwoPasswords = fmt.Errorf("cannot have two passwords in the YAML file")
2727

2828
// ErrTwoBearerTokens occurs when the YAML file contains both `bearer_token` and
2929
// `bearer_token_file`.
30-
ErrTwoBearerTokens = fmt.Errorf("Cannot have two bearer tokens in the YAML file")
30+
ErrTwoBearerTokens = fmt.Errorf("cannot have two bearer tokens in the YAML file")
3131

3232
// ErrConflictingAuthorization occurs when the YAML file contains both BasicAuth and
3333
// bearer token authorization
34-
ErrConflictingAuthorization = fmt.Errorf("Cannot have both basic auth and bearer token authorization")
34+
ErrConflictingAuthorization = fmt.Errorf("cannot have both basic auth and bearer token authorization")
35+
36+
// ErrNoBasicAuthUsername occurs when no username was provided for basic
37+
// authentication.
38+
ErrNoBasicAuthUsername = fmt.Errorf("no username provided for basic authentication")
39+
40+
// ErrNoBasicAuthPassword occurs when no password or password file was provided for
41+
// basic authentication.
42+
ErrNoBasicAuthPassword = fmt.Errorf("no password or password file provided for basic authentication")
3543
)
3644

3745
// Config contains properties the Exporter uses to export metrics data to Cortex.
@@ -54,14 +62,24 @@ type Config struct {
5462
// Validate checks a Config struct for missing required properties and property conflicts.
5563
// Additionally, it adds default values to missing properties when there is a default.
5664
func (c *Config) Validate() error {
57-
// Check for mutually exclusive properties.
65+
// Check for valid basic authentication and bearer token configuration.
5866
if c.BasicAuth != nil {
59-
if c.BearerToken != "" || c.BearerTokenFile != "" {
60-
return ErrConflictingAuthorization
67+
if c.BasicAuth["username"] == "" {
68+
return ErrNoBasicAuthUsername
69+
}
70+
71+
password := c.BasicAuth["password"]
72+
passwordFile := c.BasicAuth["password_file"]
73+
74+
if password == "" && passwordFile == "" {
75+
return ErrNoBasicAuthPassword
6176
}
62-
if c.BasicAuth["password"] != "" && c.BasicAuth["password_file"] != "" {
77+
if password != "" && passwordFile != "" {
6378
return ErrTwoPasswords
6479
}
80+
if c.BearerToken != "" || c.BearerTokenFile != "" {
81+
return ErrConflictingAuthorization
82+
}
6583
}
6684
if c.BearerToken != "" && c.BearerTokenFile != "" {
6785
return ErrTwoBearerTokens

exporters/metric/cortex/config_data_test.go

+24-3
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,30 @@ var exampleTwoAuthConfig = cortex.Config{
104104
RemoteTimeout: 30 * time.Second,
105105
PushInterval: 10 * time.Second,
106106
BasicAuth: map[string]string{
107-
"username": "user",
108-
"password": "password",
109-
"password_file": "passwordFile",
107+
"username": "user",
108+
"password": "password",
110109
},
111110
BearerToken: "bearer_token",
112111
}
112+
113+
// Example Config struct with no password for basic authentication
114+
var exampleNoPasswordConfig = cortex.Config{
115+
Endpoint: "/api/prom/push",
116+
Name: "Config",
117+
RemoteTimeout: 30 * time.Second,
118+
PushInterval: 10 * time.Second,
119+
BasicAuth: map[string]string{
120+
"username": "user",
121+
},
122+
}
123+
124+
// Example Config struct with no password for basic authentication
125+
var exampleNoUsernameConfig = cortex.Config{
126+
Endpoint: "/api/prom/push",
127+
Name: "Config",
128+
RemoteTimeout: 30 * time.Second,
129+
PushInterval: 10 * time.Second,
130+
BasicAuth: map[string]string{
131+
"password": "password",
132+
},
133+
}

exporters/metric/cortex/config_test.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ func TestValidate(t *testing.T) {
4343
expectedConfig: nil,
4444
expectedError: cortex.ErrTwoPasswords,
4545
},
46+
{
47+
testName: "Config with no Password",
48+
config: &exampleNoPasswordConfig,
49+
expectedConfig: nil,
50+
expectedError: cortex.ErrNoBasicAuthPassword,
51+
},
52+
{
53+
testName: "Config with no Username",
54+
config: &exampleNoUsernameConfig,
55+
expectedConfig: nil,
56+
expectedError: cortex.ErrNoBasicAuthUsername,
57+
},
4658
{
4759
testName: "Config with Custom Timeout",
4860
config: &exampleRemoteTimeoutConfig,
@@ -83,7 +95,7 @@ func TestValidate(t *testing.T) {
8395
for _, test := range tests {
8496
t.Run(test.testName, func(t *testing.T) {
8597
err := test.config.Validate()
86-
require.Equal(t, err, test.expectedError)
98+
require.Equal(t, test.expectedError, err)
8799
if err == nil {
88100
require.Equal(t, test.config, test.expectedConfig)
89101
}

0 commit comments

Comments
 (0)