diff --git a/build/testing/integration.go b/build/testing/integration.go index 0f3d7125bd..9c3438ac33 100644 --- a/build/testing/integration.go +++ b/build/testing/integration.go @@ -71,7 +71,6 @@ var ( type testConfig struct { name string address string - auth authConfig port int references bool } @@ -95,32 +94,6 @@ func filterCases(caseNames ...string) (map[string]testCaseFn, error) { return cases, nil } -type authConfig int - -const ( - noAuth authConfig = iota - staticAuth - jwtAuth - k8sAuth -) - -func (a authConfig) enabled() bool { - return a != noAuth -} - -func (a authConfig) method() string { - switch a { - case staticAuth: - return "static" - case jwtAuth: - return "jwt" - case k8sAuth: - return "k8s" - default: - return "" - } -} - func Integration(ctx context.Context, client *dagger.Client, base, flipt *dagger.Container, caseNames ...string) error { cases, err := filterCases(caseNames...) if err != nil { @@ -139,28 +112,13 @@ func Integration(ctx context.Context, client *dagger.Client, base, flipt *dagger var configs []testConfig for protocol, port := range protocolPorts { - for _, auth := range []authConfig{noAuth, staticAuth, jwtAuth, k8sAuth} { - auth := auth - config := testConfig{ - name: strings.ToUpper(protocol), - auth: auth, - address: fmt.Sprintf("%s://flipt:%d", protocol, port), - port: port, - } - - switch auth { - case noAuth: - config.name = fmt.Sprintf("%s without auth", config.name) - case staticAuth: - config.name = fmt.Sprintf("%s with static auth token", config.name) - case jwtAuth: - config.name = fmt.Sprintf("%s with jwt auth", config.name) - case k8sAuth: - config.name = fmt.Sprintf("%s with k8s auth", config.name) - } - - configs = append(configs, config) + config := testConfig{ + name: strings.ToUpper(protocol), + address: fmt.Sprintf("%s://flipt:%d", protocol, port), + port: port, } + + configs = append(configs, config) } var g errgroup.Group @@ -175,46 +133,57 @@ func Integration(ctx context.Context, client *dagger.Client, base, flipt *dagger ) g.Go(take(func() error { - if config.auth.enabled() { + { + // Static token auth configuration flipt = flipt. WithEnvVariable("FLIPT_AUTHENTICATION_REQUIRED", "true"). WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_ENABLED", "true"). WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_BOOTSTRAP_TOKEN", bootstrapToken) + } + { + // K8s auth configuration + flipt = flipt. + WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_KUBERNETES_ENABLED", "true") + + var saToken string + // run an OIDC server which exposes a JWKS url using a private key we own + // and generate a JWT to act as our SA token + flipt, saToken, err = serveOIDC(ctx, client, base, flipt) + if err != nil { + return err + } - switch config.auth { - case k8sAuth: - flipt = flipt. - WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_KUBERNETES_ENABLED", "true") - - var saToken string - // run an OIDC server which exposes a JWKS url using a private key we own - // and generate a JWT to act as our SA token - flipt, saToken, err = serveOIDC(ctx, client, base, flipt) - if err != nil { - return err - } - - // mount service account token into base on expected k8s sa token path - base = base.WithNewFile("/var/run/secrets/kubernetes.io/serviceaccount/token", dagger.ContainerWithNewFileOpts{ - Contents: saToken, - }) - case jwtAuth: - bytes, err := x509.MarshalPKIXPublicKey(priv.Public()) - if err != nil { - return err - } - - bytes = pem.EncodeToMemory(&pem.Block{ - Type: "public key", - Bytes: bytes, - }) - - flipt = flipt. - WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_ENABLED", "true"). - WithNewFile("/etc/flipt/jwt.pem", dagger.ContainerWithNewFileOpts{Contents: string(bytes)}). - WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_PUBLIC_KEY_FILE", "/etc/flipt/jwt.pem"). - WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_VALIDATE_CLAIMS_ISSUER", "https://flipt.io") + // mount service account token into base on expected k8s sa token path + base = base.WithNewFile("/var/run/secrets/kubernetes.io/serviceaccount/token", dagger.ContainerWithNewFileOpts{ + Contents: saToken, + }) + } + { + // JWT auth configuration + bytes, err := x509.MarshalPKIXPublicKey(priv.Public()) + if err != nil { + return err } + + bytes = pem.EncodeToMemory(&pem.Block{ + Type: "public key", + Bytes: bytes, + }) + + flipt = flipt. + WithNewFile("/etc/flipt/jwt.pem", dagger.ContainerWithNewFileOpts{Contents: string(bytes)}). + WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_ENABLED", "true"). + WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_PUBLIC_KEY_FILE", "/etc/flipt/jwt.pem"). + WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_VALIDATE_CLAIMS_ISSUER", "https://flipt.io") + + privBytes := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + }) + + base = base.WithNewFile("/var/run/secrets/flipt/private.pem", dagger.ContainerWithNewFileOpts{ + Contents: string(privBytes), + }) } name := strings.ToLower(replacer.Replace(fmt.Sprintf("flipt-test-%s-config-%s", caseName, config.name))) @@ -576,10 +545,7 @@ func oci(ctx context.Context, client *dagger.Client, base, flipt *dagger.Contain func importExport(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Container, conf testConfig) func() error { return func() error { // import testdata before running readonly suite - flags := []string{"--address", conf.address} - if conf.auth.enabled() { - flags = append(flags, "--token", bootstrapToken) - } + flags := []string{"--address", conf.address, "--token", bootstrapToken} // create unique instance for test case fliptToTest := flipt. @@ -658,34 +624,11 @@ func importExport(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Con func suite(ctx context.Context, dir string, base, flipt *dagger.Container, conf testConfig) func() error { return func() (err error) { - flags := []string{"--flipt-addr", conf.address} + flags := []string{"--flipt-addr", conf.address, "--flipt-token", bootstrapToken} if conf.references { flags = append(flags, "--flipt-supports-references") } - if conf.auth.enabled() { - flags = append(flags, "--flipt-token-type", conf.auth.method()) - - switch conf.auth.method() { - case "static": - flags = append(flags, "--flipt-token", bootstrapToken) - case "jwt": - var ( - now = time.Now() - nowUnix = float64(now.Unix()) - futureUnix = float64(now.Add(2 * jjwt.DefaultLeeway).Unix()) - ) - - token := signJWT(priv, map[string]interface{}{ - "iss": "https://flipt.io", - "iat": nowUnix, - "exp": futureUnix, - }) - - flags = append(flags, "--flipt-token", token) - } - } - _, err = base. WithWorkdir(path.Join("build/testing/integration", dir)). WithEnvVariable("UNIQUE", uuid.New().String()). diff --git a/build/testing/integration/api/api.go b/build/testing/integration/api/api.go index e286b65705..9d0445dcdc 100644 --- a/build/testing/integration/api/api.go +++ b/build/testing/integration/api/api.go @@ -16,15 +16,13 @@ import ( "go.flipt.io/flipt/build/testing/integration" "go.flipt.io/flipt/rpc/flipt" "go.flipt.io/flipt/rpc/flipt/evaluation" - sdk "go.flipt.io/flipt/sdk/go" "google.golang.org/protobuf/testing/protocmp" ) -func API(t *testing.T, ctx context.Context, client sdk.SDK, opts integration.TestOpts) { +func API(t *testing.T, ctx context.Context, opts integration.TestOpts) { var ( - addr = opts.Addr - protocol = opts.Protocol - authConfig = opts.AuthConfig + client = opts.DefaultClient(t) + protocol = opts.Protocol() ) t.Run("Namespaces", func(t *testing.T) { @@ -1336,17 +1334,13 @@ func API(t *testing.T, ctx context.Context, client sdk.SDK, opts integration.Tes } t.Run("Metrics", func(t *testing.T) { - if authConfig.Required() { - t.Skip("Skipping metrics test for now as it requires authentication") - } - if protocol == integration.ProtocolGRPC { - t.Skip("Skipping metrics test for now as it requires HTTP/HTTPS protocol") + t.Skip("TODO: we do not support metric test for grpc yet") } t.Log(`Ensure /metrics endpoint is reachable.`) - resp, err := http.Get(fmt.Sprintf("%s/metrics", addr)) + resp, err := http.Get(fmt.Sprintf("%s/metrics", opts.URL)) require.NoError(t, err) require.NotNil(t, resp) @@ -1417,11 +1411,6 @@ func API(t *testing.T, ctx context.Context, client sdk.SDK, opts integration.Tes t.Run("Auth", func(t *testing.T) { t.Run("Self", func(t *testing.T) { _, err := client.Auth().AuthenticationService().GetAuthenticationSelf(ctx) - if !authConfig.Required() { - assert.EqualError(t, err, "rpc error: code = Unauthenticated desc = request was not authenticated") - return - } - assert.NoError(t, err) }) t.Run("Public", func(t *testing.T) { @@ -1431,11 +1420,12 @@ func API(t *testing.T, ctx context.Context, client sdk.SDK, opts integration.Tes }) t.Run("Healthcheck", func(t *testing.T) { - if protocol == "grpc" { + if protocol == integration.ProtocolGRPC { t.Skip("TODO: we do not support healthcheck test for grpc yet") } + t.Run("HTTP", func(t *testing.T) { - resp, err := http.Get(fmt.Sprintf("%s/health", addr)) + resp, err := http.Get(fmt.Sprintf("%s/health", opts.URL)) require.NoError(t, err) assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) diff --git a/build/testing/integration/api/api_test.go b/build/testing/integration/api/api_test.go index cb6a683666..6150a4bd31 100644 --- a/build/testing/integration/api/api_test.go +++ b/build/testing/integration/api/api_test.go @@ -6,18 +6,15 @@ import ( "go.flipt.io/flipt/build/testing/integration" "go.flipt.io/flipt/build/testing/integration/api" - sdk "go.flipt.io/flipt/sdk/go" ) func TestAPI(t *testing.T) { - integration.Harness(t, func(t *testing.T, sdk sdk.SDK, opts integration.TestOpts) { + integration.Harness(t, func(t *testing.T, opts integration.TestOpts) { ctx := context.Background() - api.API(t, ctx, sdk, opts) + api.API(t, ctx, opts) - // run extra tests in authenticated context - if opts.AuthConfig.Required() { - api.Authenticated(t, sdk, opts) - } + // run extra authenticaiton related tests + api.Authenticated(t, opts) }) } diff --git a/build/testing/integration/api/authenticated.go b/build/testing/integration/api/authenticated.go index aa600bcfb4..c9e5cf3aef 100644 --- a/build/testing/integration/api/authenticated.go +++ b/build/testing/integration/api/authenticated.go @@ -10,12 +10,13 @@ import ( "go.flipt.io/flipt/build/testing/integration" "go.flipt.io/flipt/rpc/flipt" "go.flipt.io/flipt/rpc/flipt/auth" - sdk "go.flipt.io/flipt/sdk/go" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -func Authenticated(t *testing.T, client sdk.SDK, opts integration.TestOpts) { +func Authenticated(t *testing.T, opts integration.TestOpts) { + client := opts.DefaultClient(t) + t.Run("Authentication Methods", func(t *testing.T) { ctx := context.Background() @@ -30,10 +31,6 @@ func Authenticated(t *testing.T, client sdk.SDK, opts integration.TestOpts) { }) t.Run("Get Self", func(t *testing.T) { - if !opts.AuthConfig.StaticToken() { - t.Skip("Skipping test for non-static token authentication") - } - authn, err := client.Auth().AuthenticationService().GetAuthenticationSelf(ctx) require.NoError(t, err) @@ -68,10 +65,6 @@ func Authenticated(t *testing.T, client sdk.SDK, opts integration.TestOpts) { }) t.Run("Expire Self", func(t *testing.T) { - if !opts.AuthConfig.StaticToken() { - t.Skip("Skipping test for non-static token authentication") - } - err := client.Auth().AuthenticationService().ExpireAuthenticationSelf(ctx, &auth.ExpireAuthenticationSelfRequest{ ExpiresAt: flipt.Now(), }) diff --git a/build/testing/integration/integration.go b/build/testing/integration/integration.go index a7196c477f..6d4a91331f 100644 --- a/build/testing/integration/integration.go +++ b/build/testing/integration/integration.go @@ -1,11 +1,17 @@ package integration import ( + "crypto/x509" + "encoding/pem" "flag" - "fmt" + "net/url" + "os" "strings" "testing" + "time" + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/require" sdk "go.flipt.io/flipt/sdk/go" sdkgrpc "go.flipt.io/flipt/sdk/go/grpc" @@ -16,43 +22,10 @@ import ( var ( fliptAddr = flag.String("flipt-addr", "grpc://localhost:9000", "Address for running Flipt instance (gRPC only)") - fliptTokenType = flag.String("flipt-token-type", "static", "Type of token to be used during test suite (static, jwt, k8s)") - fliptToken = flag.String("flipt-token", "", "Authentication token to be used during test suite") + fliptToken = flag.String("flipt-token", "", "Full-Access authentication token to be used during test suite") fliptReferences = flag.Bool("flipt-supports-references", false, "Identifies the backend as supporting references") ) -type AuthConfig int - -const ( - NoAuth AuthConfig = iota - StaticTokenAuth - JWTAuth - K8sAuth -) - -func (a AuthConfig) String() string { - switch a { - case NoAuth: - return "NoAuth" - case StaticTokenAuth: - return "StaticTokenAuth" - case JWTAuth: - return "JWTAuth" - case K8sAuth: - return "K8sAuth" - default: - return "Unknown" - } -} - -func (a AuthConfig) StaticToken() bool { - return a == StaticTokenAuth -} - -func (a AuthConfig) Required() bool { - return a != NoAuth -} - type Protocol string const ( @@ -61,13 +34,6 @@ const ( ProtocolGRPC Protocol = "grpc" ) -type TestOpts struct { - Addr string - Protocol Protocol - AuthConfig AuthConfig - References bool -} - const ( DefaultNamespace = "default" ProductionNamespace = "production" @@ -82,64 +48,91 @@ var Namespaces = []struct { {Key: ProductionNamespace, Expected: ProductionNamespace}, } -func Harness(t *testing.T, fn func(t *testing.T, sdk sdk.SDK, opts TestOpts)) { - var transport sdk.Transport +func Harness(t *testing.T, fn func(t *testing.T, opts TestOpts)) { + u, err := url.Parse(*fliptAddr) + if err != nil { + t.Fatal(err) + } - p, host, _ := strings.Cut(*fliptAddr, "://") - protocol := Protocol(p) + fn(t, TestOpts{ + URL: u, + References: *fliptReferences, + Token: *fliptToken, + }) +} - switch protocol { - case ProtocolGRPC: - conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) +type TestOpts struct { + URL *url.URL + References bool + Token string +} - transport = sdkgrpc.NewTransport(conn) - case ProtocolHTTP, ProtocolHTTPS: - transport = sdkhttp.NewTransport(fmt.Sprintf("%s://%s", protocol, host)) - default: - t.Fatalf("Unexpected flipt address protocol %s://%s", protocol, host) +func (o TestOpts) Protocol() Protocol { + if o.URL.Scheme == "" { + return ProtocolHTTP + } + + return Protocol(strings.TrimSuffix(o.URL.Scheme, ":")) +} + +func (o TestOpts) DefaultClient(t *testing.T) sdk.SDK { + return sdk.New(o.newTransport(t), sdk.WithAuthenticationProvider( + sdk.StaticTokenAuthenticationProvider(o.Token), + )) +} + +func (o TestOpts) K8sClient(t *testing.T) sdk.SDK { + transport := o.newTransport(t) + return sdk.New(transport, sdk.WithAuthenticationProvider( + sdk.NewKubernetesAuthenticationProvider(transport), + )) +} + +func (o TestOpts) JWTClient(t *testing.T) sdk.SDK { + bytes, err := os.ReadFile("/var/run/secrets/flipt/private.pem") + if err != nil { + t.Fatal(err) } - var ( - opts []sdk.Option - authConfig AuthConfig - client sdk.SDK + block, _ := pem.Decode(bytes) + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + t.Fatal(err) + } + + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.RS256, Key: key}, + (&jose.SignerOptions{}).WithType("JWT"), ) - switch *fliptTokenType { - case "static": - if *fliptToken != "" { - authConfig = StaticTokenAuth - - opts = append(opts, sdk.WithAuthenticationProvider( - sdk.StaticTokenAuthenticationProvider(*fliptToken), - )) - - client = sdk.New(transport, opts...) - } - case "jwt": - if authentication := *fliptToken != ""; authentication { - authConfig = JWTAuth - opts = append(opts, sdk.WithAuthenticationProvider( - sdk.JWTAuthenticationProvider(*fliptToken), - )) - } - case "k8s": - authConfig = K8sAuth - opts = append(opts, sdk.WithAuthenticationProvider( - sdk.NewKubernetesAuthenticationProvider(transport), - )) + raw, err := jwt.Signed(sig). + Claims(map[string]any{ + "iss": "https://flipt.io", + "iat": time.Now().Unix(), + "exp": time.Now().Add(3 * time.Minute).Unix(), + }). + CompactSerialize() + if err != nil { + panic(err) } - client = sdk.New(transport, opts...) + return sdk.New(o.newTransport(t), sdk.WithAuthenticationProvider( + sdk.JWTAuthenticationProvider(raw), + )) +} - name := fmt.Sprintf("[Protocol %q; Authentication %s]", protocol, authConfig) - t.Run(name, func(t *testing.T) { - fn(t, client, TestOpts{ - Protocol: protocol, - Addr: *fliptAddr, - AuthConfig: authConfig, - References: *fliptReferences, - }) - }) +func (o TestOpts) newTransport(t *testing.T) (transport sdk.Transport) { + switch o.Protocol() { + case ProtocolGRPC: + conn, err := grpc.NewClient(o.URL.Host, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + transport = sdkgrpc.NewTransport(conn) + case ProtocolHTTP, ProtocolHTTPS: + transport = sdkhttp.NewTransport(o.URL.String()) + default: + t.Fatalf("Unexpected flipt address protocol %s", o.URL) + } + + return } diff --git a/build/testing/integration/readonly/readonly_test.go b/build/testing/integration/readonly/readonly_test.go index 4eacb22714..9572e723ea 100644 --- a/build/testing/integration/readonly/readonly_test.go +++ b/build/testing/integration/readonly/readonly_test.go @@ -10,17 +10,16 @@ import ( "go.flipt.io/flipt/build/testing/integration" "go.flipt.io/flipt/rpc/flipt" "go.flipt.io/flipt/rpc/flipt/evaluation" - sdk "go.flipt.io/flipt/sdk/go" ) // TestReadOnly is a suite of tests which presumes all the data found in the local testdata // folder has been loaded into the target instance being tested. // It then exercises a bunch of read operations via the provided SDK in the target namespace. func TestReadOnly(t *testing.T) { - integration.Harness(t, func(t *testing.T, sdk sdk.SDK, opts integration.TestOpts) { + integration.Harness(t, func(t *testing.T, opts integration.TestOpts) { var ( - ctx = context.Background() - authConfig = opts.AuthConfig + ctx = context.Background() + sdk = opts.DefaultClient(t) ) ns, err := sdk.Flipt().GetNamespace(ctx, &flipt.GetNamespaceRequest{ @@ -716,12 +715,9 @@ func TestReadOnly(t *testing.T) { t.Run("Auth", func(t *testing.T) { t.Run("Self", func(t *testing.T) { _, err := sdk.Auth().AuthenticationService().GetAuthenticationSelf(ctx) - if !authConfig.Required() { - assert.EqualError(t, err, "rpc error: code = Unauthenticated desc = request was not authenticated") - return - } assert.NoError(t, err) }) + t.Run("Public", func(t *testing.T) { _, err := sdk.Auth().PublicAuthenticationService().ListAuthenticationMethods(ctx) require.NoError(t, err)