Skip to content

Commit

Permalink
[extension/basicauth] Implement configauth.ClientAuthenticator (#8847)
Browse files Browse the repository at this point in the history
* Add support for client basic auth

* Change Readme

* Add changelog entry

* separate out client and server authenticator

* Fix Readme documentation

Co-authored-by: Stepan Rakitin <[email protected]>

* Update Readme.md

* modify internal components test file

* Apply suggestions from code review

Co-authored-by: Juraci Paixão Kröhling <[email protected]>

* add rpc support

* review comments and minor fixes

* address comments and make func signatures private

* gofmt lint error fix

Co-authored-by: Stepan Rakitin <[email protected]>
Co-authored-by: Juraci Paixão Kröhling <[email protected]>
  • Loading branch information
3 people authored Apr 19, 2022
1 parent e627875 commit e841b5e
Show file tree
Hide file tree
Showing 12 changed files with 427 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
- `datadogexporter`: Metrics payload data and Sketches payload data will be logged if collector is started in debug mode (#8929)
- `cmd/mdatagen`: Add resource attributes definition to metadata.yaml and move `pdata.Metrics` creation to the
generated code (#8555)
- `basicauthextension`: Implement `configauth.ClientAuthenticator` so that the extension can also be used as HTTP client basic authenticator.(#8847)

### 🛑 Breaking changes 🛑

Expand Down
43 changes: 28 additions & 15 deletions extension/basicauthextension/README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,60 @@
# Basic Authenticator

This extension implements `configauth.ServerAuthenticator` to authenticate clients using HTTP Basic Authentication. The authenticator type has to be set to `basicauth`.
This extension implements both `configauth.ServerAuthenticator` and `configauth.ClientAuthenticator` to authenticate clients and servers using Basic Authentication. The authenticator type has to be set to `basicauth`.

If authentication is successful `client.Info.Auth` will expose the following attributes:
When used as ServerAuthenticator, if the authentication is successful `client.Info.Auth` will expose the following attributes:

- `username`: The username of the authenticated user.
- `raw`: Raw base64 encoded credentials.

The configuration should specify only one instance of `basicauth` extension for either client or server authentication.

The following are the configuration options:

- `htpasswd.file`: The path to the htpasswd file.
- `htpasswd.inline`: The htpasswd file inline content.
- `client_auth.username`: Username to use for client authentication.
- `client_auth.password`: Password to use for client authentication.

To configure the extension as a server authenticator, either one of `htpasswd.file` or `htpasswd.inline` has to be set. If both are configured, `htpasswd.inline` credentials take precedence.

To configure the extension as a client authenticator, `client_auth` has to be set.

If both the options are configured, the extension will throw an error.
## Configuration

```yaml
extensions:
basicauth:
basicauth/server:
htpasswd:
file: .htpasswd
inline: |
${BASIC_AUTH_USERNAME}:${BASIC_AUTH_PASSWORD}
basicauth/client:
client_auth:
username: username
password: password

receivers:
otlp:
protocols:
http:
auth:
authenticator: basicauth
authenticator: basicauth/server

processors:

exporters:
logging:
logLevel: debug
otlp:
auth:
authenticator: basicauth/client

service:
extensions: [basicauth]
extensions: [basicauth/server, basicauth/client]
pipelines:
traces:
receivers: [otlp]
processors: []
exporters: [logging]
exporters: [otlp]
```
### htpasswd
- `file`: The path to the htpasswd file.
- `inline`: The htpasswd file inline content.

If both `file` and `inline` are configured, `inline` credentials take precedence.
37 changes: 35 additions & 2 deletions extension/basicauthextension/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@

package basicauthextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension"

import "go.opentelemetry.io/collector/config"
import (
"errors"

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

var (
errNoCredentialSource = errors.New("no credential source provided")
errMultipleAuthenticators = errors.New("only one of `htpasswd` or `client_auth` can be specified")
)

type HtpasswdSettings struct {
// Path to the htpasswd file.
Expand All @@ -23,9 +32,33 @@ type HtpasswdSettings struct {
Inline string `mapstructure:"inline"`
}

type ClientAuthSettings struct {
// Username holds the username to use for client authentication.
Username string `mapstructure:"username"`
// Password holds the password to use for client authentication.
Password string `mapstructure:"password"`
}
type Config struct {
config.ExtensionSettings `mapstructure:",squash"`

// Htpasswd settings.
Htpasswd HtpasswdSettings `mapstructure:"htpasswd"`
Htpasswd *HtpasswdSettings `mapstructure:"htpasswd,omitempty"`

// ClientAuth settings
ClientAuth *ClientAuthSettings `mapstructure:"client_auth,omitempty"`
}

func (cfg *Config) Validate() error {
serverCondition := cfg.Htpasswd != nil
clientCondition := cfg.ClientAuth != nil

if serverCondition && clientCondition {
return errMultipleAuthenticators
}

if !serverCondition && !clientCondition {
return errNoCredentialSource
}

return nil
}
78 changes: 78 additions & 0 deletions extension/basicauthextension/config_test.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 basicauthextension

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/service/servicetest"
)

func TestLoadConfig(t *testing.T) {
factories, err := componenttest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Extensions[typeStr] = factory
cfg, err := servicetest.LoadConfigAndValidate(filepath.Join("testdata", "valid_config.yml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

ext0 := cfg.Extensions[config.NewComponentIDWithName(typeStr, "server")]
assert.Equal(t, &Config{
ExtensionSettings: config.NewExtensionSettings(config.NewComponentIDWithName(typeStr, "server")),
Htpasswd: &HtpasswdSettings{
Inline: "username1:password1\nusername2:password2\n",
},
}, ext0)

ext1 := cfg.Extensions[config.NewComponentIDWithName(typeStr, "client")]
assert.Equal(t,
&Config{
ExtensionSettings: config.NewExtensionSettings(config.NewComponentIDWithName(typeStr, "client")),
ClientAuth: &ClientAuthSettings{
Username: "username",
Password: "password",
},
},
ext1)

assert.Equal(t, 2, len(cfg.Service.Extensions))
assert.Equal(t, config.NewComponentIDWithName(typeStr, "client"), cfg.Service.Extensions[0])
assert.Equal(t, config.NewComponentIDWithName(typeStr, "server"), cfg.Service.Extensions[1])
}

func TestLoadConfigError(t *testing.T) {
factories, err := componenttest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Extensions[typeStr] = factory
t.Run("invalid config both present", func(t *testing.T) {
_, err = servicetest.LoadConfigAndValidate(filepath.Join("testdata", "invalid_config_both.yml"), factories)
assert.Error(t, err)
})
t.Run("invalid config none present", func(t *testing.T) {
_, err = servicetest.LoadConfigAndValidate(filepath.Join("testdata", "invalid_config_none.yml"), factories)
assert.Error(t, err)
})

}
83 changes: 76 additions & 7 deletions extension/basicauthextension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,60 @@ import (
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/tg123/go-htpasswd"
"go.opentelemetry.io/collector/client"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configauth"
creds "google.golang.org/grpc/credentials"
)

var (
errNoCredentialSource = errors.New("no credential source provided")
errNoAuth = errors.New("no basic auth provided")
errInvalidCredentials = errors.New("invalid credentials")
errInvalidSchemePrefix = errors.New("invalid authorization scheme prefix")
errInvalidFormat = errors.New("invalid authorization format")
)

type basicAuth struct {
htpasswd HtpasswdSettings
matchFunc func(username, password string) bool
htpasswd *HtpasswdSettings
clientAuth *ClientAuthSettings
matchFunc func(username, password string) bool
}

func newExtension(cfg *Config) (configauth.ServerAuthenticator, error) {
if cfg.Htpasswd.File == "" && cfg.Htpasswd.Inline == "" {
func newClientAuthExtension(cfg *Config) (configauth.ClientAuthenticator, error) {
if cfg.ClientAuth == nil || cfg.ClientAuth.Username == "" {
return nil, errNoCredentialSource
}

ba := basicAuth{
clientAuth: cfg.ClientAuth,
}
return configauth.NewClientAuthenticator(
configauth.WithClientRoundTripper(ba.roundTripper),
configauth.WithPerRPCCredentials(ba.perRPCCredentials),
), nil
}

func newServerAuthExtension(cfg *Config) (configauth.ServerAuthenticator, error) {

if cfg.Htpasswd == nil || (cfg.Htpasswd.File == "" && cfg.Htpasswd.Inline == "") {
return nil, errNoCredentialSource
}

ba := basicAuth{
htpasswd: cfg.Htpasswd,
}
return configauth.NewServerAuthenticator(configauth.WithStart(ba.start), configauth.WithAuthenticate(ba.authenticate)), nil
return configauth.NewServerAuthenticator(
configauth.WithStart(ba.serverStart),
configauth.WithAuthenticate(ba.authenticate),
), nil
}

func (ba *basicAuth) start(ctx context.Context, host component.Host) error {
func (ba *basicAuth) serverStart(ctx context.Context, host component.Host) error {
var rs []io.Reader

if ba.htpasswd.File != "" {
Expand Down Expand Up @@ -177,3 +198,51 @@ func (a *authData) GetAttribute(name string) interface{} {
func (*authData) GetAttributeNames() []string {
return []string{"username", "raw"}
}

// perRPCAuth is a gRPC credentials.PerRPCCredentials implementation that returns an 'authorization' header.
type perRPCAuth struct {
metadata map[string]string
}

// GetRequestMetadata returns the request metadata to be used with the RPC.
func (p *perRPCAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return p.metadata, nil
}

// RequireTransportSecurity always returns true for this implementation.
func (p *perRPCAuth) RequireTransportSecurity() bool {
return true
}

type basicAuthRoundTripper struct {
base http.RoundTripper
authData *ClientAuthSettings
}

func (b *basicAuthRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
newRequest := request.Clone(request.Context())
newRequest.SetBasicAuth(b.authData.Username, b.authData.Password)
return b.base.RoundTrip(newRequest)
}

func (ba *basicAuth) roundTripper(base http.RoundTripper) (http.RoundTripper, error) {
if strings.Contains(ba.clientAuth.Username, ":") {
return nil, errInvalidFormat
}
return &basicAuthRoundTripper{
base: base,
authData: ba.clientAuth,
}, nil
}

func (ba *basicAuth) perRPCCredentials() (creds.PerRPCCredentials, error) {
if strings.Contains(ba.clientAuth.Username, ":") {
return nil, errInvalidFormat
}
encoded := base64.StdEncoding.EncodeToString([]byte(ba.clientAuth.Username + ":" + ba.clientAuth.Password))
return &perRPCAuth{
metadata: map[string]string{
"authorization": fmt.Sprintf("Basic %s", encoded),
},
}, nil
}
Loading

0 comments on commit e841b5e

Please sign in to comment.