Skip to content

Commit

Permalink
refactor(build): remove authentication from IT matrix (#3152)
Browse files Browse the repository at this point in the history
* refactor(build): remove authentication from IT matrix

Signed-off-by: George MacRorie <[email protected]>

* refactor(build/testing): move authentication tests into own suite

Signed-off-by: George MacRorie <[email protected]>

* chore(gh): add in auth/sqlite integration test case

Signed-off-by: George MacRorie <[email protected]>

* test(build): remove -p 1 from go test now that packages are isolated

Signed-off-by: George MacRorie <[email protected]>

* chore(build): rename auth to authn

Signed-off-by: George MacRorie <[email protected]>

---------

Signed-off-by: George MacRorie <[email protected]>
  • Loading branch information
GeorgeMac authored Jun 7, 2024
1 parent bd15e53 commit a898ee8
Show file tree
Hide file tree
Showing 10 changed files with 520 additions and 349 deletions.
1 change: 1 addition & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
"fs/azblob",
"fs/gcs",
"import/export",
"authn/sqlite",
]
steps:
- uses: actions/checkout@v4
Expand Down
225 changes: 94 additions & 131 deletions build/testing/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ var (
"fs/azblob": azblob,
"fs/gcs": gcs,
"import/export": importExport,
"authn/sqlite": authn,
}
)

type testConfig struct {
name string
address string
auth authConfig
port int
references bool
}
Expand All @@ -95,32 +95,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 {
Expand All @@ -139,28 +113,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
Expand All @@ -175,46 +134,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)))
Expand Down Expand Up @@ -573,40 +543,45 @@ func oci(ctx context.Context, client *dagger.Client, base, flipt *dagger.Contain
return suite(ctx, "readonly", base, flipt.WithExec(nil), conf)
}

func importInto(ctx context.Context, base, flipt, fliptToTest *dagger.Container, flags ...string) error {
for _, ns := range []string{"default", "production"} {
seed := base.File(path.Join(singleRevisionTestdataDir, ns+".yaml"))

var (
importCmd = append([]string{"/flipt", "import"}, append(flags, "import.yaml")...)
)
// use target flipt binary to invoke import
_, err := flipt.
WithEnvVariable("UNIQUE", uuid.New().String()).
// copy testdata import yaml from base
WithFile("import.yaml", seed).
WithServiceBinding("flipt", fliptToTest.AsService()).
// it appears it takes a little while for Flipt to come online
// For the go tests they have to compile and that seems to be enough
// time for the target Flipt to come up.
// However, in this case the flipt binary is prebuilt and needs a little sleep.
WithExec([]string{"sh", "-c", fmt.Sprintf("sleep 2 && %s", strings.Join(importCmd, " "))}).
Sync(ctx)
if err != nil {
return err
}
}

return nil
}

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.
WithEnvVariable("UNIQUE", uuid.New().String()).
WithExec(nil)

for _, ns := range []string{"default", "production"} {
seed := base.File(path.Join(singleRevisionTestdataDir, ns+".yaml"))

var (
importCmd = append([]string{"/flipt", "import"}, append(flags, "import.yaml")...)
)
// use target flipt binary to invoke import
_, err := flipt.
WithEnvVariable("UNIQUE", uuid.New().String()).
// copy testdata import yaml from base
WithFile("import.yaml", seed).
WithServiceBinding("flipt", fliptToTest.AsService()).
// it appears it takes a little while for Flipt to come online
// For the go tests they have to compile and that seems to be enough
// time for the target Flipt to come up.
// However, in this case the flipt binary is prebuilt and needs a little sleep.
WithExec([]string{"sh", "-c", fmt.Sprintf("sleep 2 && %s", strings.Join(importCmd, " "))}).
Sync(ctx)
if err != nil {
return err
}
if err := importInto(ctx, base, flipt, fliptToTest, flags...); err != nil {
return err
}

// run readonly suite against imported Flipt instance
Expand Down Expand Up @@ -656,36 +631,24 @@ func importExport(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Con
}
}

func authn(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Container, conf testConfig) func() error {
// create unique instance for test case
fliptToTest := flipt.WithEnvVariable("UNIQUE", uuid.New().String()).WithExec(nil)
// import state into instance before running test
if err := importInto(ctx, base, flipt, fliptToTest, "--address", conf.address, "--token", bootstrapToken); err != nil {
return func() error { return err }
}

return suite(ctx, "authn", base, fliptToTest, conf)
}

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()).
Expand Down
26 changes: 8 additions & 18 deletions build/testing/integration/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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"))
Expand Down
10 changes: 2 additions & 8 deletions build/testing/integration/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ 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)

// run extra tests in authenticated context
if opts.AuthConfig.Required() {
api.Authenticated(t, sdk, opts)
}
api.API(t, ctx, opts)
})
}
Loading

0 comments on commit a898ee8

Please sign in to comment.