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

[cortex] Authentication Implementation and Timestamp Fix #246

Merged
merged 15 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from 13 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
86 changes: 86 additions & 0 deletions exporters/metric/cortex/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cortex

import (
"fmt"
"io/ioutil"
"net/http"
)

// ErrFailedToReadFile occurs when a password / bearer token file exists, but could
// not be read.
var ErrFailedToReadFile = fmt.Errorf("failed to read password / bearer token file")

// addBasicAuth sets the Authorization header for basic authentication using a username
// and a password / password file.
ercl marked this conversation as resolved.
Show resolved Hide resolved
func (e *Exporter) addBasicAuth(req *http.Request) error {
ercl marked this conversation as resolved.
Show resolved Hide resolved
// No need to add basic auth if it isn't provided or if the Authorization header is
// already set.
if _, exists := e.config.Headers["Authorization"]; exists {
return nil
}
if e.config.BasicAuth == nil {
return nil
}

username := e.config.BasicAuth["username"]

// Use password from password file if it exists.
passwordFile := e.config.BasicAuth["password_file"]
if passwordFile != "" {
file, err := ioutil.ReadFile(passwordFile)
if err != nil {
return ErrFailedToReadFile
}
password := string(file)
req.SetBasicAuth(username, password)
return nil
}

// Use provided password.
password := e.config.BasicAuth["password"]
req.SetBasicAuth(username, password)

return nil
}

// addBearerTokenAuth sets the Authorization header for bearer tokens using a bearer token
// string or a bearer token file.
func (e *Exporter) addBearerTokenAuth(req *http.Request) error {
// No need to add bearer token auth if the Authorization header is already set.
if _, exists := e.config.Headers["Authorization"]; exists {
return nil
}

// Use bearer token from bearer token file if it exists.
if e.config.BearerTokenFile != "" {
file, err := ioutil.ReadFile(e.config.BearerTokenFile)
if err != nil {
return ErrFailedToReadFile
}
bearerTokenString := "Bearer " + string(file)
req.Header.Set("Authorization", bearerTokenString)
return nil
}

// Otherwise, use bearer token field.
if e.config.BearerToken != "" {
bearerTokenString := "Bearer " + e.config.BearerToken
req.Header.Set("Authorization", bearerTokenString)
}

return nil
}
155 changes: 155 additions & 0 deletions exporters/metric/cortex/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cortex

import (
"encoding/base64"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/stretchr/testify/require"
)

// TestAuthentication checks whether http requests are properly authenticated with either
// bearer tokens or basic authentication in the addHeaders method.
func TestAuthentication(t *testing.T) {
tests := []struct {
testName string
basicAuth map[string]string
basicAuthPasswordFileContents []byte
bearerToken string
bearerTokenFile string
bearerTokenFileContents []byte
expectedAuthHeaderValue string
expectedError error
}{
{
testName: "Basic Auth with password",
basicAuth: map[string]string{
"username": "TestUser",
"password": "TestPassword",
},
expectedAuthHeaderValue: "Basic " + base64.StdEncoding.EncodeToString(
[]byte("TestUser:TestPassword"),
),
expectedError: nil,
},
{
testName: "Basic Auth with password file",
basicAuth: map[string]string{
"username": "TestUser",
"password_file": "passwordFile",
},
basicAuthPasswordFileContents: []byte("TestPassword"),
expectedAuthHeaderValue: "Basic " + base64.StdEncoding.EncodeToString(
[]byte("TestUser:TestPassword"),
),
expectedError: nil,
},
{
testName: "Basic Auth with bad password file",
basicAuth: map[string]string{
"username": "TestUser",
"password_file": "missingPasswordFile",
},
expectedAuthHeaderValue: "",
expectedError: ErrFailedToReadFile,
},
{
testName: "Bearer Token",
bearerToken: "testToken",
expectedAuthHeaderValue: "Bearer testToken",
expectedError: nil,
},
{
testName: "Bearer Token with bad bearer token file",
bearerTokenFile: "missingBearerTokenFile",
expectedAuthHeaderValue: "",
expectedError: ErrFailedToReadFile,
},
{
testName: "Bearer Token with bearer token file",
bearerTokenFile: "bearerTokenFile",
expectedAuthHeaderValue: "Bearer testToken",
bearerTokenFileContents: []byte("testToken"),
expectedError: nil,
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
// Set up a test server that runs a handler function when it receives a http
// request. The server writes the request's Authorization header to the
// response body.
handler := func(rw http.ResponseWriter, req *http.Request) {
authHeaderValue := req.Header.Get("Authorization")
_, err := rw.Write([]byte(authHeaderValue))
require.Nil(t, err)
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()

// Create the necessary files for tests.
if test.basicAuth != nil {
passwordFile := test.basicAuth["password_file"]
if passwordFile != "" && test.basicAuthPasswordFileContents != nil {
filepath := "./" + test.basicAuth["password_file"]
err := createFile(test.basicAuthPasswordFileContents, filepath)
require.Nil(t, err)
defer os.Remove(filepath)
}
}
if test.bearerTokenFile != "" && test.bearerTokenFileContents != nil {
filepath := "./" + test.bearerTokenFile
err := createFile(test.bearerTokenFileContents, filepath)
require.Nil(t, err)
defer os.Remove(filepath)
}

// Create a HTTP request and add headers to it through an Exporter. Since the
// Exporter has an empty Headers map, authentication methods will be called.
exporter := Exporter{
Config{
BasicAuth: test.basicAuth,
BearerToken: test.bearerToken,
BearerTokenFile: test.bearerTokenFile,
},
}
req, err := http.NewRequest(http.MethodPost, server.URL, nil)
require.Nil(t, err)
err = exporter.addHeaders(req)

// Verify the error and if the Authorization header was correctly set.
if err != nil {
require.Equal(t, err.Error(), test.expectedError.Error())
} else {
require.Nil(t, test.expectedError)
authHeaderValue := req.Header.Get("Authorization")
require.Equal(t, authHeaderValue, test.expectedAuthHeaderValue)
}
})
}
}

