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 credential store' ); if (is_ajax()) { echo json_encode(['error' => 'noaccess']); } else { 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_children( $filter['filter_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); if ($return === false) { $return = []; } // Filter out those items of group all that cannot be edited by user. $return = array_filter( $return, function ($item) { if ($item['id_group'] == 0 && users_can_manage_group_all('AR') === false) { return false; } else { return true; } } ); return $return; } /** * 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']); $key['extra_1'] = io_output_password($key['extra_1']); $key['extra_2'] = io_output_password($key['extra_2']); 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']); $item['extra_1'] = io_output_password($item['extra_1']); $item['extra_2'] = io_output_password($item['extra_2']); $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); if ((bool) users_is_admin() === false) { $all = users_can_manage_group_all('UM'); $filter['group_list'] = array_keys( users_get_groups( $config['id_user'], 'UM', (bool) $all ) ); } 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 ($product === 'GOOGLE') { $google_creds = json_decode(io_safe_output($extra_1)); if (json_last_error() !== JSON_ERROR_NONE) { $this->ajaxMsg( 'error', __('Not a valid JSON: %s', json_last_error_msg()) ); exit; } $username = $google_creds->client_email; $password = $google_creds->private_key_id; } if (empty($identifier) === true) { $error = __('Key identifier is required'); } else if ($id_group === null) { $error = __('You must select a group where store this key!'); } else if (empty($product) === true) { $error = __('You must specify a product type'); } else if (empty($username) === true && (empty($password) === true)) { $error = __('You must specify a username and/or password'); } if (isset($error)) { $this->ajaxMsg('error', $error); exit; } // Encrypt content (if needed). $values = [ 'identifier' => $identifier, 'id_group' => $id_group, 'product' => $product, 'username' => io_input_password(io_safe_output($username)), 'password' => io_input_password(io_safe_output($password)), 'extra_1' => io_input_password(io_safe_output($extra_1)), 'extra_2' => io_input_password(io_safe_output($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_groups', 'id' => 'filter_id_group', 'name' => 'filter_id_group', 'privilege' => 'AR', 'type' => 'select_groups', 'nothing' => false, 'selected' => (defined($id_group_filter) ? $id_group_filter : 0), 'return' => true, 'size' => '80%', ], [ '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 = []; } $return_all_group = false; if (users_can_manage_group_all('AR') === true) { $return_all_group = true; } $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', 'returnAllGroup' => $return_all_group, '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_type = 'text'; $user = true; $pass = true; $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': $extra_1_label = __('Auth JSON'); $user = false; $pass = false; $extra1 = true; $extra2 = false; $extra1_type = 'textarea'; break; case 'CUSTOM': case 'SAP': $user_label = __('Account ID'); $pass_label = __('Password'); $extra1 = false; $extra2 = false; default: // Use defaults. break; } if ($user) { $inputs[] = [ 'label' => $user_label, 'id' => 'div-username', 'arguments' => [ 'name' => 'username', 'input_class' => 'flex-row', 'type' => 'text', 'value' => $values['username'], 'return' => true, ], ]; } if ($pass) { $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', 'id' => 'text-extra_1', 'input_class' => 'flex-row', 'type' => $extra1_type, '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. ?>