diff --git a/examples/sdk_example/package.json b/examples/sdk_example/package.json index a98d9881..05cfc408 100644 --- a/examples/sdk_example/package.json +++ b/examples/sdk_example/package.json @@ -36,21 +36,6 @@ "roles:role-users": "ts-node src/roles/roleUsers.ts", "roles:manage-node": "ts-node src/roles/manageNode.ts", "roles:privileges": "ts-node src/roles/rolePrivileges.ts", - "nsf:list": "ts-node src/nestedShareFolders/list_nsf.ts", - "nsf:get": "ts-node src/nestedShareFolders/get_nsf.ts", - "nsf:ln": "ts-node src/nestedShareFolders/link_nsf.ts", - "nsf:rm": "ts-node src/nestedShareFolders/remove_nsf.ts", - "nsf:record-details": "ts-node src/nestedShareFolders/get_nsf_record_details.ts", - "nsf:record-update": "ts-node src/nestedShareFolders/update_nsf_record.ts", - "nsf:record-add": "ts-node src/nestedShareFolders/add_nsf_record.ts", - "nsf:mkdir": "ts-node src/nestedShareFolders/mkdir_nsf.ts", - "nsf:rmdir": "ts-node src/nestedShareFolders/rmdir_nsf.ts", - "nsf:share-folder": "ts-node src/nestedShareFolders/share_nsf_folder.ts", - "nsf:share-record": "ts-node src/nestedShareFolders/share_nsf_record.ts", - "nsf:record-permission": "ts-node src/nestedShareFolders/nsf_record_permission.ts", - "nsf:shortcut": "ts-node src/nestedShareFolders/shortcut_nsf.ts", - "nsf:transfer-record": "ts-node src/nestedShareFolders/transfer_nsf_record.ts", - "nsf:rndir": "ts-node src/nestedShareFolders/rndir_nsf.ts", "teams:list": "ts-node src/teams/list_teams.ts", "teams:view": "ts-node src/teams/view_team.ts", "teams:add": "ts-node src/teams/add_team.ts", @@ -68,15 +53,34 @@ "reports:audit-report": "ts-node src/enterpriseReport/audit_report.ts", "reports:action-report": "ts-node src/enterpriseReport/action_report.ts", "reports:password-report": "ts-node src/enterpriseReport/password_report.ts", - "link-local": "cd ../../KeeperSdk && npm link ../keeperapi && cd ../examples/sdk_example && npm link ../../keeperapi", - "types": "tsc --watch", - "types:ci": "tsc", - "nsf:folder": "ts-node src/nestedShareFolders/nsf_folder.ts", - "nsf:rndir": "ts-node src/nestedShareFolders/rndir_nsf.ts", - "nsf:share": "ts-node src/nestedShareFolders/share_nsf.ts", + "nsf:list": "ts-node src/nestedShareFolders/list_nsf.ts", + "nsf:get": "ts-node src/nestedShareFolders/get_nsf.ts", + "nsf:ln": "ts-node src/nestedShareFolders/link_nsf.ts", + "nsf:rm": "ts-node src/nestedShareFolders/remove_nsf.ts", + "nsf:record-details": "ts-node src/nestedShareFolders/get_nsf_record_details.ts", + "nsf:record-update": "ts-node src/nestedShareFolders/update_nsf_record.ts", + "nsf:record-add": "ts-node src/nestedShareFolders/add_nsf_record.ts", + "nsf:mkdir": "ts-node src/nestedShareFolders/mkdir_nsf.ts", + "nsf:rmdir": "ts-node src/nestedShareFolders/rmdir_nsf.ts", + "nsf:share-folder": "ts-node src/nestedShareFolders/share_nsf_folder.ts", + "nsf:share-record": "ts-node src/nestedShareFolders/share_nsf_record.ts", + "nsf:record-permission": "ts-node src/nestedShareFolders/nsf_record_permission.ts", "nsf:shortcut": "ts-node src/nestedShareFolders/shortcut_nsf.ts", "nsf:transfer-record": "ts-node src/nestedShareFolders/transfer_nsf_record.ts", - "nsf:record-permission": "ts-node src/nestedShareFolders/nsf_record_permission.ts" + "nsf:rndir": "ts-node src/nestedShareFolders/rndir_nsf.ts", + "nsf:folder": "ts-node src/nestedShareFolders/nsf_folder.ts", + "pam:gateway:list": "ts-node src/pam/gateway/list_gateways.ts", + "pam:gateway:new": "ts-node src/pam/gateway/create_gateway.ts", + "pam:gateway:edit": "ts-node src/pam/gateway/edit_gateway.ts", + "pam:gateway:remove": "ts-node src/pam/gateway/remove_gateway.ts", + "pam:gateway:set-max-instances": "ts-node src/pam/gateway/set_max_instances.ts", + "pam:config:list": "ts-node src/pam/config/list_configs.ts", + "pam:config:new": "ts-node src/pam/config/create_config.ts", + "pam:config:edit": "ts-node src/pam/config/edit_config.ts", + "pam:config:remove": "ts-node src/pam/config/remove_config.ts", + "link-local": "cd ../../KeeperSdk && npm link ../keeperapi && cd ../examples/sdk_example && npm link ../../keeperapi", + "types": "tsc --watch", + "types:ci": "tsc" }, "dependencies": { "@keeper-security/keeperapi": "17.1.0", diff --git a/examples/sdk_example/src/pam/config/configFieldPrompts.ts b/examples/sdk_example/src/pam/config/configFieldPrompts.ts new file mode 100644 index 00000000..dafcdb4b --- /dev/null +++ b/examples/sdk_example/src/pam/config/configFieldPrompts.ts @@ -0,0 +1,306 @@ +import { + logger, + prompt, + PAM_CONFIG_ENVIRONMENTS, + type PamConfigEnvironment, + type PamConfigurationPermissionValue, + type PamConfigurationPermissionsInput, + type PamConfigurationRecordFieldInput, +} from '@keeper-security/keeper-sdk-javascript' +import { isYes } from '../../utils/format' + +export { PAM_CONFIG_ENVIRONMENTS } +export type { PamConfigEnvironment } + +export type PamConfigFieldsPromptResult = { + fields: PamConfigurationRecordFieldInput[] + adminCredentialUid?: string +} + +export type PamConfigFieldsPromptOptions = { + includeSchedulePrompt?: boolean +} + +type PamConfigLabeledFieldType = 'text' | 'secret' | 'multiline' | 'json' | 'email' | 'checkbox' + +type PermissionPromptDefinition = { + key: keyof PamConfigurationPermissionsInput + label: string +} + +const PERMISSION_PROMPT_DEFINITIONS: readonly PermissionPromptDefinition[] = [ + { key: 'connections', label: 'Connections (-c)' }, + { key: 'tunneling', label: 'Tunneling (-u)' }, + { key: 'rotation', label: 'Rotation (-r)' }, + { key: 'remoteBrowserIsolation', label: 'Remote browser isolation (-rbi)' }, + { key: 'connectionsRecording', label: 'Connections recording (-cr)' }, + { key: 'typescriptRecording', label: 'Typescript recording (-tr)' }, + { key: 'aiThreatDetection', label: 'AI threat detection' }, + { key: 'aiTerminateSessionOnDetection', label: 'AI terminate session on detection' }, +] + +async function promptOptionalText(label: string): Promise { + return (await prompt(`${label} (optional): `)).trim() +} + +async function promptOptionalBoolean(label: string): Promise { + const raw = (await prompt(`${label} [y/N, Enter to skip]: `)).trim() + if (!raw) return undefined + return isYes(raw) +} + +function splitCommaSeparatedList(raw: string): string[] { + return raw + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) +} + +function appendLabeledField( + fields: PamConfigurationRecordFieldInput[], + type: PamConfigLabeledFieldType, + label: string, + value: string | boolean | undefined +): void { + if (value == null) return + if (typeof value === 'string' && value.length === 0) return + fields.push({ type, label, value: [value] }) +} + +function appendMultilineField( + fields: PamConfigurationRecordFieldInput[], + label: string, + values: string[] +): void { + if (values.length === 0) return + fields.push({ type: 'multiline', label, value: [values.join('\n')] }) +} + +async function promptPermissionFlagValue(label: string): Promise { + const raw = (await prompt(`${label} [on|off|default, Enter to skip]: `)).trim().toLowerCase() + if (!raw) return undefined + if (raw === 'on' || raw === 'off' || raw === 'default') return raw + logger.info(` Invalid "${raw}". Skipping (use on, off, or default).`) + return undefined +} + +export async function promptPamConfigurationPermissions(): Promise< + PamConfigurationPermissionsInput | undefined +> { + const wantPermissions = isYes(await prompt('Set additional permissions? [y/N]: ')) + if (!wantPermissions) return undefined + + const permissions: PamConfigurationPermissionsInput = {} + let anySet = false + for (const entry of PERMISSION_PROMPT_DEFINITIONS) { + const value = await promptPermissionFlagValue(entry.label) + if (value) { + permissions[entry.key] = value + anySet = true + } + } + return anySet ? permissions : undefined +} + +async function promptAwsConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'awsId', await promptOptionalText('AWS ID (--aws-id)')) + appendLabeledField(fields, 'secret', 'accessKeyId', await promptOptionalText('Access Key ID (--access-key-id)')) + appendLabeledField( + fields, + 'secret', + 'accessSecretKey', + await promptOptionalText('Access Secret Key (--access-secret-key)') + ) + appendMultilineField( + fields, + 'regionNames', + splitCommaSeparatedList(await promptOptionalText('Region names, comma-separated (--region-name)')) + ) +} + +async function promptAzureConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'azureId', await promptOptionalText('Azure ID (--azure-id)')) + appendLabeledField(fields, 'secret', 'clientId', await promptOptionalText('Client ID (--client-id)')) + appendLabeledField(fields, 'secret', 'clientSecret', await promptOptionalText('Client Secret (--client-secret)')) + appendLabeledField( + fields, + 'secret', + 'subscriptionId', + await promptOptionalText('Subscription ID (--subscription_id)') + ) + appendLabeledField(fields, 'secret', 'tenantId', await promptOptionalText('Tenant ID (--tenant-id)')) + appendMultilineField( + fields, + 'resourceGroups', + splitCommaSeparatedList(await promptOptionalText('Resource groups, comma-separated (--resource-group)')) + ) +} + +async function promptGcpConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'pamGcpId', await promptOptionalText('GCP ID (--gcp-id)')) + appendLabeledField( + fields, + 'json', + 'pamServiceAccountKey', + await promptOptionalText('Service Account Key JSON (--service-account-key)') + ) + appendLabeledField( + fields, + 'email', + 'pamGoogleAdminEmail', + await promptOptionalText('Google Admin Email (--google-admin-email)') + ) + appendMultilineField( + fields, + 'pamGcpRegionName', + splitCommaSeparatedList(await promptOptionalText('GCP regions, comma-separated (--gcp-region)')) + ) +} + +async function promptGitHubConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'pamGitHubId', await promptOptionalText('GitHub ID (--github-id)')) + appendLabeledField( + fields, + 'secret', + 'personalAccessToken', + await promptOptionalText('Personal Access Token (--personal-access-token)') + ) + appendLabeledField( + fields, + 'text', + 'pamGitHubBaseUrl', + await promptOptionalText('GitHub Base URL (--github-base-url)') + ) +} + +async function promptDomainConfigurationFields( + fields: PamConfigurationRecordFieldInput[] +): Promise { + appendLabeledField(fields, 'text', 'pamDomainId', await promptOptionalText('Domain ID (--domain-id)')) + const hostname = await promptOptionalText('Domain hostname (--domain-hostname)') + const port = await promptOptionalText('Domain port (--domain-port)') + if (hostname || port) { + fields.push({ + type: 'pamHostname', + value: [{ hostName: hostname || '', port: port || '' }], + }) + } + const useSsl = await promptOptionalBoolean('Use SSL (--domain-use-ssl)') + if (useSsl != null) appendLabeledField(fields, 'checkbox', 'useSSL', useSsl) + const scanDcCidr = await promptOptionalBoolean('Scan DC CIDR (--domain-scan-dc-cidr)') + if (scanDcCidr != null) appendLabeledField(fields, 'checkbox', 'scanDCCIDR', scanDcCidr) + appendLabeledField( + fields, + 'text', + 'networkCIDR', + await promptOptionalText('Domain network CIDR (--domain-network-cidr)') + ) + appendLabeledField( + fields, + 'text', + 'userMatch', + await promptOptionalText('Domain user match (--domain-user-match)') + ) + const domainAdmin = await promptOptionalText('Domain admin pamUser UID/title (--domain-admin)') + return domainAdmin || undefined +} + +async function promptOciConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'pamOciId', await promptOptionalText('OCI ID (--oci-id)')) + appendLabeledField(fields, 'secret', 'adminOcid', await promptOptionalText('OCI Admin OCID (--oci-admin-id)')) + appendLabeledField( + fields, + 'secret', + 'adminPublicKey', + await promptOptionalText('OCI Admin Public Key (--oci-admin-public-key)') + ) + appendLabeledField( + fields, + 'secret', + 'adminPrivateKey', + await promptOptionalText('OCI Admin Private Key (--oci-admin-private-key)') + ) + appendLabeledField(fields, 'text', 'tenancyOci', await promptOptionalText('OCI Tenancy (--oci-tenancy)')) + appendLabeledField(fields, 'text', 'regionOci', await promptOptionalText('OCI Region (--oci-region)')) +} + +async function promptLocalConfigurationFields(fields: PamConfigurationRecordFieldInput[]): Promise { + appendLabeledField(fields, 'text', 'networkId', await promptOptionalText('Network ID (--network-id)')) + appendLabeledField(fields, 'text', 'networkCIDR', await promptOptionalText('Network CIDR (--network-cidr)')) +} + +async function promptCommonOptionalFields( + fields: PamConfigurationRecordFieldInput[], + options: PamConfigFieldsPromptOptions +): Promise { + appendLabeledField( + fields, + 'text', + 'identityProviderUid', + await promptOptionalText('Identity Provider UID (--identity-provider)') + ) + + if (options.includeSchedulePrompt !== false) { + const scheduleCron = (await prompt('Default rotation CRON (--schedule, Enter to skip): ')).trim() + if (scheduleCron) { + fields.push({ + type: 'schedule', + label: 'defaultRotationSchedule', + value: [{ type: 'CRON', cron: scheduleCron, tz: 'Etc/UTC' }], + }) + } + } + + const portMappingsRaw = await promptOptionalText( + 'Port mappings, comma-separated port=protocol (--port-mapping)' + ) + if (!portMappingsRaw) return + + const portMappingLines = portMappingsRaw + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => { + if (entry.includes('=')) return entry + const [port, protocol] = entry.split(':') + return protocol ? `${port.trim()}=${protocol.trim()}` : port.trim() + }) + .filter(Boolean) + appendMultilineField(fields, 'portMapping', portMappingLines) +} + +export async function promptPamConfigurationFields( + environment: string, + options: PamConfigFieldsPromptOptions = {} +): Promise { + const fields: PamConfigurationRecordFieldInput[] = [] + let adminCredentialUid: string | undefined + + switch (environment) { + case 'aws': + await promptAwsConfigurationFields(fields) + break + case 'azure': + await promptAzureConfigurationFields(fields) + break + case 'gcp': + await promptGcpConfigurationFields(fields) + break + case 'github': + await promptGitHubConfigurationFields(fields) + break + case 'domain': + adminCredentialUid = await promptDomainConfigurationFields(fields) + break + case 'oci': + await promptOciConfigurationFields(fields) + break + case 'local': + default: + await promptLocalConfigurationFields(fields) + break + } + + await promptCommonOptionalFields(fields, options) + return { fields, adminCredentialUid } +} diff --git a/examples/sdk_example/src/pam/config/create_config.ts b/examples/sdk_example/src/pam/config/create_config.ts new file mode 100644 index 00000000..8a5cf886 --- /dev/null +++ b/examples/sdk_example/src/pam/config/create_config.ts @@ -0,0 +1,84 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + resolvePamConfigurationRecordType, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' +import { + PAM_CONFIG_ENVIRONMENTS, + promptPamConfigurationFields, + promptPamConfigurationPermissions, +} from './configFieldPrompts' +import { formatCreatePamConfigurationOutput } from '../formatOutput' + +async function createPamConfigurationExample() { + const vault = await login() + + try { + const environment = (await prompt(`Environment (${PAM_CONFIG_ENVIRONMENTS.join('|')}): `)).trim().toLowerCase() + if (!environment) { + logger.info('Environment is required.') + return + } + const configType = resolvePamConfigurationRecordType(environment) + if (!configType) { + logger.info(`Invalid environment. Choose one of: ${PAM_CONFIG_ENVIRONMENTS.join(', ')}`) + return + } + + const title = (await prompt('Configuration title: ')).trim() + if (!title) { + logger.info('Title is required.') + return + } + + const sharedFolder = (await prompt('Shared folder or Nested Share Folder UID or name: ')).trim() + if (!sharedFolder) { + logger.info('Shared folder or Nested Share Folder is required.') + return + } + + const gateway = (await prompt('Gateway UID or name (optional): ')).trim() || undefined + const { fields, adminCredentialUid } = await promptPamConfigurationFields(environment) + const permissions = await promptPamConfigurationPermissions() + // Automation / Commander -r: print only configuration UID (no banner). + const returnValueOnly = isYes(await prompt('Return value only (automation / -r)? [y/N]: ')) + + const restore = suppressLogs() + let result + try { + result = await vault.createPamConfiguration({ + title, + configType, + sharedFolder, + gateway, + fields, + adminCredentialUid, + permissions, + }) + } finally { + restore() + } + + if (returnValueOnly) { + logger.info(result.configurationUid) + return + } + + logger.info('') + logger.info(formatCreatePamConfigurationOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(createPamConfigurationExample) diff --git a/examples/sdk_example/src/pam/config/edit_config.ts b/examples/sdk_example/src/pam/config/edit_config.ts new file mode 100644 index 00000000..cac5dd8d --- /dev/null +++ b/examples/sdk_example/src/pam/config/edit_config.ts @@ -0,0 +1,118 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + resolvePamConfigurationRecordType, + suppressLogs, + type EditPamConfigurationInput, + type EditPamConfigurationResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' +import { + PAM_CONFIG_ENVIRONMENTS, + promptPamConfigurationFields, + promptPamConfigurationPermissions, +} from './configFieldPrompts' +import { formatEditPamConfigurationOutput } from '../formatOutput' + +async function editPamConfigurationExample() { + const vault = await login() + + try { + const configurationUidOrTitle = (await prompt('PAM Configuration UID or title: ')).trim() + if (!configurationUidOrTitle) { + logger.info('Configuration UID or title is required.') + return + } + + const titleRaw = (await prompt('New title (Enter to keep): ')).trim() + const title = titleRaw || undefined + + const environmentRaw = ( + await prompt(`New environment (${PAM_CONFIG_ENVIRONMENTS.join('|')}, Enter to keep): `) + ) + .trim() + .toLowerCase() + let configType: string | undefined + let environmentForFields = '' + if (environmentRaw) { + configType = resolvePamConfigurationRecordType(environmentRaw) + if (!configType) { + logger.info(`Invalid environment. Choose one of: ${PAM_CONFIG_ENVIRONMENTS.join(', ')}`) + return + } + environmentForFields = environmentRaw + } + + const sharedFolderRaw = ( + await prompt('Shared folder or Nested Share Folder UID or name (Enter to keep): ') + ).trim() + const sharedFolder = sharedFolderRaw || undefined + + const gatewayPrompt = (await prompt('Gateway UID or name (Enter to keep, "-" to clear): ')).trim() + let gateway: string | undefined + if (gatewayPrompt === '-') gateway = '' + else if (gatewayPrompt) gateway = gatewayPrompt + + const updateFields = isYes(await prompt('Update environment / schedule / port-mapping fields? [y/N]: ')) + if (updateFields && !environmentForFields) { + environmentForFields = ( + await prompt(`Environment for field prompts (${PAM_CONFIG_ENVIRONMENTS.join('|')}): `) + ) + .trim() + .toLowerCase() + if (!resolvePamConfigurationRecordType(environmentForFields)) { + logger.info(`Invalid environment. Choose one of: ${PAM_CONFIG_ENVIRONMENTS.join(', ')}`) + return + } + } + + const fieldPrompt = updateFields ? await promptPamConfigurationFields(environmentForFields) : undefined + const fields = fieldPrompt?.fields + const adminCredentialUid = fieldPrompt?.adminCredentialUid + + const removeRaw = (await prompt('Remove resource record UIDs/titles (comma-separated, optional): ')).trim() + const removeResourceRecords = removeRaw + ? removeRaw + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + : undefined + + const permissions = await promptPamConfigurationPermissions() + + const input: EditPamConfigurationInput = { + configurationUidOrTitle, + title, + configType, + sharedFolder, + gateway, + fields, + adminCredentialUid, + removeResourceRecords, + permissions, + } + + let result: EditPamConfigurationResult + const restore = suppressLogs() + try { + result = await vault.editPamConfiguration(input) + } finally { + restore() + } + + logger.info('') + logger.info(formatEditPamConfigurationOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(editPamConfigurationExample) diff --git a/examples/sdk_example/src/pam/config/list_configs.ts b/examples/sdk_example/src/pam/config/list_configs.ts new file mode 100644 index 00000000..3d3829b0 --- /dev/null +++ b/examples/sdk_example/src/pam/config/list_configs.ts @@ -0,0 +1,54 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + PamConfigListFormat, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function listPamConfigurationsExample() { + const vault = await login() + + try { + const configUid = (await prompt('PAM Configuration UID (Enter for all): ')).trim() || undefined + const verbose = isYes(await prompt('Verbose output? [y/N]: ')) + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + + const options = { + configUid, + verbose, + format: asJson ? PamConfigListFormat.Json : PamConfigListFormat.Table, + } + + let result + const restore = suppressLogs() + try { + result = vault.listPamConfigurations(options) + } finally { + restore() + } + + if (!result.detail && result.configurations.length === 0) { + for (const warning of result.warnings) { + logger.warn(warning) + } + logger.info(result.message || 'No PAM configurations found.') + return + } + + logger.info('') + logger.info(vault.formatPamConfigurationsOutput(result, options)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listPamConfigurationsExample) diff --git a/examples/sdk_example/src/pam/config/remove_config.ts b/examples/sdk_example/src/pam/config/remove_config.ts new file mode 100644 index 00000000..a38bad0d --- /dev/null +++ b/examples/sdk_example/src/pam/config/remove_config.ts @@ -0,0 +1,49 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { formatRemovePamConfigurationOutput } from '../formatOutput' + +async function removePamConfigurationExample() { + const vault = await login() + + try { + const configurationUidOrTitle = (await prompt('PAM Configuration UID(s) or title(s), comma-separated: ')) + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + if (!configurationUidOrTitle.length) { + logger.info('Configuration UID or title is required.') + return + } + + let result + const restore = suppressLogs() + try { + result = await vault.removePamConfiguration({ configurationUidOrTitle }) + } finally { + restore() + } + + logger.info('') + const output = formatRemovePamConfigurationOutput(result, configurationUidOrTitle) + if (!result.found) { + logger.warn(output) + } else { + logger.info(output) + } + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(removePamConfigurationExample) diff --git a/examples/sdk_example/src/pam/formatOutput.ts b/examples/sdk_example/src/pam/formatOutput.ts new file mode 100644 index 00000000..377afcfd --- /dev/null +++ b/examples/sdk_example/src/pam/formatOutput.ts @@ -0,0 +1,156 @@ +import { formatTimestampMs } from '@keeper-security/keeper-sdk-javascript' +import type { + CreateGatewayResult, + CreatePamConfigurationResult, + EditGatewayResult, + EditPamConfigurationResult, + RemoveGatewayResult, + RemovePamConfigurationResult, + SetGatewayMaxInstancesResult, +} from '@keeper-security/keeper-sdk-javascript' + +export function formatCreateGatewayOutput(result: CreateGatewayResult): string { + const appLabel = result.applicationTitle || result.applicationUid + const message = result.isInitializedConfig + ? `The one-time token was created in application [${appLabel}]. Use the initialized config in the Gateway. The new Gateway named ${result.gatewayName} will show up in the gateway list once it is initialized.` + : `The one-time token was created in application [${appLabel}]. The new Gateway named ${result.gatewayName} will show up in the gateway list once it is initialized. Token expires in ${result.tokenExpiresInMin} minutes.` + + const lines = [ + message, + '', + result.isInitializedConfig ? 'Use the following initialized config in the Gateway:' : 'One-time token:', + '-----------------------------------------------', + result.tokenOrConfig, + '-----------------------------------------------', + `Token expires on: ${formatTimestampMs(result.tokenExpiresOn)}`, + ] + for (const warning of result.warnings) { + lines.push(`Warning: ${warning}`) + } + return lines.join('\n') +} + +export function formatEditGatewayOutput(result: EditGatewayResult): string { + const unchanged = !result.nameChanged && !result.nodeChanged + const message = unchanged + ? `Gateway ${result.gatewayUid} is unchanged.` + : `Gateway ${result.gatewayUid} has been edited.` + return [ + message, + result.nameChanged + ? `Name: ${result.previousName || '(none)'} → ${result.gatewayName}` + : `Name: ${result.gatewayName}`, + result.nodeChanged ? `Node ID: ${result.previousNodeId} → ${result.nodeId}` : `Node ID: ${result.nodeId}`, + ].join('\n') +} + +export function formatRemoveGatewayOutput(result: RemoveGatewayResult): string { + return `Gateway ${result.gatewayName} (${result.gatewayUid}) has been removed.` +} + +export function formatSetGatewayMaxInstancesOutput(result: SetGatewayMaxInstancesResult): string { + return `${result.gatewayName}: max instance count set to ${result.maxInstances}` +} + +export function formatCreatePamConfigurationOutput(result: CreatePamConfigurationResult): string { + const lines = [ + `PAM Configuration "${result.title}" created (${result.configurationUid}).`, + `UID: ${result.configurationUid}`, + `Type: ${result.configType}`, + `Shared Folder: ${result.sharedFolderUid}`, + `Gateway UID: ${result.gatewayUid || '(none)'}`, + `Gateway Linked: ${result.gatewayLinked ? 'yes' : 'no'}`, + `Permissions Applied: ${result.permissionsApplied ? 'yes' : 'no'}`, + ] + const populated = (result.fields || []).filter( + (field) => + field.type !== 'pamResources' && + field.type !== 'fileRef' && + Array.isArray(field.value) && + field.value.length > 0 + ) + if (populated.length) { + lines.push('Fields:') + for (const field of populated) { + const name = field.label ? `${field.type}.${field.label}` : field.type + const values = field.value + .map((entry) => (typeof entry === 'string' ? entry : JSON.stringify(entry))) + .join(', ') + lines.push(` ${name}: ${values}`) + } + } + for (const warning of result.warnings) { + lines.push(`Warning: ${warning}`) + } + return lines.join('\n') +} + +export function formatEditPamConfigurationOutput(result: EditPamConfigurationResult): string { + const lines = [ + `PAM Configuration "${result.title}" updated (${result.configurationUid}).`, + `UID: ${result.configurationUid}`, + result.typeChanged ? `Type: ${result.previousConfigType} → ${result.configType}` : `Type: ${result.configType}`, + result.titleChanged ? `Title changed: yes` : `Title: ${result.title}`, + result.folderChanged + ? `Shared Folder: ${result.previousSharedFolderUid || '(none)'} → ${result.sharedFolderUid}` + : `Shared Folder: ${result.sharedFolderUid || '(none)'}`, + result.gatewayChanged + ? `Gateway UID: ${result.previousGatewayUid || '(none)'} → ${result.gatewayUid || '(none)'}` + : `Gateway UID: ${result.gatewayUid || '(none)'}`, + `Permissions Applied: ${result.permissionsApplied ? 'yes' : 'no'}`, + ] + if (result.removedResourceRecordUids.length) { + lines.push(`Removed Resource UIDs: ${result.removedResourceRecordUids.join(', ')}`) + } + for (const warning of result.warnings) { + lines.push(`Warning: ${warning}`) + } + return lines.join('\n') +} + +export function formatRemovePamConfigurationOutput( + result: RemovePamConfigurationResult, + configurationUidOrTitle?: string | string[] +): string { + const requested = Array.isArray(configurationUidOrTitle) + ? configurationUidOrTitle.join(', ') + : configurationUidOrTitle || '' + const missing = result.notFound?.length ? result.notFound.join(', ') : requested + if (!result.found) { + return result.notFound?.length > 1 + ? `PAM Configuration(s) not found: ${missing}` + : `PAM Configuration ${missing || ''} not found`.trim() + } + + const configurations = + result.configurations?.length > 0 + ? result.configurations + : result.configurationUid + ? [ + { + configurationUid: result.configurationUid, + title: result.title, + configType: result.configType, + }, + ] + : [] + + if (configurations.length <= 1) { + const config = configurations[0] + const lines = ['PAM Configuration was removed successfully.'] + if (config?.configurationUid || result.configurationUid) { + lines.push(`UID: ${config?.configurationUid || result.configurationUid}`) + } + if (config?.title || result.title) lines.push(`Title: ${config?.title || result.title}`) + if (config?.configType || result.configType) lines.push(`Type: ${config?.configType || result.configType}`) + return lines.join('\n') + } + + const lines = [`${configurations.length} PAM Configurations were removed successfully.`] + for (const config of configurations) { + const title = config.title ? ` ${config.title}` : '' + const type = config.configType ? ` [${config.configType}]` : '' + lines.push(` ${config.configurationUid}${title}${type}`) + } + return lines.join('\n') +} diff --git a/examples/sdk_example/src/pam/gateway/create_gateway.ts b/examples/sdk_example/src/pam/gateway/create_gateway.ts new file mode 100644 index 00000000..764989e5 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/create_gateway.ts @@ -0,0 +1,83 @@ +import { + cleanup, + extractErrorMessage, + GatewayConfigInitFormat, + login, + logger, + prompt, + suppressLogs, + type CreateGatewayResult, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' +import { formatCreateGatewayOutput } from '../formatOutput' + +async function createGatewayExample() { + const vault = await login() + + try { + const name = (await prompt('Gateway name: ')).trim() + if (!name) { + logger.info('Gateway name is required.') + return + } + + const application = (await prompt('KSM application name or UID: ')).trim() + if (!application) { + logger.info('KSM application is required.') + return + } + + const expireRaw = (await prompt('Token expires in minutes [60]: ')).trim() + const tokenExpiresInMin = expireRaw ? Number(expireRaw) : 60 + + // Accept ott (default), json, b64 — also y/yes → json for convenience. + const configChoice = (await prompt('Config output — ott, json, or b64 [ott]: ')).trim().toLowerCase() + let configInit: GatewayConfigInitFormat | undefined + if ( + configChoice === GatewayConfigInitFormat.Json || + configChoice === 'json' || + configChoice === 'y' || + configChoice === 'yes' + ) { + configInit = GatewayConfigInitFormat.Json + } else if (configChoice === GatewayConfigInitFormat.B64 || configChoice === 'b64') { + configInit = GatewayConfigInitFormat.B64 + } else if (configChoice && configChoice !== 'ott' && configChoice !== 'n' && configChoice !== 'no') { + logger.info(`Invalid config output "${configChoice}". Use ott, json, or b64.`) + return + } + + // Automation / Commander -r: print only tokenOrConfig (OTT, json, or b64). + const returnValueOnly = isYes(await prompt('Return value only (automation / -r)? [y/N]: ')) + + let result: CreateGatewayResult + const restore = suppressLogs() + try { + result = await vault.createGateway({ + name, + application, + tokenExpiresInMin, + configInit, + }) + } finally { + restore() + } + + if (returnValueOnly) { + logger.info(result.tokenOrConfig) + return + } + + logger.info('') + logger.info(formatCreateGatewayOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(createGatewayExample) diff --git a/examples/sdk_example/src/pam/gateway/edit_gateway.ts b/examples/sdk_example/src/pam/gateway/edit_gateway.ts new file mode 100644 index 00000000..44c2f501 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/edit_gateway.ts @@ -0,0 +1,50 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { formatEditGatewayOutput } from '../formatOutput' + +async function editGatewayExample() { + const vault = await login() + + try { + const gatewayUidOrName = (await prompt('Gateway UID or name: ')).trim() + if (!gatewayUidOrName) { + logger.info('Gateway UID or name is required.') + return + } + + const name = (await prompt('New name (Enter to keep current): ')).trim() || undefined + const nodeIdRaw = (await prompt('New node ID or name (Enter to keep current): ')).trim() + const nodeIdOrName = nodeIdRaw || undefined + + if (!name && !nodeIdOrName) { + logger.info('Nothing to do. Provide at least a new name or node.') + return + } + + let result + const restore = suppressLogs() + try { + result = await vault.editGateway({ gatewayUidOrName, name, nodeIdOrName }) + } finally { + restore() + } + + logger.info('') + logger.info(formatEditGatewayOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(editGatewayExample) diff --git a/examples/sdk_example/src/pam/gateway/list_gateways.ts b/examples/sdk_example/src/pam/gateway/list_gateways.ts new file mode 100644 index 00000000..42d75afb --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/list_gateways.ts @@ -0,0 +1,58 @@ +import { + cleanup, + extractErrorMessage, + GatewayListFormat, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { isYes } from '../../utils/format' + +async function listGatewaysExample() { + const vault = await login() + + try { + const force = isYes(await prompt('Force list if router is down? [y/N]: ')) + const verbose = isYes(await prompt('Verbose output? [y/N]: ')) + const onlineOnly = isYes(await prompt('Online gateways only? [y/N]: ')) + const asJson = isYes(await prompt('Output as JSON? [y/N]: ')) + + const options = { + force, + verbose, + onlineOnly, + format: asJson ? GatewayListFormat.Json : GatewayListFormat.Table, + } + + let result + const restore = suppressLogs() + try { + result = await vault.listGateways(options) + } finally { + restore() + } + + if (result.aborted) { + logger.info(result.message || 'Router unavailable. Re-run with force to list gateways.') + return + } + + if (result.gateways.length === 0) { + logger.info(result.message || 'No gateways found.') + return + } + + logger.info('') + logger.info(vault.formatGatewaysOutput(result, options)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(listGatewaysExample) diff --git a/examples/sdk_example/src/pam/gateway/remove_gateway.ts b/examples/sdk_example/src/pam/gateway/remove_gateway.ts new file mode 100644 index 00000000..5f7bf413 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/remove_gateway.ts @@ -0,0 +1,44 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { formatRemoveGatewayOutput } from '../formatOutput' + +async function removeGatewayExample() { + const vault = await login() + + try { + const gatewayUidOrNames = (await prompt('Gateway UID(s) or name(s), comma-separated: ')) + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + if (!gatewayUidOrNames.length) { + logger.info('Gateway UID or name is required.') + return + } + + const restore = suppressLogs() + try { + for (const gatewayUidOrName of gatewayUidOrNames) { + const result = await vault.removeGateway({ gatewayUidOrName }) + logger.info('') + logger.info(formatRemoveGatewayOutput(result)) + } + } finally { + restore() + } + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(removeGatewayExample) diff --git a/examples/sdk_example/src/pam/gateway/set_max_instances.ts b/examples/sdk_example/src/pam/gateway/set_max_instances.ts new file mode 100644 index 00000000..993bbbf9 --- /dev/null +++ b/examples/sdk_example/src/pam/gateway/set_max_instances.ts @@ -0,0 +1,57 @@ +import { + cleanup, + extractErrorMessage, + login, + logger, + MAX_GATEWAY_MAX_INSTANCES, + MIN_GATEWAY_MAX_INSTANCES, + prompt, + suppressLogs, +} from '@keeper-security/keeper-sdk-javascript' +import { runExample } from '../../utils/runner' +import { formatSetGatewayMaxInstancesOutput } from '../formatOutput' + +async function setGatewayMaxInstancesExample() { + const vault = await login() + + try { + const gatewayUidOrName = (await prompt('Gateway UID or name: ')).trim() + if (!gatewayUidOrName) { + logger.info('Gateway UID or name is required.') + return + } + + const maxRaw = ( + await prompt(`Maximum instances (${MIN_GATEWAY_MAX_INSTANCES}-${MAX_GATEWAY_MAX_INSTANCES}): `) + ).trim() + if (!/^\d+$/.test(maxRaw)) { + logger.info( + `Maximum instances must be an integer between ${MIN_GATEWAY_MAX_INSTANCES} and ${MAX_GATEWAY_MAX_INSTANCES}.` + ) + return + } + const maxInstances = Number.parseInt(maxRaw, 10) + + let result + const restore = suppressLogs() + try { + result = await vault.setGatewayMaxInstances({ + gatewayUidOrName, + maxInstances, + }) + } finally { + restore() + } + + logger.info('') + logger.info(formatSetGatewayMaxInstancesOutput(result)) + logger.info('') + } catch (err) { + logger.error(`Operation failed: ${extractErrorMessage(err)}`) + process.exitCode = 1 + } finally { + cleanup(vault) + } +} + +runExample(setGatewayMaxInstancesExample)