Skip to content

Commit

Permalink
Support for Custom Exporter Authenticators as Extensions (#3128)
Browse files Browse the repository at this point in the history
This PR adds support to add client side (exporter) authenticators for HTTP and gRPC clients through extension based authenticators. This is built of top of what was added  for receiver (server) side authenticators via extensions in #2603 

**Link to tracking Issue:** #3115

**Testing:** 
- Did a manual testing for static bearer token.
- Added unit tests
  • Loading branch information
pavankrish123 authored May 20, 2021
1 parent db093a6 commit af71182
Show file tree
Hide file tree
Showing 49 changed files with 1,315 additions and 338 deletions.
78 changes: 78 additions & 0 deletions config/configauth/clientauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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
//
// https://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 configauth

import (
"fmt"
"net/http"

"google.golang.org/grpc/credentials"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config"
)

// ClientAuthenticator is an Extension that can be used as an authenticator for the configauth.Authentication option.
// Authenticators are then included as part of OpenTelemetry Collector builds and can be referenced by their
// names from the Authentication configuration.
type ClientAuthenticator interface {
component.Extension
}

// HTTPClientAuthenticator is a ClientAuthenticator that can be used as an authenticator
// for the configauth.Authentication option for HTTP clients.
type HTTPClientAuthenticator interface {
ClientAuthenticator
RoundTripper(base http.RoundTripper) (http.RoundTripper, error)
}

// GRPCClientAuthenticator is a ClientAuthenticator that can be used as an authenticator for
// the configauth.Authentication option for gRPC clients.
type GRPCClientAuthenticator interface {
ClientAuthenticator
PerRPCCredentials() (credentials.PerRPCCredentials, error)
}

// GetHTTPClientAuthenticator attempts to select the appropriate HTTPClientAuthenticator from the list of extensions,
// based on the component id of the extension. If an authenticator is not found, an error is returned.
// This should be only used by HTTP clients.
func GetHTTPClientAuthenticator(extensions map[config.ComponentID]component.Extension,
componentID config.ComponentID) (HTTPClientAuthenticator, error) {
for name, ext := range extensions {
if name == componentID {
if auth, ok := ext.(HTTPClientAuthenticator); ok {
return auth, nil
}
return nil, fmt.Errorf("requested authenticator is not for HTTP clients")
}
}
return nil, fmt.Errorf("failed to resolve authenticator %q: %w", componentID.String(), errAuthenticatorNotFound)
}

// GetGRPCClientAuthenticator attempts to select the appropriate GRPCClientAuthenticator from the list of extensions,
// based on the component id of the extension. If an authenticator is not found, an error is returned.
// This should only be used by gRPC clients.
func GetGRPCClientAuthenticator(extensions map[config.ComponentID]component.Extension,
componentID config.ComponentID) (GRPCClientAuthenticator, error) {
for name, ext := range extensions {
if name == componentID {
if auth, ok := ext.(GRPCClientAuthenticator); ok {
return auth, nil
}
return nil, fmt.Errorf("requested authenticator is not for gRPC clients")
}
}
return nil, fmt.Errorf("failed to resolve authenticator %q: %w", componentID.String(), errAuthenticatorNotFound)
}
24 changes: 7 additions & 17 deletions config/configauth/configauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,25 @@ import (
)

var (
errAuthenticatorNotFound = errors.New("authenticator not found")
errAuthenticatorNotProvided = errors.New("authenticator not provided")
errAuthenticatorNotFound = errors.New("authenticator not found")
)

// Authentication defines the auth settings for the receiver
type Authentication struct {
// Authenticator specifies the name of the extension to use in order to authenticate the incoming data point.
// AuthenticatorName specifies the name of the extension to use in order to authenticate the incoming data point.
AuthenticatorName string `mapstructure:"authenticator"`
}

// GetAuthenticator attempts to select the appropriate from the list of extensions, based on the requested extension name.
// GetServerAuthenticator attempts to select the appropriate from the list of extensions, based on the requested extension name.
// If an authenticator is not found, an error is returned.
func GetAuthenticator(extensions map[config.ComponentID]component.Extension, requested string) (Authenticator, error) {
if requested == "" {
return nil, errAuthenticatorNotProvided
}

reqID, err := config.NewIDFromString(requested)
if err != nil {
return nil, err
}

func GetServerAuthenticator(extensions map[config.ComponentID]component.Extension, componentID config.ComponentID) (ServerAuthenticator, error) {
for name, ext := range extensions {
if auth, ok := ext.(Authenticator); ok {
if name == reqID {
if auth, ok := ext.(ServerAuthenticator); ok {
if name == componentID {
return auth, nil
}
}
}

return nil, fmt.Errorf("failed to resolve authenticator %q: %w", requested, errAuthenticatorNotFound)
return nil, fmt.Errorf("failed to resolve authenticator %q: %w", componentID.String(), errAuthenticatorNotFound)
}
17 changes: 8 additions & 9 deletions config/configauth/configauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ func TestGetAuthenticator(t *testing.T) {
}

// test
authenticator, err := GetAuthenticator(ext, cfg.AuthenticatorName)
componentID, err := config.NewIDFromString(cfg.AuthenticatorName)
assert.NoError(t, err)

authenticator, err := GetServerAuthenticator(ext, componentID)

// verify
assert.NoError(t, err)
Expand All @@ -48,13 +51,7 @@ func TestGetAuthenticatorFails(t *testing.T) {
expected error
}{
{
desc: "Authenticator not provided",
cfg: &Authentication{},
ext: map[config.ComponentID]component.Extension{},
expected: errAuthenticatorNotProvided,
},
{
desc: "Authenticator not found",
desc: "ServerAuthenticator not found",
cfg: &Authentication{
AuthenticatorName: "does-not-exist",
},
Expand All @@ -64,7 +61,9 @@ func TestGetAuthenticatorFails(t *testing.T) {
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
authenticator, err := GetAuthenticator(tC.ext, tC.cfg.AuthenticatorName)
componentID, err := config.NewIDFromString(tC.cfg.AuthenticatorName)
assert.NoError(t, err)
authenticator, err := GetServerAuthenticator(tC.ext, componentID)
assert.ErrorIs(t, err, tC.expected)
assert.Nil(t, authenticator)
})
Expand Down
66 changes: 66 additions & 0 deletions config/configauth/mock_clientauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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
//
// https://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 configauth

import (
"context"
"errors"
"net/http"

"google.golang.org/grpc/credentials"

"go.opentelemetry.io/collector/component"
)

var (
_ HTTPClientAuthenticator = (*MockClientAuthenticator)(nil)
_ GRPCClientAuthenticator = (*MockClientAuthenticator)(nil)
errMockError = errors.New("mock Error")
)

// MockClientAuthenticator provides a mock implementation of GRPCClientAuthenticator and HTTPClientAuthenticator interfaces
type MockClientAuthenticator struct {
ResultRoundTripper http.RoundTripper
ResultPerRPCCredentials credentials.PerRPCCredentials
MustError bool
}

// Start for the MockClientAuthenticator does nothing
func (m *MockClientAuthenticator) Start(ctx context.Context, host component.Host) error {
return nil
}

// Shutdown for the MockClientAuthenticator does nothing
func (m *MockClientAuthenticator) Shutdown(ctx context.Context) error {
return nil
}

// RoundTripper for the MockClientAuthenticator either returns error if the mock authenticator is forced to or
// returns the supplied resultRoundTripper.
func (m *MockClientAuthenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) {
if m.MustError {
return nil, errMockError
}
return m.ResultRoundTripper, nil
}

// PerRPCCredentials for the MockClientAuthenticator either returns error if the mock authenticator is forced to or
// returns the supplied resultPerRPCCredentials.
func (m *MockClientAuthenticator) PerRPCCredentials() (credentials.PerRPCCredentials, error) {
if m.MustError {
return nil, errMockError
}
return m.ResultPerRPCCredentials, nil
}
139 changes: 139 additions & 0 deletions config/configauth/mock_clientauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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
//
// https://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 configauth

import (
"context"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/grpc/credentials"
)

func TestNilStartAndShutdown(t *testing.T) {
// prepare
m := &MockClientAuthenticator{}

// test and verify
origCtx := context.Background()

err := m.Start(origCtx, nil)
assert.NoError(t, err)

err = m.Shutdown(origCtx)
assert.NoError(t, err)
}

type customRoundTripper struct{}

func (c *customRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return nil, nil
}

func TestMockRoundTripper(t *testing.T) {
testcases := []struct {
name string
expectedErr bool
clientAuth MockClientAuthenticator
}{
{
name: "no_error",
expectedErr: false,
clientAuth: MockClientAuthenticator{
ResultRoundTripper: &customRoundTripper{},
MustError: false,
},
},
{
name: "error",
expectedErr: true,
clientAuth: MockClientAuthenticator{
ResultRoundTripper: &customRoundTripper{},
MustError: true,
},
},
}

for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
tripper, err := testcase.clientAuth.RoundTripper(nil)
if testcase.expectedErr {
assert.Error(t, err)
return
}
assert.NotNil(t, tripper)
assert.NoError(t, err)
// check if the resultant tripper is indeed the one provided
_, ok := tripper.(*customRoundTripper)
assert.True(t, ok)
})
}
}

type customPerRPCCredentials struct{}

var _ credentials.PerRPCCredentials = (*customPerRPCCredentials)(nil)

func (c *customPerRPCCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return nil, nil
}

func (c *customPerRPCCredentials) RequireTransportSecurity() bool {
return true
}

func TestMockPerRPCCredential(t *testing.T) {
testcases := []struct {
name string
expectedErr bool
clientAuth MockClientAuthenticator
}{
{
name: "no_error",
expectedErr: false,
clientAuth: MockClientAuthenticator{
ResultPerRPCCredentials: &customPerRPCCredentials{},
MustError: false,
},
},
{
name: "error",
expectedErr: true,
clientAuth: MockClientAuthenticator{
ResultPerRPCCredentials: &customPerRPCCredentials{},
MustError: true,
},
},
}

for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
credential, err := testcase.clientAuth.PerRPCCredentials()
if err != nil {
return
}
if testcase.expectedErr {
assert.Error(t, err)
return
}
assert.NotNil(t, credential)
assert.NoError(t, err)
// check if the resultant tripper is indeed the one provided
_, ok := credential.(*customPerRPCCredentials)
assert.True(t, ok)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
)

var (
_ Authenticator = (*MockAuthenticator)(nil)
_ ServerAuthenticator = (*MockAuthenticator)(nil)
_ component.Extension = (*MockAuthenticator)(nil)
)

Expand Down
File renamed without changes.
Loading

0 comments on commit af71182

Please sign in to comment.