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:
parent
64ee20e191
commit
df6596634e
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue