Skip to content

Commit

Permalink
Cleanup created accounts after tests
Browse files Browse the repository at this point in the history
- Without cleanup, the GetRolesForWebIdentity API ends up being
  very slow, eventually causing Vault to be unresponsive and
  thus causing timeouts, and tests failures
- The logic detects that we created an account (during cross-account
  tests), and delete all resources, before deleting the account.

Issue: ZENKO-4898
  • Loading branch information
williamlardier committed Sep 20, 2024
1 parent b32b58c commit 8f4cd25
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 3 deletions.
106 changes: 105 additions & 1 deletion tests/ctst/common/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import {
parallelCanAssignHelpers,
} from '@cucumber/cucumber';
import Zenko from '../world/Zenko';
import { Identity } from 'cli-testing';
import { IAM, Identity, IdentityEnum } from 'cli-testing';
import { prepareQuotaScenarios, teardownQuotaScenarios } from 'steps/quotas/quotas';
import { displayDebuggingInformation, preparePRA } from 'steps/pra';
import {
AttachedPolicy,
Group,
Policy,
Role,
User,
} from '@aws-sdk/client-iam';
import { listAllEntities, listAttachedPolicies } from './utils';

// HTTPS should not cause any error for CTST
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
Expand Down Expand Up @@ -41,4 +49,100 @@ After({ tags: '@Quotas' }, async function () {
await teardownQuotaScenarios(this as Zenko);
});

After(async function (this: Zenko) {
if (!this.getSaved('crossAccountName')) {
return;
}
try {
const crossAccountName = this.getSaved<string>('crossAccountName');
Identity.useIdentity(IdentityEnum.ACCOUNT, crossAccountName);

// List and detach policies for each user
const allUsers = await listAllEntities<User>(IAM.listUsers, 'Users');
for (const user of allUsers) {
const allUserPolicies = await listAttachedPolicies<AttachedPolicy>(
params => IAM.listAttachedUserPolicies({ userName: user.UserName, ...params }),
);
for (const policy of allUserPolicies) {
const result = await IAM.detachUserPolicy({
userName: user.UserName, policyArn: policy.PolicyArn });
if (result.err) {
throw new Error(result.err);
}
}
}

// List and detach policies for each group
const allGroups = await listAllEntities<Group>(IAM.listGroups, 'Groups');
for (const group of allGroups) {
const allGroupPolicies = await listAttachedPolicies<AttachedPolicy>(
params => IAM.listAttachedGroupPolicies({ groupName: group.GroupName, ...params }),
);
for (const policy of allGroupPolicies) {
const result = await IAM.detachGroupPolicy({
groupName: group.GroupName, policyArn: policy.PolicyArn });
if (result.err) {
throw new Error(result.err);
}
}
}

// List and detach policies for each role
const allRoles = await listAllEntities<Role>(IAM.listRoles, 'Roles');
for (const role of allRoles) {
const allRolePolicies = await listAttachedPolicies<AttachedPolicy>(
params => IAM.listAttachedRolePolicies({ roleName: role.RoleName, ...params }),
);
for (const policy of allRolePolicies) {
const result = await IAM.detachRolePolicy({
roleName: role.RoleName, policyArn: policy.PolicyArn });
if (result.err) {
throw new Error(result.err);
}
}
}

// Delete all policies
const allPolicies = await listAllEntities<Policy>(IAM.listPolicies, 'Policies');
for (const policy of allPolicies) {
const result = await IAM.deletePolicy({ policyArn: policy.Arn });
if (result.err) {
throw new Error(result.err);
}
}

// Delete all roles
for (const role of allRoles) {
const result = await IAM.deleteRole({ roleName: role.RoleName });
if (result.err) {
throw new Error(result.err);
}
}

// Delete all groups
for (const group of allGroups) {
const result = await IAM.deleteGroup({ groupName: group.GroupName });
if (result.err) {
throw new Error(result.err);
}
}

// Delete all users
for (const user of allUsers) {
const result = await IAM.deleteUser({ userName: user.UserName });
if (result.err) {
throw new Error(result.err);
}
}

// Finally, delete the account
await this.deleteAccount(this.getSaved('crossAccountName'));
} catch (err) {
this.logger?.error('Error while deleting cross account', {
crossAccountName: this.getSaved('crossAccountName'),
error: err,
});
}
});

export default Zenko;
59 changes: 57 additions & 2 deletions tests/ctst/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { exec } from 'child_process';
import http from 'http';
import { createHash } from 'crypto';
import { Command } from 'cli-testing';
import {
Command,
} from 'cli-testing';
AttachedPolicy,
Group,
Policy,
Role,
User,
} from '@aws-sdk/client-iam';
import AwsCliOptions from 'cli-testing/utils/AwsCliOptions';

/**
* This helper will dynamically extract a property from a CLI result
Expand Down Expand Up @@ -140,3 +146,52 @@ export async function request(options: http.RequestOptions, data: string | undef
export function hashStringAndKeepFirst20Characters(input: string) {
return createHash('sha256').update(input).digest('hex').slice(0, 20);
}

export async function listAllEntities<T extends User | Role | Group | Policy>(
listFn: (params: AwsCliOptions) => Promise<Command>,
responseKey: string,
): Promise<T[]> {
let marker;
const allEntities: T[] = [];
let parsedResponse;
do {
const response = await listFn({ marker });
if (response.err) {
throw new Error(response.err);
}
parsedResponse = JSON.parse(response.stdout);
const entities = parsedResponse[responseKey] || [];
entities.forEach((entity: T) => {
if (entity.Path?.includes('/scality-internal/')) {
return;
}
allEntities.push(entity);
});
marker = parsedResponse.Marker;
} while (parsedResponse.IsTruncated);
return allEntities;
};

export async function listAttachedPolicies<T extends AttachedPolicy>(
listFn: (params: AwsCliOptions) => Promise<Command>,
): Promise<T[]> {
let marker;
const allPolicies: T[] = [];
let parsedResponse;
do {
const response = await listFn({ marker });
if (response.err) {
throw new Error(response.err);
}
parsedResponse = JSON.parse(response.stdout);
const policies = parsedResponse.AttachedPolicies || [];
policies.forEach((policy: T) => {
if (policy.PolicyArn?.includes('/scality-internal/')) {
return;
}
allPolicies.push(policy);
});
marker = parsedResponse.Marker;
} while (parsedResponse.IsTruncated);
return allPolicies;
}
8 changes: 8 additions & 0 deletions tests/ctst/world/Zenko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,13 @@ export default class Zenko extends World<ZenkoWorldParameters> {
this.saveIdentityInformation(accountName, IdentityEnum.ACCOUNT, accountName);
}

async deleteAccount(name: string) {
if (!name) {
throw new Error('No account name provided');
}
await SuperAdmin.deleteAccount({ accountName: name });
}

/**
* Creates an assumed role session with a duration of 12 hours.
* @param {boolean} crossAccount - If true, the role will be assumed cross account.
Expand Down Expand Up @@ -476,6 +483,7 @@ export default class Zenko extends World<ZenkoWorldParameters> {
});

Identity.addIdentity(IdentityEnum.ACCOUNT, account2.account.name, account2Credentials, undefined, true);
this.addToSaved('crossAccountName', account2.account.name);

accountToBeAssumedFrom = account2.account.name;
}
Expand Down

0 comments on commit 8f4cd25

Please sign in to comment.