diff --git a/maestro/config/authentication_builder.go b/maestro/config/authentication_builder.go index 6555193a1..bbef54884 100644 --- a/maestro/config/authentication_builder.go +++ b/maestro/config/authentication_builder.go @@ -4,6 +4,7 @@ import ( "github.com/orb-community/orb/maestro/password" "github.com/orb-community/orb/pkg/types" "github.com/orb-community/orb/sinks/authentication_type/basicauth" + "github.com/orb-community/orb/sinks/authentication_type/bearertokenauth" ) const AuthenticationKey = "authentication" @@ -20,7 +21,12 @@ func GetAuthService(authType string, service password.EncryptionService) AuthBui return &BasicAuthBuilder{ encryptionService: service, } + case bearertokenauth.AuthType: + return &BearerTokenAuthBuilder{ + encryptionService: service, + } } + return nil } @@ -65,3 +71,50 @@ func (b *BasicAuthBuilder) EncodeAuth(config types.Metadata) (types.Metadata, er config[AuthenticationKey] = authcfg return config, nil } + +type BearerTokenAuthBuilder struct { + encryptionService password.EncryptionService +} + +func (b *BearerTokenAuthBuilder) GetExtensionsFromMetadata(c types.Metadata) (Extensions, string) { + authcfg := c.GetSubMetadata(AuthenticationKey) + scheme := authcfg["scheme"].(string) + token := authcfg["token"].(string) + + return Extensions{ + BearerAuth: &BearerTokenAuthExtension{ + Scheme: scheme, + Token: token, + }, + }, "bearertokenauth/withscheme" +} + +func (b *BearerTokenAuthBuilder) DecodeAuth(config types.Metadata) (types.Metadata, error) { + authCfg := config.GetSubMetadata(AuthenticationKey) + token := authCfg["token"].(string) + + decodedToken, err := b.encryptionService.DecodePassword(token) + if err != nil { + return nil, err + } + + authCfg["token"] = decodedToken + config[AuthenticationKey] = authCfg + + return config, nil +} + +func (b *BearerTokenAuthBuilder) EncodeAuth(config types.Metadata) (types.Metadata, error) { + authcfg := config.GetSubMetadata(AuthenticationKey) + token := authcfg["token"].(string) + + encodedToken, err := b.encryptionService.EncodePassword(token) + if err != nil { + return nil, err + } + + authcfg["token"] = encodedToken + config[AuthenticationKey] = authcfg + + return config, nil +} diff --git a/maestro/config/config_builder_test.go b/maestro/config/config_builder_test.go index 8981220a6..0c33732a0 100644 --- a/maestro/config/config_builder_test.go +++ b/maestro/config/config_builder_test.go @@ -3,10 +3,12 @@ package config import ( "context" "fmt" + "testing" + + "go.uber.org/zap" + "github.com/orb-community/orb/maestro/password" "github.com/orb-community/orb/pkg/types" - "go.uber.org/zap" - "testing" ) func TestReturnConfigYamlFromSink(t *testing.T) { @@ -97,6 +99,30 @@ func TestReturnConfigYamlFromSink(t *testing.T) { want: `---\nreceivers:\n kafka:\n brokers:\n - kafka:9092\n topic: otlp_metrics-sink-id-22\n protocol_version: 2.0.0\nextensions:\n pprof:\n endpoint: 0.0.0.0:1888\n basicauth/exporter:\n client_auth:\n username: otlp-user\n password: dbpass\nexporters:\n otlphttp:\n endpoint: https://acme.com/otlphttp/push\n auth:\n authenticator: basicauth/exporter\nservice:\n extensions:\n - pprof\n - basicauth/exporter\n pipelines:\n metrics:\n receivers:\n - kafka\n exporters:\n - otlphttp\n`, wantErr: false, }, + { + name: "otlp, token auth", + args: args{ + in0: context.Background(), + kafkaUrlConfig: "kafka:9092", + sink: &DeploymentRequest{ + SinkID: "sink-id-22", + OwnerID: "22", + Backend: "otlphttp", + Config: types.Metadata{ + "exporter": types.Metadata{ + "endpoint": "https://acme.com/otlphttp/push", + }, + "authentication": types.Metadata{ + "type": "bearertokenauth", + "scheme": "Api-Token", + "token": "abcdefg", + }, + }, + }, + }, + want: `---\nreceivers:\n kafka:\n brokers:\n - kafka:9092\n topic: otlp_metrics-sink-id-22\n protocol_version: 2.0.0\nextensions:\n pprof:\n endpoint: 0.0.0.0:1888\n bearertokenauth/withscheme:\n scheme: Api-Token\n token: abcdefg\nexporters:\n otlphttp:\n endpoint: https://acme.com/otlphttp/push\n auth:\n authenticator: bearertokenauth/withscheme\nservice:\n extensions:\n - pprof\n - bearertokenauth/withscheme\n pipelines:\n metrics:\n receivers:\n - kafka\n exporters:\n - otlphttp\n`, + wantErr: false, + }, } for _, tt := range tests { logger := zap.NewNop() diff --git a/maestro/config/types.go b/maestro/config/types.go index 637abb2c2..118098bf6 100644 --- a/maestro/config/types.go +++ b/maestro/config/types.go @@ -2,8 +2,9 @@ package config import ( "database/sql/driver" - "github.com/orb-community/orb/pkg/types" "time" + + "github.com/orb-community/orb/pkg/types" ) type SinkData struct { @@ -82,8 +83,8 @@ type Extensions struct { PProf *PProfExtension `json:"pprof,omitempty" yaml:"pprof,omitempty" :"p_prof"` ZPages *ZPagesExtension `json:"zpages,omitempty" yaml:"zpages,omitempty" :"z_pages"` // Exporters Authentication - BasicAuth *BasicAuthenticationExtension `json:"basicauth/exporter,omitempty" yaml:"basicauth/exporter,omitempty" :"basic_auth"` - //BearerAuth *BearerAuthExtension `json:"bearerauth/exporter,omitempty" yaml:"bearerauth/exporter,omitempty" :"bearer_auth"` + BasicAuth *BasicAuthenticationExtension `json:"basicauth/exporter,omitempty" yaml:"basicauth/exporter,omitempty" :"basic_auth"` + BearerAuth *BearerTokenAuthExtension `json:"bearertokenauth/withscheme,omitempty" yaml:"bearertokenauth/withscheme,omitempty"` } type HealthCheckExtension struct { @@ -115,10 +116,9 @@ type BasicAuthenticationExtension struct { ClientAuth *ClientAuth `json:"client_auth" yaml:"client_auth"` } -type BearerAuthExtension struct { - BearerAuth *struct { - Token string `json:"token" yaml:"token"` - } `json:"client_auth" yaml:"client_auth"` +type BearerTokenAuthExtension struct { + Scheme string `json:"scheme" yaml:"scheme"` + Token string `json:"token" yaml:"token"` } type Exporters struct { diff --git a/pkg/errors/types.go b/pkg/errors/types.go index 744570246..d9c214d27 100644 --- a/pkg/errors/types.go +++ b/pkg/errors/types.go @@ -36,29 +36,44 @@ var ( // ErrExporterFieldNotFound indicates that exporter field was not found ErrExporterFieldNotFound = New("malformed entity specification. exporter field is expected on configuration field") + // ErrEndpointNotFound indicates that endpoint field was not found on exporter field for otlp backend + ErrEndpointNotFound = New("malformed entity specification. endpoint field is expected on exporter field") + + // ErrInvalidEndpoint indicates that endpoint field is not valid + ErrInvalidEndpoint = New("malformed entity specification. endpoint field is invalid") + // ErrAuthFieldNotFound indicates that authentication field was not found on configuration field ErrAuthFieldNotFound = New("malformed entity specification. authentication fields are expected on configuration field") // ErrAuthTypeNotFound indicates that authentication type field was not found on the authentication field ErrAuthTypeNotFound = New("malformed entity specification: authentication type field is expected on configuration field") - // ErrInvalidAuthType indicates invalid authentication type - ErrInvalidAuthType = New("malformed entity specification. type key on authentication field is invalid") + // ErrAuthInvalidType indicates invalid authentication type + ErrAuthInvalidType = New("malformed entity specification. type key on authentication field is invalid") - // ErrPasswordNotFound indicates that password key was not found - ErrPasswordNotFound = New("malformed entity specification. password key is expected on authentication field") + // ErrAuthUsernameNotFound indicates that username key was not found + ErrAuthUsernameNotFound = New("malformed entity specification. username key is expected on authentication field") - // ErrEndPointNotFound indicates that endpoint field was not found on exporter field for otlp backend - ErrEndpointNotFound = New("malformed entity specification. endpoint field is expected on exporter field") + // ErrAuthPasswordNotFound indicates that password key was not found + ErrAuthPasswordNotFound = New("malformed entity specification. password key is expected on authentication field") - // ErrInvalidEndpoint indicates that endpoint field is not valid - ErrInvalidEndpoint = New("malformed entity specification. endpoint field is invalid") + // ErrAuthSchemeNotFound indicates that scheme key was not found + ErrAuthSchemeNotFound = New("malformed entity specification. scheme key is expected on authentication field") + + // ErrAuthTokenNotFound indicates that token key was not found + ErrAuthTokenNotFound = New("malformed entity specification. token key is expected on authentication field") + + // ErrAuthInvalidPasswordType indicates invalid password key on authentication field + ErrAuthInvalidPasswordType = New("malformed entity specification. password key on authentication field is invalid") + + // ErrAuthInvalidSchemeType indicates invalid scheme key on authentication field + ErrAuthInvalidSchemeType = New("malformed entity specification. scheme key on authentication field is invalid") - // ErrInvalidPasswordType indicates invalid password key on authentication field - ErrInvalidPasswordType = New("malformed entity specification. password key on authentication field is invalid") + // ErrAuthInvalidTokenType indicates invalid token key on authentication field + ErrAuthInvalidTokenType = New("malformed entity specification. token key on authentication field is invalid") - // ErrInvalidUsernameType indicates invalid username key on authentication field - ErrInvalidUsernameType = New("malformed entity specification. username key on authentication field is invalid") + // ErrAuthInvalidUsernameType indicates invalid username key on authentication field + ErrAuthInvalidUsernameType = New("malformed entity specification. username key on authentication field is invalid") // ErrRemoteHostNotFound indicates that remote host field was not found ErrRemoteHostNotFound = New("malformed entity specification. remote host is expected on exporter field") diff --git a/sinks/api/http/endpoint_test.go b/sinks/api/http/endpoint_test.go index 4903031c1..d45c606f3 100644 --- a/sinks/api/http/endpoint_test.go +++ b/sinks/api/http/endpoint_test.go @@ -857,7 +857,7 @@ func TestAuthenticationTypesEndpoints(t *testing.T) { err = json.Unmarshal(body, &authResponse) require.NoError(t, err, "must not error") require.NotNil(t, authResponse, "response must not be nil") - require.Equal(t, 1, len(authResponse.AuthenticationTypes), "must contain basicauth for now") + require.Equal(t, 2, len(authResponse.AuthenticationTypes), "must contain basicauth and bearertokenauth") }, }, "view authentication type basicauth": { diff --git a/sinks/api/http/requests.go b/sinks/api/http/requests.go index 9d7922d5e..02994fa41 100644 --- a/sinks/api/http/requests.go +++ b/sinks/api/http/requests.go @@ -76,7 +76,7 @@ func GetConfigurationAndMetadataFromMeta(backendName string, config types.Metada } authTypeSvc, ok := authentication_type.GetAuthType(authtype.(string)) if !ok { - err = errors.Wrap(errors.ErrInvalidAuthType, errors.New("invalid required field authentication type")) + err = errors.Wrap(errors.ErrAuthInvalidType, errors.New("invalid required field authentication type")) return } configSvc.Authentication = authTypeSvc @@ -119,12 +119,12 @@ func GetConfigurationAndMetadataFromYaml(backendName string, config string) (con case string: break default: - err = errors.ErrInvalidAuthType + err = errors.ErrAuthInvalidType return } authTypeSvc, ok := authentication_type.GetAuthType(authtype.(string)) if !ok { - err = errors.Wrap(errors.ErrInvalidAuthType, errors.New("invalid required field authentication type")) + err = errors.Wrap(errors.ErrAuthInvalidType, errors.New("invalid required field authentication type")) return } configSvc.Authentication = authTypeSvc diff --git a/sinks/api/http/transport.go b/sinks/api/http/transport.go index c919c0980..a03370740 100644 --- a/sinks/api/http/transport.go +++ b/sinks/api/http/transport.go @@ -242,15 +242,25 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) { w.WriteHeader(http.StatusBadRequest) case errors.Contains(errorVal, errors.ErrBackendNotFound): w.WriteHeader(http.StatusBadRequest) - case errors.Contains(errorVal, errors.ErrPasswordNotFound): + case errors.Contains(errorVal, errors.ErrAuthUsernameNotFound): + w.WriteHeader(http.StatusBadRequest) + case errors.Contains(errorVal, errors.ErrAuthPasswordNotFound): w.WriteHeader(http.StatusBadRequest) case errors.Contains(errorVal, errors.ErrAuthTypeNotFound): w.WriteHeader(http.StatusBadRequest) - case errors.Contains(errorVal, errors.ErrInvalidUsernameType): + case errors.Contains(errorVal, errors.ErrAuthInvalidUsernameType): + w.WriteHeader(http.StatusBadRequest) + case errors.Contains(errorVal, errors.ErrAuthInvalidPasswordType): + w.WriteHeader(http.StatusBadRequest) + case errors.Contains(errorVal, errors.ErrAuthInvalidTokenType): + w.WriteHeader(http.StatusBadRequest) + case errors.Contains(errorVal, errors.ErrAuthTokenNotFound): + w.WriteHeader(http.StatusBadRequest) + case errors.Contains(errorVal, errors.ErrAuthInvalidSchemeType): w.WriteHeader(http.StatusBadRequest) - case errors.Contains(errorVal, errors.ErrInvalidPasswordType): + case errors.Contains(errorVal, errors.ErrAuthSchemeNotFound): w.WriteHeader(http.StatusBadRequest) - case errors.Contains(errorVal, errors.ErrInvalidAuthType): + case errors.Contains(errorVal, errors.ErrAuthInvalidType): w.WriteHeader(http.StatusBadRequest) case errors.Contains(errorVal, errors.ErrRemoteHostNotFound): w.WriteHeader(http.StatusBadRequest) diff --git a/sinks/authentication_type/basicauth/authentication.go b/sinks/authentication_type/basicauth/authentication.go index ba25beabf..cf0e1e7d2 100644 --- a/sinks/authentication_type/basicauth/authentication.go +++ b/sinks/authentication_type/basicauth/authentication.go @@ -1,14 +1,18 @@ package basicauth import ( + "strings" + + "gopkg.in/yaml.v3" + "github.com/orb-community/orb/pkg/errors" "github.com/orb-community/orb/pkg/types" "github.com/orb-community/orb/sinks/authentication_type" "github.com/orb-community/orb/sinks/backend" - "gopkg.in/yaml.v3" ) const ( + AuthType = "basicauth" UsernameConfigFeature = "username" PasswordConfigFeature = "password" ) @@ -32,11 +36,9 @@ var ( } ) -const AuthType = "basicauth" - type AuthConfig struct { - Username string `json:"username" ,yaml:"username"` - Password string `json:"password" ,yaml:"password"` + Username *string `json:"username" yaml:"username"` + Password *string `json:"password" yaml:"password"` encryptionService authentication_type.PasswordService } @@ -56,27 +58,32 @@ func (a *AuthConfig) GetFeatureConfig() []authentication_type.ConfigFeature { func (a *AuthConfig) ValidateConfiguration(inputFormat string, input interface{}) error { switch inputFormat { case "object": + if _, ok := input.(types.Metadata)[UsernameConfigFeature]; !ok { + return errors.Wrap(errors.ErrAuthUsernameNotFound, errors.New("username field was not found")) + } + + if _, ok := input.(types.Metadata)[PasswordConfigFeature]; !ok { + return errors.Wrap(errors.ErrAuthPasswordNotFound, errors.New("password field was not found")) + } + for key, value := range input.(types.Metadata) { - if _, ok := value.(string); !ok { - if key == "password" { - return errors.Wrap(errors.ErrInvalidPasswordType, errors.New("invalid auth type for field: "+key)) - } - if key == "type" { - return errors.Wrap(errors.ErrInvalidAuthType, errors.New("invalid auth type for field: "+key)) - } - if key == "username" { - return errors.Wrap(errors.ErrInvalidUsernameType, errors.New("invalid auth type for field: "+key)) - } - } - vs := value.(string) if key == UsernameConfigFeature { - if len(vs) == 0 { - return errors.New("username cannot be empty") + if _, ok := value.(string); !ok { + return errors.Wrap(errors.ErrAuthInvalidUsernameType, errors.New("invalid auth type for field: "+key)) + } + + if len(strings.Fields(value.(string))) == 0 { + return errors.Wrap(errors.ErrAuthInvalidUsernameType, errors.New("invalid authentication username")) } } + if key == PasswordConfigFeature { - if len(vs) == 0 { - return errors.New("password cannot be empty") + if _, ok := value.(string); !ok { + return errors.Wrap(errors.ErrAuthInvalidPasswordType, errors.New("invalid auth type for field: "+key)) + } + + if len(strings.Fields(value.(string))) == 0 { + return errors.Wrap(errors.ErrAuthInvalidPasswordType, errors.New("invalid authentication password")) } } } @@ -85,12 +92,24 @@ func (a *AuthConfig) ValidateConfiguration(inputFormat string, input interface{} if err != nil { return err } - if len(a.Username) == 0 { - return errors.New("username cannot be empty") - } else if len(a.Password) == 0 { - return errors.New("password cannot be empty") + + if a.Username == nil { + return errors.Wrap(errors.ErrAuthUsernameNotFound, errors.New("username field was not found")) + } + + if len(strings.Fields(*a.Username)) == 0 { + return errors.Wrap(errors.ErrAuthInvalidUsernameType, errors.New("invalid authentication username")) + } + + if a.Password == nil { + return errors.Wrap(errors.ErrAuthPasswordNotFound, errors.New("password field was not found")) + } + + if len(strings.Fields(*a.Password)) == 0 { + return errors.Wrap(errors.ErrAuthInvalidPasswordType, errors.New("invalid authentication password")) } } + return nil } @@ -156,7 +175,7 @@ func (a *AuthConfig) EncodeInformation(outputFormat string, input interface{}) ( inputMeta := input.(types.Metadata) authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) if _, ok := authMeta[PasswordConfigFeature].(string); !ok { - return nil, errors.Wrap(errors.ErrPasswordNotFound, errors.New("password field was not found")) + return nil, errors.Wrap(errors.ErrAuthPasswordNotFound, errors.New("password field was not found")) } encoded, err := a.encryptionService.EncodePassword(authMeta[PasswordConfigFeature].(string)) if err != nil { diff --git a/sinks/authentication_type/basicauth/authentication_test.go b/sinks/authentication_type/basicauth/authentication_test.go new file mode 100644 index 000000000..c0c950154 --- /dev/null +++ b/sinks/authentication_type/basicauth/authentication_test.go @@ -0,0 +1,132 @@ +package basicauth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/orb-community/orb/pkg/errors" + "github.com/orb-community/orb/pkg/types" +) + +func TestAuthConfig_ValidateConfiguration(t *testing.T) { + type args struct { + inputFormat string + input types.Metadata + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "missing_username", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "password": "test-password", + }, + }, + wantErr: errors.ErrAuthUsernameNotFound, + }, + { + name: "empty_username", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": "", + "password": "test-password", + }, + }, + wantErr: errors.ErrAuthInvalidUsernameType, + }, + { + name: "invalid_username_type", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": 1234, + "password": "test-password", + }, + }, + wantErr: errors.ErrAuthInvalidUsernameType, + }, + { + name: "invalid_username", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": " ", + "password": "test-password", + }, + }, + wantErr: errors.ErrAuthInvalidUsernameType, + }, + { + name: "missing_password", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": "test-user", + }, + }, + wantErr: errors.ErrAuthPasswordNotFound, + }, + { + name: "empty_password", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": "test-user", + "password": "", + }, + }, + wantErr: errors.ErrAuthInvalidPasswordType, + }, + { + name: "invalid_password_type", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": "test-user", + "password": 1234, + }, + }, + wantErr: errors.ErrAuthInvalidPasswordType, + }, + { + name: "invalid_password", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": "test-user", + "password": " ", + }, + }, + wantErr: errors.ErrAuthInvalidPasswordType, + }, + { + name: "valid", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "username": "test-user", + "password": "test-password", + }, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var a AuthConfig + err := a.ValidateConfiguration(tt.args.inputFormat, tt.args.input) + if tt.wantErr != nil { + assert.ErrorContains(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/sinks/authentication_type/bearertokenauth/authentication.go b/sinks/authentication_type/bearertokenauth/authentication.go new file mode 100644 index 000000000..fdc01c47a --- /dev/null +++ b/sinks/authentication_type/bearertokenauth/authentication.go @@ -0,0 +1,291 @@ +package bearertokenauth + +import ( + "strings" + + "gopkg.in/yaml.v2" + + "github.com/orb-community/orb/pkg/errors" + "github.com/orb-community/orb/pkg/types" + "github.com/orb-community/orb/sinks/authentication_type" + "github.com/orb-community/orb/sinks/backend" +) + +const ( + AuthType = "bearertokenauth" + SchemeConfigFeature = "scheme" + TokenConfigFeature = "token" +) + +var features = []authentication_type.ConfigFeature{ + { + Type: backend.ConfigFeatureTypeText, + Input: "text", + Title: "Scheme", + Name: SchemeConfigFeature, + Required: true, + }, + { + Type: backend.ConfigFeatureTypeText, + Input: "text", + Title: "Token", + Name: TokenConfigFeature, + Required: true, + }, +} + +type AuthConfig struct { + encryptionService authentication_type.PasswordService + + Scheme *string `json:"scheme" yaml:"scheme"` + Token *string `json:"token" yaml:"token"` +} + +func (a *AuthConfig) GetFeatureConfig() []authentication_type.ConfigFeature { + return features +} + +func (a *AuthConfig) ValidateConfiguration(inputFormat string, input any) error { + switch inputFormat { + case "object": + if _, ok := input.(types.Metadata)[SchemeConfigFeature]; !ok { + return errors.Wrap(errors.ErrAuthSchemeNotFound, errors.New("scheme field was not found")) + } + + if _, ok := input.(types.Metadata)[TokenConfigFeature]; !ok { + return errors.Wrap(errors.ErrAuthTokenNotFound, errors.New("token field was not found")) + } + + for key, value := range input.(types.Metadata) { + if key == SchemeConfigFeature { + if _, ok := value.(string); !ok { + return errors.Wrap(errors.ErrAuthInvalidSchemeType, errors.New("invalid auth type for field: "+key)) + } + + if len(strings.Fields(value.(string))) != 1 { + return errors.Wrap(errors.ErrAuthInvalidSchemeType, errors.New("invalid authentication scheme")) + } + } + + if key == TokenConfigFeature { + if _, ok := value.(string); !ok { + return errors.Wrap(errors.ErrAuthInvalidTokenType, errors.New("invalid auth type for field: "+key)) + } + if len(strings.Fields(value.(string))) != 1 { + return errors.Wrap(errors.ErrAuthInvalidTokenType, errors.New("invalid authentication token")) + } + } + } + case "yaml": + err := yaml.Unmarshal([]byte(input.(string)), &a) + if err != nil { + return err + } + + if a.Scheme == nil { + return errors.Wrap(errors.ErrAuthSchemeNotFound, errors.New("scheme field was not found")) + } + + if len(strings.Fields(*a.Scheme)) != 1 { + return errors.Wrap(errors.ErrAuthInvalidSchemeType, errors.New("invalid authentication scheme")) + } + + if a.Token == nil { + return errors.Wrap(errors.ErrAuthTokenNotFound, errors.New("token field was not found")) + } + + if len(strings.Fields(*a.Token)) != 1 { + return errors.Wrap(errors.ErrAuthInvalidTokenType, errors.New("invalid authentication token")) + } + } + + return nil +} + +func (a *AuthConfig) ConfigToFormat(outputFormat string, input any) (any, error) { + switch input.(type) { + case types.Metadata: + if outputFormat == "yaml" { + retVal, err := yaml.Marshal(input) + + return string(retVal), err + } + case string: + if outputFormat == "object" { + retVal := make(types.Metadata) + val := input.(string) + err := yaml.Unmarshal([]byte(val), &retVal) + + return retVal, err + } + } + + return nil, errors.New("unsupported format") +} + +func (a *AuthConfig) OmitInformation(outputFormat string, input any) (any, error) { + switch input.(type) { + case types.Metadata: + inputMeta := input.(types.Metadata) + authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) + authMeta[TokenConfigFeature] = "" + inputMeta[authentication_type.AuthenticationKey] = authMeta + + if outputFormat == "yaml" { + return a.ConfigToFormat("yaml", inputMeta) + } + + if outputFormat == "object" { + return inputMeta, nil + } + + return nil, errors.New("unsupported format") + case string: + iia, err := a.ConfigToFormat("object", input) + if err != nil { + return nil, err + } + inputMeta := iia.(types.Metadata) + authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) + authMeta[TokenConfigFeature] = "" + inputMeta[authentication_type.AuthenticationKey] = authMeta + + if outputFormat == "yaml" { + return a.ConfigToFormat("yaml", inputMeta) + } + + if outputFormat == "object" { + return inputMeta, nil + } + + return nil, errors.New("unsupported format") + } + + return nil, errors.New("unsupported format") +} + +func (a *AuthConfig) EncodeInformation(outputFormat string, input interface{}) (interface{}, error) { + switch input.(type) { + case types.Metadata: + inputMeta := input.(types.Metadata) + authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) + + if _, ok := authMeta[TokenConfigFeature].(string); !ok { + return nil, errors.Wrap(errors.ErrAuthTokenNotFound, errors.New("token field was not found")) + } + + encoded, err := a.encryptionService.EncodePassword(authMeta[TokenConfigFeature].(string)) + if err != nil { + return nil, err + } + + authMeta[TokenConfigFeature] = encoded + inputMeta[authentication_type.AuthenticationKey] = authMeta + + if outputFormat == "yaml" { + return a.ConfigToFormat("yaml", inputMeta) + } + + if outputFormat == "object" { + return inputMeta, nil + } + + return nil, errors.New("unsupported format") + case string: + iia, err := a.ConfigToFormat("object", input) + if err != nil { + return nil, err + } + inputMeta := iia.(types.Metadata) + authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) + + encoded, err := a.encryptionService.EncodePassword(authMeta[TokenConfigFeature].(string)) + if err != nil { + return nil, err + } + + authMeta[TokenConfigFeature] = encoded + inputMeta[authentication_type.AuthenticationKey] = authMeta + + if outputFormat == "yaml" { + return a.ConfigToFormat("yaml", inputMeta) + } + + if outputFormat == "object" { + return inputMeta, nil + } + + return nil, errors.New("unsupported format") + } + + return nil, errors.New("unsupported format") +} + +func (a *AuthConfig) DecodeInformation(outputFormat string, input any) (any, error) { + switch input.(type) { + case types.Metadata: + inputMeta := input.(types.Metadata) + authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) + + decoded, err := a.encryptionService.DecodePassword(authMeta[TokenConfigFeature].(string)) + if err != nil { + return nil, err + } + + authMeta[TokenConfigFeature] = decoded + inputMeta[authentication_type.AuthenticationKey] = authMeta + + if outputFormat == "yaml" { + return a.ConfigToFormat("yaml", inputMeta) + } + + if outputFormat == "object" { + return inputMeta, nil + } + + return nil, errors.New("unsupported format") + case string: + iia, err := a.ConfigToFormat("object", input) + if err != nil { + return nil, err + } + + inputMeta := iia.(types.Metadata) + authMeta := inputMeta.GetSubMetadata(authentication_type.AuthenticationKey) + + decoded, err := a.encryptionService.DecodePassword(authMeta[TokenConfigFeature].(string)) + if err != nil { + return nil, err + } + + authMeta[TokenConfigFeature] = decoded + inputMeta[authentication_type.AuthenticationKey] = authMeta + + if outputFormat == "yaml" { + return a.ConfigToFormat("yaml", inputMeta) + } + + if outputFormat == "object" { + return inputMeta, nil + } + + return nil, errors.New("unsupported format") + } + + return nil, errors.New("unsupported format") +} + +func (a *AuthConfig) Metadata() authentication_type.AuthenticationTypeConfig { + return authentication_type.AuthenticationTypeConfig{ + Type: AuthType, + Description: "Token authentication", + Config: features, + } +} + +func Register(encryptionService authentication_type.PasswordService) { + bearerTokenAuth := AuthConfig{ + encryptionService: encryptionService, + } + authentication_type.Register(AuthType, &bearerTokenAuth) +} diff --git a/sinks/authentication_type/bearertokenauth/authentication_test.go b/sinks/authentication_type/bearertokenauth/authentication_test.go new file mode 100644 index 000000000..f62cb073d --- /dev/null +++ b/sinks/authentication_type/bearertokenauth/authentication_test.go @@ -0,0 +1,247 @@ +package bearertokenauth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/orb-community/orb/pkg/errors" + "github.com/orb-community/orb/pkg/types" + "github.com/orb-community/orb/sinks/authentication_type" +) + +func TestAuthConfig_ValidateConfiguration(t *testing.T) { + type args struct { + inputFormat string + input types.Metadata + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "missing_schema", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "token": "test_api_key", + }, + }, + wantErr: errors.ErrAuthSchemeNotFound, + }, + { + name: "empty_schema", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "", + "token": "test_api_key", + }, + }, + wantErr: errors.ErrAuthInvalidSchemeType, + }, + { + name: "invalid_schema_type", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": 1234, + "token": "test_api_key", + }, + }, + wantErr: errors.ErrAuthInvalidSchemeType, + }, + { + name: "invalid_schema", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "fake schema", + "token": "test_api_key", + }, + }, + wantErr: errors.ErrAuthInvalidSchemeType, + }, + { + name: "missing_token", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "Bearer", + }, + }, + wantErr: errors.ErrAuthTokenNotFound, + }, + { + name: "empty_token", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "Bearer", + "token": "", + }, + }, + wantErr: errors.ErrAuthInvalidTokenType, + }, + { + name: "invalid_token_type", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "Bearer", + "token": 1234, + }, + }, + wantErr: errors.ErrAuthInvalidTokenType, + }, + { + name: "invalid_token_value", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "Bearer", + "token": "invalid api key", + }, + }, + wantErr: errors.ErrAuthInvalidTokenType, + }, + { + name: "valid", + args: args{ + inputFormat: "object", + input: types.Metadata{ + "scheme": "Bearer", + "token": "abcdefg", + }, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var a AuthConfig + err := a.ValidateConfiguration(tt.args.inputFormat, tt.args.input) + if tt.wantErr != nil { + assert.ErrorContains(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAuthConfig_OmitInformation(t *testing.T) { + t.Run("invalid output format", func(t *testing.T) { + input := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "abcdefg", + }, + } + + var a AuthConfig + + _, err := a.OmitInformation("blah", input) + assert.Error(t, err) + }) + t.Run("successfully stripped the secret token", func(t *testing.T) { + input := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "abcdefg", + }, + } + + want := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "", + }, + } + + var a AuthConfig + + got, err := a.OmitInformation("object", input) + require.NoError(t, err) + assert.Equal(t, want, got) + }) +} + +func TestAuthConfig_EncodeInformation(t *testing.T) { + t.Run("invalid output format", func(t *testing.T) { + input := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "abcdefg", + }, + } + + a := AuthConfig{ + encryptionService: authentication_type.NewPasswordService(nil, "test"), + } + + _, err := a.EncodeInformation("blah", input) + assert.Error(t, err) + }) + + t.Run("successfully encrypted token", func(t *testing.T) { + input := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "abcdefg", + }, + } + + a := AuthConfig{ + encryptionService: authentication_type.NewPasswordService(nil, "test"), + } + + _, err := a.EncodeInformation("object", input) + require.NoError(t, err) + }) +} + +func TestAuthConfig_DecodeInformation(t *testing.T) { + t.Run("invalid output format", func(t *testing.T) { + input := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "dca8757dee5dfcc592c97355396dc2bdb95c6a3f58d4acb4453717c960827602acaf49", + }, + } + + a := AuthConfig{ + encryptionService: authentication_type.NewPasswordService(nil, "test"), + } + + _, err := a.DecodeInformation("blah", input) + assert.Error(t, err) + }) + + t.Run("successfully decrypted token", func(t *testing.T) { + input := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "dca8757dee5dfcc592c97355396dc2bdb95c6a3f58d4acb4453717c960827602acaf49", + }, + } + + want := types.Metadata{ + "authentication": types.Metadata{ + "scheme": "Bearer", + "token": "abcdefg", + }, + } + + a := AuthConfig{ + encryptionService: authentication_type.NewPasswordService(nil, "test"), + } + + got, err := a.DecodeInformation("object", input) + require.NoError(t, err) + assert.Equal(t, want, got) + }) +} diff --git a/sinks/service.go b/sinks/service.go index 24c73a174..31a5e9b6a 100644 --- a/sinks/service.go +++ b/sinks/service.go @@ -10,16 +10,19 @@ package sinks import ( "context" + "time" + "github.com/mainflux/mainflux" mfsdk "github.com/mainflux/mainflux/pkg/sdk/go" + "go.uber.org/zap" + "github.com/orb-community/orb/pkg/errors" "github.com/orb-community/orb/pkg/types" "github.com/orb-community/orb/sinks/authentication_type" "github.com/orb-community/orb/sinks/authentication_type/basicauth" + "github.com/orb-community/orb/sinks/authentication_type/bearertokenauth" "github.com/orb-community/orb/sinks/backend/otlphttpexporter" "github.com/orb-community/orb/sinks/backend/prometheus" - "go.uber.org/zap" - "time" ) // PageMetadata contains page metadata that helps navigation @@ -67,7 +70,7 @@ func NewSinkService(logger *zap.Logger, auth mainflux.AuthServiceClient, sinkRep otlphttpexporter.Register() prometheus.Register() basicauth.Register(passwordService) - // bearerauth.Register(passwordService) + bearertokenauth.Register(passwordService) return &sinkService{ logger: logger, auth: auth, diff --git a/sinks/sinks_service.go b/sinks/sinks_service.go index ab1617266..b181ed840 100644 --- a/sinks/sinks_service.go +++ b/sinks/sinks_service.go @@ -96,12 +96,12 @@ func validateAuthType(s *Sink) (authentication_type.AuthenticationType, error) { } if _, ok := authTypeStr.(string); !ok { - return nil, errors.Wrap(errors.ErrInvalidAuthType, errors.New("invalid authentication type")) + return nil, errors.Wrap(errors.ErrAuthInvalidType, errors.New("invalid authentication type")) } authType, ok := authentication_type.GetAuthType(authTypeStr.(string)) if !ok { - return nil, errors.Wrap(errors.ErrInvalidAuthType, errors.New("invalid authentication type")) + return nil, errors.Wrap(errors.ErrAuthInvalidType, errors.New("invalid authentication type")) } err := authType.ValidateConfiguration("object", authMetadata) diff --git a/ui/src/app/pages/sinks/add/sink-add.component.ts b/ui/src/app/pages/sinks/add/sink-add.component.ts index 71ac47e5f..66cdc096e 100644 --- a/ui/src/app/pages/sinks/add/sink-add.component.ts +++ b/ui/src/app/pages/sinks/add/sink-add.component.ts @@ -79,14 +79,7 @@ export class SinkAddComponent { return !this.editor.checkEmpty(config.authentication) && !this.editor.checkEmpty(config.exporter) - && detailsValid - && !this.checkString(config); - } - checkString(config: any): boolean { - if (typeof config.authentication.password !== 'string' || typeof config.authentication.username !== 'string') { - return true; - } - return false; + && detailsValid; } createSink() {