Skip to content

Commit 5461c89

Browse files
committed
fix: FGAC cleanup on destroy with Secrets Manager or --opensearch-password
Destroy now removes backend role mappings from the OpenSearch domain before deleting the Application. Gets password from Secrets Manager (CLI-created domains) or --opensearch-password flag (reused domains). Skips gracefully if no password available. Signed-off-by: Kyle Hounslow <kylhouns@amazon.com>
1 parent d49de3a commit 5461c89

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

aws/cli-installer/src/cli.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ function parseDestroyArgs(argv) {
8080
.name('observability-stack-aws-cli destroy')
8181
.description('Tear down all AWS resources created by the CLI for a given pipeline name')
8282
.requiredOption('--pipeline-name <name>', 'Pipeline name used during creation')
83-
.option('--region <region>', 'AWS region', process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION);
83+
.option('--region <region>', 'AWS region', process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)
84+
.option('--opensearch-password <password>', 'Master password (if domain was not created by CLI)');
8485

8586
program.parse(argv.slice(1));
8687
return { _command: 'destroy', ...program.opts() };

aws/cli-installer/src/destroy.mjs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,67 @@
33
* Deletes in reverse dependency order: EC2 → Application → DQS → OSIS → IAM → (preserves AOS/AMP).
44
*/
55
import { OSISClient, DeletePipelineCommand, GetPipelineCommand } from '@aws-sdk/client-osis';
6-
import { OpenSearchClient, ListApplicationsCommand, DeleteApplicationCommand, DeleteDirectQueryDataSourceCommand } from '@aws-sdk/client-opensearch';
6+
import { OpenSearchClient, ListApplicationsCommand, DeleteApplicationCommand, DeleteDirectQueryDataSourceCommand, GetApplicationCommand, DescribeDomainCommand } from '@aws-sdk/client-opensearch';
77
import { IAMClient, DeleteRolePolicyCommand, DeleteRoleCommand, ListRolePoliciesCommand } from '@aws-sdk/client-iam';
88
import { printStep, printSuccess, printWarning, printInfo, createSpinner } from './ui.mjs';
99
import { teardownDemoInstance } from './ec2-demo.mjs';
1010

11+
async function cleanupFgacRoles(region, pipelineName, opensearchPassword, os) {
12+
try {
13+
const { ApplicationSummaries } = await os.send(new ListApplicationsCommand({}));
14+
const app = (ApplicationSummaries || []).find(a => a.name === pipelineName);
15+
if (!app) return;
16+
17+
const { dataSources } = await os.send(new GetApplicationCommand({ id: app.id }));
18+
const domainArn = (dataSources || []).find(d => d.dataSourceArn?.includes(':domain/'))?.dataSourceArn;
19+
if (!domainArn) return;
20+
21+
const domainName = domainArn.split('/').pop();
22+
const { DomainStatus } = await os.send(new DescribeDomainCommand({ DomainName: domainName }));
23+
if (!DomainStatus?.Endpoint) return;
24+
25+
// Get password from Secrets Manager or flag
26+
let masterPass = opensearchPassword || '';
27+
if (!masterPass) {
28+
try {
29+
const { SecretsManagerClient, GetSecretValueCommand } = await import('@aws-sdk/client-secrets-manager');
30+
const sm = new SecretsManagerClient({ region });
31+
const { SecretString } = await sm.send(new GetSecretValueCommand({
32+
SecretId: `open-stack/${pipelineName}/master-password`,
33+
}));
34+
masterPass = SecretString;
35+
} catch { /* no secret found */ }
36+
}
37+
if (!masterPass) {
38+
printWarning('No master password available. FGAC cleanup skipped. Pass --opensearch-password to clean up role mappings.');
39+
return;
40+
}
41+
42+
const endpoint = `https://${DomainStatus.Endpoint}`;
43+
const url = `${endpoint}/_plugins/_security/api/rolesmapping/all_access`;
44+
const auth = Buffer.from(`admin:${masterPass}`).toString('base64');
45+
const headers = { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}` };
46+
47+
const getResp = await fetch(url, { headers });
48+
if (!getResp.ok) return;
49+
50+
const data = await getResp.json();
51+
const existing = data?.all_access?.backend_roles || [];
52+
const filtered = existing.filter(r => !r.includes(pipelineName));
53+
54+
if (filtered.length !== existing.length) {
55+
await fetch(url, {
56+
method: 'PATCH',
57+
headers,
58+
body: JSON.stringify([{ op: 'add', path: '/backend_roles', value: filtered }]),
59+
});
60+
printSuccess('FGAC backend role mappings cleaned up');
61+
}
62+
} catch (e) {
63+
printWarning(`FGAC cleanup: ${e.message}`);
64+
}
65+
}
66+
1167
export async function destroy(cfg) {
1268
const { pipelineName, region } = cfg;
1369
if (!pipelineName) throw new Error('--pipeline-name is required');
@@ -18,8 +74,11 @@ export async function destroy(cfg) {
1874
// 1. EC2 demo instance + SG + instance profile
1975
await teardownDemoInstance(cfg);
2076

21-
// 2. OpenSearch Application
77+
// 2. Clean up FGAC backend role mappings (before deleting Application)
2278
const os = new OpenSearchClient({ region });
79+
await cleanupFgacRoles(region, pipelineName, cfg.opensearchPassword, os);
80+
81+
// 3. OpenSearch Application
2382
try {
2483
const { ApplicationSummaries } = await os.send(new ListApplicationsCommand({}));
2584
const app = (ApplicationSummaries || []).find(a => a.name === pipelineName);

0 commit comments

Comments
 (0)