Feature-Flag service: signing middleware for cloud usecase (#107745)

This commit is contained in:
Charandas
2025-07-17 09:50:25 -07:00
committed by GitHub
parent 08e8a71ad6
commit d9e099d480
7 changed files with 273 additions and 16 deletions
@@ -0,0 +1,22 @@
package featuremgmt
import (
"context"
"errors"
authlib "github.com/grafana/authlib/authn"
"github.com/stretchr/testify/mock"
)
type fakeTokenExchangeClient struct {
expectedErr error
*mock.Mock
}
func (c *fakeTokenExchangeClient) Exchange(ctx context.Context, r authlib.TokenExchangeRequest) (*authlib.TokenExchangeResponse, error) {
c.Called(ctx, r)
if c.expectedErr != nil {
return nil, errors.New("error signing token")
}
return &authlib.TokenExchangeResponse{Token: "signed-token"}, c.expectedErr
}
+2 -5
View File
@@ -2,19 +2,16 @@ package featuremgmt
import (
"net/http"
"time"
gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg"
"github.com/open-feature/go-sdk/openfeature"
)
func newGOFFProvider(url string) (openfeature.FeatureProvider, error) {
func newGOFFProvider(url string, client *http.Client) (openfeature.FeatureProvider, error) {
options := gofeatureflag.ProviderOptions{
Endpoint: url,
// consider using github.com/grafana/grafana/pkg/infra/httpclient/provider.go
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
},
HTTPClient: client,
}
provider, err := gofeatureflag.NewProvider(options)
return provider, err
+55 -6
View File
@@ -2,20 +2,41 @@ package featuremgmt
import (
"fmt"
"net/http"
"net/url"
"time"
clientauthmiddleware "github.com/grafana/grafana/pkg/clientauth/middleware"
"github.com/grafana/grafana/pkg/setting"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/open-feature/go-sdk/openfeature"
)
const (
featuresProviderAudience = "features.grafana.app"
)
func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles"))
if err != nil {
return fmt.Errorf("failed to read feature flags from config: %w", err)
}
err = initOpenFeature(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, confFlags)
var httpcli *http.Client
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType {
m, err := clientauthmiddleware.NewTokenExchangeMiddleware(cfg)
if err != nil {
return fmt.Errorf("failed to create token exchange middleware: %w", err)
}
httpcli, err = goffHTTPClient(m)
if err != nil {
return err
}
}
err = initOpenFeature(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, confFlags, httpcli)
if err != nil {
return fmt.Errorf("failed to initialize OpenFeature: %w", err)
}
@@ -24,10 +45,15 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
return nil
}
func initOpenFeature(providerType string, u *url.URL, staticFlags map[string]bool) error {
p, err := createProvider(providerType, u, staticFlags)
func initOpenFeature(
providerType string,
u *url.URL,
staticFlags map[string]bool,
httpClient *http.Client,
) error {
p, err := createProvider(providerType, u, staticFlags, httpClient)
if err != nil {
return fmt.Errorf("failed to create feature provider: type %s, %w", providerType, err)
return err
}
if err := openfeature.SetProviderAndWait(p); err != nil {
@@ -37,7 +63,12 @@ func initOpenFeature(providerType string, u *url.URL, staticFlags map[string]boo
return nil
}
func createProvider(providerType string, u *url.URL, staticFlags map[string]bool) (openfeature.FeatureProvider, error) {
func createProvider(
providerType string,
u *url.URL,
staticFlags map[string]bool,
httpClient *http.Client,
) (openfeature.FeatureProvider, error) {
if providerType != setting.GOFFProviderType {
return newStaticProvider(staticFlags)
}
@@ -46,5 +77,23 @@ func createProvider(providerType string, u *url.URL, staticFlags map[string]bool
return nil, fmt.Errorf("feature provider url is required for GOFFProviderType")
}
return newGOFFProvider(u.String())
return newGOFFProvider(u.String(), httpClient)
}
func goffHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
httpcli, err := sdkhttpclient.NewProvider().New(sdkhttpclient.Options{
TLS: &sdkhttpclient.TLSOptions{InsecureSkipVerify: true},
Timeouts: &sdkhttpclient.TimeoutOptions{
Timeout: 10 * time.Second,
},
Middlewares: []sdkhttpclient.Middleware{
m.New([]string{featuresProviderAudience}),
},
})
if err != nil {
return nil, fmt.Errorf("failed to create http client for openfeature: %w", err)
}
return httpcli, nil
}
+82 -5
View File
@@ -1,24 +1,33 @@
package featuremgmt
import (
"context"
"errors"
"net/url"
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/clientauth/middleware"
"github.com/grafana/grafana/pkg/setting"
authlib "github.com/grafana/authlib/authn"
gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg"
"github.com/open-feature/go-sdk/openfeature"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestCreateProvider(t *testing.T) {
u, err := url.Parse("http://localhost:1031")
u, err := url.Parse("http://localhost:10333")
require.NoError(t, err)
testCases := []struct {
name string
cfg setting.OpenFeatureSettings
expectedProvider string
name string
cfg setting.OpenFeatureSettings
expectedProvider string
expectExchangeRequest *authlib.TokenExchangeRequest
failSigning bool
}{
{
name: "static provider",
@@ -31,8 +40,26 @@ func TestCreateProvider(t *testing.T) {
URL: u,
TargetingKey: "grafana",
},
expectExchangeRequest: &authlib.TokenExchangeRequest{
Namespace: "*",
Audiences: []string{"features.grafana.app"},
},
expectedProvider: setting.GOFFProviderType,
},
{
name: "goff provider with failing token exchange",
cfg: setting.OpenFeatureSettings{
ProviderType: setting.GOFFProviderType,
URL: u,
TargetingKey: "grafana",
},
expectExchangeRequest: &authlib.TokenExchangeRequest{
Namespace: "*",
Audiences: []string{"features.grafana.app"},
},
expectedProvider: setting.GOFFProviderType,
failSigning: true,
},
{
name: "invalid provider",
cfg: setting.OpenFeatureSettings{
@@ -42,14 +69,46 @@ func TestCreateProvider(t *testing.T) {
},
}
require.NoError(t, err)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil)
cfg := setting.NewCfg()
cfg.OpenFeature = tc.cfg
var tokenExchangeClient *fakeTokenExchangeClient
if tc.expectExchangeRequest != nil {
tokenExchangeClient = &fakeTokenExchangeClient{
Mock: &mock.Mock{
ExpectedCalls: []*mock.Call{
{
Method: "Exchange",
Arguments: mock.Arguments{mock.Anything, *tc.expectExchangeRequest},
},
},
},
}
if tc.failSigning {
tokenExchangeClient.expectedErr = errors.New("failed signing access token")
}
}
tokenExchangeMiddleware := middleware.TestingTokenExchangeMiddleware(tokenExchangeClient)
goffClient, err := goffHTTPClient(tokenExchangeMiddleware)
require.NoError(t, err, "failed to create goff http client")
provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, goffClient)
require.NoError(t, err)
err = openfeature.SetProviderAndWait(provider)
require.NoError(t, err, "failed to set provider")
if tc.expectedProvider == setting.GOFFProviderType {
_, ok := provider.(*gofeatureflag.Provider)
assert.True(t, ok, "expected provider to be of type goff.Provider")
testGoFFProvider(t, tc.failSigning)
} else {
_, ok := provider.(*inMemoryBulkProvider)
assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider")
@@ -57,3 +116,21 @@ func TestCreateProvider(t *testing.T) {
})
}
}
func testGoFFProvider(t *testing.T, failSigning bool) {
// this tests with a fake identity with * namespace access, but in any case, it proves what the requester
// is scoped to is what is used to sign the token with
ctx, _ := identity.WithServiceIdentity(context.Background(), 1)
// Test that the flag evaluation can be attempted (though it will fail due to non-existent service)
// The important thing is that the authentication middleware is properly integrated
_, err := openfeature.GetApiInstance().GetClient().BooleanValueDetails(ctx, "test", false, openfeature.NewEvaluationContext("test", map[string]interface{}{"test": "test"}))
// Error related to the token exchange should be returned if signing fails
// otherwise, it should return a connection refused error since the goff URL is not set
if failSigning {
assert.ErrorContains(t, err, "failed to exchange token: error signing token", "should return an error when signing fails")
} else {
assert.ErrorContains(t, err, "connect: connection refused", "should return an error when goff url is not set")
}
}
@@ -51,6 +51,7 @@ func setup(t *testing.T, conf []byte) {
t.Helper()
cfg, err := setting.NewCfgFromBytes(conf)
require.NoError(t, err)
err = InitOpenFeatureWithCfg(cfg)
require.NoError(t, err)
}