From 216ef3d3ec0bb975514f98b7f33e371bf5ab2574 Mon Sep 17 00:00:00 2001 From: SquirrelDevelopper Date: Thu, 7 Nov 2024 14:27:05 +0100 Subject: [PATCH 1/3] Remove DeviceAuthUseCases and enhance SSH key handling Deleted the DeviceAuthUseCases.ts to streamline the operations and updated various modules and test cases to use generated Ansible temporary private keys more effectively. Also improved the handling of playbook statuses and added new states such as canceled and timeout. --- .../CheckDeviceConnection.tsx | 8 +- .../PlaybookExecutionHandler.ts | 9 +- .../PlaybookExecutionTerminalModal.tsx | 8 +- .../TaskStatusTimeline.tsx | 14 ++- .../src/pages/Admin/Logs/TaskLogsColumns.tsx | 8 +- client/tests/PlaybookExecutionHandler.test.ts | 2 +- server/src/controllers/rest/devices/device.ts | 4 - .../controllers/rest/devices/deviceauth.ts | 4 - server/src/controllers/rest/playbooks/hook.ts | 14 +++ .../controllers/rest/playbooks/inventory.ts | 8 +- .../controllers/rest/playbooks/playbook.ts | 2 - server/src/core/startup/index.ts | 4 +- server/src/data/database/model/AnsibleTask.ts | 4 + .../src/helpers/ansible/AnsibleTaskHelper.ts | 10 ++ .../ansible/utils/InventoryTransformer.ts | 39 +++++--- .../actions/PlaybookActionComponent.ts | 11 ++- .../managers/AnsibleShellCommandsManager.ts | 91 +++++++++++-------- .../shell/managers/FileSystemManager.ts | 4 + .../managers/SshPrivateKeyFileManager.ts | 41 ++++++--- server/src/services/DeviceAuthUseCases.ts | 16 ---- server/src/services/DeviceUseCases.ts | 45 ++++----- server/src/services/PlaybookUseCases.ts | 4 + .../ansible/InventoryTransformer.test.ts | 86 +++++++++++------- shared-lib/src/enums/ansible.ts | 9 ++ 24 files changed, 278 insertions(+), 167 deletions(-) create mode 100644 server/src/helpers/ansible/AnsibleTaskHelper.ts delete mode 100644 server/src/services/DeviceAuthUseCases.ts diff --git a/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx b/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx index 83237ddc..c36e48f7 100644 --- a/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx +++ b/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx @@ -14,7 +14,7 @@ import { import { Alert, Button, Popover, Steps, Typography } from 'antd'; import { motion } from 'framer-motion'; import React, { useEffect, useRef, useState } from 'react'; -import { API } from 'ssm-shared-lib'; +import { API, SsmAnsible } from 'ssm-shared-lib'; export type CheckDeviceConnectionProps = { execId?: string; @@ -52,7 +52,11 @@ const CheckDeviceConnection: React.FC = (props) => { const [count, setCount] = useState(0); const isFinalStatusFailed = async () => { - if (savedStatuses?.find((status) => status._status === 'failed')) { + if ( + savedStatuses?.find( + (status) => status._status === SsmAnsible.AnsibleTaskStatus.FAILED, + ) + ) { const res = await getAnsibleSmartFailure({ execId: execId }); if (res.data) { setSmartFailure(res.data); diff --git a/client/src/components/PlaybookExecutionModal/PlaybookExecutionHandler.ts b/client/src/components/PlaybookExecutionModal/PlaybookExecutionHandler.ts index 0f1889d2..989b7799 100644 --- a/client/src/components/PlaybookExecutionModal/PlaybookExecutionHandler.ts +++ b/client/src/components/PlaybookExecutionModal/PlaybookExecutionHandler.ts @@ -2,7 +2,7 @@ import taskStatusTimeline from '@/components/PlaybookExecutionModal/TaskStatusTi import { getExecLogs, getTaskStatuses } from '@/services/rest/playbooks'; import { StepsProps } from 'antd'; import React, { ReactNode } from 'react'; -import { API } from 'ssm-shared-lib'; +import { API, SsmAnsible } from 'ssm-shared-lib'; export type TaskStatusTimelineType = StepsProps & { _status: string; @@ -42,7 +42,12 @@ export default class PlaybookExecutionHandler { } static isFinalStatus = (status: string): boolean => { - return status === 'failed' || status === 'successful'; + return ( + status === SsmAnsible.AnsibleTaskStatus.FAILED || + status === SsmAnsible.AnsibleTaskStatus.SUCCESS || + status === SsmAnsible.AnsibleTaskStatus.CANCELED || + status === SsmAnsible.AnsibleTaskStatus.TIMEOUT + ); }; resetTerminal = () => { diff --git a/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx b/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx index 8c7c102e..7595a769 100644 --- a/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx +++ b/client/src/components/PlaybookExecutionModal/PlaybookExecutionTerminalModal.tsx @@ -19,7 +19,7 @@ import React, { useRef, useState, } from 'react'; -import { API } from 'ssm-shared-lib'; +import { API, SsmAnsible } from 'ssm-shared-lib'; export interface PlaybookExecutionTerminalModalHandles { resetTerminal: () => void; @@ -110,7 +110,11 @@ const PlaybookExecutionTerminalModal = React.forwardRef< }; const isFinalStatusFailed = async () => { - if (savedStatuses?.find((status) => status._status === 'failed')) { + if ( + savedStatuses?.find( + (status) => status._status === SsmAnsible.AnsibleTaskStatus.FAILED, + ) + ) { const res = await getAnsibleSmartFailure({ execId: execId }); if (res.data) { api.open({ diff --git a/client/src/components/PlaybookExecutionModal/TaskStatusTimeline.tsx b/client/src/components/PlaybookExecutionModal/TaskStatusTimeline.tsx index 3c352598..653addcf 100644 --- a/client/src/components/PlaybookExecutionModal/TaskStatusTimeline.tsx +++ b/client/src/components/PlaybookExecutionModal/TaskStatusTimeline.tsx @@ -8,7 +8,7 @@ import { } from '@ant-design/icons'; import { StepsProps } from 'antd'; import React, { ReactNode } from 'react'; -import { API } from 'ssm-shared-lib'; +import { API, SsmAnsible } from 'ssm-shared-lib'; const transformToTaskStatusTimeline = ( execStatus: API.ExecStatus, @@ -16,19 +16,23 @@ const transformToTaskStatusTimeline = ( // status?: 'wait' | 'process' | 'finish' | 'error'; let status: StepsProps['status'] = undefined; let icon: ReactNode = ; - if (execStatus.status === 'starting') { + if (execStatus.status === SsmAnsible.AnsibleTaskStatus.STARTING) { status = 'finish'; icon = ; } - if (execStatus.status === 'running') { + if (execStatus.status === SsmAnsible.AnsibleTaskStatus.RUNNING) { status = 'process'; icon = ; } - if (execStatus.status === 'failed') { + if ( + execStatus.status === SsmAnsible.AnsibleTaskStatus.FAILED || + execStatus.status === SsmAnsible.AnsibleTaskStatus.CANCELED || + execStatus.status === SsmAnsible.AnsibleTaskStatus.TIMEOUT + ) { status = 'error'; icon = ; } - if (execStatus.status === 'successful') { + if (execStatus.status === SsmAnsible.AnsibleTaskStatus.SUCCESS) { status = 'finish'; icon = ; } diff --git a/client/src/pages/Admin/Logs/TaskLogsColumns.tsx b/client/src/pages/Admin/Logs/TaskLogsColumns.tsx index 3eb599b1..f6dfce19 100644 --- a/client/src/pages/Admin/Logs/TaskLogsColumns.tsx +++ b/client/src/pages/Admin/Logs/TaskLogsColumns.tsx @@ -1,7 +1,7 @@ import { ProColumns } from '@ant-design/pro-components'; import { Tag } from 'antd'; import React from 'react'; -import { API } from 'ssm-shared-lib'; +import { API, SsmAnsible } from 'ssm-shared-lib'; const TaskLogsColumns: ProColumns[] = [ { @@ -24,15 +24,17 @@ const TaskLogsColumns: ProColumns[] = [ failed: { text: 'failed' }, successful: { text: 'successful' }, starting: { text: 'starting' }, + timeout: { text: 'timeout' }, + canceled: { text: 'canceled' }, }, render: (dom, entity) => { return ( { it('should identify final statuses correctly', () => { expect(PlaybookExecutionHandler.isFinalStatus('failed')).toBe(true); expect(PlaybookExecutionHandler.isFinalStatus('successful')).toBe(true); - expect(PlaybookExecutionHandler.isFinalStatus('pending')).toBe(false); + expect(PlaybookExecutionHandler.isFinalStatus('running')).toBe(false); }); }); diff --git a/server/src/controllers/rest/devices/device.ts b/server/src/controllers/rest/devices/device.ts index efae54d0..1eb54d03 100644 --- a/server/src/controllers/rest/devices/device.ts +++ b/server/src/controllers/rest/devices/device.ts @@ -12,7 +12,6 @@ import { BadRequestError, ForbiddenError, NotFoundError } from '../../../middlew import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; import { DEFAULT_VAULT_ID, vaultEncrypt } from '../../../modules/ansible-vault/ansible-vault'; import WatcherEngine from '../../../modules/docker/core/WatcherEngine'; -import Shell from '../../../modules/shell'; import DeviceUseCases from '../../../services/DeviceUseCases'; export const addDevice = async (req, res) => { @@ -53,9 +52,6 @@ export const addDevice = async (req, res) => { becomeMethod: becomeMethod, becomePass: becomePass ? await vaultEncrypt(becomePass, DEFAULT_VAULT_ID) : undefined, } as DeviceAuth); - if (sshKey) { - await Shell.SshPrivateKeyFileManager.saveSshKey(sshKey, createdDevice.uuid); - } void WatcherEngine.registerWatcher(createdDevice); new SuccessResponse('Add device successful', { device: createdDevice as API.DeviceItem }).send( res, diff --git a/server/src/controllers/rest/devices/deviceauth.ts b/server/src/controllers/rest/devices/deviceauth.ts index 836b4ac7..fe1cc36f 100644 --- a/server/src/controllers/rest/devices/deviceauth.ts +++ b/server/src/controllers/rest/devices/deviceauth.ts @@ -7,7 +7,6 @@ import { InternalError, NotFoundError } from '../../../middlewares/api/ApiError' import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; import { DEFAULT_VAULT_ID, vaultEncrypt } from '../../../modules/ansible-vault/ansible-vault'; import WatcherEngine from '../../../modules/docker/core/WatcherEngine'; -import Shell from '../../../modules/shell'; const SENSITIVE_PLACEHOLDER = 'REDACTED'; @@ -113,9 +112,6 @@ export const addOrUpdateDeviceAuth = async (req, res) => { : undefined, becomeUser: becomeUser, } as DeviceAuth); - if (sshKey) { - await Shell.SshPrivateKeyFileManager.saveSshKey(sshKey, device.uuid); - } void WatcherEngine.deregisterWatchers(); void WatcherEngine.registerWatchers(); new SuccessResponse('Add or update device auth successful', { type: deviceAuth.authType }).send( diff --git a/server/src/controllers/rest/playbooks/hook.ts b/server/src/controllers/rest/playbooks/hook.ts index 4fc9a3f2..736c9084 100644 --- a/server/src/controllers/rest/playbooks/hook.ts +++ b/server/src/controllers/rest/playbooks/hook.ts @@ -2,8 +2,11 @@ import AnsibleLog from '../../../data/database/model/AnsibleLogs'; import AnsibleLogsRepo from '../../../data/database/repository/AnsibleLogsRepo'; import AnsibleTaskRepo from '../../../data/database/repository/AnsibleTaskRepo'; import AnsibleTaskStatusRepo from '../../../data/database/repository/AnsibleTaskStatusRepo'; +import { isFinalStatus } from '../../../helpers/ansible/AnsibleTaskHelper'; +import logger from '../../../logger'; import { BadRequestError, NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; +import sshPrivateKeyFileManager from '../../../modules/shell/managers/SshPrivateKeyFileManager'; export const addTaskStatus = async (req, res) => { if (!req.body.runner_ident || !req.body.status) { @@ -17,6 +20,17 @@ export const addTaskStatus = async (req, res) => { ident: ident, status: status, }); + if (isFinalStatus(status)) { + if (ansibleTask.target && ansibleTask.target.length > 0) { + logger.warn('Removing temporary private key'); + ansibleTask.target?.map((e) => + sshPrivateKeyFileManager.removeAnsibleTemporaryPrivateKey(e, ident), + ); + } else { + logger.warn('Removing temporary private keys'); + sshPrivateKeyFileManager.removeAllAnsibleExecTemporaryPrivateKeys('all'); + } + } new SuccessResponse('Added task status').send(res); } else { throw new NotFoundError('Task not found'); diff --git a/server/src/controllers/rest/playbooks/inventory.ts b/server/src/controllers/rest/playbooks/inventory.ts index af57266a..03564e2e 100644 --- a/server/src/controllers/rest/playbooks/inventory.ts +++ b/server/src/controllers/rest/playbooks/inventory.ts @@ -8,7 +8,7 @@ import InventoryTransformer from '../../../modules/ansible/utils/InventoryTransf export const getInventory = async (req, res) => { logger.info(`[CONTROLLER] - GET - /ansible/inventory`); let devicesAuth: DeviceAuth[] | null = []; - if (req.body.target) { + if (req.body?.target) { logger.info(`[CONTROLLER][ANSIBLE[Inventory] - Target is ${req.body.target}`); devicesAuth = await DeviceAuthRepo.findOneByDeviceUuid(req.body.target); } else { @@ -16,9 +16,9 @@ export const getInventory = async (req, res) => { devicesAuth = await DeviceAuthRepo.findAllPop(); } if (devicesAuth) { - new SuccessResponse('Get inventory', InventoryTransformer.inventoryBuilder(devicesAuth)).send( - res, - ); + const inventory = await InventoryTransformer.inventoryBuilder(devicesAuth, 'all'); + logger.info(JSON.stringify(inventory)); + new SuccessResponse('Get inventory', inventory).send(res); } else { throw new NotFoundError('No devices auth found'); } diff --git a/server/src/controllers/rest/playbooks/playbook.ts b/server/src/controllers/rest/playbooks/playbook.ts index cd0d785c..344dd0ec 100644 --- a/server/src/controllers/rest/playbooks/playbook.ts +++ b/server/src/controllers/rest/playbooks/playbook.ts @@ -1,9 +1,7 @@ import PlaybookRepo from '../../../data/database/repository/PlaybookRepo'; -import logger from '../../../logger'; import { InternalError, NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; import Shell from '../../../modules/shell'; -import FileSystemManager from '../../../modules/shell/managers/FileSystemManager'; import PlaybooksRepositoryUseCases from '../../../services/PlaybooksRepositoryUseCases'; import PlaybookUseCases from '../../../services/PlaybookUseCases'; diff --git a/server/src/core/startup/index.ts b/server/src/core/startup/index.ts index 5035b30b..c4f140c9 100644 --- a/server/src/core/startup/index.ts +++ b/server/src/core/startup/index.ts @@ -14,9 +14,9 @@ import NotificationComponent from '../../modules/notifications/NotificationCompo import ContainerCustomStacksRepositoryEngine from '../../modules/repository/ContainerCustomStacksRepositoryEngine'; import { createADefaultLocalUserRepository } from '../../modules/repository/default-playbooks-repositories'; import PlaybooksRepositoryEngine from '../../modules/repository/PlaybooksRepositoryEngine'; +import sshPrivateKeyFileManager from '../../modules/shell/managers/SshPrivateKeyFileManager'; import UpdateChecker from '../../modules/update/UpdateChecker'; import ContainerRegistryUseCases from '../../services/ContainerRegistryUseCases'; -import DeviceAuthUseCases from '../../services/DeviceAuthUseCases'; import { setAnsibleVersions } from '../system/ansible-versions'; class Startup { @@ -39,8 +39,8 @@ class Startup { } private async initializeModules() { - await DeviceAuthUseCases.saveAllDeviceAuthSshKeys(); await PlaybooksRepositoryEngine.init(); + void sshPrivateKeyFileManager.removeAllAnsibleTemporaryPrivateKeys(); void NotificationComponent.init(); void Crons.initScheduledJobs(); void WatcherEngine.init(); diff --git a/server/src/data/database/model/AnsibleTask.ts b/server/src/data/database/model/AnsibleTask.ts index eb16fcef..454b3586 100644 --- a/server/src/data/database/model/AnsibleTask.ts +++ b/server/src/data/database/model/AnsibleTask.ts @@ -7,6 +7,7 @@ export default interface AnsibleTask { ident: string; status?: string; cmd?: string; + target?: string[]; createdAt?: string; } @@ -25,6 +26,9 @@ const schema = new Schema( type: Schema.Types.String, required: true, }, + target: { + type: Schema.Types.Array, + }, }, { timestamps: true, diff --git a/server/src/helpers/ansible/AnsibleTaskHelper.ts b/server/src/helpers/ansible/AnsibleTaskHelper.ts new file mode 100644 index 00000000..5aa61d28 --- /dev/null +++ b/server/src/helpers/ansible/AnsibleTaskHelper.ts @@ -0,0 +1,10 @@ +import { SsmAnsible } from 'ssm-shared-lib'; + +export const isFinalStatus = (status: string): boolean => { + return ( + status === SsmAnsible.AnsibleTaskStatus.FAILED || + status === SsmAnsible.AnsibleTaskStatus.SUCCESS || + status === SsmAnsible.AnsibleTaskStatus.CANCELED || + status === SsmAnsible.AnsibleTaskStatus.TIMEOUT + ); +}; diff --git a/server/src/modules/ansible/utils/InventoryTransformer.ts b/server/src/modules/ansible/utils/InventoryTransformer.ts index 4dbf890d..d8b63189 100644 --- a/server/src/modules/ansible/utils/InventoryTransformer.ts +++ b/server/src/modules/ansible/utils/InventoryTransformer.ts @@ -2,12 +2,13 @@ import { SsmAnsible } from 'ssm-shared-lib'; import DeviceAuth from '../../../data/database/model/DeviceAuth'; import logger from '../../../logger'; import { Playbooks } from '../../../types/typings'; +import SshPrivateKeyFileManager from '../../shell/managers/SshPrivateKeyFileManager'; function generateDeviceKey(uuid: string) { return `device${uuid.replaceAll('-', '')}`; } -function inventoryBuilder(devicesAuth: DeviceAuth[]) { +async function inventoryBuilder(devicesAuth: DeviceAuth[], execUuid: string) { logger.info(`[TRANSFORMERS][INVENTORY] - Inventory for ${devicesAuth.length} device(s)`); // @ts-expect-error generic type const ansibleInventory: Playbooks.Hosts = { @@ -15,7 +16,7 @@ function inventoryBuilder(devicesAuth: DeviceAuth[]) { all: { children: [] }, }; - devicesAuth.forEach((deviceAuth) => { + for (const deviceAuth of devicesAuth) { const { device } = deviceAuth; const deviceKey = generateDeviceKey(device.uuid); @@ -26,30 +27,30 @@ function inventoryBuilder(devicesAuth: DeviceAuth[]) { ansibleInventory.all.children.push(deviceKey); ansibleInventory[deviceKey] = { hosts: device.ip ? [device.ip] : [], - vars: getInventoryConnectionVars(deviceAuth), + vars: await getInventoryConnectionVars(deviceAuth, execUuid), }; - }); + } logger.debug(ansibleInventory); return ansibleInventory; } -function inventoryBuilderForTarget(devicesAuth: Partial[]) { +async function inventoryBuilderForTarget(devicesAuth: Partial[], execUuid: string) { logger.info(`[TRANSFORMERS][INVENTORY] - Inventory for ${devicesAuth.length} device(s)`); const ansibleInventory: Playbooks.All & Playbooks.HostGroups = { // @ts-expect-error I cannot comprehend generic typescript type all: {}, }; - devicesAuth.forEach((e) => { - logger.info(`[TRANSFORMERS][INVENTORY] - Building inventory for ${e.device?.uuid}`); + for (const deviceAuth of devicesAuth) { + logger.info(`[TRANSFORMERS][INVENTORY] - Building inventory for ${deviceAuth.device?.uuid}`); ansibleInventory[ - `device${e.device?.uuid.replaceAll('-', '')}` as keyof typeof ansibleInventory + `device${deviceAuth.device?.uuid.replaceAll('-', '')}` as keyof typeof ansibleInventory ] = { // @ts-expect-error I cannot comprehend generic typescript type - hosts: e.device.ip as string, - vars: getInventoryConnectionVars(e), + hosts: deviceAuth.device.ip as string, + vars: await getInventoryConnectionVars(deviceAuth, execUuid), }; - }); + } logger.debug(ansibleInventory); return ansibleInventory; } @@ -60,12 +61,17 @@ interface Auth { ansible_ssh_pass?: { __ansible_vault: any }; } -function getAuth(deviceAuth: Partial): Auth { +async function getAuth(deviceAuth: Partial, execUuid: string): Promise { const auth: Auth = {}; switch (deviceAuth.authType) { case SsmAnsible.SSHType.KeyBased: - auth.ansible_ssh_private_key_file = `/tmp/${deviceAuth.device?.uuid}.key`; + auth.ansible_ssh_private_key_file = + await SshPrivateKeyFileManager.genAnsibleTemporaryPrivateKey( + deviceAuth.sshKey as string, + deviceAuth.device?.uuid as string, + execUuid, + ); if (deviceAuth.sshKeyPass) { if (deviceAuth.sshConnection !== SsmAnsible.SSHConnection.PARAMIKO) { throw new Error('Ssh key is not supported for non-paramiko connection'); @@ -96,7 +102,10 @@ interface ConnectionVars { ansible_ssh_port?: number; } -function getInventoryConnectionVars(deviceAuth: Partial): ConnectionVars { +async function getInventoryConnectionVars( + deviceAuth: Partial, + execUuid: string, +): Promise { // See https://docs.ansible.com/ansible/latest/collections/ansible/builtin/paramiko_ssh_connection.html let connection = deviceAuth.sshConnection; if (!connection) { @@ -118,7 +127,7 @@ function getInventoryConnectionVars(deviceAuth: Partial): Connection ansible_ssh_port: deviceAuth.sshPort, }; - return { ...vars, ...getAuth(deviceAuth) }; + return { ...vars, ...(await getAuth(deviceAuth, execUuid)) }; } export default { diff --git a/server/src/modules/automations/actions/PlaybookActionComponent.ts b/server/src/modules/automations/actions/PlaybookActionComponent.ts index 074a88ec..580df5af 100644 --- a/server/src/modules/automations/actions/PlaybookActionComponent.ts +++ b/server/src/modules/automations/actions/PlaybookActionComponent.ts @@ -1,4 +1,4 @@ -import { API, Automations } from 'ssm-shared-lib'; +import { API, Automations, SsmAnsible } from 'ssm-shared-lib'; import User from '../../../data/database/model/User'; import AnsibleTaskStatusRepo from '../../../data/database/repository/AnsibleTaskStatusRepo'; import PlaybookRepo from '../../../data/database/repository/PlaybookRepo'; @@ -51,7 +51,12 @@ class PlaybookActionComponent extends AbstractActionComponent { } static isFinalStatus = (status: string): boolean => { - return status === 'failed' || status === 'successful'; + return ( + status === SsmAnsible.AnsibleTaskStatus.FAILED || + status === SsmAnsible.AnsibleTaskStatus.SUCCESS || + status === SsmAnsible.AnsibleTaskStatus.CANCELED || + status === SsmAnsible.AnsibleTaskStatus.TIMEOUT + ); }; async waitForResult(execId: string, timeoutCount = 0) { @@ -72,7 +77,7 @@ class PlaybookActionComponent extends AbstractActionComponent { const lastExecStatus = execStatuses[0]; this.childLogger.info(`Latest execution status ${lastExecStatus.status}`); if (PlaybookActionComponent.isFinalStatus(lastExecStatus.status as string)) { - if (lastExecStatus.status === 'successful') { + if (lastExecStatus.status === SsmAnsible.AnsibleTaskStatus.SUCCESS) { await this.onSuccess(); } else { await this.onError(); diff --git a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts index 85ee34f6..5dbc43e5 100644 --- a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts +++ b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts @@ -11,6 +11,7 @@ import ansibleCmd from '../../ansible/AnsibleCmd'; import AnsibleGalaxyCmd from '../../ansible/AnsibleGalaxyCmd'; import Inventory from '../../ansible/utils/InventoryTransformer'; import { AbstractShellCommander } from '../AbstractShellCommander'; +import SshPrivateKeyFileManager from './SshPrivateKeyFileManager'; class AnsibleShellCommandsManager extends AbstractShellCommander { constructor() { @@ -31,8 +32,10 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { target?: string[], extraVars?: API.ExtraVars, mode: SsmAnsible.ExecutionMode = SsmAnsible.ExecutionMode.APPLY, + execUuid?: string, ) { this.logger.info('executePlaybook - Starting...'); + execUuid = execUuid || uuidv4(); let inventoryTargets: (Playbooks.All & Playbooks.HostGroups) | undefined; if (target) { @@ -44,7 +47,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { `Exec failed, no matching target (Device Authentication not found for target ${target})`, ); } - inventoryTargets = Inventory.inventoryBuilderForTarget(devicesAuth); + inventoryTargets = await Inventory.inventoryBuilderForTarget(devicesAuth, execUuid); } return await this.executePlaybookOnInventory( playbookPath, @@ -52,6 +55,8 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { inventoryTargets, extraVars, mode, + target, + execUuid, ); } @@ -61,43 +66,57 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { inventoryTargets?: Playbooks.All & Playbooks.HostGroups, extraVars?: API.ExtraVars, mode: SsmAnsible.ExecutionMode = SsmAnsible.ExecutionMode.APPLY, + target?: string[], + execUuid?: string, ) { - shell.cd(this.ANSIBLE_PATH); - shell.rm(`${SSM_INSTALL_PATH}/server/src/playbooks/inventory/hosts`); - shell.rm(`${SSM_INSTALL_PATH}/server/src/playbooks/env/_extravars`); - const uuid = uuidv4(); - const result = await new Promise((resolve) => { - const cmd = ansibleCmd.buildAnsibleCmd( - playbookPath, - uuid, - inventoryTargets, - user, - extraVars, - mode, - ); - this.logger.info(`executePlaybook - Executing "${cmd}"`); - const child = shell.exec(cmd, { - async: true, - }); - child.stdout?.on('data', function (data) { - resolve(data); - }); - child.on('exit', function () { - resolve(null); - }); - }); - this.logger.info('executePlaybook - launched'); - if (result) { - this.logger.info(`executePlaybook - ExecId is ${uuid}`); - await AnsibleTaskRepo.create({ - ident: uuid, - status: 'created', - cmd: `playbook ${playbookPath}`, + execUuid = execUuid || uuidv4(); + + try { + shell.cd(this.ANSIBLE_PATH); + shell.rm(`${SSM_INSTALL_PATH}/server/src/ansible/inventory/hosts.json`); + shell.rm(`${SSM_INSTALL_PATH}/server/src/ansible/env/extravars`); + + const result = await new Promise((resolve) => { + const cmd = ansibleCmd.buildAnsibleCmd( + playbookPath, + execUuid, + inventoryTargets, + user, + extraVars, + mode, + ); + this.logger.info(`executePlaybook - Executing "${cmd}"`); + const child = shell.exec(cmd, { + async: true, + }); + child.stdout?.on('data', function (data) { + resolve(data); + }); + child.on('exit', function () { + resolve(null); + }); }); - return result; - } else { - this.logger.error('executePlaybook - Result was not properly set'); - throw new Error('Exec failed'); + this.logger.info('executePlaybook - launched'); + if (result) { + this.logger.info(`executePlaybook - ExecId is ${execUuid}`); + await AnsibleTaskRepo.create({ + ident: execUuid, + status: 'created', + cmd: `playbook ${playbookPath}`, + target: target, + }); + return result; + } else { + this.logger.error('executePlaybook - Result was not properly set'); + throw new Error('Exec failed'); + } + } catch (error: any) { + if (target) { + target?.map((e) => SshPrivateKeyFileManager.removeAnsibleTemporaryPrivateKey(e, execUuid)); + } else { + SshPrivateKeyFileManager.removeAllAnsibleExecTemporaryPrivateKeys('all'); + } + throw error; } } diff --git a/server/src/modules/shell/managers/FileSystemManager.ts b/server/src/modules/shell/managers/FileSystemManager.ts index 67cd82c4..d370b729 100644 --- a/server/src/modules/shell/managers/FileSystemManager.ts +++ b/server/src/modules/shell/managers/FileSystemManager.ts @@ -21,6 +21,10 @@ class FileSystemManager extends AbstractShellCommander { this.executeCommand(shellWrapper.rm, '-rf', directory); } + deleteFile(filePath: string): void { + this.executeCommand(shellWrapper.rm, '-f', filePath); + } + writeFile(content: string, path: string): void { this.executeCommand(shellWrapper.to, content, path); } diff --git a/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts b/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts index c9bf0caa..da22d496 100644 --- a/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts +++ b/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts @@ -1,29 +1,46 @@ +import * as os from 'node:os'; import path from 'path'; import logger from '../../../logger'; -import shellWrapper from '../ShellWrapper'; +import { DEFAULT_VAULT_ID, vaultDecrypt } from '../../ansible-vault/ansible-vault'; import { AbstractShellCommander } from '../AbstractShellCommander'; +import FileSystemManager from './FileSystemManager'; class SshPrivateKeyFileManager extends AbstractShellCommander { constructor() { super(logger.child({ module: 'SshPrivateKeyFileManager' }), 'SshPrivateKey'); } - async saveSshKey(key: string, uuid: string) { - try { - this.logger.info('vaultSshKey Starting...'); + getTmpKeyFileName(execUuid: string, deviceUuid: string) { + return `${execUuid}_${deviceUuid}`; + } - const keyFilePath = path.join('/tmp', `${uuid}.key`); + getTmpKeyFilePath(fileName: string) { + return path.join(os.tmpdir(), `${fileName}_dec.key`); + } - this.executeCommand(shellWrapper.to, key, keyFilePath); + async genAnsibleTemporaryPrivateKey(sskVaultedKey: string, deviceUuid: string, execUuid: string) { + const decryptedContent = await vaultDecrypt(sskVaultedKey, DEFAULT_VAULT_ID); + const tmpKeyFilePath = this.getTmpKeyFilePath(this.getTmpKeyFileName(execUuid, deviceUuid)); + FileSystemManager.writeFile(decryptedContent as string, tmpKeyFilePath); + return tmpKeyFilePath; + } - if (this.executeCommand(shellWrapper.chmod, '600', keyFilePath).code !== 0) { - throw new Error('vaultSshKey - Error chmoding file'); - } - } catch (error) { - this.logger.error('vaultSshKey'); - throw error; + removeAnsibleTemporaryPrivateKey(deviceUuid: string, execUuid: string) { + const tmpKeyFilePath = this.getTmpKeyFilePath(`${execUuid}_${deviceUuid}`); + if (!FileSystemManager.test('-f', tmpKeyFilePath)) { + this.logger.warn(`remoteAnsibleTemporaryPrivateKey - File not found (${tmpKeyFilePath})`); + } else { + FileSystemManager.deleteFile(tmpKeyFilePath); } } + + removeAllAnsibleExecTemporaryPrivateKeys(execUuid: string) { + FileSystemManager.deleteFile(this.getTmpKeyFilePath(this.getTmpKeyFileName(execUuid, '*'))); + } + + removeAllAnsibleTemporaryPrivateKeys() { + FileSystemManager.deleteFile(this.getTmpKeyFilePath('*')); + } } export default new SshPrivateKeyFileManager(); diff --git a/server/src/services/DeviceAuthUseCases.ts b/server/src/services/DeviceAuthUseCases.ts deleted file mode 100644 index a97761e4..00000000 --- a/server/src/services/DeviceAuthUseCases.ts +++ /dev/null @@ -1,16 +0,0 @@ -import DeviceAuthRepo from '../data/database/repository/DeviceAuthRepo'; -import Shell from '../modules/shell'; - -async function saveAllDeviceAuthSshKeys() { - const devicesAuth = (await DeviceAuthRepo.findAllPopWithSshKey()) || []; - for (const deviceAuth of devicesAuth) { - await Shell.SshPrivateKeyFileManager.saveSshKey( - deviceAuth.sshKey as string, - deviceAuth.device.uuid, - ); - } -} - -export default { - saveAllDeviceAuthSshKeys, -}; diff --git a/server/src/services/DeviceUseCases.ts b/server/src/services/DeviceUseCases.ts index 3c66a666..7cea7fa4 100644 --- a/server/src/services/DeviceUseCases.ts +++ b/server/src/services/DeviceUseCases.ts @@ -1,5 +1,6 @@ import DockerModem from 'docker-modem'; import Dockerode from 'dockerode'; +import { uuidv4 } from 'mongodb-memory-server-core/lib/util/utils'; import { API, SsmAnsible, SsmStatus } from 'ssm-shared-lib'; import { setToCache } from '../data/cache'; import Device, { DeviceModel } from '../data/database/model/Device'; @@ -16,7 +17,6 @@ import { InternalError } from '../middlewares/api/ApiError'; import { DEFAULT_VAULT_ID, vaultEncrypt } from '../modules/ansible-vault/ansible-vault'; import Inventory from '../modules/ansible/utils/InventoryTransformer'; import { getCustomAgent } from '../modules/docker/core/CustomAgent'; -import Shell from '../modules/shell'; import PlaybookUseCases from './PlaybookUseCases'; const logger = PinoLogger.child({ module: 'DeviceUseCases' }, { msgPrefix: '[DEVICE] - ' }); @@ -145,28 +145,29 @@ async function checkAnsibleConnection( if (masterNodeUrl) { await setToCache(SsmAnsible.DefaultSharedExtraVarsList.MASTER_NODE_URL, masterNodeUrl); } - if (sshKey) { - await Shell.SshPrivateKeyFileManager.saveSshKey(sshKey, 'tmp'); - } - const mockedInventoryTarget = Inventory.inventoryBuilderForTarget([ - { - device: { - _id: 'tmp', - ip, - uuid: 'tmp', - status: SsmStatus.DeviceStatus.REGISTERING, + const execUuid = uuidv4(); + const mockedInventoryTarget = await Inventory.inventoryBuilderForTarget( + [ + { + device: { + _id: 'tmp', + ip, + uuid: 'tmp', + status: SsmStatus.DeviceStatus.REGISTERING, + }, + authType, + sshKey: sshKey ? await vaultEncrypt(sshKey, DEFAULT_VAULT_ID) : undefined, + sshUser, + sshPwd: sshPwd ? await vaultEncrypt(sshPwd, DEFAULT_VAULT_ID) : undefined, + sshPort: sshPort || 22, + becomeMethod, + sshConnection, + becomePass: becomePass ? await vaultEncrypt(becomePass, DEFAULT_VAULT_ID) : undefined, + sshKeyPass: sshKeyPass ? await vaultEncrypt(sshKeyPass, DEFAULT_VAULT_ID) : undefined, }, - authType, - sshKey: sshKey ? await vaultEncrypt(sshKey, DEFAULT_VAULT_ID) : undefined, - sshUser, - sshPwd: sshPwd ? await vaultEncrypt(sshPwd, DEFAULT_VAULT_ID) : undefined, - sshPort: sshPort || 22, - becomeMethod, - sshConnection, - becomePass: becomePass ? await vaultEncrypt(becomePass, DEFAULT_VAULT_ID) : undefined, - sshKeyPass: sshKeyPass ? await vaultEncrypt(sshKeyPass, DEFAULT_VAULT_ID) : undefined, - }, - ]); + ], + execUuid, + ); const playbook = await PlaybookRepo.findOneByUniqueQuickReference('checkDeviceBeforeAdd'); if (!playbook) { throw new InternalError('_checkDeviceBeforeAdd.yml not found.'); diff --git a/server/src/services/PlaybookUseCases.ts b/server/src/services/PlaybookUseCases.ts index aff1ff72..fbc4c3d2 100644 --- a/server/src/services/PlaybookUseCases.ts +++ b/server/src/services/PlaybookUseCases.ts @@ -49,6 +49,7 @@ async function executePlaybookOnInventory( user: User, inventoryTargets?: Playbooks.All & Playbooks.HostGroups, extraVarsForcedValues?: API.ExtraVars, + execUuid?: string, ) { const substitutedExtraVars: API.ExtraVars | undefined = await completeExtraVar( playbook, @@ -60,6 +61,9 @@ async function executePlaybookOnInventory( user, inventoryTargets, substitutedExtraVars, + undefined, + undefined, + execUuid, ); } diff --git a/server/src/tests/unit-tests/modules/ansible/InventoryTransformer.test.ts b/server/src/tests/unit-tests/modules/ansible/InventoryTransformer.test.ts index 5dedeb5f..95d2f053 100644 --- a/server/src/tests/unit-tests/modules/ansible/InventoryTransformer.test.ts +++ b/server/src/tests/unit-tests/modules/ansible/InventoryTransformer.test.ts @@ -1,9 +1,25 @@ +import * as os from 'node:os'; +import path from 'path'; import { SsmAnsible } from 'ssm-shared-lib'; -import { describe, expect, test } from 'vitest'; -import InventoryTransformer from '../../../../modules/ansible/utils/InventoryTransformer'; // replace with actual file path +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import InventoryTransformer from '../../../../modules/ansible/utils/InventoryTransformer'; + +// Mock the vaultDecrypt function +vi.mock('../../../../modules/ansible-vault/ansible-vault', async (importOriginal) => { + return { + ...(await importOriginal()), + vaultDecrypt: async (value: string, vault: string) => { + return value + '-decrypted'; + }, + }; +}); describe('test InventoryTransformer', () => { - test('inventoryBuilder', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + test('inventoryBuilder', async () => { const deviceAuth = { device: { uuid: '1234-5678-9101', ip: '192.168.1.1' }, authType: SsmAnsible.SSHType.UserPassword, @@ -13,7 +29,7 @@ describe('test InventoryTransformer', () => { }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilder([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilder([deviceAuth], ''); const expectedResult = { _meta: { hostvars: { @@ -41,7 +57,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTarget', () => { + test('inventoryBuilderForTarget', async () => { const deviceAuth = { device: { uuid: '1234-5678-9102', ip: '192.168.1.2' }, authType: SsmAnsible.SSHType.UserPassword, @@ -50,7 +66,7 @@ describe('test InventoryTransformer', () => { sshUser: 'admin', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilderForTarget([deviceAuth], ''); const expectedResult = { all: {}, device123456789102: { @@ -69,7 +85,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTargetPasswordless', () => { + test('inventoryBuilderForTargetPasswordless', async () => { const deviceAuth = { device: { uuid: '1234-5678-9102', ip: '192.168.1.2' }, authType: SsmAnsible.SSHType.PasswordLess, @@ -78,7 +94,7 @@ describe('test InventoryTransformer', () => { sshUser: 'admin', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilderForTarget([deviceAuth], ''); const expectedResult = { all: {}, device123456789102: { @@ -96,7 +112,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTargetWithSshKey', () => { + test('inventoryBuilderForTargetWithSshKey', async () => { const deviceAuth = { device: { uuid: '1234-5678-9102', ip: '192.168.1.2' }, authType: SsmAnsible.SSHType.KeyBased, @@ -112,7 +128,7 @@ describe('test InventoryTransformer', () => { 'NrRFi9wrf+M7Q== test@test.local', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilderForTarget([deviceAuth], 'execId'); const expectedResult = { all: {}, device123456789102: { @@ -123,7 +139,7 @@ describe('test InventoryTransformer', () => { ansible_become_pass: { __ansible_vault: 'qwerty' }, ansible_ssh_host_key_checking: false, ansible_user: 'admin', - ansible_ssh_private_key_file: '/tmp/1234-5678-9102.key', + ansible_ssh_private_key_file: path.join(os.tmpdir(), 'execId_1234-5678-9102_dec.key'), }, }, }; @@ -131,7 +147,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTargetWithSshKeyAndKeyPhrase', () => { + test('inventoryBuilderForTargetWithSshKeyAndKeyPhrase', async () => { const deviceAuth = { device: { uuid: '1234-5678-9102', ip: '192.168.1.2' }, authType: SsmAnsible.SSHType.KeyBased, @@ -149,7 +165,7 @@ describe('test InventoryTransformer', () => { 'NrRFi9wrf+M7Q== test@test.local', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilderForTarget([deviceAuth], 'execId'); const expectedResult = { all: {}, device123456789102: { @@ -161,7 +177,7 @@ describe('test InventoryTransformer', () => { ansible_paramiko_pass: { __ansible_vault: 'test' }, ansible_user: 'admin', ansible_ssh_host_key_checking: false, - ansible_ssh_private_key_file: '/tmp/1234-5678-9102.key', + ansible_ssh_private_key_file: path.join(os.tmpdir(), 'execId_1234-5678-9102_dec.key'), }, }, }; @@ -169,8 +185,8 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilder with no devicesAuth', () => { - const result = InventoryTransformer.inventoryBuilder([]); + test('inventoryBuilder with no devicesAuth', async () => { + const result = await InventoryTransformer.inventoryBuilder([], ''); const expectedResult = { _meta: { hostvars: {} }, all: { children: [] }, @@ -179,7 +195,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilder for deviceAuth with strict host key checking', () => { + test('inventoryBuilder for deviceAuth with strict host key checking', async () => { const deviceAuth = { device: { uuid: '1234-5678-9103', ip: '192.168.1.3' }, authType: SsmAnsible.SSHType.UserPassword, @@ -188,7 +204,7 @@ describe('test InventoryTransformer', () => { becomePass: 'adminpassword', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilder([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilder([deviceAuth], ''); const expectedResult = { _meta: { hostvars: { @@ -216,13 +232,13 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTarget for deviceAuth with undefined IP', () => { + test('inventoryBuilderForTarget for deviceAuth with undefined IP', async () => { const deviceAuth = { device: { uuid: '1234-5678-9104' }, authType: SsmAnsible.SSHType.UserPassword, }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth]); + const result = await InventoryTransformer.inventoryBuilderForTarget([deviceAuth], ''); const expectedResult = { all: {}, device123456789104: { @@ -241,7 +257,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTarget with multiple devicesAuth', () => { + test('inventoryBuilderForTarget with multiple devicesAuth', async () => { const deviceAuth1 = { device: { uuid: '1234-5678-9105', ip: '192.168.1.4' }, authType: SsmAnsible.SSHType.UserPassword, @@ -258,7 +274,10 @@ describe('test InventoryTransformer', () => { sshUser: 'root', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth1, deviceAuth2]); + const result = await InventoryTransformer.inventoryBuilderForTarget( + [deviceAuth1, deviceAuth2], + '', + ); const expectedResult = { all: {}, device123456789105: { @@ -288,7 +307,7 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilderForTarget with multiple mixed devicesAuth', () => { + test('inventoryBuilderForTarget with multiple mixed devicesAuth', async () => { const deviceAuth1 = { device: { uuid: '1234-5678-9105', ip: '192.168.1.4' }, authType: SsmAnsible.SSHType.UserPassword, @@ -305,7 +324,10 @@ describe('test InventoryTransformer', () => { sshUser: 'root', }; // @ts-expect-error partial type - const result = InventoryTransformer.inventoryBuilderForTarget([deviceAuth1, deviceAuth2]); + const result = await InventoryTransformer.inventoryBuilderForTarget( + [deviceAuth1, deviceAuth2], + '', + ); const expectedResult = { all: {}, device123456789105: { @@ -334,17 +356,17 @@ describe('test InventoryTransformer', () => { expect(result).toEqual(expectedResult); }); - test('inventoryBuilder error handling', () => { - expect(() => { + test('inventoryBuilder error handling', async () => { + await expect( // @ts-expect-error partial type - InventoryTransformer.inventoryBuilder(['not a DeviceAuth instance']); - }).toThrow(TypeError); + InventoryTransformer.inventoryBuilder(['not a DeviceAuth instance'], ''), + ).rejects.toThrow(TypeError); }); - test('inventoryBuilderForTarget error handling', () => { - expect(() => { + test('inventoryBuilderForTarget error handling', async () => { + await expect( // @ts-expect-error partial type - InventoryTransformer.inventoryBuilderForTarget(['not a DeviceAuth instance']); - }).toThrow(TypeError); + InventoryTransformer.inventoryBuilderForTarget(['not a DeviceAuth instance'], ''), + ).rejects.toThrow(TypeError); }); }); diff --git a/shared-lib/src/enums/ansible.ts b/shared-lib/src/enums/ansible.ts index f276965e..af831fe2 100644 --- a/shared-lib/src/enums/ansible.ts +++ b/shared-lib/src/enums/ansible.ts @@ -43,3 +43,12 @@ export enum ExecutionMode { CHECK = 'check', CHECK_AND_DIFF = 'check-diff' } + +export enum AnsibleTaskStatus { + STARTING = 'starting', + RUNNING = 'running', + SUCCESS = 'successful', + FAILED = 'failed', + TIMEOUT = 'timeout', + CANCELED = 'canceled', +} From ea0e5755cba65191021e589df79872d7f959a451 Mon Sep 17 00:00:00 2001 From: SquirrelDevelopper Date: Thu, 7 Nov 2024 14:37:10 +0100 Subject: [PATCH 2/3] Add execUuid to API requests and improve key removal Updated the inventory retrieval to include execUuid in the query parameters, ensuring better traceability of requests. Adjusted key removal functions to use execUuid, enhancing the precision of related operations. --- server/src/ansible/inventory/inventory.py | 2 +- server/src/ansible/ssm-ansible-run.py | 2 +- server/src/controllers/rest/playbooks/hook.ts | 2 +- server/src/controllers/rest/playbooks/inventory.ts | 4 ++-- .../src/modules/shell/managers/AnsibleShellCommandsManager.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/ansible/inventory/inventory.py b/server/src/ansible/inventory/inventory.py index 51e540a8..9bac739a 100755 --- a/server/src/ansible/inventory/inventory.py +++ b/server/src/ansible/inventory/inventory.py @@ -49,7 +49,7 @@ def parse_args(): def load_inventory(): headers = {'Accept': 'application/json', 'Authorization': "Bearer {}".format(os.getenv("SSM_API_KEY"))} - r = requests.get('http://localhost:3000/playbooks/inventory', headers=headers) + r = requests.get('http://localhost:3000/playbooks/inventory?execUuid={}'.format(os.getenv('SSM_EXEC_UUID')), headers=headers) return r.json()['data'] diff --git a/server/src/ansible/ssm-ansible-run.py b/server/src/ansible/ssm-ansible-run.py index c2043b99..0a550bab 100644 --- a/server/src/ansible/ssm-ansible-run.py +++ b/server/src/ansible/ssm-ansible-run.py @@ -79,7 +79,7 @@ def execute(): debug = True if args.specific_host is not None: specific_host = json.loads(args.specific_host) - + os.environ['SSM_EXEC_UUID'] = args.ident runner_args = { 'ident': args.ident, 'private_data_dir': './', diff --git a/server/src/controllers/rest/playbooks/hook.ts b/server/src/controllers/rest/playbooks/hook.ts index 736c9084..dfd465c3 100644 --- a/server/src/controllers/rest/playbooks/hook.ts +++ b/server/src/controllers/rest/playbooks/hook.ts @@ -28,7 +28,7 @@ export const addTaskStatus = async (req, res) => { ); } else { logger.warn('Removing temporary private keys'); - sshPrivateKeyFileManager.removeAllAnsibleExecTemporaryPrivateKeys('all'); + sshPrivateKeyFileManager.removeAllAnsibleExecTemporaryPrivateKeys(ident); } } new SuccessResponse('Added task status').send(res); diff --git a/server/src/controllers/rest/playbooks/inventory.ts b/server/src/controllers/rest/playbooks/inventory.ts index 03564e2e..13710800 100644 --- a/server/src/controllers/rest/playbooks/inventory.ts +++ b/server/src/controllers/rest/playbooks/inventory.ts @@ -6,6 +6,7 @@ import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; import InventoryTransformer from '../../../modules/ansible/utils/InventoryTransformer'; export const getInventory = async (req, res) => { + const { execUuid } = req.query; logger.info(`[CONTROLLER] - GET - /ansible/inventory`); let devicesAuth: DeviceAuth[] | null = []; if (req.body?.target) { @@ -16,8 +17,7 @@ export const getInventory = async (req, res) => { devicesAuth = await DeviceAuthRepo.findAllPop(); } if (devicesAuth) { - const inventory = await InventoryTransformer.inventoryBuilder(devicesAuth, 'all'); - logger.info(JSON.stringify(inventory)); + const inventory = await InventoryTransformer.inventoryBuilder(devicesAuth, execUuid); new SuccessResponse('Get inventory', inventory).send(res); } else { throw new NotFoundError('No devices auth found'); diff --git a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts index 5dbc43e5..bf260c67 100644 --- a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts +++ b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts @@ -114,7 +114,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { if (target) { target?.map((e) => SshPrivateKeyFileManager.removeAnsibleTemporaryPrivateKey(e, execUuid)); } else { - SshPrivateKeyFileManager.removeAllAnsibleExecTemporaryPrivateKeys('all'); + SshPrivateKeyFileManager.removeAllAnsibleExecTemporaryPrivateKeys(execUuid); } throw error; } From d8d3ae35ae9c246621a581c2759dc22bb0716553 Mon Sep 17 00:00:00 2001 From: SquirrelDevelopper Date: Thu, 7 Nov 2024 14:39:14 +0100 Subject: [PATCH 3/3] Add chmod command for temporary private key files Ensure that temporary private key files are securely permissioned by applying chmod 600. This enhances the security by restricting file access to the owner only. --- server/src/modules/shell/managers/SshPrivateKeyFileManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts b/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts index da22d496..02b4ed45 100644 --- a/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts +++ b/server/src/modules/shell/managers/SshPrivateKeyFileManager.ts @@ -3,6 +3,7 @@ import path from 'path'; import logger from '../../../logger'; import { DEFAULT_VAULT_ID, vaultDecrypt } from '../../ansible-vault/ansible-vault'; import { AbstractShellCommander } from '../AbstractShellCommander'; +import shellWrapper from '../ShellWrapper'; import FileSystemManager from './FileSystemManager'; class SshPrivateKeyFileManager extends AbstractShellCommander { @@ -22,6 +23,9 @@ class SshPrivateKeyFileManager extends AbstractShellCommander { const decryptedContent = await vaultDecrypt(sskVaultedKey, DEFAULT_VAULT_ID); const tmpKeyFilePath = this.getTmpKeyFilePath(this.getTmpKeyFileName(execUuid, deviceUuid)); FileSystemManager.writeFile(decryptedContent as string, tmpKeyFilePath); + if (this.executeCommand(shellWrapper.chmod, '600', tmpKeyFilePath).code !== 0) { + throw new Error(`genAnsibleTemporaryPrivateKey - Error chmoding file ${tmpKeyFilePath}`); + } return tmpKeyFilePath; }