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

Port client side authentication interfaces from PR #3128 #3321

Merged
merged 10 commits into from
May 28, 2021
4 changes: 2 additions & 2 deletions config/configauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This module allows server types, such as gRPC and HTTP, to be configured to perf

The currently known authenticators:

- [oidc](../../extension/authoidcextension)
- [oidc](../../extension/oidcauthextension)

Examples:
```yaml
Expand Down Expand Up @@ -32,6 +32,6 @@ receivers:

## Creating an authenticator

New authenticators can be added by creating a new extension that also implements the `configauth.Authenticator` extension. Generic authenticators that may be used by a good number of users might be accepted as part of the core distribution, or as part of the contrib distribution. If you have interest in contributing one authenticator, open an issue with your proposal.
New authenticators can be added by creating a new extension that also implements the `configauth.ServerAuthenticator` extension. Generic authenticators that may be used by a good number of users might be accepted as part of the core distribution, or as part of the contrib distribution. If you have interest in contributing one authenticator, open an issue with your proposal.

For other cases, you'll need to include your custom authenticator as part of your custom OpenTelemetry Collector, perhaps being built using the [OpenTelemetry Collector Builder](https://github.com/open-telemetry/opentelemetry-collector-builder).
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
//
// 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 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 id, ext := range extensions {
if id == 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 id, ext := range extensions {
if id == 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)
}
26 changes: 8 additions & 18 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 Authenticator 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
}

for name, ext := range extensions {
if auth, ok := ext.(Authenticator); ok {
if name == reqID {
func GetServerAuthenticator(extensions map[config.ComponentID]component.Extension, componentID config.ComponentID) (ServerAuthenticator, error) {
for id, ext := range extensions {
if auth, ok := ext.(ServerAuthenticator); ok {
if id == 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
//
// 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 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
//
// 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 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
Loading