feat(core): support any auth scheme in HTTP-sync auth header (#1152)

## This PR

- adds support for any auth scheme in HTTP-sync auth header

### Related Issues

Closes #1150

---------

Signed-off-by: Best Olunusi <olunusibest@gmail.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
Best Olunusi 2024-01-18 14:35:06 -06:00 committed by GitHub
parent 64ee20e191
commit df6596634e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 134 additions and 26 deletions

View File

@ -148,6 +148,7 @@ func (sb *SyncBuilder) newHTTP(config sync.SourceConfig, logger *logger.Logger)
zap.String("sync", "remote"), zap.String("sync", "remote"),
), ),
BearerToken: config.BearerToken, BearerToken: config.BearerToken,
AuthHeader: config.AuthHeader,
Interval: interval, Interval: interval,
Cron: cron.New(), Cron: cron.New(),
} }

View File

@ -198,6 +198,11 @@ func Test_SyncsFromFromConfig(t *testing.T) {
Provider: syncProviderHTTP, Provider: syncProviderHTTP,
BearerToken: "token", BearerToken: "token",
}, },
{
URI: "https://host:port",
Provider: syncProviderHTTP,
AuthHeader: "scheme credentials/token",
},
{ {
URI: "/tmp/flags.json", URI: "/tmp/flags.json",
Provider: syncProviderFile, Provider: syncProviderFile,
@ -211,6 +216,7 @@ func Test_SyncsFromFromConfig(t *testing.T) {
wantSyncs: []sync.ISync{ wantSyncs: []sync.ISync{
&grpc.Sync{}, &grpc.Sync{},
&http.Sync{}, &http.Sync{},
&http.Sync{},
&file.Sync{}, &file.Sync{},
&kubernetes.Sync{}, &kubernetes.Sync{},
}, },

View File

@ -22,6 +22,11 @@ func ParseSources(sourcesFlag string) ([]sync.SourceConfig, error) {
if sp.Provider == "" { if sp.Provider == "" {
return syncProvidersParsed, errors.New("sync provider argument parse: provider is a required field") return syncProvidersParsed, errors.New("sync provider argument parse: provider is a required field")
} }
if sp.AuthHeader != "" && sp.BearerToken != "" {
return syncProvidersParsed, errors.New(
"sync provider argument parse: both authHeader and bearerToken are defined, only one is allowed at a time",
)
}
} }
return syncProvidersParsed, nil return syncProvidersParsed, nil
} }

View File

