diff --git a/client/src/components/Template/Title.tsx b/client/src/components/Template/Title.tsx index 77025251..800be132 100644 --- a/client/src/components/Template/Title.tsx +++ b/client/src/components/Template/Title.tsx @@ -22,6 +22,7 @@ export enum TitleColors { ANSIBLE_CONF = '#c1660e', COMPOSE = '#8e7418', HOST_URL = '#368e18', + SECRET = '#16739a', } export type PageContainerTitleProps = { diff --git a/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx b/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx index 5d25ea6e..92732c18 100644 --- a/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx +++ b/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx @@ -2,10 +2,13 @@ import { ErrorCircleSettings20Regular, SimpleIconsGit, StreamlineLocalStorageFolderSolid, + UserSecret, } from '@/components/Icons/CustomIcons'; import Title, { TitleColors } from '@/components/Template/Title'; +import CustomVaultModal from '@/pages/Admin/Settings/components/subcomponents/CustomVaultModal'; import PlaybooksGitRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal'; import PlaybooksLocalRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal'; +import { getAnsibleVaults } from '@/services/rest/ansible'; import { getGitPlaybooksRepositories, getPlaybooksLocalRepositories, @@ -51,6 +54,7 @@ const PlaybookSettings: React.FC = () => { const [localRepositories, setLocalRepositories] = useState< API.LocalPlaybooksRepository[] >([]); + const [customVaults, setCustomVaults] = useState([]); const asyncFetch = async () => { await getGitPlaybooksRepositories().then((list) => { @@ -63,6 +67,11 @@ const PlaybookSettings: React.FC = () => { setLocalRepositories(list.data); } }); + await getAnsibleVaults().then((list) => { + if (list?.data) { + setCustomVaults(list.data); + } + }); }; useEffect(() => { @@ -70,9 +79,14 @@ const PlaybookSettings: React.FC = () => { }, []); const [gitModalOpened, setGitModalOpened] = useState(false); - const [selectedGitRecord, setSelectedGitRecord] = useState(); + const [selectedGitRecord, setSelectedGitRecord] = + useState(); const [localModalOpened, setLocalModalOpened] = useState(false); - const [selectedLocalRecord, setSelectedLocalRecord] = useState(); + const [selectedLocalRecord, setSelectedLocalRecord] = + useState(); + const [selectedVaultRecord, setSelectedVaultRecord] = + useState(); + const [vaultModalOpened, setVaultModalOpened] = useState(false); const onChange = async (newValue: number | null) => { if (newValue !== null) { @@ -93,14 +107,21 @@ const PlaybookSettings: React.FC = () => { setModalOpened={setGitModalOpened} modalOpened={gitModalOpened} asyncFetch={asyncFetch} - selectedRecord={selectedGitRecord} + selectedRecord={selectedGitRecord as API.GitPlaybooksRepository} /> + { dataSource={gitRepositories} /> + } + /> + } + style={{ marginTop: 16 }} + extra={ + + + + + + + } + > + + ghost={true} + itemCardProps={{ + ghost: true, + }} + pagination={ + customVaults?.length > 8 + ? { + defaultPageSize: 8, + showSizeChanger: false, + showQuickJumper: false, + } + : false + } + rowSelection={false} + grid={{ gutter: 0, xs: 1, sm: 2, md: 2, lg: 2, xl: 4, xxl: 4 }} + onItem={(record: API.AnsibleVault) => { + return { + onMouseEnter: () => { + console.log(record); + }, + onClick: () => { + setSelectedVaultRecord(record); + setVaultModalOpened(true); + }, + }; + }} + metas={{ + title: { + dataIndex: 'vaultId', + }, + avatar: { + render: () => } />, + }, + }} + dataSource={customVaults} + /> + ); }; diff --git a/client/src/pages/Admin/Settings/components/subcomponents/CustomVaultModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/CustomVaultModal.tsx new file mode 100644 index 00000000..90c2ca52 --- /dev/null +++ b/client/src/pages/Admin/Settings/components/subcomponents/CustomVaultModal.tsx @@ -0,0 +1,136 @@ +import { UserSecret } from '@/components/Icons/CustomIcons'; +import { + deleteAnsibleVault, + postAnsibleVault, + updateAnsibleVault, +} from '@/services/rest/ansible'; +import { DeleteOutlined } from '@ant-design/icons'; +import { ModalForm, ProForm, ProFormText } from '@ant-design/pro-components'; +import { Avatar, Button, message, Popconfirm } from 'antd'; +import React, { FC, useState } from 'react'; +import { API } from 'ssm-shared-lib'; + +type CustomVaultModalProps = { + selectedRecord?: Partial; + modalOpened: boolean; + setModalOpened: any; + asyncFetch: () => Promise; + vaults: API.CustomVault[]; +}; + +const PlaybooksLocalRepositoryModal: FC = ({ + selectedRecord, + modalOpened, + setModalOpened, + asyncFetch, + vaults, +}) => { + const [loading, setLoading] = useState(false); + + const editionMode = selectedRecord + ? [ + { + setLoading(true); + if (selectedRecord && selectedRecord.vaultId) { + await deleteAnsibleVault(selectedRecord.vaultId) + .then(() => + message.warning({ + content: 'Vault deleted', + duration: 5, + }), + ) + .finally(() => { + setModalOpened(false); + }); + await asyncFetch(); + } + setLoading(false); + }} + > + + , + ] + : []; + return ( + + title={ + <> + } + /> + {(selectedRecord && <>Edit vault {selectedRecord?.vaultId}) || ( + <>Add a new vault + )} + + } + open={modalOpened} + autoFocusFirstInput + modalProps={{ + destroyOnClose: true, + onCancel: () => setModalOpened(false), + }} + onFinish={async (values) => { + if (selectedRecord) { + await updateAnsibleVault(values); + setModalOpened(false); + await asyncFetch(); + } else { + await postAnsibleVault(values); + setModalOpened(false); + await asyncFetch(); + } + }} + submitter={{ + searchConfig: { + submitText: 'Save', + }, + render: (_, defaultDoms) => { + return [...editionMode, ...defaultDoms]; + }, + }} + > + + e.vaultId === value) === undefined || + selectedRecord?.vaultId === value + ) { + return Promise.resolve(); + } + return Promise.reject('Vault ID already exists'); + }, + }, + ]} + /> + + + + ); +}; + +export default PlaybooksLocalRepositoryModal; diff --git a/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx index 7d92e22b..1b30396b 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx @@ -1,4 +1,5 @@ import { SimpleIconsGit } from '@/components/Icons/CustomIcons'; +import CustomVault from '@/pages/Admin/Settings/components/subcomponents/forms/CustomVault'; import DirectoryExclusionForm from '@/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm'; import GitForm from '@/pages/Admin/Settings/components/subcomponents/forms/GitForm'; import { @@ -220,6 +221,7 @@ const PlaybooksGitRepositoryModal: React.FC< /> + ); diff --git a/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx index 3f1451f7..79f6c008 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx @@ -1,4 +1,5 @@ -import { SetAction, SimpleIconsGit } from '@/components/Icons/CustomIcons'; +import { SimpleIconsGit } from '@/components/Icons/CustomIcons'; +import CustomVault from '@/pages/Admin/Settings/components/subcomponents/forms/CustomVault'; import DirectoryExclusionForm from '@/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm'; import { deletePlaybooksLocalRepository, @@ -6,11 +7,7 @@ import { putPlaybooksLocalRepositories, syncToDatabasePlaybooksLocalRepository, } from '@/services/rest/playbooks-repositories'; -import { - DeleteOutlined, - TableOutlined, - UnorderedListOutlined, -} from '@ant-design/icons'; +import { DeleteOutlined, UnorderedListOutlined } from '@ant-design/icons'; import { ModalForm, ProForm, ProFormText } from '@ant-design/pro-components'; import { history } from '@umijs/max'; import { Avatar, Button, Dropdown, MenuProps, message, Popconfirm } from 'antd'; @@ -144,6 +141,7 @@ const PlaybooksLocalRepositoryModal: FC = ( ...props.selectedRecord, name: values.name, directoryExclusionList: values.directoryExclusionList, + vaults: values.vaults, }, ); props.setModalOpened(false); @@ -193,6 +191,9 @@ const PlaybooksLocalRepositoryModal: FC = ( + {!props.selectedRecord?.default && ( + + )} ); diff --git a/client/src/pages/Admin/Settings/components/subcomponents/forms/CustomVault.tsx b/client/src/pages/Admin/Settings/components/subcomponents/forms/CustomVault.tsx new file mode 100644 index 00000000..a131e431 --- /dev/null +++ b/client/src/pages/Admin/Settings/components/subcomponents/forms/CustomVault.tsx @@ -0,0 +1,36 @@ +import { getAnsibleVaults } from '@/services/rest/ansible'; +import { ProFormSelect } from '@ant-design/pro-form'; +import React from 'react'; +import { API } from 'ssm-shared-lib'; + +type CustomVaultProps = { + selectedRecord?: + | Partial + | undefined; +}; + +const CustomVault: React.FC = ({ selectedRecord }) => { + return ( + + (await getAnsibleVaults())?.data?.map((e: API.AnsibleVault) => ({ + label: e.vaultId, + value: e._id, + })) + } + fieldProps={{ + mode: 'tags', + }} + placeholder={'Your custom vaults'} + /> + ); +}; + +export default CustomVault; diff --git a/client/src/services/rest/ansible.ts b/client/src/services/rest/ansible.ts index 637162b0..36f51c04 100644 --- a/client/src/services/rest/ansible.ts +++ b/client/src/services/rest/ansible.ts @@ -83,3 +83,66 @@ export async function getAnsibleSmartFailure( ...(options || {}), }); } + +export async function getAnsibleVaults( + params?: any, + options?: Record, +): Promise> { + return request>(`/api/ansible/vaults`, { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +export async function postAnsibleVault( + data: API.AnsibleVault, + params?: any, + options?: Record, +): Promise> { + return request>(`/api/ansible/vaults`, { + method: 'POST', + data, + params: { + ...params, + }, + ...(options || {}), + }); +} + +export async function updateAnsibleVault( + data: API.AnsibleVault, + params?: any, + options?: Record, +): Promise> { + return request>( + `/api/ansible/vaults/${data.vaultId}`, + { + method: 'POST', + data, + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function deleteAnsibleVault( + vaultId: string, + params?: any, + options?: Record, +): Promise> { + return request>( + `/api/ansible/vaults/${vaultId}`, + { + method: 'DELETE', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} diff --git a/client/src/services/rest/playbooks-repositories.ts b/client/src/services/rest/playbooks-repositories.ts index b0412e3a..ba7101b0 100644 --- a/client/src/services/rest/playbooks-repositories.ts +++ b/client/src/services/rest/playbooks-repositories.ts @@ -289,3 +289,26 @@ export async function deleteAnyInRepository( }, ); } + +export async function getPlaybookVaults(playbooksRepositoryUuid: string) { + return request>( + `/api/playbooks-repository/${playbooksRepositoryUuid}`, + { + method: 'GET', + ...{}, + }, + ); +} + +export async function postPlaybookVaults( + playbooksRepositoryUuid: string, + vaults: API.CustomVault, +) { + return request>( + `/api/playbooks-repository/${playbooksRepositoryUuid}`, + { + method: 'POST', + ...{ vaults }, + }, + ); +} diff --git a/server/package-lock.json b/server/package-lock.json index 974ac02a..97b3a89b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -29,6 +29,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.5.0", + "mongoose-autopopulate": "^1.1.0", "mongoose": "^8.10.1", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", @@ -9266,6 +9267,15 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose-autopopulate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongoose-autopopulate/-/mongoose-autopopulate-1.1.0.tgz", + "integrity": "sha512-nTlTMlu1fLQ1bmJT7ILKbZmPGt2fHErLO4UJwzMDsHSigjtUYz0l3nvFhg511QkOkZcKBRzOnPn3DmmLIUENzg==", + "license": "Apache 2.0", + "peerDependencies": { + "mongoose": "6.x || 7.x || 8.0.0-rc0 || 8.x" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", diff --git a/server/package.json b/server/package.json index 563bc484..4bedf80a 100644 --- a/server/package.json +++ b/server/package.json @@ -34,6 +34,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.5.0", + "mongoose-autopopulate": "^1.1.0", "mongoose": "^8.10.1", "node-cron": "^3.0.3", "node-ssh": "^13.2.0", diff --git a/server/src/ansible/ssm-ansible-run.py b/server/src/ansible/ssm-ansible-run.py index 0a550bab..f961e4d0 100644 --- a/server/src/ansible/ssm-ansible-run.py +++ b/server/src/ansible/ssm-ansible-run.py @@ -1,11 +1,12 @@ -import ansible_runner +import argparse +import json +import logging import os +import sys + +import ansible_runner import requests import requests_unixsocket -import logging -import argparse -import sys -import json logger = logging.getLogger('ansible-runner') @@ -63,6 +64,13 @@ def parse_args(): arg_parser.add_argument("--debug", help="Debug", required=False, default=False) arg_parser.add_argument("--check", help="Run in check (dry-run) mode", action='store_true') arg_parser.add_argument("--diff", help="Show diffs", action='store_true') + arg_parser.add_argument( + "--vault-id", + action="append", # This allows multiple --vault-id arguments + help="Specify vault identity to use (can be used multiple times)", + default=[], # Default to empty list + metavar="identity@source" + ) group = arg_parser.add_mutually_exclusive_group(required=False) group.add_argument("--specific-host", help="Specify a host manually in json", default=None) group.add_argument("--host-pattern", help="Specify a host pattern in inventory", default="all") @@ -95,6 +103,7 @@ def execute(): 'json_mode': True, 'verbosity': args.log_level } + # Add additional arguments for dry-run and diff if specified if args.check: runner_args['cmdline'] = '--check' @@ -103,8 +112,16 @@ def execute(): runner_args['cmdline'] += ' --diff' else: runner_args['cmdline'] = '--diff' + + vault_cmd = '' + if args.vault_id: + vault_cmd = ' ' + ' '.join(f'--vault-id {vid}' for vid in args.vault_id) + if not args.vault_id: + vault_cmd = ' --vault-id ssm@ssm-ansible-vault-password-client.py' if 'cmdline' in runner_args: - runner_args['cmdline'] += ' --vault-id ssm@ssm-ansible-vault-password-client.py' + runner_args['cmdline'] += vault_cmd + else: + runner_args['cmdline'] = vault_cmd.strip() thread_obj, runner_obj = ansible_runner.run_async(**runner_args) sys.stdout.write(runner_obj.config.ident) diff --git a/server/src/ansible/ssm-ansible-vault-password-client.py b/server/src/ansible/ssm-ansible-vault-password-client.py index f1a591db..2f316821 100755 --- a/server/src/ansible/ssm-ansible-vault-password-client.py +++ b/server/src/ansible/ssm-ansible-vault-password-client.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import argparse import logging import os from sys import stdout @@ -7,16 +8,25 @@ logger = logging.getLogger('ansible-runner') -def send_request(): - url_actual = "http://localhost:3000/playbooks/vault" +def send_request(vault_id): + url_actual = "http://localhost:3000/playbooks/vaults/{}".format(vault_id) headers = { 'Authorization': "Bearer {}".format(os.getenv("SSM_API_KEY"))} session = requests.Session() logger.debug("Getting {}".format(url_actual)) return session.get(url_actual, headers=headers) +def parse_args(): + arg_parser = argparse.ArgumentParser( + description="SSM Ansible Vault Password Client" + ) + arg_parser.add_argument("--vault-id", help="Vault id", required=True) + return arg_parser.parse_args() + def main(): - response = send_request() + args = parse_args() + vault_id = args.vault_id + response = send_request(vault_id) stdout.write("{}\n".format(response.json()['data']['pwd'])) diff --git a/server/src/controllers/rest/ansible/vault.ts b/server/src/controllers/rest/ansible/vault.ts new file mode 100644 index 00000000..d2ed44ab --- /dev/null +++ b/server/src/controllers/rest/ansible/vault.ts @@ -0,0 +1,57 @@ +import { VAULT_PWD } from '../../../config'; +import AnsibleVaultRepo from '../../../data/database/repository/AnsibleVaultRepo'; +import logger from '../../../logger'; +import { NotFoundError } from '../../../middlewares/api/ApiError'; +import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; +import { DEFAULT_VAULT_ID } from '../../../modules/ansible-vault/ansible-vault'; + +export const postVault = async (req, res) => { + const { vaultId, password } = req.body; + + await AnsibleVaultRepo.create({ vaultId, password }); + new SuccessResponse('Vault created').send(res); +}; + +export const deleteVault = async (req, res) => { + const { vaultId } = req.params; + + const ansibleVault = await AnsibleVaultRepo.findOneById(vaultId); + if (!ansibleVault) { + throw new NotFoundError('Vault not found'); + } + await AnsibleVaultRepo.deleteOne(ansibleVault); + new SuccessResponse('Vault deleted').send(res); +}; + +export const getVaultPwd = async (req, res) => { + const { vaultId } = req.params; + + if (vaultId !== 'default' && vaultId !== DEFAULT_VAULT_ID) { + const vault = await AnsibleVaultRepo.findOneById(vaultId); + if (!vault) { + throw new NotFoundError('Vault not found'); + } + logger.info(`Vault password for vault ${vaultId} found ` + vault.password); + new SuccessResponse('Successfully got vault pwd', { pwd: vault.password }).send(res); + return; + } + new SuccessResponse('Successfully got vault pwd', { pwd: VAULT_PWD }).send(res); +}; + +export const getVaults = async (req, res) => { + const ansibleVaults = await AnsibleVaultRepo.findAll(); + new SuccessResponse('Vaults found', ansibleVaults).send(res); +}; + +export const updateVault = async (req, res) => { + const { vaultId } = req.params; + const { password } = req.body; + + const ansibleVault = await AnsibleVaultRepo.findOneById(vaultId); + if (!ansibleVault) { + throw new NotFoundError('Vault not found'); + } + ansibleVault.password = password; + await AnsibleVaultRepo.updateOne(ansibleVault); + new SuccessResponse('Vault updated').send(res); +}; diff --git a/server/src/controllers/rest/ansible/vault.validator.ts b/server/src/controllers/rest/ansible/vault.validator.ts new file mode 100644 index 00000000..837ceb05 --- /dev/null +++ b/server/src/controllers/rest/ansible/vault.validator.ts @@ -0,0 +1,24 @@ +import { body, param } from 'express-validator'; +import validator from '../../../middlewares/Validator'; +import { DEFAULT_VAULT_ID } from '../../../modules/ansible-vault/ansible-vault'; + +export const postVaultValidator = [ + body('vaultId') + .exists() + .notEmpty() + .isString() + .not() + .equals(DEFAULT_VAULT_ID) + .withMessage('VaultId must be different than "ssm"'), + body('password').exists().notEmpty().isString(), + validator, +]; + +export const deleteVaultValidator = [param('vaultId').exists().notEmpty().isString(), validator]; + +export const updateVaultValidator = [ + param('vaultId').exists().notEmpty().isString(), + body('vaultId').exists().notEmpty().isString(), + body('password').exists().notEmpty().isString(), + validator, +]; diff --git a/server/src/controllers/rest/playbooks-repository/git.ts b/server/src/controllers/rest/playbooks-repository/git.ts index 42449f34..db98d7ca 100644 --- a/server/src/controllers/rest/playbooks-repository/git.ts +++ b/server/src/controllers/rest/playbooks-repository/git.ts @@ -18,6 +18,7 @@ export const addGitRepository = async (req, res) => { remoteUrl, directoryExclusionList, gitService, + vaults, }: API.GitPlaybooksRepository = req.body; await GitRepositoryUseCases.addGitRepository( name, @@ -28,6 +29,7 @@ export const addGitRepository = async (req, res) => { remoteUrl, gitService, directoryExclusionList, + vaults, ); new SuccessResponse('Added playbooks git repository').send(res); }; @@ -54,6 +56,7 @@ export const updateGitRepository = async (req, res) => { remoteUrl, directoryExclusionList, gitService, + vaults, }: API.GitPlaybooksRepository = req.body; await GitRepositoryUseCases.updateGitRepository( @@ -66,6 +69,7 @@ export const updateGitRepository = async (req, res) => { remoteUrl, gitService, directoryExclusionList, + vaults, ); new SuccessResponse('Updated playbooks git repository').send(res); }; diff --git a/server/src/controllers/rest/playbooks-repository/local.ts b/server/src/controllers/rest/playbooks-repository/local.ts index 2423a1db..851d354c 100644 --- a/server/src/controllers/rest/playbooks-repository/local.ts +++ b/server/src/controllers/rest/playbooks-repository/local.ts @@ -1,4 +1,4 @@ -import { Repositories } from 'ssm-shared-lib'; +import { API, Repositories } from 'ssm-shared-lib'; import PlaybooksRepositoryRepo from '../../../data/database/repository/PlaybooksRepositoryRepo'; import logger from '../../../logger'; import { NotFoundError } from '../../../middlewares/api/ApiError'; @@ -19,14 +19,8 @@ export const getLocalRepositories = async (req, res) => { export const updateLocalRepository = async (req, res) => { const { uuid } = req.params; logger.info(`[CONTROLLER] - POST - /local/:uuid`); - const { - name, - directoryExclusionList, - }: { - name: string; - directoryExclusionList?: string[]; - } = req.body; - await LocalRepositoryUseCases.updateLocalRepository(uuid, name, directoryExclusionList); + const { name, directoryExclusionList, vaults }: API.LocalPlaybooksRepository = req.body; + await LocalRepositoryUseCases.updateLocalRepository(uuid, name, directoryExclusionList, vaults); new SuccessResponse('Updated playbooks local repository').send(res); }; @@ -43,14 +37,8 @@ export const deleteLocalRepository = async (req, res) => { export const addLocalRepository = async (req, res) => { logger.info(`[CONTROLLER] - PUT - /local/`); - const { - name, - directoryExclusionList, - }: { - name: string; - directoryExclusionList?: string[]; - } = req.body; - await LocalRepositoryUseCases.addLocalRepository(name, directoryExclusionList); + const { name, directoryExclusionList, vaults }: API.LocalPlaybooksRepository = req.body; + await LocalRepositoryUseCases.addLocalRepository(name, directoryExclusionList, vaults); new SuccessResponse('Added playbooks local repository').send(res); }; diff --git a/server/src/controllers/rest/playbooks/vault.ts b/server/src/controllers/rest/playbooks/vault.ts deleted file mode 100644 index 60609583..00000000 --- a/server/src/controllers/rest/playbooks/vault.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { VAULT_PWD } from '../../../config'; -import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; - -export const getVaultPwd = async (req, res) => { - new SuccessResponse('Successfully got vault pwd', { pwd: VAULT_PWD }).send(res); -}; diff --git a/server/src/data/database/model/AnsibleVault.ts b/server/src/data/database/model/AnsibleVault.ts new file mode 100644 index 00000000..31010a00 --- /dev/null +++ b/server/src/data/database/model/AnsibleVault.ts @@ -0,0 +1,24 @@ +import { Schema, model } from 'mongoose'; + +export interface AnsibleVault { + _id?: string; + vaultId: string; + password: string; +} + +export const DOCUMENT_NAME = 'AnsibleVault'; +export const COLLECTION_NAME = 'ansiblevaults'; + +const schema = new Schema({ + vaultId: { + type: Schema.Types.String, + required: true, + unique: true, + }, + password: { + type: Schema.Types.String, + required: true, + }, +}); + +export const AnsibleVaultModel = model(DOCUMENT_NAME, schema, COLLECTION_NAME); diff --git a/server/src/data/database/model/PlaybooksRepository.ts b/server/src/data/database/model/PlaybooksRepository.ts index ad30e19f..5ff81598 100644 --- a/server/src/data/database/model/PlaybooksRepository.ts +++ b/server/src/data/database/model/PlaybooksRepository.ts @@ -1,5 +1,7 @@ import { Schema, model } from 'mongoose'; +import mongooseAutopopulate from 'mongoose-autopopulate'; import { Repositories, SsmGit } from 'ssm-shared-lib'; +import { AnsibleVault } from './AnsibleVault'; export const DOCUMENT_NAME = 'PlaybooksRepository'; export const COLLECTION_NAME = 'playbooksrepository'; @@ -22,6 +24,7 @@ export default interface PlaybooksRepository { onError?: boolean; onErrorMessage?: string; gitService?: SsmGit.Services; + vaults?: AnsibleVault[] | string[]; createdAt?: Date; updatedAt?: Date; } @@ -104,6 +107,14 @@ const schema = new Schema( type: Schema.Types.String, required: false, }, + vaults: [ + { + type: Schema.Types.ObjectId, + ref: 'AnsibleVault', // References the AnsibleVault model + required: false, + autopopulate: true, // This is the key part + }, + ], }, { timestamps: true, @@ -111,6 +122,8 @@ const schema = new Schema( }, ); +schema.plugin(mongooseAutopopulate); + export const PlaybooksRepositoryModel = model( DOCUMENT_NAME, schema, diff --git a/server/src/data/database/repository/AnsibleVaultRepo.ts b/server/src/data/database/repository/AnsibleVaultRepo.ts new file mode 100644 index 00000000..925e04ad --- /dev/null +++ b/server/src/data/database/repository/AnsibleVaultRepo.ts @@ -0,0 +1,37 @@ +import { AnsibleVault, AnsibleVaultModel } from '../model/AnsibleVault'; +import { PlaybooksRepositoryModel } from '../model/PlaybooksRepository'; + +async function findAll() { + return await AnsibleVaultModel.find().select('-password').lean().exec(); +} + +async function create(ansibleVault: Partial) { + return await AnsibleVaultModel.create(ansibleVault); +} + +async function deleteOne(ansibleVault: AnsibleVault) { + await PlaybooksRepositoryModel.updateMany( + { vaults: ansibleVault._id }, + { $pull: { vaults: ansibleVault._id } }, + ); + // Delete the vault + await AnsibleVaultModel.deleteOne(ansibleVault); +} + +async function findOneById(vaultId: string) { + return await AnsibleVaultModel.findOne({ vaultId: vaultId }).lean().exec(); +} + +async function updateOne(ansibleVault: AnsibleVault) { + return await AnsibleVaultModel.updateOne({ vaultId: ansibleVault.vaultId }, ansibleVault) + .lean() + .exec(); +} + +export default { + findAll, + create, + deleteOne, + findOneById, + updateOne, +}; diff --git a/server/src/data/database/repository/PlaybookRepo.ts b/server/src/data/database/repository/PlaybookRepo.ts index 5ffe1dca..f71a6d7a 100644 --- a/server/src/data/database/repository/PlaybookRepo.ts +++ b/server/src/data/database/repository/PlaybookRepo.ts @@ -26,14 +26,14 @@ async function findAllWithActiveRepositories(): Promise { async function findOneByName(name: string): Promise { return await PlaybookModel.findOne({ name: name }) - .populate({ path: 'playbooksRepository' }) + .populate({ path: 'playbooksRepository', populate: { path: 'vaults' } }) .lean() .exec(); } async function findOneByUuid(uuid: string): Promise { return await PlaybookModel.findOne({ uuid: uuid }) - .populate({ path: 'playbooksRepository' }) + .populate({ path: 'playbooksRepository', populate: { path: 'vaults' } }) .lean() .exec(); } @@ -50,14 +50,14 @@ async function deleteByUuid(uuid: string): Promise { async function findOneByPath(path: string): Promise { return await PlaybookModel.findOne({ path: path }) - .populate({ path: 'playbooksRepository' }) + .populate({ path: 'playbooksRepository', populate: { path: 'vaults' } }) .lean() .exec(); } async function findOneByUniqueQuickReference(quickRef: string): Promise { return await PlaybookModel.findOne({ uniqueQuickRef: quickRef }) - .populate({ path: 'playbooksRepository' }) + .populate({ path: 'playbooksRepository', populate: { path: 'vaults' } }) .lean() .exec(); } diff --git a/server/src/modules/ansible/AnsibleCmd.ts b/server/src/modules/ansible/AnsibleCmd.ts index bdefcad5..66ad5500 100644 --- a/server/src/modules/ansible/AnsibleCmd.ts +++ b/server/src/modules/ansible/AnsibleCmd.ts @@ -1,7 +1,9 @@ import { API, SsmAnsible } from 'ssm-shared-lib'; +import { AnsibleVault } from '../../data/database/model/AnsibleVault'; import User from '../../data/database/model/User'; import { ANSIBLE_CONFIG_FILE } from '../../helpers/ansible/AnsibleConfigurationHelper'; import { Playbooks } from '../../types/typings'; +import { DEFAULT_VAULT_ID } from '../ansible-vault/ansible-vault'; import ExtraVarsTransformer from './extravars/ExtraVarsTransformer'; class AnsibleCommandBuilder { @@ -38,6 +40,14 @@ class AnsibleCommandBuilder { } } + getVaults(vaults?: Partial[]): string { + return vaults + ? vaults + .map((vault) => `--vault-id ${vault.vaultId}@ssm-ansible-vault-password-client.py`) + .join(' ') + : ''; + } + buildAnsibleCmd( playbook: string, uuid: string, @@ -45,14 +55,16 @@ class AnsibleCommandBuilder { user: User, extraVars?: API.ExtraVars, mode: SsmAnsible.ExecutionMode = SsmAnsible.ExecutionMode.APPLY, + vaults?: AnsibleVault[], ) { const inventoryTargetsCmd = this.getInventoryTargets(inventoryTargets); const logLevel = this.getLogLevel(user); const extraVarsCmd = this.getExtraVars(extraVars); const ident = `--ident '${uuid}'`; const dryRun = this.getDryRun(mode); + const vaultsCmd = this.getVaults([...(vaults || []), { vaultId: DEFAULT_VAULT_ID }]); - return `${AnsibleCommandBuilder.sudo} ${AnsibleCommandBuilder.ssmApiKeyEnv}=${user.apiKey} ${AnsibleCommandBuilder.ansibleConfigKeyEnv}=${ANSIBLE_CONFIG_FILE} ANSIBLE_FORCE_COLOR=1 ${AnsibleCommandBuilder.python} ${AnsibleCommandBuilder.ansibleRunner} --playbook '${playbook}' ${ident} ${inventoryTargetsCmd} ${logLevel} ${dryRun} ${extraVarsCmd}`; + return `${AnsibleCommandBuilder.sudo} ${AnsibleCommandBuilder.ssmApiKeyEnv}=${user.apiKey} ${AnsibleCommandBuilder.ansibleConfigKeyEnv}=${ANSIBLE_CONFIG_FILE} ANSIBLE_FORCE_COLOR=1 ${AnsibleCommandBuilder.python} ${AnsibleCommandBuilder.ansibleRunner} --playbook '${playbook}' ${ident} ${inventoryTargetsCmd} ${logLevel} ${dryRun} ${extraVarsCmd} ${vaultsCmd}`; } } diff --git a/server/src/modules/ansible/extravars/ExtraVars.ts b/server/src/modules/ansible/extravars/ExtraVars.ts index 47844030..1bdd69a7 100644 --- a/server/src/modules/ansible/extravars/ExtraVars.ts +++ b/server/src/modules/ansible/extravars/ExtraVars.ts @@ -3,7 +3,6 @@ import { getFromCache } from '../../../data/cache'; import DeviceRepo from '../../../data/database/repository/DeviceRepo'; import UserRepo from '../../../data/database/repository/UserRepo'; import pinoLogger from '../../../logger'; -import { DEFAULT_VAULT_ID, vaultDecrypt } from '../../ansible-vault/ansible-vault'; class ExtraVars { private logger = pinoLogger.child( diff --git a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts index 68aa435f..be2dc0ec 100644 --- a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts +++ b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts @@ -2,6 +2,7 @@ import shell from 'shelljs'; import { API, SsmAnsible } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import { SSM_INSTALL_PATH } from '../../../config'; +import { AnsibleVault } from '../../../data/database/model/AnsibleVault'; import User from '../../../data/database/model/User'; import AnsibleTaskRepo from '../../../data/database/repository/AnsibleTaskRepo'; import DeviceAuthRepo from '../../../data/database/repository/DeviceAuthRepo'; @@ -33,6 +34,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { extraVars?: API.ExtraVars, mode: SsmAnsible.ExecutionMode = SsmAnsible.ExecutionMode.APPLY, execUuid?: string, + vaults?: AnsibleVault[], ) { this.logger.info(`executePlaybook - Starting... (playbook: ${playbookPath})`); execUuid = execUuid || uuidv4(); @@ -59,6 +61,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { mode, target, execUuid, + vaults, ); } @@ -70,6 +73,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { mode: SsmAnsible.ExecutionMode = SsmAnsible.ExecutionMode.APPLY, target?: string[], execUuid?: string, + vaults?: AnsibleVault[], ) { execUuid = execUuid || uuidv4(); @@ -86,6 +90,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { user, extraVars, mode, + vaults, ); this.logger.info(`executePlaybook - Executing "${cmd}"`); const child = shell.exec(cmd, { diff --git a/server/src/routes/ansible.ts b/server/src/routes/ansible.ts index cd93c8ed..b91f9c34 100644 --- a/server/src/routes/ansible.ts +++ b/server/src/routes/ansible.ts @@ -7,6 +7,12 @@ import { } from '../controllers/rest/ansible/configuration.validator'; import { getSmartFailure } from '../controllers/rest/ansible/smart-failure'; import { getSmartFailureValidator } from '../controllers/rest/ansible/smart-failure.validator'; +import { deleteVault, getVaults, postVault, updateVault } from '../controllers/rest/ansible/vault'; +import { + deleteVaultValidator, + postVaultValidator, + updateVaultValidator, +} from '../controllers/rest/ansible/vault.validator'; const router = express.Router(); @@ -20,4 +26,11 @@ router .delete(deleteConfValidator, deleteConf); router.route('/smart-failure').get(getSmartFailureValidator, getSmartFailure); + +router.route('/vaults').get(getVaults).post(postVaultValidator, postVault); +router + .route('/vaults/:vaultId') + .delete(deleteVaultValidator, deleteVault) + .post(updateVaultValidator, updateVault); + export default router; diff --git a/server/src/routes/playbooks.ts b/server/src/routes/playbooks.ts index ef612903..5f21ebc5 100644 --- a/server/src/routes/playbooks.ts +++ b/server/src/routes/playbooks.ts @@ -1,5 +1,6 @@ import express from 'express'; import passport from 'passport'; +import { getVaultPwd } from '../controllers/rest/ansible/vault'; import { execPlaybook, execPlaybookByQuickRef, @@ -20,8 +21,8 @@ import { postInstallAnsibleGalaxyCollection, } from '../controllers/rest/playbooks/galaxy'; import { - getAnsibleGalaxyCollectionsValidator, getAnsibleGalaxyCollectionValidator, + getAnsibleGalaxyCollectionsValidator, postInstallAnsibleGalaxyCollectionValidator, } from '../controllers/rest/playbooks/galaxy.validator'; import { addTaskEvent, addTaskStatus } from '../controllers/rest/playbooks/hook'; @@ -41,7 +42,6 @@ import { editPlaybookValidator, getPlaybookValidator, } from '../controllers/rest/playbooks/playbook.validator'; -import { getVaultPwd } from '../controllers/rest/playbooks/vault'; const router = express.Router(); @@ -51,8 +51,8 @@ router.post( addTaskStatus, ); router.post(`/hook/task/event`, passport.authenticate('bearer', { session: false }), addTaskEvent); -router.get('/vault', passport.authenticate('bearer', { session: false }), getVaultPwd); router.get(`/inventory`, passport.authenticate('bearer', { session: false }), getInventory); +router.get('/vaults/:vaultId', passport.authenticate('bearer', { session: false }), getVaultPwd); router.use(passport.authenticate('jwt', { session: false })); diff --git a/server/src/services/GitPlaybooksRepositoryUseCases.ts b/server/src/services/GitPlaybooksRepositoryUseCases.ts index 0d69e3ee..56bb4d40 100644 --- a/server/src/services/GitPlaybooksRepositoryUseCases.ts +++ b/server/src/services/GitPlaybooksRepositoryUseCases.ts @@ -12,6 +12,7 @@ async function addGitRepository( remoteUrl: string, gitService: SsmGit.Services, directoryExclusionList?: string[], + vaults?: string[], ) { const uuid = uuidv4(); const gitRepository = await PlaybooksRepositoryEngine.registerRepository({ @@ -40,6 +41,7 @@ async function addGitRepository( enabled: true, directoryExclusionList, gitService, + vaults, }); void gitRepository.clone(true); } @@ -54,6 +56,7 @@ async function updateGitRepository( remoteUrl: string, gitService: SsmGit.Services, directoryExclusionList?: string[], + vaults?: string[], ) { await PlaybooksRepositoryEngine.deregisterRepository(uuid); const gitRepository = await PlaybooksRepositoryEngine.registerRepository({ @@ -82,6 +85,7 @@ async function updateGitRepository( enabled: true, directoryExclusionList, gitService, + vaults, }); } diff --git a/server/src/services/LocalPlaybooksRepositoryUseCases.ts b/server/src/services/LocalPlaybooksRepositoryUseCases.ts index d4935043..164c715b 100644 --- a/server/src/services/LocalPlaybooksRepositoryUseCases.ts +++ b/server/src/services/LocalPlaybooksRepositoryUseCases.ts @@ -11,7 +11,11 @@ const logger = PinoLogger.child( { msgPrefix: '[LOCAL_REPOSITORY] - ' }, ); -async function addLocalRepository(name: string, directoryExclusionList?: string[]) { +async function addLocalRepository( + name: string, + directoryExclusionList?: string[], + vaults?: string[], +) { const uuid = uuidv4(); const localRepository = await PlaybooksRepositoryEngine.registerRepository({ uuid, @@ -28,6 +32,7 @@ async function addLocalRepository(name: string, directoryExclusionList?: string[ directory: localRepository.getDirectory(), enabled: true, directoryExclusionList, + vaults, }); try { await localRepository.init(); @@ -41,6 +46,7 @@ async function updateLocalRepository( uuid: string, name: string, directoryExclusionList?: string[], + vaults?: string[], ) { const playbooksRepository = await PlaybooksRepositoryRepo.findByUuid(uuid); if (!playbooksRepository) { @@ -49,6 +55,7 @@ async function updateLocalRepository( await PlaybooksRepositoryEngine.deregisterRepository(uuid); playbooksRepository.name = name; playbooksRepository.directoryExclusionList = directoryExclusionList; + playbooksRepository.vaults = vaults; await PlaybooksRepositoryEngine.registerRepository(playbooksRepository); await PlaybooksRepositoryRepo.update(playbooksRepository); } diff --git a/server/src/services/PlaybookUseCases.ts b/server/src/services/PlaybookUseCases.ts index fbc4c3d2..c0c6fcbb 100644 --- a/server/src/services/PlaybookUseCases.ts +++ b/server/src/services/PlaybookUseCases.ts @@ -1,5 +1,6 @@ import { API, SsmAnsible } from 'ssm-shared-lib'; import { setToCache } from '../data/cache'; +import { AnsibleVault } from '../data/database/model/AnsibleVault'; import Playbook, { PlaybookModel } from '../data/database/model/Playbook'; import User from '../data/database/model/User'; import ExtraVars from '../modules/ansible/extravars/ExtraVars'; @@ -41,6 +42,8 @@ async function executePlaybook( target, substitutedExtraVars, mode, + undefined, + playbook.playbooksRepository?.vaults as AnsibleVault[] | undefined, ); } @@ -64,6 +67,7 @@ async function executePlaybookOnInventory( undefined, undefined, execUuid, + playbook.playbooksRepository?.vaults as AnsibleVault[] | undefined, ); } diff --git a/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts b/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts index c154bf1d..3fca64db 100644 --- a/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts +++ b/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts @@ -128,6 +128,7 @@ describe('buildAnsibleCmd() function', () => { `--specific-host '${JSON.stringify(inventory)}'`, '--log-level 1', '--check --diff', + ' --vault-id ssm@ssm-ansible-vault-password-client.py', ]; const expectedCmd = ansibleCmdPartsWithDiffCheck.join(' ').trim(); @@ -159,6 +160,7 @@ describe('buildAnsibleCmd() function', () => { `--specific-host '${JSON.stringify(inventory)}'`, '--log-level 1', '--check', + ' --vault-id ssm@ssm-ansible-vault-password-client.py', ]; const expectedCmd = ansibleCmdPartsWithCheck.join(' ').trim(); @@ -189,6 +191,7 @@ describe('buildAnsibleCmd() function', () => { `--ident '${uuid}'`, `--specific-host '${JSON.stringify(inventory)}'`, '--log-level 1', + ' --vault-id ssm@ssm-ansible-vault-password-client.py', ]; const expectedCmd = ansibleCmdPartsWithApply.join(' ').trim(); diff --git a/shared-lib/src/types/api.ts b/shared-lib/src/types/api.ts index c16dcd89..40bb464f 100644 --- a/shared-lib/src/types/api.ts +++ b/shared-lib/src/types/api.ts @@ -412,6 +412,11 @@ export type PlaybookFile = { playableInBatch?: boolean; }; +export type CustomVault = { + vaultId: string; + password: string; +} + export type PlaybooksRepository = { name: string; uuid: string; @@ -419,6 +424,7 @@ export type PlaybooksRepository = { type: RepositoryType; children?: ExtendedTreeNode[]; directoryExclusionList?: string[]; + vaults?: string[]; }; export type ServerLog = { @@ -927,6 +933,11 @@ export type SFTPContent = { gid: number; }; +export type AnsibleVault = { + _id: string; + vaultId: string; + password: string; +} /* const data = { // Memory metrics