Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ component_management:
name: extension_httpforwarder
paths:
- extension/httpforwarderextension/**
- component_id: extension_internal_basicauth
name: extension_internal_basicauth
paths:
- extension/internal/basicauth/**
- component_id: extension_internal_credentialsfile
name: extension_internal_credentialsfile
paths:
Expand Down
168 changes: 12 additions & 156 deletions extension/basicauthextension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,29 @@ package basicauthextension // import "github.com/open-telemetry/opentelemetry-co

import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync/atomic"

"github.com/tg123/go-htpasswd"
"go.opentelemetry.io/collector/client"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/extension"
"go.opentelemetry.io/collector/extension/extensionauth"
"go.uber.org/zap"
creds "google.golang.org/grpc/credentials"

"github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/basicauth"
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/credentialsfile"
)

var (
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")
errNoAuth = basicauth.ErrNoAuth
errInvalidCredentials = basicauth.ErrInvalidCredentials
errInvalidSchemePrefix = basicauth.ErrInvalidSchemePrefix
errInvalidFormat = basicauth.ErrInvalidFormat
)

func newClientAuthExtension(cfg *Config) *basicAuthClient {
Expand Down Expand Up @@ -86,129 +84,7 @@ func (ba *basicAuthServer) Start(_ context.Context, _ component.Host) error {
}

func (ba *basicAuthServer) Authenticate(ctx context.Context, headers map[string][]string) (context.Context, error) {
auth := getAuthHeader(headers)
if auth == "" {
return ctx, errNoAuth
}

authData, err := parseBasicAuth(auth)
if err != nil {
return ctx, err
}

if !ba.matchFunc(authData.username, authData.password) {
return ctx, errInvalidCredentials
}

cl := client.FromContext(ctx)
cl.Auth = authData
return client.NewContext(ctx, cl), nil
}

func getAuthHeader(h map[string][]string) string {
const (
canonicalHeaderKey = "Authorization"
metadataKey = "authorization"
)

authHeaders, ok := h[canonicalHeaderKey]

if !ok {
authHeaders, ok = h[metadataKey]
}

if !ok {
for k, v := range h {
if strings.EqualFold(k, metadataKey) {
authHeaders = v
break
}
}
}

if len(authHeaders) == 0 {
return ""
}

return authHeaders[0]
}

// See: https://github.com/golang/go/blob/1a8b4e05b1ff7a52c6d40fad73bcad612168d094/src/net/http/request.go#L950
func parseBasicAuth(auth string) (*authData, error) {
const prefix = "Basic "
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return nil, errInvalidSchemePrefix
}

encoded := auth[len(prefix):]
decodedBytes, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, errInvalidFormat
}
decoded := string(decodedBytes)

before, after, ok := strings.Cut(decoded, ":")
if !ok {
return nil, errInvalidFormat
}

return &authData{
username: before,
password: after,
raw: encoded,
}, nil
}

var _ client.AuthData = (*authData)(nil)

type authData struct {
username string
password string
raw string
}

func (a *authData) GetAttribute(name string) any {
switch name {
case "username":
return a.username
case "raw":
return a.raw
default:
return nil
}
}

func (*authData) GetAttributeNames() []string {
return []string{"username", "raw"}
}

// perRPCAuth is a gRPC credentials.PerRPCCredentials implementation that returns an 'authorization' header.
type perRPCAuth struct {
client *basicAuthClient
}

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

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

type basicAuthRoundTripper struct {
base http.RoundTripper
client *basicAuthClient
}

func (b *basicAuthRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
newRequest := request.Clone(request.Context())
if newRequest.Header == nil {
newRequest.Header = make(http.Header)
}
newRequest.SetBasicAuth(b.client.username(), b.client.password())
return b.base.RoundTrip(newRequest)
return basicauth.Authenticate(ctx, headers, ba.matchFunc)
}

var (
Expand All @@ -222,25 +98,15 @@ type basicAuthClient struct {
logger *zap.Logger
usernameResolver credentialsfile.ValueResolver
passwordResolver credentialsfile.ValueResolver
grpcMetadata atomic.Pointer[map[string]string]
}

func (ba *basicAuthClient) updateGRPCMetadata() {
encoded := base64.StdEncoding.EncodeToString([]byte(ba.username() + ":" + ba.password()))
m := map[string]string{
"authorization": fmt.Sprintf("Basic %s", encoded),
}
ba.grpcMetadata.Store(&m)
}

func (ba *basicAuthClient) Start(ctx context.Context, _ component.Host) error {
if ba.clientAuth == nil {
return errNoCredentialSource
}
onChange := func(_ string) { ba.updateGRPCMetadata() }
ca := ba.clientAuth
if ca.Username != "" || ca.UsernameFile != "" {
r, err := credentialsfile.NewValueResolver(ca.Username, ca.UsernameFile, ba.logger, credentialsfile.WithOnChange(onChange))
r, err := credentialsfile.NewValueResolver(ca.Username, ca.UsernameFile, ba.logger)
if err != nil {
return err
}
Expand All @@ -250,7 +116,7 @@ func (ba *basicAuthClient) Start(ctx context.Context, _ component.Host) error {
ba.usernameResolver = r
}
if string(ca.Password) != "" || ca.PasswordFile != "" {
r, err := credentialsfile.NewValueResolver(string(ca.Password), ca.PasswordFile, ba.logger, credentialsfile.WithOnChange(onChange))
r, err := credentialsfile.NewValueResolver(string(ca.Password), ca.PasswordFile, ba.logger)
if err != nil {
return err
}
Expand All @@ -259,7 +125,6 @@ func (ba *basicAuthClient) Start(ctx context.Context, _ component.Host) error {
}
ba.passwordResolver = r
}
ba.updateGRPCMetadata()
return nil
}

Expand All @@ -274,7 +139,7 @@ func (ba *basicAuthClient) Shutdown(_ context.Context) error {
return errors.Join(errs...)
}

func (ba *basicAuthClient) username() string {
func (ba *basicAuthClient) Username() string {
if ba.usernameResolver != nil {
return ba.usernameResolver.Value()
}
Expand All @@ -284,7 +149,7 @@ func (ba *basicAuthClient) username() string {
return ""
}

func (ba *basicAuthClient) password() string {
func (ba *basicAuthClient) Password() string {
if ba.passwordResolver != nil {
return ba.passwordResolver.Value()
}
Expand All @@ -295,18 +160,9 @@ func (ba *basicAuthClient) password() string {
}

func (ba *basicAuthClient) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) {
if strings.Contains(ba.username(), ":") {
return nil, errInvalidFormat
}
return &basicAuthRoundTripper{
base: base,
client: ba,
}, nil
return basicauth.NewRoundTripper(base, ba)
}

func (ba *basicAuthClient) PerRPCCredentials() (creds.PerRPCCredentials, error) {
if strings.Contains(ba.username(), ":") {
return nil, errInvalidFormat
}
return &perRPCAuth{client: ba}, nil
return basicauth.NewPerRPCCredentials(ba)
}
7 changes: 4 additions & 3 deletions extension/basicauthextension/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,16 @@ func TestBasicAuth_ServerInvalid(t *testing.T) {

func TestPerRPCAuth(t *testing.T) {
t.Parallel()
client := &basicAuthClient{
ext := &basicAuthClient{
clientAuth: &ClientAuthSettings{
Username: "username",
Password: "passwordxxx",
},
}
require.NoError(t, client.Start(t.Context(), componenttest.NewNopHost()))
require.NoError(t, ext.Start(t.Context(), componenttest.NewNopHost()))

rpcAuth := &perRPCAuth{client: client}
rpcAuth, err := ext.PerRPCCredentials()
require.NoError(t, err)
md, err := rpcAuth.GetRequestMetadata(t.Context())
assert.NoError(t, err)
assert.Equal(t, map[string]string{
Expand Down
3 changes: 3 additions & 0 deletions extension/basicauthextension/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/extension/basic
go 1.25.0

require (
github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/basicauth v0.152.0
github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/credentialsfile v0.152.0
github.com/stretchr/testify v1.11.1
github.com/tg123/go-htpasswd v1.2.4
Expand Down Expand Up @@ -63,4 +64,6 @@ retract (
v0.65.0
)

replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/basicauth => ../internal/basicauth

replace github.com/open-telemetry/opentelemetry-collector-contrib/extension/internal/credentialsfile => ../internal/credentialsfile
4 changes: 2 additions & 2 deletions extension/basicauthextension/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions extension/internal/basicauth/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../../Makefile.Common
Loading
Loading