From 8bd883a37da132a149014c285e65c292b25ddfbf Mon Sep 17 00:00:00 2001 From: Michael Li Date: Thu, 15 Dec 2022 15:49:18 -0500 Subject: [PATCH] test(e2e): Add tests to validate that session ends after target is deleted (#2709) * test(e2e): Add more "session end" tests These tests validate that the session ends when the respective project or target is deleted. It also ensures that the session can still be read after the deletion of the corresponding item. * Removing test from PR due to bug This test found a bug that will be resolved in the future. Taking this test out of the PR and will add it once the bug is fixed. --- testing/internal/e2e/boundary/boundary.go | 5 + testing/internal/e2e/helpers.go | 5 - .../e2e/tests/static/credential_store_test.go | 2 +- .../tests/static/session_cancel_group_test.go | 2 +- .../tests/static/session_cancel_user_test.go | 2 +- .../session_end_delete_host_set_test.go | 10 +- .../static/session_end_delete_host_test.go | 10 +- .../static/session_end_delete_target_test.go | 140 ++++++++++++++++++ .../static/session_end_delete_user_test.go | 10 +- 9 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 testing/internal/e2e/tests/static/session_end_delete_target_test.go diff --git a/testing/internal/e2e/boundary/boundary.go b/testing/internal/e2e/boundary/boundary.go index 0878711d44..48029abb7e 100644 --- a/testing/internal/e2e/boundary/boundary.go +++ b/testing/internal/e2e/boundary/boundary.go @@ -39,6 +39,11 @@ type DbInitInfo struct { AuthMethod AuthMethodInfo `json:"auth_method"` } +// CliError parses the Stderr from running a boundary command +type CliError struct { + Status int `json:"status"` +} + func loadConfig() (*config, error) { var c config err := envconfig.Process("", &c) diff --git a/testing/internal/e2e/helpers.go b/testing/internal/e2e/helpers.go index 9595e678a4..34dc15a2c7 100644 --- a/testing/internal/e2e/helpers.go +++ b/testing/internal/e2e/helpers.go @@ -18,11 +18,6 @@ type CommandResult struct { Err error } -// CliError parses the Stderr from running a boundary command -type CliError struct { - Status int `json:"status"` -} - // Option is a func that sets optional attributes for a call. This does not need // to be used directly, but instead option arguments are built from the // functions in this package. WithX options set a value to that given in the diff --git a/testing/internal/e2e/tests/static/credential_store_test.go b/testing/internal/e2e/tests/static/credential_store_test.go index 44707813cb..0bb5d015d9 100644 --- a/testing/internal/e2e/tests/static/credential_store_test.go +++ b/testing/internal/e2e/tests/static/credential_store_test.go @@ -85,7 +85,7 @@ func TestCliStaticCredentialStore(t *testing.T) { return fmt.Errorf("Deleted credential can still be read: '%s'", output.Stdout) } - var response e2e.CliError + var response boundary.CliError err := json.Unmarshal(output.Stderr, &response) require.NoError(t, err) statusCode := response.Status diff --git a/testing/internal/e2e/tests/static/session_cancel_group_test.go b/testing/internal/e2e/tests/static/session_cancel_group_test.go index 7d6ea01a50..40fd350bdb 100644 --- a/testing/internal/e2e/tests/static/session_cancel_group_test.go +++ b/testing/internal/e2e/tests/static/session_cancel_group_test.go @@ -80,7 +80,7 @@ func TestCliSessionCancelGroup(t *testing.T) { ), ) require.Error(t, output.Err, string(output.Stdout), string(output.Stderr)) - var response e2e.CliError + var response boundary.CliError err = json.Unmarshal(output.Stderr, &response) require.NoError(t, err) require.Equal(t, 403, int(response.Status)) diff --git a/testing/internal/e2e/tests/static/session_cancel_user_test.go b/testing/internal/e2e/tests/static/session_cancel_user_test.go index 4ef3260837..48f3b5e164 100644 --- a/testing/internal/e2e/tests/static/session_cancel_user_test.go +++ b/testing/internal/e2e/tests/static/session_cancel_user_test.go @@ -79,7 +79,7 @@ func TestCliSessionCancelUser(t *testing.T) { ), ) require.Error(t, output.Err, string(output.Stdout), string(output.Stderr)) - var response e2e.CliError + var response boundary.CliError err = json.Unmarshal(output.Stderr, &response) require.NoError(t, err) require.Equal(t, 403, int(response.Status)) diff --git a/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go b/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go index 5a04a66f93..e09623f9f0 100644 --- a/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go +++ b/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go @@ -97,9 +97,17 @@ func TestCliSessionEndWhenHostSetIsDeleted(t *testing.T) { // Check if session has terminated t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + err = backoff.RetryNotify( func() error { - // Check if session is active + // Check that session has been canceled or terminated output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), ) diff --git a/testing/internal/e2e/tests/static/session_end_delete_host_test.go b/testing/internal/e2e/tests/static/session_end_delete_host_test.go index 472d339fe7..887b7228ec 100644 --- a/testing/internal/e2e/tests/static/session_end_delete_host_test.go +++ b/testing/internal/e2e/tests/static/session_end_delete_host_test.go @@ -97,9 +97,17 @@ func TestCliSessionEndWhenHostIsDeleted(t *testing.T) { // Check if session has terminated t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + err = backoff.RetryNotify( func() error { - // Check if session is active + // Check that session has been canceled or terminated output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), ) diff --git a/testing/internal/e2e/tests/static/session_end_delete_target_test.go b/testing/internal/e2e/tests/static/session_end_delete_target_test.go new file mode 100644 index 0000000000..32743caeff --- /dev/null +++ b/testing/internal/e2e/tests/static/session_end_delete_target_test.go @@ -0,0 +1,140 @@ +package static_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/boundary/api/sessions" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCliSessionEndWhenTargetIsDeleted tests that an active session is canceled when the respective +// target for the session is deleted. +func TestCliSessionEndWhenTargetIsDeleted(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newProjectId := boundary.CreateNewProjectCli(t, ctx, newOrgId) + newHostCatalogId := boundary.CreateNewHostCatalogCli(t, ctx, newProjectId) + newHostSetId := boundary.CreateNewHostSetCli(t, ctx, newHostCatalogId) + newHostId := boundary.CreateNewHostCli(t, ctx, newHostCatalogId, c.TargetIp) + boundary.AddHostToHostSetCli(t, ctx, newHostSetId, newHostId) + newTargetId := boundary.CreateNewTargetCli(t, ctx, newProjectId, c.TargetPort) + boundary.AddHostSourceToTargetCli(t, ctx, newTargetId, newHostSetId) + acctName := "e2e-account" + newAccountId, acctPassword := boundary.CreateNewAccountCli(t, ctx, acctName) + t.Cleanup(func() { + boundary.AuthenticateAdminCli(t, context.Background()) + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("accounts", "delete", "-id", newAccountId), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newUserId := boundary.CreateNewUserCli(t, ctx, "global") + t.Cleanup(func() { + boundary.AuthenticateAdminCli(t, context.Background()) + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("users", "delete", "-id", newUserId), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + boundary.SetAccountToUserCli(t, ctx, newUserId, newAccountId) + newRoleId := boundary.CreateNewRoleCli(t, ctx, newProjectId) + boundary.AddGrantToRoleCli(t, ctx, newRoleId, "id=*;type=target;actions=authorize-session") + boundary.AddPrincipalToRoleCli(t, ctx, newRoleId, newUserId) + + // Connect to target to create a session + ctxCancel, cancel := context.WithCancel(context.Background()) + errChan := make(chan *e2e.CommandResult) + go func() { + token := boundary.GetAuthenticationTokenCli(t, ctx, acctName, acctPassword) + t.Log("Starting session as user...") + errChan <- e2e.RunCommand(ctxCancel, "boundary", + e2e.WithArgs( + "connect", + "-token", "env://E2E_AUTH_TOKEN", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname -i; sleep 60", + ), + e2e.WithEnv("E2E_AUTH_TOKEN", token), + ) + }() + t.Cleanup(cancel) + session := boundary.WaitForSessionToBeActiveCli(t, ctx, newProjectId) + assert.Equal(t, newTargetId, session.TargetId) + assert.Equal(t, newHostId, session.HostId) + + // Delete Target + t.Log("Deleting target...") + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("targets", "delete", "-id", newTargetId)) + require.NoError(t, output.Err, string(output.Stderr)) + + // Check if session has terminated + t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + + err = backoff.RetryNotify( + func() error { + // Check that session has been canceled or terminated + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), + ) + if output.Err != nil { + return backoff.Permanent(errors.New(string(output.Stderr))) + } + + var sessionReadResult sessions.SessionReadResult + err = json.Unmarshal(output.Stdout, &sessionReadResult) + if err != nil { + return backoff.Permanent(err) + } + + if sessionReadResult.Item.Status != "canceling" && sessionReadResult.Item.Status != "terminated" { + return errors.New(fmt.Sprintf("Session has unexpected status. Expected: %s, Actual: %s", + "`canceling` or `terminated`", + sessionReadResult.Item.Status, + )) + } + + return nil + }, + backoff.WithMaxRetries(backoff.NewConstantBackOff(3*time.Second), 5), + func(err error, td time.Duration) { + t.Logf("%s. Retrying...", err.Error()) + }, + ) + require.NoError(t, err) + t.Log("Session successfully ended after target was deleted") +} diff --git a/testing/internal/e2e/tests/static/session_end_delete_user_test.go b/testing/internal/e2e/tests/static/session_end_delete_user_test.go index faf1080db7..007c7ec00b 100644 --- a/testing/internal/e2e/tests/static/session_end_delete_user_test.go +++ b/testing/internal/e2e/tests/static/session_end_delete_user_test.go @@ -90,9 +90,17 @@ func TestCliSessionEndWhenUserIsDeleted(t *testing.T) { // Check if session has terminated t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + err = backoff.RetryNotify( func() error { - // Check if session is active + // Check that session has been canceled or terminated output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), )