diff --git a/package-lock.json b/package-lock.json index fc5f849..5a54e20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@aws-sdk/credential-providers": "^3.1024.0", "@smithy/shared-ini-file-loader": "^4.4.7", - "@tigrisdata/iam": "^1.4.1", + "@tigrisdata/iam": "^2.1.0", "@tigrisdata/storage": "^3.0.0", "commander": "^14.0.3", "enquirer": "^2.4.1", @@ -4006,9 +4006,9 @@ "license": "MIT" }, "node_modules/@tigrisdata/iam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@tigrisdata/iam/-/iam-1.4.1.tgz", - "integrity": "sha512-AQbFKbdosGuQoyKE0Mi90oUqbtsQ1sN1u67oABHod9hCqa3y558wxVZ9t/hkGdiDBwA1IkxAC6vvKTeJkrGDMA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tigrisdata/iam/-/iam-2.1.0.tgz", + "integrity": "sha512-jOFNjthKgugzEy5JIAKz8pw+bjSg+VEw7RcpbwUrPCf9AFmutojhF4KRptgFK7YYzSidoIzPCjWlDoyM+12I/w==", "license": "MIT", "dependencies": { "dotenv": "^17.3.1" diff --git a/package.json b/package.json index 4532e17..5e757f9 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "dependencies": { "@aws-sdk/credential-providers": "^3.1024.0", "@smithy/shared-ini-file-loader": "^4.4.7", - "@tigrisdata/iam": "^1.4.1", + "@tigrisdata/iam": "^2.1.0", "@tigrisdata/storage": "^3.0.0", "commander": "^14.0.3", "enquirer": "^2.4.1", diff --git a/src/lib/access-keys/attach-policy.ts b/src/lib/access-keys/attach-policy.ts new file mode 100644 index 0000000..38ce608 --- /dev/null +++ b/src/lib/access-keys/attach-policy.ts @@ -0,0 +1,83 @@ +import enquirer from 'enquirer'; +const { prompt } = enquirer; +import { getIAMConfig } from '@auth/iam.js'; +import { + attachPolicyToAccessKey, + listPolicies, + listPoliciesForAccessKey, +} from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { requireInteractive } from '@utils/interactive.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +const context = msg('access-keys', 'attach-policy'); + +export default async function attachPolicy(options: Record) { + printStart(context); + + const format = getFormat(options); + + const id = getOption(options, ['id']); + let policyArn = getOption(options, ['policyArn', 'policy-arn']); + + if (!id) { + failWithError(context, 'Access key ID is required'); + } + + const config = await getIAMConfig(context); + + if (!policyArn) { + requireInteractive('Use --policy-arn to specify the policy ARN'); + + // Fetch all policies and assigned policies in parallel + const [allPoliciesResult, assignedResult] = await Promise.all([ + listPolicies({ config }), + listPoliciesForAccessKey(id, { config }), + ]); + + if (allPoliciesResult.error) { + failWithError(context, allPoliciesResult.error); + } + + if (assignedResult.error) { + failWithError(context, assignedResult.error); + } + + const assignedNames = new Set(assignedResult.data.policies); + const available = allPoliciesResult.data.policies.filter( + (p) => !assignedNames.has(p.name) + ); + + if (available.length === 0) { + failWithError( + context, + 'No unassigned policies available. All policies are already attached to this access key.' + ); + } + + const { selected } = await prompt<{ selected: string }>({ + type: 'select', + name: 'selected', + message: 'Select a policy to attach:', + choices: available.map((p) => ({ + name: p.resource, + message: `${p.name} (${p.resource})`, + })), + }); + + policyArn = selected; + } + + const { error } = await attachPolicyToAccessKey(id, policyArn, { config }); + + if (error) { + failWithError(context, error); + } + + if (format === 'json') { + console.log(JSON.stringify({ action: 'attached', id, policyArn })); + } + + printSuccess(context); +} diff --git a/src/lib/access-keys/detach-policy.ts b/src/lib/access-keys/detach-policy.ts new file mode 100644 index 0000000..0588636 --- /dev/null +++ b/src/lib/access-keys/detach-policy.ts @@ -0,0 +1,92 @@ +import enquirer from 'enquirer'; +const { prompt } = enquirer; +import { getIAMConfig } from '@auth/iam.js'; +import { + detachPolicyFromAccessKey, + listPolicies, + listPoliciesForAccessKey, +} from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { confirm, requireInteractive } from '@utils/interactive.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +const context = msg('access-keys', 'detach-policy'); + +export default async function detachPolicy(options: Record) { + printStart(context); + + const format = getFormat(options); + + const id = getOption(options, ['id']); + let policyArn = getOption(options, ['policyArn', 'policy-arn']); + const force = getOption(options, ['yes', 'y', 'force']); + + if (!id) { + failWithError(context, 'Access key ID is required'); + } + + const config = await getIAMConfig(context); + + if (!policyArn) { + requireInteractive('Use --policy-arn to specify the policy ARN'); + + // Fetch assigned policy names and all policies to resolve ARNs + const [assignedResult, allPoliciesResult] = await Promise.all([ + listPoliciesForAccessKey(id, { config }), + listPolicies({ config }), + ]); + + if (assignedResult.error) { + failWithError(context, assignedResult.error); + } + + if (allPoliciesResult.error) { + failWithError(context, allPoliciesResult.error); + } + + const assignedNames = new Set(assignedResult.data.policies); + const assigned = allPoliciesResult.data.policies.filter((p) => + assignedNames.has(p.name) + ); + + if (assigned.length === 0) { + failWithError(context, 'No policies are attached to this access key.'); + } + + const { selected } = await prompt<{ selected: string }>({ + type: 'select', + name: 'selected', + message: 'Select a policy to detach:', + choices: assigned.map((p) => ({ + name: p.resource, + message: `${p.name} (${p.resource})`, + })), + }); + + policyArn = selected; + } + + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm( + `Detach policy '${policyArn}' from access key '${id}'?` + ); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + + const { error } = await detachPolicyFromAccessKey(id, policyArn, { config }); + + if (error) { + failWithError(context, error); + } + + if (format === 'json') { + console.log(JSON.stringify({ action: 'detached', id, policyArn })); + } + + printSuccess(context); +} diff --git a/src/lib/access-keys/list-policies.ts b/src/lib/access-keys/list-policies.ts new file mode 100644 index 0000000..05863a5 --- /dev/null +++ b/src/lib/access-keys/list-policies.ts @@ -0,0 +1,73 @@ +import { getIAMConfig } from '@auth/iam.js'; +import { listPoliciesForAccessKey } from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { formatPaginatedOutput } from '@utils/format.js'; +import { + msg, + printEmpty, + printPaginationHint, + printStart, + printSuccess, +} from '@utils/messages.js'; +import { getFormat, getPaginationOptions } from '@utils/options.js'; +import { getOption } from '@utils/options.js'; + +const context = msg('access-keys', 'list-policies'); + +export default async function listPolicies(options: Record) { + printStart(context); + + const format = getFormat(options); + const { limit, pageToken } = getPaginationOptions(options); + + const id = getOption(options, ['id']); + + if (!id) { + failWithError(context, 'Access key ID is required'); + } + + const config = await getIAMConfig(context); + + const { data, error } = await listPoliciesForAccessKey(id, { + ...(limit !== undefined ? { limit } : {}), + ...(pageToken ? { paginationToken: pageToken } : {}), + config, + }); + + if (error) { + failWithError(context, error); + } + + if (!data.policies || data.policies.length === 0) { + printEmpty(context); + return; + } + + const policies = data.policies.map((name) => ({ policy: name })); + + const columns = [ + { + key: 'policy', + header: policies.length > 1 ? 'Attached Policies' : 'Attached Policy', + }, + ]; + + const nextToken = data.paginationToken || undefined; + + const output = formatPaginatedOutput( + policies, + format!, + 'policies', + 'policy', + columns, + { paginationToken: nextToken } + ); + + console.log(output); + + if (format !== 'json' && format !== 'xml') { + printPaginationHint(nextToken); + } + + printSuccess(context, { count: policies.length }); +} diff --git a/src/lib/access-keys/rotate.ts b/src/lib/access-keys/rotate.ts new file mode 100644 index 0000000..6e62a72 --- /dev/null +++ b/src/lib/access-keys/rotate.ts @@ -0,0 +1,65 @@ +import { getIAMConfig } from '@auth/iam.js'; +import { rotateAccessKey } from '@tigrisdata/iam'; +import { + failWithError, + getSuccessNextActions, + printNextActions, +} from '@utils/exit.js'; +import { confirm, requireInteractive } from '@utils/interactive.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +const context = msg('access-keys', 'rotate'); + +export default async function rotate(options: Record) { + printStart(context); + + const format = getFormat(options); + + const id = getOption(options, ['id']); + const force = getOption(options, ['yes', 'y', 'force']); + + if (!id) { + failWithError(context, 'Access key ID is required'); + } + + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm( + `Rotate access key '${id}'? The current secret will be invalidated.` + ); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + + const config = await getIAMConfig(context); + + const { data, error } = await rotateAccessKey(id, { config }); + + if (error) { + failWithError(context, error); + } + + if (format === 'json') { + const nextActions = getSuccessNextActions(context, { id: data.id }); + const output: Record = { + action: 'rotated', + id: data.id, + secret: data.newSecret, + }; + if (nextActions.length > 0) output.nextActions = nextActions; + console.log(JSON.stringify(output)); + } else { + console.log(` Access Key ID: ${data.id}`); + console.log(` New Secret Access Key: ${data.newSecret}`); + console.log(''); + console.log( + ' Save these credentials securely. The secret will not be shown again.' + ); + } + + printSuccess(context); + printNextActions(context, { id: data.id }); +} diff --git a/src/lib/iam/policies/delete.ts b/src/lib/iam/policies/delete.ts index d652f3c..6ea9b5a 100644 --- a/src/lib/iam/policies/delete.ts +++ b/src/lib/iam/policies/delete.ts @@ -1,12 +1,12 @@ -import enquirer from 'enquirer'; -const { prompt } = enquirer; import { getOAuthIAMConfig } from '@auth/iam.js'; -import { deletePolicy, listPolicies } from '@tigrisdata/iam'; +import { deletePolicy } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; import { confirm, requireInteractive } from '@utils/interactive.js'; -import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; import { getFormat, getOption } from '@utils/options.js'; +import { selectPolicy } from './select-policy.js'; + const context = msg('iam policies', 'delete'); export default async function del(options: Record) { @@ -19,33 +19,13 @@ export default async function del(options: Record) { const iamConfig = await getOAuthIAMConfig(context); - // If no resource provided, list policies and let user select if (!resource) { - const { data: listData, error: listError } = await listPolicies({ - config: iamConfig, - }); - - if (listError) { - failWithError(context, listError); - } - - if (!listData.policies || listData.policies.length === 0) { - printEmpty(context); - return; - } - - requireInteractive('Provide the policy ARN as a positional argument'); - - const { selected } = await prompt<{ selected: string }>({ - type: 'select', - name: 'selected', - message: 'Select a policy to delete:', - choices: listData.policies.map((p) => ({ - name: p.resource, - message: `${p.name} (${p.resource})`, - })), - }); - + const selected = await selectPolicy( + iamConfig, + context, + 'Select a policy to delete:' + ); + if (!selected) return; resource = selected; } diff --git a/src/lib/iam/policies/edit.ts b/src/lib/iam/policies/edit.ts index e8f721b..415fadf 100644 --- a/src/lib/iam/policies/edit.ts +++ b/src/lib/iam/policies/edit.ts @@ -1,19 +1,12 @@ import { existsSync, readFileSync } from 'node:fs'; -import enquirer from 'enquirer'; -const { prompt } = enquirer; import { getOAuthIAMConfig } from '@auth/iam.js'; -import { - editPolicy, - getPolicy, - listPolicies, - type PolicyDocument, -} from '@tigrisdata/iam'; +import { editPolicy, getPolicy, type PolicyDocument } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; -import { requireInteractive } from '@utils/interactive.js'; -import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; import { getFormat, getOption } from '@utils/options.js'; +import { selectPolicy } from './select-policy.js'; import { parseDocument, readStdin } from './utils.js'; const context = msg('iam policies', 'edit'); @@ -29,8 +22,6 @@ export default async function edit(options: Record) { const iamConfig = await getOAuthIAMConfig(context); - // If no resource provided, list policies and let user select - // But if stdin is piped, we can't use interactive selection if (!resource) { if (!process.stdin.isTTY) { failWithError( @@ -39,31 +30,12 @@ export default async function edit(options: Record) { ); } - const { data: listData, error: listError } = await listPolicies({ - config: iamConfig, - }); - - if (listError) { - failWithError(context, listError); - } - - if (!listData.policies || listData.policies.length === 0) { - printEmpty(context); - return; - } - - requireInteractive('Provide the policy ARN as a positional argument'); - - const { selected } = await prompt<{ selected: string }>({ - type: 'select', - name: 'selected', - message: 'Select a policy to edit:', - choices: listData.policies.map((p) => ({ - name: p.resource, - message: `${p.name} (${p.resource})`, - })), - }); - + const selected = await selectPolicy( + iamConfig, + context, + 'Select a policy to edit:' + ); + if (!selected) return; resource = selected; } diff --git a/src/lib/iam/policies/get.ts b/src/lib/iam/policies/get.ts index ca27996..bb2ce26 100644 --- a/src/lib/iam/policies/get.ts +++ b/src/lib/iam/policies/get.ts @@ -1,12 +1,12 @@ -import enquirer from 'enquirer'; -const { prompt } = enquirer; import { getOAuthIAMConfig } from '@auth/iam.js'; -import { getPolicy, listPolicies } from '@tigrisdata/iam'; +import { getPolicy } from '@tigrisdata/iam'; import { failWithError } from '@utils/exit.js'; import { formatOutput } from '@utils/format.js'; -import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; import { getFormat, getOption } from '@utils/options.js'; +import { selectPolicy } from './select-policy.js'; + const context = msg('iam policies', 'get'); export default async function get(options: Record) { @@ -17,31 +17,9 @@ export default async function get(options: Record) { const iamConfig = await getOAuthIAMConfig(context); - // If no resource provided, list policies and let user select if (!resource) { - const { data: listData, error: listError } = await listPolicies({ - config: iamConfig, - }); - - if (listError) { - failWithError(context, listError); - } - - if (!listData.policies || listData.policies.length === 0) { - printEmpty(context); - return; - } - - const { selected } = await prompt<{ selected: string }>({ - type: 'select', - name: 'selected', - message: 'Select a policy:', - choices: listData.policies.map((p) => ({ - name: p.resource, - message: `${p.name} (${p.resource})`, - })), - }); - + const selected = await selectPolicy(iamConfig, context); + if (!selected) return; resource = selected; } @@ -82,7 +60,7 @@ export default async function get(options: Record) { if (data.users && data.users.length > 0) { console.log('Attached Users:'); for (const user of data.users) { - console.log(` - ${user}`); + console.log(` - ${user.name} (${user.id})`); } console.log(); } diff --git a/src/lib/iam/policies/link-key.ts b/src/lib/iam/policies/link-key.ts new file mode 100644 index 0000000..0cbd726 --- /dev/null +++ b/src/lib/iam/policies/link-key.ts @@ -0,0 +1,91 @@ +import enquirer from 'enquirer'; +const { prompt } = enquirer; +import { getOAuthIAMConfig } from '@auth/iam.js'; +import { + attachPolicyToAccessKey, + getPolicy, + listAccessKeys, +} from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { requireInteractive } from '@utils/interactive.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +import { selectPolicy } from './select-policy.js'; + +const context = msg('iam policies', 'link-key'); + +export default async function linkKey(options: Record) { + printStart(context); + + const format = getFormat(options); + + let resource = getOption(options, ['resource']); + let id = getOption(options, ['id']); + + const iamConfig = await getOAuthIAMConfig(context); + + if (!resource) { + const selected = await selectPolicy(iamConfig, context); + if (!selected) return; + resource = selected; + } + + // If no access key ID provided, let user select from unattached keys + if (!id) { + requireInteractive('Use --id to specify the access key ID'); + + const [keysResult, policyResult] = await Promise.all([ + listAccessKeys({ config: iamConfig }), + getPolicy(resource, { config: iamConfig }), + ]); + + if (keysResult.error) { + failWithError(context, keysResult.error); + } + + if (policyResult.error) { + failWithError(context, policyResult.error); + } + + const attachedIds = new Set( + (policyResult.data.users ?? []).map((u) => u.id) + ); + const available = keysResult.data.accessKeys.filter( + (k) => !attachedIds.has(k.id) + ); + + if (available.length === 0) { + failWithError( + context, + 'No unlinked access keys available. All access keys are already linked to this policy.' + ); + } + + const { selected } = await prompt<{ selected: string }>({ + type: 'select', + name: 'selected', + message: 'Select an access key to link:', + choices: available.map((k) => ({ + name: k.id, + message: `${k.name} (${k.id})`, + })), + }); + + id = selected; + } + + const { error } = await attachPolicyToAccessKey(id, resource, { + config: iamConfig, + }); + + if (error) { + failWithError(context, error); + } + + if (format === 'json') { + console.log(JSON.stringify({ action: 'linked', policyArn: resource, id })); + } + + printSuccess(context); +} diff --git a/src/lib/iam/policies/list-keys.ts b/src/lib/iam/policies/list-keys.ts new file mode 100644 index 0000000..5f8e7ae --- /dev/null +++ b/src/lib/iam/policies/list-keys.ts @@ -0,0 +1,50 @@ +import { getOAuthIAMConfig } from '@auth/iam.js'; +import { getPolicy } from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { formatOutput } from '@utils/format.js'; +import { msg, printEmpty, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +import { selectPolicy } from './select-policy.js'; + +const context = msg('iam policies', 'list-keys'); + +export default async function listKeys(options: Record) { + printStart(context); + + const format = getFormat(options); + + let resource = getOption(options, ['resource']); + + const iamConfig = await getOAuthIAMConfig(context); + + if (!resource) { + const selected = await selectPolicy(iamConfig, context); + if (!selected) return; + resource = selected; + } + + const { data, error } = await getPolicy(resource, { config: iamConfig }); + + if (error) { + failWithError(context, error); + } + + if (!data.users || data.users.length === 0) { + printEmpty(context); + return; + } + + const keys = data.users.map((u) => ({ name: u.name, id: u.id })); + + const columns = [ + { key: 'name', header: 'Name' }, + { key: 'id', header: 'ID' }, + ]; + + const output = formatOutput(keys, format!, 'keys', 'key', columns); + + console.log(output); + + printSuccess(context, { count: keys.length }); +} diff --git a/src/lib/iam/policies/select-policy.ts b/src/lib/iam/policies/select-policy.ts new file mode 100644 index 0000000..4f21b21 --- /dev/null +++ b/src/lib/iam/policies/select-policy.ts @@ -0,0 +1,42 @@ +import enquirer from 'enquirer'; +const { prompt } = enquirer; +import { listPolicies } from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { requireInteractive } from '@utils/interactive.js'; +import { type MessageContext, printEmpty } from '@utils/messages.js'; + +/** + * Interactively select a policy ARN. Returns undefined if no policies exist. + */ +export async function selectPolicy( + iamConfig: NonNullable[0]>['config'], + context: MessageContext, + message: string = 'Select a policy:' +): Promise { + const { data: listData, error: listError } = await listPolicies({ + config: iamConfig, + }); + + if (listError) { + failWithError(context, listError); + } + + if (!listData.policies || listData.policies.length === 0) { + printEmpty(context); + return undefined; + } + + requireInteractive('Provide the policy ARN as a positional argument'); + + const { selected } = await prompt<{ selected: string }>({ + type: 'select', + name: 'selected', + message, + choices: listData.policies.map((p) => ({ + name: p.resource, + message: `${p.name} (${p.resource})`, + })), + }); + + return selected; +} diff --git a/src/lib/iam/policies/unlink-key.ts b/src/lib/iam/policies/unlink-key.ts new file mode 100644 index 0000000..16033d4 --- /dev/null +++ b/src/lib/iam/policies/unlink-key.ts @@ -0,0 +1,86 @@ +import enquirer from 'enquirer'; +const { prompt } = enquirer; +import { getOAuthIAMConfig } from '@auth/iam.js'; +import { detachPolicyFromAccessKey, getPolicy } from '@tigrisdata/iam'; +import { failWithError } from '@utils/exit.js'; +import { confirm, requireInteractive } from '@utils/interactive.js'; +import { msg, printStart, printSuccess } from '@utils/messages.js'; +import { getFormat, getOption } from '@utils/options.js'; + +import { selectPolicy } from './select-policy.js'; + +const context = msg('iam policies', 'unlink-key'); + +export default async function unlinkKey(options: Record) { + printStart(context); + + const format = getFormat(options); + + let resource = getOption(options, ['resource']); + let id = getOption(options, ['id']); + const force = getOption(options, ['yes', 'y', 'force']); + + const iamConfig = await getOAuthIAMConfig(context); + + if (!resource) { + const selected = await selectPolicy(iamConfig, context); + if (!selected) return; + resource = selected; + } + + // If no access key ID provided, let user select from attached keys + if (!id) { + requireInteractive('Use --id to specify the access key ID'); + + const { data: policyData, error: policyError } = await getPolicy(resource, { + config: iamConfig, + }); + + if (policyError) { + failWithError(context, policyError); + } + + if (!policyData.users || policyData.users.length === 0) { + failWithError(context, 'No access keys are linked to this policy.'); + } + + const { selected } = await prompt<{ selected: string }>({ + type: 'select', + name: 'selected', + message: 'Select an access key to unlink:', + choices: policyData.users.map((u) => ({ + name: u.id, + message: `${u.name} (${u.id})`, + })), + }); + + id = selected; + } + + if (!force) { + requireInteractive('Use --yes to skip confirmation'); + const confirmed = await confirm( + `Unlink access key '${id}' from policy '${resource}'?` + ); + if (!confirmed) { + console.log('Aborted'); + return; + } + } + + const { error } = await detachPolicyFromAccessKey(id, resource, { + config: iamConfig, + }); + + if (error) { + failWithError(context, error); + } + + if (format === 'json') { + console.log( + JSON.stringify({ action: 'unlinked', policyArn: resource, id }) + ); + } + + printSuccess(context); +} diff --git a/src/specs.yaml b/src/specs.yaml index 0692b7d..3c8b1ee 100644 --- a/src/specs.yaml +++ b/src/specs.yaml @@ -1494,6 +1494,99 @@ commands: - name: revoke-roles description: Revoke all bucket roles from the access key type: flag + - name: rotate + description: Rotate an access key's secret. The current secret is immediately invalidated and a new one is returned (shown only once) + alias: r + examples: + - "tigris access-keys rotate tid_AaBbCcDdEeFf --yes" + messages: + onStart: 'Rotating access key...' + onSuccess: 'Access key rotated' + onFailure: 'Failed to rotate access key' + nextActions: + - command: 'tigris access-keys get {{id}}' + description: 'Verify the rotated access key' + arguments: + - name: id + description: Access key ID + type: positional + required: true + examples: + - tid_AaBbCcDdEeFf + - name: force + type: flag + description: Skip confirmation prompts (alias for --yes) + - name: attach-policy + description: Attach an IAM policy to an access key. If no policy ARN is provided, shows interactive selection of available policies + alias: ap + examples: + - "tigris access-keys attach-policy tid_AaBb --policy-arn arn:aws:iam::org_id:policy/my-policy" + - "tigris access-keys attach-policy tid_AaBb" + messages: + onStart: 'Attaching policy...' + onSuccess: 'Policy attached' + onFailure: 'Failed to attach policy' + arguments: + - name: id + description: Access key ID + type: positional + required: true + examples: + - tid_AaBbCcDdEeFf + - name: policy-arn + description: ARN of the policy to attach + examples: + - arn:aws:iam::org_id:policy/my-policy + - name: detach-policy + description: Detach an IAM policy from an access key. If no policy ARN is provided, shows interactive selection of attached policies + alias: dp + examples: + - "tigris access-keys detach-policy tid_AaBb --policy-arn arn:aws:iam::org_id:policy/my-policy --yes" + - "tigris access-keys detach-policy tid_AaBb" + messages: + onStart: 'Detaching policy...' + onSuccess: 'Policy detached' + onFailure: 'Failed to detach policy' + arguments: + - name: id + description: Access key ID + type: positional + required: true + examples: + - tid_AaBbCcDdEeFf + - name: policy-arn + description: ARN of the policy to detach + examples: + - arn:aws:iam::org_id:policy/my-policy + - name: force + type: flag + description: Skip confirmation prompts (alias for --yes) + - name: list-policies + description: List all IAM policies attached to an access key + alias: lp + examples: + - "tigris access-keys list-policies tid_AaBbCcDdEeFf" + messages: + onStart: '' + onSuccess: '' + onFailure: 'Failed to list policies for access key' + onEmpty: 'No policies attached to this access key' + arguments: + - name: id + description: Access key ID + type: positional + required: true + examples: + - tid_AaBbCcDdEeFf + - name: format + description: Output format + options: [json, table, xml] + default: table + - name: limit + description: Maximum number of items to return per page + - name: page-token + description: Pagination token from a previous request to fetch the next page + alias: pt ######################### # IAM - Identity and Access Management @@ -1626,6 +1719,75 @@ commands: - name: force type: flag description: Skip confirmation prompts (alias for --yes) + - name: link-key + description: Link an access key to a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of unlinked keys + alias: lnk + examples: + - "tigris iam policies link-key arn:aws:iam::org_id:policy/my-policy --id tid_AaBb" + - "tigris iam policies link-key" + messages: + onStart: 'Linking access key...' + onSuccess: 'Access key linked' + onFailure: 'Failed to link access key' + onEmpty: 'No policies found' + arguments: + - name: resource + description: Policy ARN. If omitted, shows interactive selection + type: positional + required: false + examples: + - arn:aws:iam::org_id:policy/my-policy + - name: id + description: Access key ID to attach + examples: + - tid_AaBbCcDdEeFf + - name: unlink-key + description: Unlink an access key from a policy. If no policy ARN is provided, shows interactive selection. If no access key ID is provided, shows interactive selection of linked keys + alias: ulnk + examples: + - "tigris iam policies unlink-key arn:aws:iam::org_id:policy/my-policy --id tid_AaBb --yes" + - "tigris iam policies unlink-key" + messages: + onStart: 'Unlinking access key...' + onSuccess: 'Access key unlinked' + onFailure: 'Failed to unlink access key' + onEmpty: 'No policies found' + arguments: + - name: resource + description: Policy ARN. If omitted, shows interactive selection + type: positional + required: false + examples: + - arn:aws:iam::org_id:policy/my-policy + - name: id + description: Access key ID to detach + examples: + - tid_AaBbCcDdEeFf + - name: force + type: flag + description: Skip confirmation prompts (alias for --yes) + - name: list-keys + description: List all access keys attached to a policy. If no policy ARN is provided, shows interactive selection + alias: lk + examples: + - "tigris iam policies list-keys arn:aws:iam::org_id:policy/my-policy" + - "tigris iam policies list-keys" + messages: + onStart: '' + onSuccess: '' + onFailure: 'Failed to list access keys for policy' + onEmpty: 'No access keys attached to this policy' + arguments: + - name: resource + description: Policy ARN. If omitted, shows interactive selection + type: positional + required: false + examples: + - arn:aws:iam::org_id:policy/my-policy + - name: format + description: Output format + options: [json, table, xml] + default: table - name: users description: Manage organization users and invitations