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"),
),
BearerToken: config.BearerToken,
AuthHeader: config.AuthHeader,
Interval: interval,
Cron: cron.New(),
}

View File

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

View File

@ -22,6 +22,11 @@ func ParseSources(sourcesFlag string) ([]sync.SourceConfig, error) {
if sp.Provider == "" {
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
}

View File

@ -52,14 +52,17 @@ func TestParseSource(t *testing.T) {
},
},
"multiple-syncs-with-options": {
in: `[{"uri":"config/samples/example_flags.json","provider":"file"},
{"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"https://secure-remote","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"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"}]
`,
in: `[
{"uri":"config/samples/example_flags.json","provider":"file"},
{"uri":"http://my-flag-source.json","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"https://secure-remote","provider":"http","bearerToken":"bearer-dji34ld2l"},
{"uri":"https://secure-remote","provider":"http","authHeader":"Bearer bearer-dji34ld2l"},
{"uri":"https://secure-remote","provider":"http","authHeader":"Basic dXNlcjpwYXNz"},
{"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,
out: []sync.SourceConfig{
{
@ -76,6 +79,16 @@ func TestParseSource(t *testing.T) {
Provider: syncProviderHTTP,
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",
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": {
in: `[]`,
expectErr: false,

View File

@ -21,6 +21,7 @@ type Sync struct {
LastBodySHA string
Logger *logger.Logger
BearerToken string
AuthHeader string
Interval uint32
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 {
// noop
if hs.BearerToken != "" {
hs.Logger.Warn("Deprecation Alert: bearerToken option is deprecated, please use authHeader instead")
}
return nil
}
@ -120,7 +123,9 @@ func (hs *Sync) fetchBodyFromURL(ctx context.Context, url string) ([]byte, error
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)
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)
uri string
bearerToken string
authHeader string
lastBodySHA string
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) {
client.EXPECT().Do(gomock.Any()).Return(&http.Response{
Body: io.NopCloser(strings.NewReader("test response")),
}, nil)
expectedToken := "bearer-1234"
client.EXPECT().Do(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
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",
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: "",
handleResponse: func(t *testing.T, httpSync Sync, _ string, err error) {
if err != nil {
@ -144,6 +178,7 @@ func TestHTTPSync_Fetch(t *testing.T) {
URI: tt.uri,
Client: mockClient,
BearerToken: tt.bearerToken,
AuthHeader: tt.authHeader,
LastBodySHA: tt.lastBodySHA,
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) {
ctrl := gomock.NewController(t)

View File

@ -66,6 +66,7 @@ type SourceConfig struct {
Provider string `json:"provider"`
BearerToken string `json:"bearerToken,omitempty"`
AuthHeader string `json:"authHeader,omitempty"`
CertPath string `json:"certPath,omitempty"`
TLS bool `json:"tls,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.
| Field | Type | Note |
| ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| uri | required `string` | Flag configuration source of the sync |
| 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) |
| interval | optional `uint32` | Used for http sync; requests will be made at this interval. Defaults to 5 seconds. |
| 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 |
| providerID | optional `string` | Value binds to grpc connection's providerID field. gRPC server implementations may use this to identify connecting flagd instance |
| 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 |
| Field | Type | Note |
| ----------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| uri | required `string` | Flag configuration source of the sync |
| provider | required `string` | Provider type - `file`, `kubernetes`, `http`, or `grpc` |
| 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` |
| 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` |
| interval | optional `uint32` | Used for http sync; requests will be made at this interval. Defaults to 5 seconds. |
| 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 |
| providerID | optional `string` | Value binds to grpc connection's providerID field. gRPC server implementations may use this to identify connecting flagd instance |
| 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
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:
```sh
./bin/flagd start
./bin/flagd start
--sources='[{"uri":"config/samples/example_flags.json","provider":"file"},
{"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":"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"}]'
@ -82,5 +85,5 @@ sources:
certPath: /certs/ca.cert
tls: true
providerID: flagd-weatherapp-sidecar
selector: 'source=database,app=weatherapp'
selector: "source=database,app=weatherapp"
```