// createFile writes a file with a slice of bytes at a specified filepath.
func createFile(bytes []byte, filepath string) error {
err := ioutil.WriteFile(filepath, bytes, 0644)
if err != nil {
return err
}
return nil
}
32 changes: 25 additions & 7 deletions exporters/metric/cortex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,23 @@ import (
var (
// ErrTwoPasswords occurs when the YAML file contains both `password` and
// `password_file`.
ErrTwoPasswords = fmt.Errorf("Cannot have two passwords in the YAML file")
ErrTwoPasswords = fmt.Errorf("cannot have two passwords in the YAML file")

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

// ErrConflictingAuthorization occurs when the YAML file contains both BasicAuth and
// bearer token authorization
ErrConflictingAuthorization = fmt.Errorf("Cannot have both basic auth and bearer token authorization")
ErrConflictingAuthorization = fmt.Errorf("cannot have both basic auth and bearer token authorization")

// ErrNoBasicAuthUsername occurs when no username was provided for basic
// authentication.
ErrNoBasicAuthUsername = fmt.Errorf("no username provided for basic authentication")

// ErrNoBasicAuthPassword occurs when no password or password file was provided for
// basic authentication.
ErrNoBasicAuthPassword = fmt.Errorf("no password or password file provided for basic authentication")
)

// Config contains properties the Exporter uses to export metrics data to Cortex.
Expand All @@ -54,14 +62,24 @@ type Config struct {
// Validate checks a Config struct for missing required properties and property conflicts.
// Additionally, it adds default values to missing properties when there is a default.
func (c *Config) Validate() error {
// Check for mutually exclusive properties.
// Check for valid basic authentication and bearer token configuration.
if c.BasicAuth != nil {
if c.BearerToken != "" || c.BearerTokenFile != "" {
return ErrConflictingAuthorization
if c.BasicAuth["username"] == "" {
return ErrNoBasicAuthUsername
}

password := c.BasicAuth["password"]
passwordFile := c.BasicAuth["password_file"]

if password == "" && passwordFile == "" {
return ErrNoBasicAuthPassword
}
if c.BasicAuth["password"] != "" && c.BasicAuth["password_file"] != "" {
if password != "" && passwordFile != "" {
return ErrTwoPasswords
}
if c.BearerToken != "" || c.BearerTokenFile != "" {
return ErrConflictingAuthorization
}
}
if c.BearerToken != "" && c.BearerTokenFile != "" {
return ErrTwoBearerTokens
Expand Down
27 changes: 24 additions & 3 deletions exporters/metric/cortex/config_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,30 @@ var exampleTwoAuthConfig = cortex.Config{
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
BasicAuth: map[string]string{
"username": "user",
"password": "password",
"password_file": "passwordFile",
"username": "user",
"password": "password",
},
BearerToken: "bearer_token",
}

// Example Config struct with no password for basic authentication
var exampleNoPasswordConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
BasicAuth: map[string]string{
"username": "user",
},
}

// Example Config struct with no password for basic authentication
var exampleNoUsernameConfig = cortex.Config{
Endpoint: "/api/prom/push",
Name: "Config",
RemoteTimeout: 30 * time.Second,
PushInterval: 10 * time.Second,
BasicAuth: map[string]string{
"password": "password",
},
}
14 changes: 13 additions & 1 deletion exporters/metric/cortex/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ func TestValidate(t *testing.T) {
expectedConfig: nil,
expectedError: cortex.ErrTwoPasswords,
},
{
testName: "Config with no Password",
config: &exampleNoPasswordConfig,
expectedConfig: nil,
expectedError: cortex.ErrNoBasicAuthPassword,
},
{
testName: "Config with no Username",
config: &exampleNoUsernameConfig,
expectedConfig: nil,
expectedError: cortex.ErrNoBasicAuthUsername,
},
{
testName: "Config with Custom Timeout",
config: &exampleRemoteTimeoutConfig,
Expand Down Expand Up @@ -83,7 +95,7 @@ func TestValidate(t *testing.T) {
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
err := test.config.Validate()
require.Equal(t, err, test.expectedError)
require.Equal(t, test.expectedError, err)
if err == nil {
require.Equal(t, test.config, test.expectedConfig)
}
Expand Down
Loading