Parcourir la source

#5529 Protect confidential information: API keys

Aleksandr Trofimov il y a 2 mois
Parent
commit
75cfcf3de0

+ 41 - 0
inc/classes/BxDolGrid.php

@@ -140,6 +140,29 @@
  *
  *
  *
+ * @section grid_display_secret_cell Displaying secret cell
+ *
+ * To display a field as a masked secret (for example `abcd********xyz1`) with an eye icon to reveal it
+ * after the current account password is confirmed, call `_getCellSecret` from a custom cell method:
+ *
+ * @code
+ * protected function _getCellApiKey ($mixedValue, $sKey, $aField, $aRow)
+ * {
+ *     return $this->_getCellSecret($mixedValue, $sKey, $aField, $aRow);
+ * }
+ * @endcode
+ *
+ * Alternatively, set `secret` to `true` in the field `params` (serialized array in `sys_grid_fields.params`):
+ *
+ * @code
+ * a:1:{s:6:"secret";b:1;}
+ * @endcode
+ *
+ * The full value is never included in the page HTML. Revealing it requires the logged-in user's current password.
+ * Override `_getSecretValue` when the secret is stored encrypted or computed rather than read from the row field.
+ *
+ *
+ *
  * @section grid_display_custom_header Displaying custom column header
  *
  * This is working similar to displaying custom cell. It easily customize its design by specifying custom attributes as 'attr_head' in params field in sys_grid_fields table.
@@ -729,6 +752,24 @@ class BxDolGrid extends BxDolFactory implements iBxDolFactoryObject, iBxDolRepla
         return $oDb->getAll($sQuery);
     }
 
