AJAXMethods); } /** * Generates a JSON error. * * @param string $msg Error message. * * @return void */ public function error($msg) { echo json_encode( ['error' => $msg] ); } /** * Minor function to dump json message as ajax response. * * @param string $type Type: result || error. * @param string $msg Message. * @param boolean $delete Deletion messages. * * @return void */ private function ajaxMsg($type, $msg, $delete=false) { $msg_err = 'Failed while saving: %s'; $msg_ok = 'Successfully saved into keystore '; if ($delete) { $msg_err = 'Failed while removing: %s'; $msg_ok = 'Successfully deleted '; } if ($type == 'error') { echo json_encode( [ $type => ui_print_error_message( __( $msg_err, $msg ), '', true ), ] ); } else { echo json_encode( [ $type => ui_print_success_message( __( $msg_ok, $msg ), '', true ), ] ); } exit; } /** * Initializes object and validates user access. * * @param string $ajax_controller Path of ajaxController, is the 'page' * variable sent in ajax calls. * * @return Object */ public function __construct($ajax_controller) { global $config; // Check access. check_login(); if (! check_acl($config['id_user'], 0, 'AR')) { db_pandora_audit( 'ACL Violation', 'Trying to access event viewer' ); if (is_ajax()) { echo json_encode(['error' => 'noaccess']); } include 'general/noaccess.php'; exit; } $this->ajaxController = $ajax_controller; return $this; } /** * Returns an array with all the credentials matching filter and ACL. * * @param array $fields Fields array or 'count' keyword to retrieve count. * @param array $filter Filters to be applied. * @param integer $offset Offset (pagination). * @param integer $limit Limit (pagination). * @param string $order Sort order. * @param string $sort_field Sort field. * * @return array With all results or false if error. * @throws Exception On error. */ public static function getAll( $fields, $filter, $offset=null, $limit=null, $order=null, $sort_field=null ) { $sql_filters = []; $order_by = ''; $pagination = ''; global $config; if (!is_array($filter)) { error_log('[credential_get_all] Filter must be an array.'); throw new Exception('[credential_get_all] Filter must be an array.'); } $count = false; if (!is_array($fields) && $fields == 'count') { $fields = ['cs.*']; $count = true; } else if (!is_array($fields)) { error_log('[credential_get_all] Fields must be an array or "count".'); throw new Exception('[credential_get_all] Fields must be an array or "count".'); } if (isset($filter['product']) && !empty($filter['product'])) { $sql_filters[] = sprintf(' AND cs.product = "%s"', $filter['product']); } if (isset($filter['free_search']) && !empty($filter['free_search'])) { $sql_filters[] = vsprintf( ' AND (lower(cs.username) like lower("%%%s%%") OR cs.identifier like "%%%s%%" OR lower(cs.product) like lower("%%%s%%"))', array_fill(0, 3, $filter['free_search']) ); } if (isset($filter['filter_id_group']) && $filter['filter_id_group'] > 0) { $propagate = db_get_value( 'propagate', 'tgrupo', 'id_grupo', $filter['filter_id_group'] ); if (!$propagate) { $sql_filters[] = sprintf( ' AND cs.id_group = %d ', $filter['filter_id_group'] ); } else { $groups = [ $filter['filter_id_group'] ]; $childrens = groups_get_childrens($id_group, null, true); if (!empty($childrens)) { foreach ($childrens as $child) { $groups[] = (int) $child['id_grupo']; } } $filter['filter_id_group'] = $groups; $sql_filters[] = sprintf( ' AND cs.id_group IN (%s) ', join(',', $filter['filter_id_group']) ); } } if (isset($filter['group_list']) && is_array($filter['group_list'])) { $sql_filters[] = sprintf( ' AND cs.id_group IN (%s) ', join(',', $filter['group_list']) ); } else if (users_is_admin() !== true) { $user_groups = users_get_groups( $config['id_user'], 'AR' ); // Always add group 'ALL' because 'ALL' group credentials // must be available for all users. if (is_array($user_groups) === true) { $user_groups = ([0] + array_keys($user_groups)); } else { $user_groups = [0]; } $sql_filters[] = sprintf( ' AND cs.id_group IN (%s) ', join(',', $user_groups) ); } if (isset($filter['identifier'])) { $sql_filters[] = sprintf( ' AND cs.identifier = "%s" ', $filter['identifier'] ); } if (isset($order)) { $dir = 'asc'; if ($order == 'desc') { $dir = 'desc'; }; if (in_array( $sort_field, [ 'group', 'identifier', 'product', 'username', 'options', ] ) ) { $order_by = sprintf( 'ORDER BY `%s` %s', $sort_field, $dir ); } } if (isset($limit) && $limit > 0 && isset($offset) && $offset >= 0 ) { $pagination = sprintf( ' LIMIT %d OFFSET %d ', $limit, $offset ); } $sql = sprintf( 'SELECT %s FROM tcredential_store cs LEFT JOIN tgrupo tg ON tg.id_grupo = cs.id_group WHERE 1=1 %s %s %s', join(',', $fields), join(' ', $sql_filters), $order_by, $pagination ); if ($count) { $sql = sprintf('SELECT count(*) as n FROM ( %s ) tt', $sql); return db_get_value_sql($sql); } return db_get_all_rows_sql($sql); } /** * Retrieves target key from keystore or false in case of error. * * @param string $identifier Key identifier. * * @return array Key or false if error. */ public static function getKey($identifier) { global $config; if (empty($identifier)) { return false; } $keys = self::getAll( [ 'cs.*', 'tg.nombre as `group`', ], ['identifier' => $identifier] ); if (is_array($keys) === true) { // Only 1 must exist. $key = $keys[0]; // Decrypt content. $key['username'] = io_output_password($key['username']); $key['password'] = io_output_password($key['password']); return $key; } return false; } /** * Return all keys avaliable for current user. * * @param string $product Filter by product. * * @return array Keys or false if error. */ public static function getKeys($product=false) { global $config; $filter = []; if ($product !== false) { $filter['product'] = $product; } $keys = self::getAll( [ 'cs.*', 'tg.nombre as `group`', ], $filter ); if (is_array($keys) === true) { // Improve usage and decode output. $return = array_reduce( $keys, function ($carry, $item) { $item['username'] = io_output_password($item['username']); $item['password'] = io_output_password($item['password']); $carry[$item['identifier']] = $item['identifier']; return $carry; } ); return $return; } return []; } /** * Ajax method invoked by datatables to draw content. * * @return void */ public function draw() { global $config; // Datatables offset, limit and order. $filter = get_parameter('filter', []); $start = get_parameter('start', 0); $length = get_parameter('length', $config['block_size']); $order = get_datatable_order(true); try { ob_start(); $fields = [ 'cs.*', 'tg.nombre as `group`', ]; // Retrieve data. $data = $this->getAll( // Fields. $fields, // Filter. $filter, // Offset. $start, // Limit. $length, // Order. $order['direction'], // Sort field. $order['field'] ); // Retrieve counter. $count = $this->getAll( 'count', $filter ); if ($data) { $data = array_reduce( $data, function ($carry, $item) { // Transforms array of arrays $data into an array // of objects, making a post-process of certain fields. $tmp = (object) $item; $tmp->username = io_output_password($tmp->username); if (empty($tmp->group)) { $tmp->group = __('All'); } else { $tmp->group = io_safe_output($tmp->group); } $carry[] = $tmp; return $carry; } ); } // Datatables format: RecordsTotal && recordsfiltered. echo json_encode( [ 'data' => $data, 'recordsTotal' => $count, 'recordsFiltered' => $count, ] ); // Capture output. $response = ob_get_clean(); } catch (Exception $e) { echo json_encode(['error' => $e->getMessage()]); exit; } // If not valid, show error with issue. json_decode($response); if (json_last_error() == JSON_ERROR_NONE) { // If valid dump. echo $response; } else { echo json_encode( ['error' => $response] ); } exit; } /** * Prints inputs for modal "Add key". * * @return void */ public function loadModal() { $identifier = get_parameter('identifier', null); $key = self::getKey($identifier); echo $this->printInputs($key); } /** * Prepare variables received using form. AJAX environment only. * * @return array of values processed or false in case of error. */ private function prepareKeyValues() { $identifier = get_parameter('identifier', null); $id_group = get_parameter('id_group', null); $product = get_parameter('product', null); $username = get_parameter('username', null); $password = get_parameter('password', null); $extra_1 = get_parameter('extra_1', null); $extra_2 = get_parameter('extra_2', null); if (empty($identifier)) { $error = __('Key identifier is required'); } else if ($id_group === null) { $error = __('You must select a group where store this key!'); } else if (empty($product)) { $error = __('You must specify a product type'); } else if (empty($username) && (empty($password))) { $error = __('You must specify a username and/or password'); } // Encrypt content (if needed). $values = [ 'identifier' => $identifier, 'id_group' => $id_group, 'product' => $product, 'username' => io_input_password($username), 'password' => io_input_password($password), 'extra_1' => $extra_1, 'extra_2' => $extra_2, ]; // Spaces are not allowed. $values['identifier'] = preg_replace('/\s+/', '-', trim($identifier)); return $values; } /** * Stores a key into credential store. * * @param array $values Key definition. * @param string $identifier Update or create. * * @return boolean True if ok, false if not ok. */ private function storeKey($values, $identifier=false) { if ($identifier === false) { // New. return db_process_sql_insert('tcredential_store', $values); } else { // Update. return db_process_sql_update( 'tcredential_store', $values, ['identifier' => $identifier] ); } } /** * Add a new key into Credential Store * * @return void */ public function addKey() { global $config; $values = $this->prepareKeyValues(); if ($this->storeKey($values) === false) { $this->ajaxMsg('error', $config['dbconnection']->error); } else { $this->ajaxMsg('result', $values['identifier']); } exit; } /** * Add a new key into Credential Store * * @return void */ public function updateKey() { global $config; $values = $this->prepareKeyValues(); $identifier = $values['identifier']; if ($this->storeKey($values, $identifier) === false) { $this->ajaxMsg('error', $config['dbconnection']->error); } else { $this->ajaxMsg('result', $identifier); } exit; } /** * AJAX method. Delete key from keystore. * * @return void */ public function deleteKey() { global $config; $identifier = get_parameter('identifier', null); if (empty($identifier)) { $this->ajaxMsg('error', __('identifier cannot be empty'), true); } if (self::getKey($identifier) === false) { // User has no grants to delete target key. $this->ajaxMsg('error', __('Not allowed'), true); } if (db_process_sql_delete( 'tcredential_store', ['identifier' => $identifier] ) === false ) { $this->ajaxMsg('error', $config['dbconnection']->error, true); } else { $this->ajaxMsg('result', $identifier, true); } } /** * Run CredentialStore (main page). * * @return void */ public function run() { global $config; // Require specific CSS and JS. ui_require_css_file('wizard'); ui_require_css_file('discovery'); ui_require_css_file('credential_store'); if (!isset($config['encryption_passphrase'])) { $url = 'https://pandorafms.com/docs/index.php?title=Pandora:Documentation_en:Password_Encryption'; if ($config['language'] == 'es') { $url = 'https://pandorafms.com/docs/index.php?title=Pandora:Documentation_es:Cifrado_Contrase%C3%B1as'; } ui_print_warning_message( __( 'Database encryption is not enabled. Credentials will be stored in plaintext. %s', ''.__('How to configure encryption.').'' ) ); } // Datatables list. try { $columns = [ 'group', 'identifier', 'product', 'username', 'options', ]; $column_names = [ __('Group'), __('Identifier'), __('Product'), __('User'), [ 'text' => __('Options'), 'class' => 'action_buttons', ], ]; $this->tableId = 'keystore'; // Load datatables user interface. ui_print_datatable( [ 'id' => $this->tableId, 'class' => 'info_table', 'style' => 'width: 100%', 'columns' => $columns, 'column_names' => $column_names, 'ajax_url' => $this->ajaxController, 'ajax_data' => ['method' => 'draw'], 'ajax_postprocess' => 'process_datatables_item(item)', 'no_sortable_columns' => [-1], 'order' => [ 'field' => 'identifier', 'direction' => 'asc', ], 'search_button_class' => 'sub filter float-right', 'form' => [ 'inputs' => [ [ 'label' => __('Group'), 'type' => 'select', 'id' => 'filter_id_group', 'name' => 'filter_id_group', 'options' => users_get_groups_for_select( $config['id_user'], 'AR', true, true, false ), ], [ 'label' => __('Free search'), 'type' => 'text', 'class' => 'mw250px', 'id' => 'free_search', 'name' => 'free_search', ], ], ], ] ); } catch (Exception $e) { echo $e->getMessage(); } // Auxiliar div. $modal = ''; $msg = ''; $aux = ''; echo $modal.$msg.$aux; // Create button. echo '
'; html_print_submit_button( __('Add key'), 'create', false, 'class="sub next"' ); echo '
'; echo $this->loadJS(); } /** * Generates inputs for new/update forms. * * @param array $values Values or null. * * @return string Inputs. */ public function printInputs($values=null) { if (!is_array($values)) { $values = []; } $form = [ 'action' => '#', 'id' => 'modal_form', 'onsubmit' => 'return false;', 'class' => 'modal', 'extra' => 'autocomplete="new-password"', ]; $inputs = []; $inputs[] = [ 'label' => __('Identifier'), 'id' => 'div-identifier', 'arguments' => [ 'name' => 'identifier', 'type' => 'text', 'value' => $values['identifier'], 'disabled' => (bool) $values['identifier'], 'return' => true, ], ]; $inputs[] = [ 'label' => __('Group'), 'arguments' => [ 'name' => 'id_group', 'id' => 'id_group', 'input_class' => 'flex-row', 'type' => 'select_groups', 'selected' => $values['id_group'], 'return' => true, 'class' => 'w50p', ], ]; $inputs[] = [ 'label' => __('Product'), 'id' => 'div-product', 'arguments' => [ 'name' => 'product', 'input_class' => 'flex-row', 'type' => 'select', 'script' => 'calculate_inputs()', 'fields' => [ 'CUSTOM' => __('Custom'), 'AWS' => __('Aws'), 'AZURE' => __('Azure'), 'SAP' => __('SAP'), // 'GOOGLE' => __('Google'), ], 'selected' => (isset($values['product']) ? $values['product'] : 'CUSTOM'), 'disabled' => (bool) $values['product'], 'return' => true, ], ]; $user_label = __('Username'); $pass_label = __('Password'); $extra_1_label = __('Extra'); $extra_2_label = __('Extra (2)'); $extra1 = true; $extra2 = true; // Remember to update credential_store.php also. switch ($values['product']) { case 'AWS': $user_label = __('Access key ID'); $pass_label = __('Secret access key'); $extra1 = false; $extra2 = false; break; case 'AZURE': $user_label = __('Account ID'); $pass_label = __('Application secret'); $extra_1_label = __('Tenant or domain name'); $extra_2_label = __('Subscription id'); break; case 'GOOGLE': // Need further investigation. case 'CUSTOM': case 'SAP': $user_label = __('Account ID'); $pass_label = __('Password'); $extra1 = false; $extra2 = false; default: // Use defaults. break; } $inputs[] = [ 'label' => $user_label, 'id' => 'div-username', 'arguments' => [ 'name' => 'username', 'input_class' => 'flex-row', 'type' => 'text', 'value' => $values['username'], 'return' => true, ], ]; $inputs[] = [ 'label' => $pass_label, 'id' => 'div-password', 'arguments' => [ 'name' => 'password', 'input_class' => 'flex-row', 'type' => 'password', 'value' => $values['password'], 'return' => true, ], ]; if ($extra1) { $inputs[] = [ 'label' => $extra_1_label, 'id' => 'div-extra_1', 'arguments' => [ 'name' => 'extra_1', 'input_class' => 'flex-row', 'type' => 'text', 'value' => $values['extra_1'], 'return' => true, ], ]; } if ($extra2) { $inputs[] = [ 'label' => $extra_2_label, 'id' => 'div-extra_2', 'arguments' => [ 'name' => 'extra_2', 'input_class' => 'flex-row', 'type' => 'text', 'value' => $values['extra_2'], 'return' => true, 'display' => $extra2, ], ]; } return $this->printForm( [ 'form' => $form, 'inputs' => $inputs, ], true ); } /** * Loads JS content. * * @return string JS content. */ public function loadJS() { ob_start(); // Javascript content. ?>