diff --git a/testing/internal/e2e/boundary/authmethod.go b/testing/internal/e2e/boundary/authmethod.go new file mode 100644 index 0000000000..edf143b5d1 --- /dev/null +++ b/testing/internal/e2e/boundary/authmethod.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package boundary + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/stretchr/testify/require" +) + +// CreateNewAuthMethodApi creates a new auth method using the Go api. +// Returns the id of the new auth method +func CreateNewAuthMethodApi(t testing.TB, ctx context.Context, client *api.Client, scopeId string) string { + aClient := authmethods.NewClient(client) + newAMResult, err := aClient.Create(ctx, "password", scopeId) + require.NoError(t, err) + + authMethodId := newAMResult.Item.Id + t.Logf("Created Auth Method: %s", authMethodId) + return authMethodId +} diff --git a/testing/internal/e2e/tests/base/paginate_auth_token_test.go b/testing/internal/e2e/tests/base/paginate_auth_token_test.go new file mode 100644 index 0000000000..ccbd2462b5 --- /dev/null +++ b/testing/internal/e2e/tests/base/paginate_auth_token_test.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_test + +import ( + "context" + "encoding/json" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/authtokens" + "github.com/hashicorp/boundary/api/scopes" + "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" +) + +// TestCliPaginateAuthTokens asserts that the CLI automatically paginates to retrieve +// all auth tokens in a single invocation. +func TestCliPaginateAuthTokens(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + client, err := boundary.NewApiClient() + require.NoError(t, err) + 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)) + }) + userId := boundary.CreateNewUserApi(t, ctx, client, newOrgId) + amId := boundary.CreateNewAuthMethodApi(t, ctx, client, newOrgId) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("auth-methods", "delete", "-id", amId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + accId, password := boundary.CreateNewAccountCli(t, ctx, amId, "testuser") + boundary.SetAccountToUserCli(t, ctx, userId, accId) + + boundary.AuthenticateCli(t, ctx, amId, "testuser", password) + + // Create enough auth tokens to overflow a single page. + // We created an auth token when we authenticated, so we + // need to remove it from the total auth token count. + numPrecreatedAuthTokens := 1 + var authTokenIds []string + for i := 0; i < c.MaxPageSize+1-numPrecreatedAuthTokens; i++ { + authTokenIds = append(authTokenIds, createAuthToken(t, ctx, client, amId, "testuser", password)) + } + + // List auth tokens + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-tokens", "list", + "-recursive", + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var initialAuthTokens authtokens.AuthTokenListResult + err = json.Unmarshal(output.Stdout, &initialAuthTokens) + require.NoError(t, err) + + var returnedIds []string + // Ignore the precreated auth token, which will appear at the end + for _, authtoken := range initialAuthTokens.Items[:len(initialAuthTokens.Items)-numPrecreatedAuthTokens] { + returnedIds = append(returnedIds, authtoken.Id) + } + + require.Len(t, initialAuthTokens.Items, c.MaxPageSize+1) + assert.Empty(t, cmp.Diff(returnedIds, authTokenIds, cmpopts.SortSlices(func(i, j string) bool { return i < j }))) + assert.Empty(t, initialAuthTokens.ResponseType) + assert.Empty(t, initialAuthTokens.RemovedIds) + assert.Empty(t, initialAuthTokens.ListToken) + + // Create a new auth token and destroy one of the other auth tokens + newAuthTokenId := createAuthToken(t, ctx, client, amId, "testuser", password) + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-tokens", "delete", + "-id", initialAuthTokens.Items[0].Id, + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // List again, should have the new auth token but not the deleted auth token + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-tokens", "list", + "-recursive", + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var newAuthTokens authtokens.AuthTokenListResult + err = json.Unmarshal(output.Stdout, &newAuthTokens) + require.NoError(t, err) + + require.Len(t, newAuthTokens.Items, c.MaxPageSize+1) + // The first item should be the most recently created, which + // should be our new auth token + firstItem := newAuthTokens.Items[0] + assert.Equal(t, newAuthTokenId, firstItem.Id) + assert.Empty(t, newAuthTokens.ResponseType) + assert.Empty(t, newAuthTokens.RemovedIds) + assert.Empty(t, newAuthTokens.ListToken) + // Ensure the deleted auth token isn't returned + for _, authtoken := range newAuthTokens.Items { + assert.NotEqual(t, authtoken.Id, initialAuthTokens.Items[0].Id) + } +} + +// TestApiPaginateAuthTokens asserts that the API automatically paginates to retrieve +// all auth tokens in a single invocation. +func TestApiPaginateAuthTokens(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + client, err := boundary.NewApiClient() + require.NoError(t, err) + adminToken := client.Token() + ctx := context.Background() + sClient := scopes.NewClient(client) + amClient := authmethods.NewClient(client) + atClient := authtokens.NewClient(client) + newOrgId := boundary.CreateNewOrgApi(t, ctx, client) + t.Cleanup(func() { + ctx := context.Background() + client.SetToken(adminToken) + _, err = sClient.Delete(ctx, newOrgId) + require.NoError(t, err) + }) + userId := boundary.CreateNewUserApi(t, ctx, client, newOrgId) + amId := boundary.CreateNewAuthMethodApi(t, ctx, client, newOrgId) + t.Cleanup(func() { + ctx := context.Background() + client.SetToken(adminToken) + _, err := amClient.Delete(ctx, amId) + require.NoError(t, err) + }) + accId, password := boundary.CreateNewAccountCli(t, ctx, amId, "testuser") + boundary.SetAccountToUserCli(t, ctx, userId, accId) + + // Authenticate as the user + authenticationResult, err := amClient.Authenticate(ctx, amId, "login", + map[string]any{ + "login_name": "testuser", + "password": password, + }, + ) + require.NoError(t, err) + at, err := authenticationResult.GetAuthToken() + require.NoError(t, err) + client.SetToken(at.Token) + + // Create enough auth tokens to overflow a single page. + // Creating an org comes with two automatically created auth tokens, + // "Login and Default Grants" and "Administration", so we + // need to remove them from the total auth token count. + numPrecreatedAuthTokens := 1 + var authtokenIds []string + for i := 0; i < c.MaxPageSize+1-numPrecreatedAuthTokens; i++ { + authtokenIds = append(authtokenIds, createAuthToken(t, ctx, client, amId, "testuser", password)) + } + + // List auth tokens + initialAuthTokens, err := atClient.List(ctx, newOrgId, authtokens.WithRecursive(true)) + require.NoError(t, err) + + var returnedIds []string + // Ignore the precreated auth tokens, which will appear at the end + for _, authtoken := range initialAuthTokens.Items[:len(initialAuthTokens.Items)-numPrecreatedAuthTokens] { + returnedIds = append(returnedIds, authtoken.Id) + } + + require.Len(t, initialAuthTokens.Items, c.MaxPageSize+1) + assert.Empty(t, cmp.Diff(returnedIds, authtokenIds, cmpopts.SortSlices(func(i, j string) bool { return i < j }))) + assert.Equal(t, "complete", initialAuthTokens.ResponseType) + assert.Empty(t, initialAuthTokens.RemovedIds) + assert.NotEmpty(t, initialAuthTokens.ListToken) + mapItems, ok := initialAuthTokens.GetResponse().Map["items"] + require.True(t, ok) + mapSliceItems, ok := mapItems.([]any) + require.True(t, ok) + assert.Len(t, mapSliceItems, c.MaxPageSize+1) + + // Create a new auth token and destroy one of the other auth tokens + newAuthTokenId := createAuthToken(t, ctx, client, amId, "testuser", password) + _, err = atClient.Delete(ctx, initialAuthTokens.Items[0].Id) + require.NoError(t, err) + + // List again, should have the new and deleted auth token + newAuthTokens, err := atClient.List(ctx, newOrgId, authtokens.WithListToken(initialAuthTokens.ListToken), authtokens.WithRecursive(true)) + require.NoError(t, err) + + // Note that this will likely contain all the auth tokens, + // since they were created very shortly before the listing, + // and we add a 30 second buffer to the lower bound of update + // times when listing. + require.GreaterOrEqual(t, len(newAuthTokens.Items), 1) + // The first item should be the most recently created, which + // should be our new auth token + firstItem := newAuthTokens.Items[0] + assert.Equal(t, newAuthTokenId, firstItem.Id) + assert.Equal(t, "complete", newAuthTokens.ResponseType) + // Note that the removed IDs may contain entries from other tests, + // so just check that there is at least 1 entry and that our entry + // is somewhere in the list. + require.GreaterOrEqual(t, len(newAuthTokens.RemovedIds), 1) + assert.True(t, slices.ContainsFunc(newAuthTokens.RemovedIds, func(authtokenId string) bool { + return authtokenId == initialAuthTokens.Items[0].Id + })) + assert.NotEmpty(t, newAuthTokens.ListToken) + // Check that the response map contains all entries + mapItems, ok = newAuthTokens.GetResponse().Map["items"] + require.True(t, ok) + mapSliceItems, ok = mapItems.([]any) + require.True(t, ok) + assert.GreaterOrEqual(t, len(mapSliceItems), 1) +} + +func createAuthToken(t testing.TB, ctx context.Context, client *api.Client, authMethodId, loginName, password string) string { + authmethodsClient := authmethods.NewClient(client) + authenticationResult, err := authmethodsClient.Authenticate(ctx, authMethodId, "login", + map[string]any{ + "login_name": loginName, + "password": password, + }, + ) + require.NoError(t, err) + at, err := authenticationResult.GetAuthToken() + require.NoError(t, err) + return at.Id +}