Skip to content

Commit f6f869e

Browse files
authored
release: 2.17 #82
release: 2.17
2 parents a73fbf3 + 1ca3853 commit f6f869e

15 files changed

Lines changed: 777 additions & 103 deletions

.github/workflows/sync-release-to-main.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ jobs:
4343
--body "Automated sync of release tags back to main."
4444
fi
4545
46-
- name: Auto-merge sync PR
46+
- name: Enable auto-merge on sync PR
4747
if: steps.check.outputs.ahead != '0'
4848
env:
4949
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5050
run: |
5151
PR=$(gh pr list --repo ${{ github.repository }} --base main --head release --state open --json number --jq '.[0].number')
5252
if [ -n "$PR" ]; then
53-
gh pr merge "$PR" --repo ${{ github.repository }} --merge
53+
gh pr merge "$PR" --repo ${{ github.repository }} --merge --auto
5454
fi

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
"dependencies": {
9393
"@aws-sdk/credential-providers": "^3.1024.0",
9494
"@smithy/shared-ini-file-loader": "^4.4.7",
95-
"@tigrisdata/iam": "^1.4.1",
95+
"@tigrisdata/iam": "^2.1.0",
9696
"@tigrisdata/storage": "^3.0.0",
9797
"commander": "^14.0.3",
9898
"enquirer": "^2.4.1",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import enquirer from 'enquirer';
2+
const { prompt } = enquirer;
3+
import { getIAMConfig } from '@auth/iam.js';
4+
import {
5+
attachPolicyToAccessKey,
6+
listPolicies,
7+
listPoliciesForAccessKey,
8+
} from '@tigrisdata/iam';
9+
import { failWithError } from '@utils/exit.js';
10+
import { requireInteractive } from '@utils/interactive.js';
11+
import { msg, printStart, printSuccess } from '@utils/messages.js';
12+
import { getFormat, getOption } from '@utils/options.js';
13+
14+
const context = msg('access-keys', 'attach-policy');
15+
16+
export default async function attachPolicy(options: Record<string, unknown>) {
17+
printStart(context);
18+
19+
const format = getFormat(options);
20+
21+
const id = getOption<string>(options, ['id']);
22+
let policyArn = getOption<string>(options, ['policyArn', 'policy-arn']);
23+
24+
if (!id) {
25+
failWithError(context, 'Access key ID is required');
26+
}
27+
28+
const config = await getIAMConfig(context);
29+
30+
if (!policyArn) {
31+
requireInteractive('Use --policy-arn to specify the policy ARN');
32+
33+
// Fetch all policies and assigned policies in parallel
34+
const [allPoliciesResult, assignedResult] = await Promise.all([
35+
listPolicies({ config }),
36+
listPoliciesForAccessKey(id, { config }),
37+
]);
38+
39+
if (allPoliciesResult.error) {
40+
failWithError(context, allPoliciesResult.error);
41+
}
42+
43+
if (assignedResult.error) {
44+
failWithError(context, assignedResult.error);
45+
}
46+
47+
const assignedNames = new Set(assignedResult.data.policies);
48+
const available = allPoliciesResult.data.policies.filter(
49+
(p) => !assignedNames.has(p.name)
50+
);
51+
52+
if (available.length === 0) {
53+
failWithError(
54+
context,
55+
'No unassigned policies available. All policies are already attached to this access key.'
56+
);
57+
}
58+
59+
const { selected } = await prompt<{ selected: string }>({
60+
type: 'select',
61+
name: 'selected',
62+
message: 'Select a policy to attach:',
63+
choices: available.map((p) => ({
64+
name: p.resource,
65+
message: `${p.name} (${p.resource})`,
66+
})),
67+
});
68+
69+
policyArn = selected;
70+
}
71+
72+
const { error } = await attachPolicyToAccessKey(id, policyArn, { config });
73+
74+
if (error) {
75+
failWithError(context, error);
76+
}
77+
78+
if (format === 'json') {
79+
console.log(JSON.stringify({ action: 'attached', id, policyArn }));
80+
}
81+
82+
printSuccess(context);
83+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import enquirer from 'enquirer';
2+
const { prompt } = enquirer;
3+
import { getIAMConfig } from '@auth/iam.js';
4+
import {
5+
detachPolicyFromAccessKey,
6+
listPolicies,
7+
listPoliciesForAccessKey,
8+
} from '@tigrisdata/iam';
9+
import { failWithError } from '@utils/exit.js';
10+
import { confirm, requireInteractive } from '@utils/interactive.js';
11+
import { msg, printStart, printSuccess } from '@utils/messages.js';
12+
import { getFormat, getOption } from '@utils/options.js';
13+
14+
const context = msg('access-keys', 'detach-policy');
15+
16+
export default async function detachPolicy(options: Record<string, unknown>) {
17+
printStart(context);
18+
19+
const format = getFormat(options);
20+
21+
const id = getOption<string>(options, ['id']);
22+
let policyArn = getOption<string>(options, ['policyArn', 'policy-arn']);
23+
const force = getOption<boolean>(options, ['yes', 'y', 'force']);
24+
25+
if (!id) {
26+
failWithError(context, 'Access key ID is required');
27+
}
28+
29+
const config = await getIAMConfig(context);
30+
31+
if (!policyArn) {
32+
requireInteractive('Use --policy-arn to specify the policy ARN');
33+
34+
// Fetch assigned policy names and all policies to resolve ARNs
35+
const [assignedResult, allPoliciesResult] = await Promise.all([
36+
listPoliciesForAccessKey(id, { config }),
37+
listPolicies({ config }),
38+
]);
39+
40+
if (assignedResult.error) {
41+
failWithError(context, assignedResult.error);
42+
}
43+
44+
if (allPoliciesResult.error) {
45+
failWithError(context, allPoliciesResult.error);
46+
}
47+
48+
const assignedNames = new Set(assignedResult.data.policies);
49+
const assigned = allPoliciesResult.data.policies.filter((p) =>
50+
assignedNames.has(p.name)
51+
);
52+
53+
if (assigned.length === 0) {
54+
failWithError(context, 'No policies are attached to this access key.');
55+
}
56+
57+
const { selected } = await prompt<{ selected: string }>({
58+
type: 'select',
59+
name: 'selected',
60+
message: 'Select a policy to detach:',
61+
choices: assigned.map((p) => ({
62+
name: p.resource,
63+
message: `${p.name} (${p.resource})`,
64+
})),
65+
});
66+
67+
policyArn = selected;
68+
}
69+
70+
if (!force) {
71+
requireInteractive('Use --yes to skip confirmation');
72+
const confirmed = await confirm(
73+
`Detach policy '${policyArn}' from access key '${id}'?`
74+
);
75+
if (!confirmed) {
76+
console.log('Aborted');
77+
return;
78+
}
79+
}
80+
81+
const { error } = await detachPolicyFromAccessKey(id, policyArn, { config });
82+
83+
if (error) {
84+
failWithError(context, error);
85+
}
86+
87+
if (format === 'json') {
88+
console.log(JSON.stringify({ action: 'detached', id, policyArn }));
89+
}
90+
91+
printSuccess(context);
92+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { getIAMConfig } from '@auth/iam.js';
2+
import { listPoliciesForAccessKey } from '@tigrisdata/iam';
3+
import { failWithError } from '@utils/exit.js';
4+
import { formatPaginatedOutput } from '@utils/format.js';
5+
import {
6+
msg,
7+
printEmpty,
8+
printPaginationHint,
9+
printStart,
10+
printSuccess,
11+
} from '@utils/messages.js';
12+
import { getFormat, getPaginationOptions } from '@utils/options.js';
13+
import { getOption } from '@utils/options.js';
14+
15+
const context = msg('access-keys', 'list-policies');
16+
17+
export default async function listPolicies(options: Record<string, unknown>) {
18+
printStart(context);
19+
20+
const format = getFormat(options);
21+
const { limit, pageToken } = getPaginationOptions(options);
22+
23+
const id = getOption<string>(options, ['id']);
24+
25+
if (!id) {
26+
failWithError(context, 'Access key ID is required');
27+
}
28+
29+
const config = await getIAMConfig(context);
30+
31+
const { data, error } = await listPoliciesForAccessKey(id, {
32+
...(limit !== undefined ? { limit } : {}),
33+
...(pageToken ? { paginationToken: pageToken } : {}),
34+
config,
35+
});
36+
37+
if (error) {
38+
failWithError(context, error);
39+
}
40+
41+
if (!data.policies || data.policies.length === 0) {
42+
printEmpty(context);
43+
return;
44+
}
45+
46+
const policies = data.policies.map((name) => ({ policy: name }));
47+
48+
const columns = [
49+
{
50+
key: 'policy',
51+
header: policies.length > 1 ? 'Attached Policies' : 'Attached Policy',
52+
},
53+
];
54+
55+
const nextToken = data.paginationToken || undefined;
56+
57+
const output = formatPaginatedOutput(
58+
policies,
59+
format!,
60+
'policies',
61+
'policy',
62+
columns,
63+
{ paginationToken: nextToken }
64+
);
65+
66+
console.log(output);
67+
68+
if (format !== 'json' && format !== 'xml') {
69+
printPaginationHint(nextToken);
70+
}
71+
72+
printSuccess(context, { count: policies.length });
73+
}

src/lib/access-keys/rotate.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { getIAMConfig } from '@auth/iam.js';
2+
import { rotateAccessKey } from '@tigrisdata/iam';
3+
import {
4+
failWithError,
5+
getSuccessNextActions,
6+
printNextActions,
7+
} from '@utils/exit.js';
8+
import { confirm, requireInteractive } from '@utils/interactive.js';
9+
import { msg, printStart, printSuccess } from '@utils/messages.js';
10+
import { getFormat, getOption } from '@utils/options.js';
11+
12+
const context = msg('access-keys', 'rotate');
13+
14+
export default async function rotate(options: Record<string, unknown>) {
15+
printStart(context);
16+
17+
const format = getFormat(options);
18+
19+
const id = getOption<string>(options, ['id']);
20+
const force = getOption<boolean>(options, ['yes', 'y', 'force']);
21+
22+
if (!id) {
23+
failWithError(context, 'Access key ID is required');
24+
}
25+
26+
if (!force) {
27+
requireInteractive('Use --yes to skip confirmation');
28+
const confirmed = await confirm(
29+
`Rotate access key '${id}'? The current secret will be invalidated.`
30+
);
31+
if (!confirmed) {
32+
console.log('Aborted');
33+
return;
34+
}
35+
}
36+
37+
const config = await getIAMConfig(context);
38+
39+
const { data, error } = await rotateAccessKey(id, { config });
40+
41+
if (error) {
42+
failWithError(context, error);
43+
}
44+
45+
if (format === 'json') {
46+
const nextActions = getSuccessNextActions(context, { id: data.id });
47+
const output: Record<string, unknown> = {
48+
action: 'rotated',
49+
id: data.id,
50+
secret: data.newSecret,
51+
};
52+
if (nextActions.length > 0) output.nextActions = nextActions;
53+
console.log(JSON.stringify(output));
54+
} else {
55+
console.log(` Access Key ID: ${data.id}`);
56+
console.log(` New Secret Access Key: ${data.newSecret}`);
57+
console.log('');
58+
console.log(
59+
' Save these credentials securely. The secret will not be shown again.'
60+
);
61+
}
62+
63+
printSuccess(context);
64+
printNextActions(context, { id: data.id });
65+
}

0 commit comments

Comments
 (0)