+    protected function _getDataArrayUpdated ($aIds)
+    {
+        if ($this->_aOptions['source'] && !is_array($this->_aOptions['source']))
+            $this->_aOptions['source'] = unserialize($this->_aOptions['source']);
+
+        if (empty($this->_aOptions['source']) || !is_array($this->_aOptions['source']) || !$this->_aOptions['field_id'] || !$aIds || !is_array($aIds))
+            return [];
+
+        $sFieldId = $this->_aOptions['field_id'];
+        $aResult = [];
+        foreach ($this->_aOptions['source'] as $aRow) {
+            if (isset($aRow[$sFieldId]) && in_array($aRow[$sFieldId], $aIds))
+                $aResult[] = $aRow;
+        }
+
+        return $aResult;
+    }
+
     protected function _getCellData($sKey, $aField, $aRow)
     {
         if (isset($aRow[$sKey])) {

+ 65 - 0
inc/js/classes/BxDolGrid.js

@@ -308,6 +308,9 @@ BxDolGrid.prototype.processJson = function (oData, sAction, isDisableLoading) {
         if (oData && undefined != oData.eval) {
             eval(oData.eval);
         }
+        if (oData && undefined != oData.secret) {
+            $this.setSecretValue(oData.secret.id, oData.secret.field, oData.secret.value);
+        }
     };
 
     if (typeof(isDisableLoading) == 'undefined' || !isDisableLoading)
@@ -416,6 +419,15 @@ BxDolGrid.prototype._bindActions = function (isSkipSearchInput) {
     });
 
     this._bindActionsSingle ();
+
+    jQuery('#' + this._sIdWrapper + ' .bx-grid-secret-toggle').bind('click', function () {
+        var eWrap = $(this).closest('.bx-grid-secret');
+        if (eWrap.hasClass('bx-grid-secret-shown')) {
+            $this.hideSecret(eWrap);
+            return;
+        }
+        $this.revealSecret($(this).attr('bx_grid_secret_id'), $(this).attr('bx_grid_secret_field'));
+    });
     
     if (jQuery('#' + this._sIdWrapper + ' .bx-switcher-cont input').length) 
         jQuery('#' + this._sIdWrapper).addWebForms(); 
@@ -493,4 +505,57 @@ BxDolGrid.prototype._checkAppend = function (oInput, oData) {
     return oData;
 };
 
+BxDolGrid.prototype.revealSecret = function (sId, sField) {
+    var $this = this;
+    var oInput = $('#bx-popup-prompt-value');
+
+    $(document).dolPopupPrompt({
+        message: _t('_sys_grid_secret_password_prompt'),
+        value: '',
+        onClickOk: function (oPopup) {
+            var oData = {
+                o: $this._sObject,
+                a: 'reveal_secret',
+                'ids[]': sId,
+                field: sField,
+                password: oPopup.getValue(),
+                csrf_token: $this._sCsrfToken,
+                _r: Math.random()
+            };
+            if (typeof($this._oQueryAppend) == 'object')
+                oData = $.extend({}, $this._oQueryAppend, oData);
+
+            $this.loading(true);
+            $.post(sUrlRoot + 'grid.php', oData, function (oResult) {
+                $this.processJson(oResult, 'reveal_secret', false);
+            }, 'json');
+        },
+        onShow: function () {
+            oInput.attr('type', 'password').attr('autocomplete', 'current-password').val('').focus();
+        },
+        onHide: function () {
+            oInput.attr('type', 'text').removeAttr('autocomplete').val('');
+        }
+    });
+};
+
+BxDolGrid.prototype.hideSecret = function (eWrap) {
+    var sMasked = eWrap.find('.bx-grid-secret-value').attr('data-masked') || '';
+    eWrap.removeClass('bx-grid-secret-shown');
+    eWrap.find('.bx-grid-secret-value').text(sMasked);
+    eWrap.find('.bx-grid-secret-toggle').attr('title', _t('_sys_grid_secret_reveal'));
+    eWrap.find('.bx-grid-secret-toggle .sys-icon').removeClass('eye-slash').addClass('eye');
+};
+
+BxDolGrid.prototype.setSecretValue = function (sId, sField, sValue) {
+    var eToggle = jQuery('#' + this._sIdWrapper + ' .bx-grid-secret-toggle').filter(function () {
+        return $(this).attr('bx_grid_secret_id') == sId && $(this).attr('bx_grid_secret_field') == sField;
+    });
+    var eWrap = eToggle.closest('.bx-grid-secret');
+    eWrap.addClass('bx-grid-secret-shown');
+    eWrap.find('.bx-grid-secret-value').text(sValue);
+    eToggle.attr('title', _t('_sys_grid_secret_hide'));
+    eToggle.find('.sys-icon').removeClass('eye').addClass('eye-slash');
+};
+
 /** @} */

+ 3 - 0
modules/boonex/english/data/langs/system/en.xml

@@ -3272,6 +3272,9 @@ If it is not enabled then please consider implementing this optimization, since
 	<string name="_sys_grid_confirmation"><![CDATA[Are you sure?]]></string>
 	<string name="_sys_grid_delete_failed"><![CDATA[Delete failed]]></string>
   <string name="_sys_grid_search"><![CDATA[Search...]]></string>
+	<string name="_sys_grid_secret_hide"><![CDATA[Hide]]></string>
+	<string name="_sys_grid_secret_password_prompt"><![CDATA[Enter your current password]]></string>
+	<string name="_sys_grid_secret_reveal"><![CDATA[Show]]></string>
   <string name="_sys_grid_total_count"><![CDATA[Сount: {0}]]></string>
   <string name="_sys_grid_txt_account_manager"><![CDATA[Redirect to manage account.]]></string>
 	<string name="_sys_inst_audit_failed"><![CDATA[{0} failed]]></string>

+ 3 - 0
modules/boonex/russian/data/langs/system/ru.xml

@@ -3245,6 +3245,9 @@
 	<string name="_sys_grid_confirmation"><![CDATA[Вы уверены?]]></string>
 	<string name="_sys_grid_delete_failed"><![CDATA[Удалить ошибочные]]></string>
 	<string name="_sys_grid_search"><![CDATA[Поиск...]]></string>
+	<string name="_sys_grid_secret_hide"><![CDATA[Скрыть]]></string>
+	<string name="_sys_grid_secret_password_prompt"><![CDATA[Введите текущий пароль]]></string>
+	<string name="_sys_grid_secret_reveal"><![CDATA[Показать]]></string>
   <string name="_sys_grid_total_count"><![CDATA[Всего записей: {0}]]></string>
   <string name="_sys_grid_txt_account_manager"><![CDATA[Перенаправить в Управление аккаунтом.]]></string>
 	<string name="_sys_inst_audit_failed"><![CDATA[{0} ошибок]]></string>

+ 20 - 0
template/css/grid.css

@@ -172,3 +172,23 @@ td:last-child .bx-grid-cell-single-actions-wrapper {
 .bx-grid-table-wrapper td .bx-def-unit {
     margin-bottom:0;
 }
+
+.bx-grid-secret {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 0.5rem;
+    min-width: 0;
+}
+.bx-grid-secret-value {
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-family: monospace;
+}
+.bx-grid-secret-toggle {
+    flex-shrink: 0;
+    line-height: 1;
+    cursor: pointer;
+}

+ 108 - 3
template/scripts/BxBaseGrid.php

@@ -126,6 +126,36 @@ class BxBaseGrid extends BxDolGrid
         echo echoJson(array($sAction => $aAffectedIds));
     }
 
+    public function performActionRevealSecret()
+    {
+        $this->_replaceMarkers();
+
+        if (!isLogged())
+            return $this->_getActionResult(['msg' => _t('_Access denied')]);
+
+        if (!$this->_isCurrentPasswordValid(bx_get('password')))
+            return $this->_getActionResult(['msg' => _t('_sys_form_account_input_password_current_error')]);
+
+        $aIds = bx_get('ids');
+        $sKey = bx_process_input(bx_get('field'));
+        $mixedId = is_array($aIds) && $aIds ? reset($aIds) : false;
+
+        if ($mixedId === false || $sKey === false || $sKey === '' || empty($this->_aOptions['fields'][$sKey]))
+            return $this->_getActionResult(['msg' => _t('_sys_txt_error_occured')]);
+
+        $aRow = $this->_getSecretRow($mixedId);
+        if (!$aRow)
+            return $this->_getActionResult(['msg' => _t('_sys_txt_error_occured')]);
+
+        return $this->_getActionResult([
+            'secret' => [
+                'id' => $mixedId,
+                'field' => $sKey,
+                'value' => $this->_getSecretValue($mixedId, $sKey, $aRow),
+            ]
+        ]);
+    }
+
     public function getCode ($isDisplayHeader = true)
     {
         $this->_replaceMarkers ();
@@ -362,7 +392,7 @@ class BxBaseGrid extends BxDolGrid
             foreach($this->_aOptions['fields'] as $sKey => $aField) {
                 $sMethod = '_getCell' . $this->_genMethodName($sKey);
                 if(!method_exists($this, $sMethod))
-                    $sMethod = $sMethodDefault;
+                    $sMethod = !empty($aField['secret']) ? '_getCellSecret' : $sMethodDefault;
 
                 if(($aCell = $this->$sMethod(isset($aData[$iKey][$sKey]) ? $aData[$iKey][$sKey] : _t('_undefined'), $sKey, $aField, $aRow)))
                     $aDataRv[$iKey][$sKey] = $aCell;
@@ -644,13 +674,16 @@ class BxBaseGrid extends BxDolGrid
         $sCustomMethod = '_getCell' . $this->_genMethodName($sKey);
         if (method_exists($this, $sCustomMethod))
             $sMethod = $sCustomMethod;
+        else if (!empty($aField['secret']))
+            $sMethod = '_getCellSecret';
 
         $mixedValue = $this->_getCellData($sKey, $aField, $aRow);
 
         if ($aField['translatable'])
             $mixedValue = _t($mixedValue);
 
-        $mixedValue = $this->_limitMaxLength($mixedValue, $sKey, $aField, $aRow, $this->_isDisplayPopupOnTextOverflow);
+        if ($sMethod != '_getCellSecret')
+            $mixedValue = $this->_limitMaxLength($mixedValue, $sKey, $aField, $aRow, $this->_isDisplayPopupOnTextOverflow);
 
         return $this->$sMethod($mixedValue, $sKey, $aField, $aRow);
     }
@@ -752,6 +785,73 @@ class BxBaseGrid extends BxDolGrid
         return '<td ' . $sAttr . '>' . $mixedValue . '</td>';
     }
 
+    /**
+     * Display a cell as a masked secret with an eye icon to reveal it after password confirmation.
+     * @see BxDolGrid grid_display_secret_cell
+     */
+    protected function _getCellSecret ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sRaw = '';
+        if (isset($aRow[$sKey]) && $aRow[$sKey] !== '' && $aRow[$sKey] !== null)
+            $sRaw = (string)$aRow[$sKey];
+        else if ($mixedValue !== '' && $mixedValue !== null && $mixedValue !== false)
+            $sRaw = (string)$mixedValue;
+
+        $sMasked = $sRaw !== '' ? bx_gen_secret($sRaw) : '';
+
+        if ($this->_bIsApi)
+            return ['type' => 'secret', 'value' => $sMasked];
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-grid-cell' . (!empty($aField['name']) ? ' bx-gc-' . $aField['name'] : '') . ' bx-def-padding-sec-bottom bx-def-padding-sec-top',
+            isset($aField['width']) ? 'width:' . $aField['width'] : false
+        );
+
+        $sToggle = '';
+        if ($sRaw !== '' && isLogged()) {
+            $mixedId = $aRow[$this->_aOptions['field_id']];
+            $sToggle = '<a href="javascript:void(0);" class="bx-grid-secret-toggle" title="' . bx_html_attribute(_t('_sys_grid_secret_reveal')) . '" bx_grid_secret_id="' . bx_html_attribute($mixedId) . '" bx_grid_secret_field="' . bx_html_attribute($sKey) . '"><i class="sys-icon eye"></i></a>';
+        }
+
+        return '<td ' . $sAttr . '><div class="bx-grid-secret"><span class="bx-grid-secret-value" data-masked="' . bx_html_attribute($sMasked) . '">' . bx_process_output($sMasked) . '</span>' . $sToggle . '</div></td>';
+    }
+
+    protected function _getSecretRow($mixedId)
+    {
+        $aRows = $this->_getDataUpdated([$mixedId]);
+        if (!$aRows || !is_array($aRows))
+            return false;
+
+        $sFieldId = $this->_aOptions['field_id'];
+        foreach ($aRows as $aRow) {
+            if (isset($aRow[$sFieldId]) && (string)$aRow[$sFieldId] === (string)$mixedId)
+                return $aRow;
+        }
+
+        return reset($aRows);
+    }
+
+    /**
+     * Return the secret value for a revealed cell. Override when the value is encrypted or computed.
+     */
+    protected function _getSecretValue($mixedId, $sKey, $aRow)
+    {
+        return isset($aRow[$sKey]) ? $aRow[$sKey] : '';
+    }
+
+    protected function _isCurrentPasswordValid($sPassword)
+    {
+        if ($sPassword === false || $sPassword === '')
+            return false;
+
+        if (!class_exists('BxFormAccountCheckerHelper'))
+            bx_import('BxBaseFormAccount');
+
+        $oChecker = new BxFormAccountCheckerHelper();
+        return true === $oChecker->checkPasswordCurrent($sPassword);
+    }
+
     protected function _getActions ($sType, $sActionData = false, $isSmall = false, $isDisabled = false, $isPermanentState = false, $aRow = array())
     {
         $sActionsType = 'actions_' . $sType;
@@ -1077,7 +1177,12 @@ class BxBaseGrid extends BxDolGrid
         $this->_oTemplate->addJs('BxDolGrid.js');
         $this->_oTemplate->addCss('grid.css');
 
-        $this->_oTemplate->addJsTranslation('_sys_grid_confirmation');
+        $this->_oTemplate->addJsTranslation([
+            '_sys_grid_confirmation',
+            '_sys_grid_secret_reveal',
+            '_sys_grid_secret_hide',
+            '_sys_grid_secret_password_prompt',
+        ]);
     }
 }