@ -52,14 +52,17 @@ func TestParseSource(t *testing.T) {
}, },
}, },
"multiple-syncs-with-options": { "multiple-syncs-with-options": {
in: `[{"uri":"config/samples/example_flags.json","provider":"file"}, in: `[
{"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"}, {"uri":"config/samples/example_flags.json","provider":"file"},
{"uri":"https://secure-remote","provider":"http","bearerToken":"bearer-dji34ld2l"}, {"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"http://site.com","provider":"http","interval":77 }, {"uri":"https://secure-remote","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"default/my-flag-config","provider":"kubernetes"}, {"uri":"https://secure-remote","provider":"http","authHeader":"Bearer bearer-dji34ld2l"},
{"uri":"grpc-source:8080","provider":"grpc"}, {"uri":"https://secure-remote","provider":"http","authHeader":"Basic dXNlcjpwYXNz"},
{"uri":"my-flag-source:8080","provider":"grpc", "tls":true, "certPath": "/certs/ca.cert", "providerID": "flagd-weatherapp-sidecar", "selector": "source=database,app=weatherapp"}] {"uri":"http://site.com","provider":"http","interval":77 },
`, {"uri":"default/my-flag-config","provider":"kubernetes"},
{"uri":"grpc-source:8080","provider":"grpc"},
{"uri":"my-flag-source:8080","provider":"grpc", "tls":true, "certPath": "/certs/ca.cert", "providerID": "flagd-weatherapp-sidecar", "selector": "source=database,app=weatherapp"}
]`,
expectErr: false, expectErr: false,
out: []sync.SourceConfig{ out: []sync.SourceConfig{
{ {
@ -76,6 +79,16 @@ func TestParseSource(t *testing.T) {
Provider: syncProviderHTTP, Provider: syncProviderHTTP,
BearerToken: "bearer-dji34ld2l", BearerToken: "bearer-dji34ld2l",
}, },
{
URI: "https://secure-remote",
Provider: syncProviderHTTP,
AuthHeader: "Bearer bearer-dji34ld2l",
},
{
URI: "https://secure-remote",
Provider: syncProviderHTTP,
AuthHeader: "Basic dXNlcjpwYXNz",
},
{ {
URI: "http://site.com", URI: "http://site.com",
Provider: syncProviderHTTP, Provider: syncProviderHTTP,
@ -99,6 +112,22 @@ func TestParseSource(t *testing.T) {
}, },
}, },
}, },
"multiple-auth-options": {
in: `[
{"uri":"https://secure-remote","provider":"http","authHeader":"Bearer bearer-dji34ld2l","bearerToken":"bearer-dji34ld2l"}
]`,
expectErr: true,
out: []sync.SourceConfig{
{
URI: "https://secure-remote",
Provider: syncProviderHTTP,
AuthHeader: "Bearer bearer-dji34ld2l",
BearerToken: "bearer-dji34ld2l",
TLS: false,
Interval: 0,
},
},
},
"empty": { "empty": {
in: `[]`, in: `[]`,
expectErr: false, expectErr: false,

View File

@ -21,6 +21,7 @@ type Sync struct {
LastBodySHA string LastBodySHA string
Logger *logger.Logger Logger *logger.Logger
BearerToken string BearerToken string
AuthHeader string
Interval uint32 Interval uint32
ready bool ready bool
} }
@ -47,7 +48,9 @@ func (hs *Sync) ReSync(ctx context.Context, dataSync chan<- sync.DataSync) error
} }
func (hs *Sync) Init(_ context.Context) error { func (hs *Sync) Init(_ context.Context) error {
// noop if hs.BearerToken != "" {
hs.Logger.Warn("Deprecation Alert: bearerToken option is deprecated, please use authHeader instead")
}
return nil return nil
} }
@ -120,7 +123,9 @@ func (hs *Sync) fetchBodyFromURL(ctx context.Context, url string) ([]byte, error
req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "application/json")
if hs.BearerToken != "" { if hs.AuthHeader != "" {
req.Header.Set("Authorization", hs.AuthHeader)
} else if hs.BearerToken != "" {
bearer := fmt.Sprintf("Bearer %s", hs.BearerToken) bearer := fmt.Sprintf("Bearer %s", hs.BearerToken)
req.Header.Set("Authorization", bearer) req.Header.Set("Authorization", bearer)
} }

View File

@ -62,6 +62,7 @@ func TestHTTPSync_Fetch(t *testing.T) {
setup func(t *testing.T, client *syncmock.MockClient) setup func(t *testing.T, client *syncmock.MockClient)
uri string uri string
bearerToken string bearerToken string
authHeader string
lastBodySHA string lastBodySHA string
handleResponse func(*testing.T, Sync, string, error) handleResponse func(*testing.T, Sync, string, error)
}{ }{
@ -111,13 +112,46 @@ func TestHTTPSync_Fetch(t *testing.T) {
} }
}, },
}, },
"authorization header": { "authorization with bearerToken": {
setup: func(t *testing.T, client *syncmock.MockClient) { setup: func(t *testing.T, client *syncmock.MockClient) {
client.EXPECT().Do(gomock.Any()).Return(&http.Response{ expectedToken := "bearer-1234"
Body: io.NopCloser(strings.NewReader("test response")), client.EXPECT().Do(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
}, nil) actualAuthHeader := req.Header.Get("Authorization")
if actualAuthHeader != "Bearer "+expectedToken {
t.Fatalf("expected Authorization header to be 'Bearer %s', got %s", expectedToken, actualAuthHeader)
}
return &http.Response{Body: io.NopCloser(strings.NewReader("test response"))}, nil
})
}, },
uri: "http://localhost", uri: "http://localhost",
bearerToken: "bearer-1234",
lastBodySHA: "",
handleResponse: func(t *testing.T, httpSync Sync, _ string, err error) {
if err != nil {
t.Fatalf("fetch: %v", err)
}
expectedLastBodySHA := "UjeJHtCU_wb7OHK-tbPoHycw0TqlHzkWJmH4y6cqg50="
if httpSync.LastBodySHA != expectedLastBodySHA {
t.Errorf(
"expected last body sha to be: '%s', got: '%s'", expectedLastBodySHA, httpSync.LastBodySHA,
)
}
},
},
"authorization with authHeader": {
setup: func(t *testing.T, client *syncmock.MockClient) {
expectedHeader := "Basic dXNlcjpwYXNz"
client.EXPECT().Do(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
actualAuthHeader := req.Header.Get("Authorization")
if actualAuthHeader != expectedHeader {
t.Fatalf("expected Authorization header to be '%s', got %s", expectedHeader, actualAuthHeader)
}
return &http.Response{Body: io.NopCloser(strings.NewReader("test response"))}, nil
})
},
uri: "http://localhost",
authHeader: "Basic dXNlcjpwYXNz",
lastBodySHA: "", lastBodySHA: "",
handleResponse: func(t *testing.T, httpSync Sync, _ string, err error) { handleResponse: func(t *testing.T, httpSync Sync, _ string, err error) {
if err != nil { if err != nil {
@ -144,6 +178,7 @@ func TestHTTPSync_Fetch(t *testing.T) {
URI: tt.uri, URI: tt.uri,
Client: mockClient, Client: mockClient,
BearerToken: tt.bearerToken, BearerToken: tt.bearerToken,
AuthHeader: tt.authHeader,
LastBodySHA: tt.lastBodySHA, LastBodySHA: tt.lastBodySHA,
Logger: logger.NewLogger(nil, false), Logger: logger.NewLogger(nil, false),
} }
@ -154,6 +189,29 @@ func TestHTTPSync_Fetch(t *testing.T) {
} }
} }
func TestSync_Init(t *testing.T) {
tests := []struct {
name string
bearerToken string
}{
{"with bearerToken", "bearer-1234"},
{"without bearerToken", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
httpSync := Sync{
BearerToken: tt.bearerToken,
Logger: logger.NewLogger(nil, false),
}
if err := httpSync.Init(context.Background()); err != nil {
t.Errorf("Init() error = %v", err)
}
})
}
}
func TestHTTPSync_Resync(t *testing.T) { func TestHTTPSync_Resync(t *testing.T) {
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)

View File

@ -66,6 +66,7 @@ type SourceConfig struct {
Provider string `json:"provider"` Provider string `json:"provider"`
BearerToken string `json:"bearerToken,omitempty"` BearerToken string `json:"bearerToken,omitempty"`
AuthHeader string `json:"authHeader,omitempty"`
CertPath string `json:"certPath,omitempty"` CertPath string `json:"certPath,omitempty"`
TLS bool `json:"tls,omitempty"` TLS bool `json:"tls,omitempty"`
ProviderID string `json:"providerID,omitempty"` ProviderID string `json:"providerID,omitempty"`

View File

@ -27,16 +27,17 @@ The flagd accepts a string argument, which should be a JSON representation of an
Alternatively, these configurations can be passed to flagd via config file, specified using the `--config` flag. Alternatively, these configurations can be passed to flagd via config file, specified using the `--config` flag.
| Field | Type | Note | | Field | Type | Note |
| ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | ----------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| uri | required `string` | Flag configuration source of the sync | | uri | required `string` | Flag configuration source of the sync |
| provider | required `string` | Provider type - `file`, `kubernetes`, `http` or `grpc` | | provider | required `string` | Provider type - `file`, `kubernetes`, `http`, or `grpc` |
| bearerToken | optional `string` | Used for http sync; token gets appended to `Authorization` header with [bearer schema](https://www.rfc-editor.org/rfc/rfc6750#section-2.1) | | authHeader | optional `string` | Used for http sync; set this to include the complete `Authorization` header value for any authentication scheme (e.g., "Bearer token_here", "Basic base64_credentials", etc.). Cannot be used with `bearerToken` |
| interval | optional `uint32` | Used for http sync; requests will be made at this interval. Defaults to 5 seconds. | | bearerToken | optional `string` | (Deprecated) Used for http sync; token gets appended to `Authorization` header with [bearer schema](https://www.rfc-editor.org/rfc/rfc6750#section-2.1). Cannot be used with `authHeader` |
| tls | optional `boolean` | Enable/Disable secure TLS connectivity. Currently used only by gRPC sync. Default(ex:- if unset) is false, which will use an insecure connection | | interval | optional `uint32` | Used for http sync; requests will be made at this interval. Defaults to 5 seconds. |
| providerID | optional `string` | Value binds to grpc connection's providerID field. gRPC server implementations may use this to identify connecting flagd instance | | tls | optional `boolean` | Enable/Disable secure TLS connectivity. Currently used only by gRPC sync. Default (ex: if unset) is false, which will use an insecure connection |
| selector | optional `string` | Value binds to grpc connection's selector field. gRPC server implementations may use this to filter flag configurations | | providerID | optional `string` | Value binds to grpc connection's providerID field. gRPC server implementations may use this to identify connecting flagd instance |
| certPath | optional `string` | Used for grpcs sync when TLS certificate is needed. If not provided, system certificates will be used for TLS connection | | selector | optional `string` | Value binds to grpc connection's selector field. gRPC server implementations may use this to filter flag configurations |
| certPath | optional `string` | Used for grpcs sync when TLS certificate is needed. If not provided, system certificates will be used for TLS connection |
The `uri` field values **do not** follow the [URI patterns](#uri-patterns). The provider type is instead derived The `uri` field values **do not** follow the [URI patterns](#uri-patterns). The provider type is instead derived
from the `provider` field. Only exception is the remote provider where `http(s)://` is expected by default. Incorrect from the `provider` field. Only exception is the remote provider where `http(s)://` is expected by default. Incorrect
@ -56,9 +57,11 @@ Sync providers:
Startup command: Startup command:
```sh ```sh
./bin/flagd start ./bin/flagd start
--sources='[{"uri":"config/samples/example_flags.json","provider":"file"}, --sources='[{"uri":"config/samples/example_flags.json","provider":"file"},
{"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"}, {"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"https://secure-remote/bearer-auth","provider":"http","authHeader":"Bearer bearer-dji34ld2l"},
{"uri":"https://secure-remote/basic-auth","provider":"http","authHeader":"Basic dXNlcjpwYXNz"},
{"uri":"default/my-flag-config","provider":"kubernetes"}, {"uri":"default/my-flag-config","provider":"kubernetes"},
{"uri":"grpc-source:8080","provider":"grpc"}, {"uri":"grpc-source:8080","provider":"grpc"},
{"uri":"my-flag-source:8080","provider":"grpc", "certPath": "/certs/ca.cert", "tls": true, "providerID": "flagd-weatherapp-sidecar", "selector": "source=database,app=weatherapp"}]' {"uri":"my-flag-source:8080","provider":"grpc", "certPath": "/certs/ca.cert", "tls": true, "providerID": "flagd-weatherapp-sidecar", "selector": "source=database,app=weatherapp"}]'
@ -82,5 +85,5 @@ sources:
certPath: /certs/ca.cert certPath: /certs/ca.cert
tls: true tls: true
providerID: flagd-weatherapp-sidecar providerID: flagd-weatherapp-sidecar
selector: 'source=database,app=weatherapp' selector: "source=database,app=weatherapp"
``` ```