You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by al...@apache.org on 2015/10/07 17:07:52 UTC

ambari git commit: AMBARI-13340. Kerberos: Enhance UI to set KDC admin credentials

Repository: ambari
Updated Branches:
  refs/heads/trunk 15ac28364 -> 78e49db0a


AMBARI-13340. Kerberos: Enhance UI to set KDC admin credentials


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/78e49db0
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/78e49db0
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/78e49db0

Branch: refs/heads/trunk
Commit: 78e49db0a1ba569a8e580c4ef182246475e0693f
Parents: 15ac283
Author: Alex Antonenko <hi...@gmail.com>
Authored: Wed Oct 7 18:03:46 2015 +0300
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Wed Oct 7 18:03:46 2015 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |   1 +
 ambari-web/app/config.js                        |   3 +-
 .../main/admin/kerberos/step2_controller.js     |  14 +-
 .../main/admin/kerberos/wizard_controller.js    |  16 +-
 ambari-web/app/messages.js                      |   3 +
 ambari-web/app/mixins.js                        |   1 +
 .../common/kdc_credentials_controller_mixin.js  | 150 +++++++++++++
 .../configs/objects/service_config_property.js  |  15 ++
 ambari-web/app/styles/application.less          |  10 +
 ambari-web/app/styles/common.less               |  10 +
 .../common/configs/service_config_category.hbs  |  28 ++-
 ambari-web/app/utils/ajax/ajax.js               |  47 ++++
 ambari-web/app/utils/credentials.js             | 225 +++++++++++++++++++
 ambari-web/app/views/common/controls_view.js    |  18 +-
 .../kdc_credentials_controller_mixin_test.js    | 195 ++++++++++++++++
 15 files changed, 721 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index b3b0e2c..06f1d2e 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -143,6 +143,7 @@ var files = [
   'test/mixins/common/widgets/export_metrics_mixin_test',
   'test/mixins/common/widgets/time_range_mixin_test',
   'test/mixins/common/widgets/widget_section_test',
+  'test/mixins/common/kdc_credentials_controller_mixin_test',
   'test/mixins/common/localStorage_test',
   'test/mixins/common/reload_popup_test',
   'test/mixins/common/serverValidator_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/config.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/config.js b/ambari-web/app/config.js
index d33239a..0a53fdf 100644
--- a/ambari-web/app/config.js
+++ b/ambari-web/app/config.js
@@ -75,7 +75,8 @@ App.supports = {
   customizedWidgetLayout: false,
   enhancedConfigs: true,
   showPageLoadTime: false,
-  skipComponentStartAfterInstall: false
+  skipComponentStartAfterInstall: false,
+  storeKDCCredentials: false
 };
 
 if (App.enableExperimental) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/controllers/main/admin/kerberos/step2_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/admin/kerberos/step2_controller.js b/ambari-web/app/controllers/main/admin/kerberos/step2_controller.js
index f87fb77..77a27e5 100644
--- a/ambari-web/app/controllers/main/admin/kerberos/step2_controller.js
+++ b/ambari-web/app/controllers/main/admin/kerberos/step2_controller.js
@@ -19,7 +19,7 @@
 var App = require('app');
 require('controllers/wizard/step7_controller');
 
-App.KerberosWizardStep2Controller = App.WizardStep7Controller.extend({
+App.KerberosWizardStep2Controller = App.WizardStep7Controller.extend(App.KDCCredentialsControllerMixin, {
   name: "kerberosWizardStep2Controller",
 
   isKerberosWizard: true,
@@ -36,6 +36,10 @@ App.KerberosWizardStep2Controller = App.WizardStep7Controller.extend({
 
   addMiscTabToPage: false,
 
+  isStorePersisted: function() {
+    return this.get('wizardController.content.secureStoragePersisted');
+  }.property('wizardController.content.secureStoragePersisted'),
+
   /**
    * @type {boolean} true if test connection to hosts is in progress
    */
@@ -93,6 +97,9 @@ App.KerberosWizardStep2Controller = App.WizardStep7Controller.extend({
     App.config.setPreDefinedServiceConfigs(this.get('addMiscTabToPage'));
 
     this.filterConfigs(this.get('configs'));
+    if (App.get('supports.storeKDCCredentials') && !this.get('wizardController.skipClientInstall')) {
+      this.initilizeKDCStoreProperties(this.get('configs'));
+    }
     this.applyServicesConfigs(this.get('configs'), storedConfigs);
   },
 
@@ -136,8 +143,11 @@ App.KerberosWizardStep2Controller = App.WizardStep7Controller.extend({
     if (this.get('isSubmitDisabled')) return false;
     this.set('isSubmitDisabled', true);
     var self = this;
-    this.deleteKerberosService().always(function (data) {
+    this.deleteKerberosService().always(function () {
       self.configureKerberos();
+      if (App.get('supports.storeKDCCredentials') && !self.get('wizardController.skipClientInstall')) {
+        self.createKDCCredentials(self.get('stepConfigs.0.configs'));
+      }
     });
   },
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/controllers/main/admin/kerberos/wizard_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/admin/kerberos/wizard_controller.js b/ambari-web/app/controllers/main/admin/kerberos/wizard_controller.js
index 6fb0f8b..6209f29 100644
--- a/ambari-web/app/controllers/main/admin/kerberos/wizard_controller.js
+++ b/ambari-web/app/controllers/main/admin/kerberos/wizard_controller.js
@@ -18,6 +18,7 @@
 
 
 var App = require('app');
+var credentialsUtils = require('utils/credentials');
 
 App.KerberosWizardController = App.WizardController.extend(App.InstallComponent, {
 
@@ -63,7 +64,8 @@ App.KerberosWizardController = App.WizardController.extend(App.InstallComponent,
     services: [],
     advancedServiceConfig: null,
     serviceConfigProperties: [],
-    failedTask: null
+    failedTask: null,
+    secureStoragePersisted: null
   }),
 
   /**
@@ -255,6 +257,18 @@ App.KerberosWizardController = App.WizardController.extend(App.InstallComponent,
             }, this);
           }
         }
+      },
+      {
+        type: 'async',
+        callback: function() {
+          var self = this;
+          var dfd = $.Deferred();
+          credentialsUtils.isStorePersisted(App.get('clusterName')).then(function(isPersisted) {
+            self.set('content.secureStoragePersisted', isPersisted);
+            dfd.resolve();
+          });
+          return dfd.promise();
+        }
       }
     ],
     '3': [

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index ab9a874..80902ce 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -1021,6 +1021,9 @@ Em.I18n.translations = {
   'admin.authentication.form.test.success':'The configuration passes the test',
   'admin.authentication.form.test.fail':'The configuration fails the test',
 
+
+  'admin.kerberos.credentials.store.hint.supported': 'When checked, Ambari will store the KDC Admin credentials so they are not required to be re-entered during future changes of services, hosts, and components.',
+  'admin.kerberos.credentials.store.hint.not.supported': 'Ambari is not configured for storing credentials',
   'admin.kerberos.wizard.configuration.note': 'This is the initial configuration created by Enable Kerberos wizard.',
   'admin.kerberos.wizard.header':'Enable Kerberos Wizard',
   'admin.kerberos.button.enable': 'Enable Kerberos',

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/mixins.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins.js b/ambari-web/app/mixins.js
index 4c9da09..276e777 100644
--- a/ambari-web/app/mixins.js
+++ b/ambari-web/app/mixins.js
@@ -20,6 +20,7 @@
 // load all mixins here
 
 require('mixins/common/blueprint');
+require('mixins/common/kdc_credentials_controller_mixin');
 require('mixins/common/localStorage');
 require('mixins/common/userPref');
 require('mixins/common/reload_popup');

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/mixins/common/kdc_credentials_controller_mixin.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/kdc_credentials_controller_mixin.js b/ambari-web/app/mixins/common/kdc_credentials_controller_mixin.js
new file mode 100644
index 0000000..7be3056
--- /dev/null
+++ b/ambari-web/app/mixins/common/kdc_credentials_controller_mixin.js
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+var credentialsUtils = require('utils/credentials');
+
+App.KDCCredentialsControllerMixin = Em.Mixin.create({
+
+  /**
+   * Alias name used to store KDC credentials
+   *
+   * @type {string}
+   */
+  credentialAlias: 'kdc.admin.credential',
+
+  /**
+   * Returns <code>true</code> if persisted secure storage available.
+   * Should be implemented as computed property.
+   *
+   * @type {boolean}
+   */
+  isStorePersisted: function() {
+    Em.assert("Should be implemented", false);
+  }.property(),
+
+  /**
+   * List of required UI-only properties needed for storing KDC credentials
+   *
+   * @type {object[]}
+   */
+  credentialsStoreConfigs: [
+    {
+      name: 'persist_credentials',
+      displayType: 'checkbox',
+      value: 'false',
+      recommendedValue: 'false',
+      supportsFinal: false,
+      recommendedIsFinal: false,
+      displayName: 'Save Admin credentials',
+      category: 'Kadmin',
+      isRequired: false,
+      isRequiredByAgent: false,
+      hintMessage: false,
+      rightSideLabel: true,
+      isEditable: true,
+      index: 3
+    }
+  ],
+
+  /**
+   * @param {object} resource resource info to set e.g.
+   * <code>
+   * {
+   *   principal: "USERNAME",
+   *   key: "SecretKey",
+   *   type: "persisted"
+   * }
+   * </code>
+   *
+   * Where:
+   * <ul>
+   *   <li>principal: the principal (or username) part of the credential to store</li>
+   *   <li>key: the secret key part of the credential to store</li>
+   *   <li>type: declares the storage facility type: "persisted" or "temporary"</li>
+   * </ul>
+   * @returns {$.Deferred} promise object
+   */
+  createKDCCredentials: function(configs) {
+    var self = this;
+    var resource = {
+      type: this._getStorageTypeValue(configs),
+      key: configs.findProperty('name', 'admin_password').get('value'),
+      principal:  configs.findProperty('name', 'admin_principal').get('value')
+    };
+    return credentialsUtils.createCredentials(App.get('clusterName'), this.get('credentialAlias'), resource).fail(function() {
+      return self.updateKDCCredentials(resource);
+    });
+  },
+
+  /**
+   * Remove KDC credentials
+   *
+   * @returns {$.Deferred} promise object
+   */
+  removeKDCCredentials: function() {
+    return credentialsUtils.removeCredentials(App.get('clusterName'), this.get('credentialAlias'));
+  },
+
+  /**
+   * @see createKDCCredentials
+   * @param {object} resource
+   * @returns {$.Deferred} promise object
+   */
+  updateKDCCredentials: function(resource) {
+    return credentialsUtils.updateCredentials(App.get('clusterName'), this.get('credentialAlias'), resource);
+  },
+
+  /**
+   * Generate additional properties regarding KDC credential storage
+   *
+   * @param {App.ServiceConfigProperty[]} configs list of configs
+   */
+  initilizeKDCStoreProperties: function(configs) {
+    var self = this;
+    this.get('credentialsStoreConfigs').forEach(function(item) {
+      var configObject = App.config.createDefaultConfig(item.name, 'KERBEROS', 'krb5-conf.xml', false, false);
+      $.extend(configObject, item);
+      if (item.name === 'persist_credentials') {
+        if (self.get('isStorePersisted')) {
+          configObject.hintMessage = Em.I18n.t('admin.kerberos.credentials.store.hint.supported');
+        } else {
+          configObject.hintMessage = Em.I18n.t('admin.kerberos.credentials.store.hint.not.supported');
+          configObject.isEditable = false;
+        }
+      }
+      configs.pushObject(configObject);
+    });
+  },
+
+  /**
+   * Return storage type e.g. <b>temporary</b>, <b>persisted</b>
+   *
+   * @param {App.ServiceConfigProperty[]} configs configs array from step configs
+   * @returns {string} storage type value
+   */
+  _getStorageTypeValue: function(configs) {
+    if (this.get('isStorePersisted')) {
+      return configs.findProperty('name', 'persist_credentials').get('value') === "true" ?
+        credentialsUtils.STORE_TYPES.PERSISTENT :
+        credentialsUtils.STORE_TYPES.TEMPORARY;
+    }
+    return credentialsUtils.STORE_TYPES.TEMPORARY;
+  }
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/models/configs/objects/service_config_property.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/configs/objects/service_config_property.js b/ambari-web/app/models/configs/objects/service_config_property.js
index 01b4eb2..100de5d 100644
--- a/ambari-web/app/models/configs/objects/service_config_property.js
+++ b/ambari-web/app/models/configs/objects/service_config_property.js
@@ -77,6 +77,21 @@ App.ServiceConfigProperty = Em.Object.extend({
    */
   supportsFinal: false,
 
+  /**
+   * Hint message to display in tooltip. Tooltip will be wrapped on question mark icon.
+   * If value is <code>false</code> no tooltip and question mark icon.
+   *
+   * @type {boolean|string}
+   */
+  hintMessage: false,
+
+  /**
+   * Display label on the right side from input. In general used for checkbox only.
+   *
+   * @type {boolean}
+   */
+  rightSideLabel: false,
+
   retypedPassword: '',
   description: '',
   displayType: 'string', // string, digits, number, directories, custom

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index fcc4863..d08ca34 100644
--- a/ambari-web/app/styles/application.less
+++ b/ambari-web/app/styles/application.less
@@ -6036,3 +6036,13 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
     margin-bottom: 0;
   }
 }
+
+[class^="icon-"],
+[class*="icon-"] {
+  &.icon-blue {
+    color: @blue;
+  }
+  &:hover {
+    text-decoration: none;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/styles/common.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/common.less b/ambari-web/app/styles/common.less
index 6559c06..b4cb61d 100644
--- a/ambari-web/app/styles/common.less
+++ b/ambari-web/app/styles/common.less
@@ -355,4 +355,14 @@
   min-width: 60px;
   font-size: 14px;
   cursor: default;
+}
+
+.bootstrap-checkbox {
+  &>button.btn {
+    &:focus {
+      border-color: none;
+      box-shadow: 0;
+      outline: 0 none;
+    }
+  }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/templates/common/configs/service_config_category.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/configs/service_config_category.hbs b/ambari-web/app/templates/common/configs/service_config_category.hbs
index 8cb65ae..6cd1203 100644
--- a/ambari-web/app/templates/common/configs/service_config_category.hbs
+++ b/ambari-web/app/templates/common/configs/service_config_category.hbs
@@ -33,20 +33,30 @@
         {{#unless widget}} {{! configs with widgets should be shown only on the EnhancedConfigs tabs }}
           <div {{bindAttr class=":entry-row isHiddenByFilter:hide isOverridden:overridden-property hasCompareDiffs:overridden-property"}}>
             {{#if showLabel}}
-              <span {{bindAttr class="errorMessage:error: :control-group :control-label-span"}}>
-                <label class="control-label">
-                  {{formatWordBreak displayName}}
-                  {{#if isSecureConfig}}
-                    <a href="javascript:void(null);"><i class="icon-lock" rel="tooltip" data-toggle="tooltip"
-                                                        title="security knob"></i></a>
-                  {{/if}}
-                </label>
-              </span>
+              {{#unless rightSideLabel}}
+                <span {{bindAttr class="errorMessage:error: :control-group :control-label-span"}}>
+                  <label class="control-label">
+                    {{formatWordBreak displayName}}
+                    {{#if isSecureConfig}}
+                      <a href="javascript:void(null);"><i class="icon-lock" rel="tooltip" data-toggle="tooltip"
+                                                          title="security knob"></i></a>
+                    {{/if}}
+                  </label>
+                </span>
+              {{else}}
+                <span class="control-group control-label-span"> </span>
+              {{/unless}}
             {{/if}}
             <div {{bindAttr class="showLabel:controls"}}>
               {{! Here serviceConfigBinding should ideally be serviceConfigPropertyBinding }}
               <div {{bindAttr class="errorMessage:error: warnMessage:warning: :control-group"}}>
                 {{view viewClass serviceConfigBinding="this" categoryConfigsAllBinding="view.categoryConfigsAll" }}
+                {{#if rightSideLabel}}
+                  <span {{bindAttr class="isEditable::muted"}}>{{formatWordBreak displayName}}</span>
+                {{/if}}
+                {{#if hintMessage}}
+                  <a class="icon-question-sign icon-blue" href="#" data-toggle="tooltip" {{bindAttr data-original-title="hintMessage"}}><a/>
+                {{/if}}
                 {{#if this.isComparison}}
                   {{#if controller.selectedConfigGroup.isDefault}}
                     <span

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/utils/ajax/ajax.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ajax/ajax.js b/ambari-web/app/utils/ajax/ajax.js
index f10eff1..9e5e8d2 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/ambari-web/app/utils/ajax/ajax.js
@@ -791,6 +791,53 @@ var urls = {
     'mock': '/data/configuration/cluster_env_site.json'
   },
 
+  'credentials.store.info': {
+    'real': '/clusters/{clusterName}?fields=Clusters/credential_store_properties',
+    'mock': ''
+  },
+
+  'credentials.list': {
+    'real': '/clusters/{clusterName}/credentials',
+    'mock': ''
+  },
+
+  'credentials.get': {
+    'real': '/clusters/{clusterName}/credentials/{alias}',
+    'mock': ''
+  },
+
+  'credentials.create': {
+    'real': '/clusters/{clusterName}/credentials/{alias}',
+    'mock': '',
+    type: 'POST',
+    'format': function(data) {
+      return {
+        data: JSON.stringify({
+          Credential: data.resource
+        })
+      };
+    }
+  },
+
+  'credentials.update': {
+    'real': '/clusters/{clusterName}/credentials/{alias}',
+    'mock': '',
+    'type': 'PUT',
+    'format': function(data) {
+      return {
+        data: JSON.stringify({
+          Credential: data.resource
+        })
+      };
+    }
+  },
+
+  'credentials.delete': {
+    'real': '/clusters/{clusterName}/credentials/{alias}',
+    'mock': '',
+    'type':'DELETE'
+  },
+
   'host.host_component.add_new_component': {
     'real': '/clusters/{clusterName}/hosts?Hosts/host_name={hostName}',
     'mock': '/data/wizard/deploy/poll_1.json',

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/utils/credentials.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/credentials.js b/ambari-web/app/utils/credentials.js
new file mode 100644
index 0000000..7a567e9
--- /dev/null
+++ b/ambari-web/app/utils/credentials.js
@@ -0,0 +1,225 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+var App = require('app');
+
+/** @module utils.credentials **/
+module.exports = {
+
+  STORE_TYPES: {
+    TEMPORARY: 'temporary',
+    PERSISTENT: 'persisted',
+    PERSISTENT_KEY: 'persistent',
+    TEMPORARY_KEY: 'temporary',
+    PERSISTENT_PATH: 'storage.persistent',
+    TEMPORARY_PATH: 'storage.temporary'
+  },
+
+  /**
+   * Store credentials to server
+   *
+   * @member utils.credentials
+   * @param {string} clusterName cluster name
+   * @param {string} alias credential alias name e.g. "kdc.admin.credentials"
+   * @param {object} resource resource info to set e.g.
+   * <code>
+   * {
+   *   principal: "USERNAME",
+   *   key: "SecretKey",
+   *   type: "persisted"
+   * }
+   * </code>
+   *
+   * Where:
+   * <ul>
+   *   <li>principal: the principal (or username) part of the credential to store</li>
+   *   <li>key: the secret key part of the credential to store</li>
+   *   <li>type: declares the storage facility type: "persisted" or "temporary"</li>
+   * </ul>
+   * @returns {$.Deferred} promise object
+   */
+  createCredentials: function(clusterName, alias, resource) {
+    return App.ajax.send({
+      sender: this,
+      name: 'credentials.create',
+      data: {
+        clusterName: clusterName,
+        resource: resource,
+        alias: alias
+      },
+      error: 'createCredentialsErrorCallback'
+    });
+  },
+
+  createCredentialsErrorCallback: function(req, ajaxOpts, error) {
+    console.error('createCredentials ERROR:', error);
+  },
+
+  /**
+   * Retrieve single credential from cluster by specified alias name
+   *
+   * @member utils.credentials
+   * @param {string} clusterName cluster name
+   * @param {string} alias credential alias name e.g. "kdc.admin.credentials"
+   * @returns {$.Deferred} promise object
+   */
+  getCredential: function(clusterName, alias, callback) {
+    return App.ajax.send({
+      sender: this,
+      name: 'credentials.get',
+      data: {
+        clusterName: clusterName,
+        alias: alias,
+        callback: callback
+      },
+      success: 'getCredentialSuccessCallback'
+    });
+  },
+
+  getCredentialSuccessCallback: function(data, opt, params) {
+    params.callback(Em.getWithDefault(data, 'Credential', null));
+  },
+
+  /**
+   * Update credential by alias and cluster name
+   *
+   * @see createCredentials
+   * @param {string} clusterName
+   * @param {string} alias
+   * @param {object} resource
+   * @returns {$.Deferred} promise object
+   */
+  updateCredentials: function(clusterName, alias, resource) {
+    return App.ajax.send({
+      sender: this,
+      name: 'credentials.update',
+      data: {
+        clusterName: clusterName,
+        alias: alias,
+        resource: resource
+      }
+    });
+  },
+
+  /**
+   * Get credenial list from server by specified cluster name
+   *
+   * @param {string} clusterName cluster name
+   * @param {function} callback
+   * @returns {$.Deferred} promise object
+   */
+  credentials: function(clusterName, callback) {
+    return App.ajax.send({
+      sender: this,
+      name: 'credentials.list',
+      data: {
+        clusterName: clusterName
+      },
+      success: 'credentialsSuccessCallback'
+    });
+  },
+
+  credentialsSuccessCallback: function(data, opt, params) {
+    params.callback(data.items.length ? data.items.mapProperty('Credential') : []);
+  },
+
+  /**
+   * Remove credential from server by specified cluster name and alias
+   *
+   * @param {string} clusterName cluster name
+   * @param {string} alias credential alias name e.g. "kdc.admin.credentials"
+   */
+  removeCredentials: function(clusterName, alias) {
+    return App.ajax.send({
+      sender: this,
+      name: 'credentials.delete',
+      data: {
+        clusterName: clusterName,
+        alias: alias
+      }
+    });
+  },
+
+  /**
+   * Get info regarding credential storage type like <code>persistent</code> and <code>temporary</code>
+   *
+   * @param {string} clusterName cluster name
+   * @param {function} callback
+   * @returns {$.Deferred} promise object
+   */
+  storageInfo: function(clusterName, callback) {
+    return App.ajax.send({
+      sender: this,
+      name: 'credentials.store.info',
+      data: {
+        clusterName: clusterName,
+        callback: callback
+      },
+      success: 'storageInfoSuccessCallback'
+    });
+  },
+
+  storageInfoSuccessCallback: function(json, opt, params, request) {
+    if (json.Clusters) {
+      var storage = Em.getWithDefault(json, 'Clusters.credential_store_properties', {});
+      var storeTypesObject = {};
+
+      storeTypesObject[this.STORE_TYPES.PERSISTENT_KEY] = storage[this.STORE_TYPES.PERSISTENT_PATH] === "true";
+      storeTypesObject[this.STORE_TYPES.TEMPORARY_KEY] = storage[this.STORE_TYPES.TEMPORARY_PATH] === "true";
+      params.callback(storeTypesObject);
+    } else {
+      params.callback(null);
+    }
+  },
+
+  /**
+   * Resolves promise with <code>true</code> value if secure store is persistent
+   *
+   * @param {string} clusterName
+   * @returns {$.Deferred} promise object
+   */
+  isStorePersisted: function(clusterName) {
+    return this.storeTypeStatus(clusterName, this.STORE_TYPES.PERSISTENT_KEY);
+  },
+
+  /**
+   * Resolves promise with <code>true</code> value if secure store is temporary
+   *
+   * @param {string} clusterName
+   * @returns {$.Deferred} promise object
+   */
+  isStoreTemporary: function(clusterName) {
+    return this.storeTypeStatus(clusterName, this.STORE_TYPES.TEMPORARY_KEY);
+  },
+
+  /**
+   * Get store type value for specified cluster and store type e.g. <b>persistent</b> or <b>temporary</b>
+   *
+   * @param {string} clusterName
+   * @param {string} type store type e.g. <b>persistent</b> or <b>temporary</b>
+   * @returns {$.Deferred} promise object
+   */
+  storeTypeStatus: function(clusterName, type) {
+    var dfd = $.Deferred();
+    this.storageInfo(clusterName, function(storage) {
+      dfd.resolve(Em.get(storage, type));
+    }).fail(function(error) {
+      dfd.reject(error);
+    });
+    return dfd.promise();
+  }
+};

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/app/views/common/controls_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/controls_view.js b/ambari-web/app/views/common/controls_view.js
index 085ae11..578e975 100644
--- a/ambari-web/app/views/common/controls_view.js
+++ b/ambari-web/app/views/common/controls_view.js
@@ -328,6 +328,7 @@ App.ServiceConfigCheckbox = Ember.Checkbox.extend(App.ServiceConfigPopoverSuppor
    * and what value is negative (unchecked) proeprty
    */
   didInsertElement: function() {
+    var self = this;
     this._super();
     this.addObserver('serviceConfig.value', this, 'toggleChecker');
     Object.keys(this.get('allowedPairs')).forEach(function(key) {
@@ -336,7 +337,17 @@ App.ServiceConfigCheckbox = Ember.Checkbox.extend(App.ServiceConfigPopoverSuppor
         this.set('falseValue', this.get('allowedPairs')[key][1]);
       }
     }, this);
-    this.set('checked', this.get('serviceConfig.value') === this.get('trueValue'))
+    this.set('checked', this.get('serviceConfig.value') === this.get('trueValue'));
+    this.propertyDidChange('checked');
+    Em.run.next(function () {
+      if (self.$())
+        self.$().checkbox({
+          defaultState: self.get('serviceConfig.value'),
+          buttonStyle: 'btn-link btn-large',
+          checkedClass: 'icon-check',
+          uncheckedClass: 'icon-check-empty'
+        });
+    });
   },
 
   willDestroyElement: function() {
@@ -366,8 +377,11 @@ App.ServiceConfigCheckbox = Ember.Checkbox.extend(App.ServiceConfigPopoverSuppor
    * change checkbox value if click on undo
    */
   toggleChecker: function() {
-    if (this.isNotAppropriateValue())
+    if (this.isNotAppropriateValue()) {
       this.set('checked', !this.get('checked'));
+      // change bootstrap-checkbox state
+      this.$().change();
+    }
   },
 
   disabled: function () {

http://git-wip-us.apache.org/repos/asf/ambari/blob/78e49db0/ambari-web/test/mixins/common/kdc_credentials_controller_mixin_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mixins/common/kdc_credentials_controller_mixin_test.js b/ambari-web/test/mixins/common/kdc_credentials_controller_mixin_test.js
new file mode 100644
index 0000000..a584979
--- /dev/null
+++ b/ambari-web/test/mixins/common/kdc_credentials_controller_mixin_test.js
@@ -0,0 +1,195 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+require('mixins/common/kdc_credentials_controller_mixin');
+
+var App = require('app');
+var credentialsUtils = require('utils/credentials');
+
+var mixedObject;
+
+describe('App.KDCCredentialsControllerMixin', function() {
+
+  beforeEach(function() {
+    mixedObject = Em.Object.create(App.KDCCredentialsControllerMixin);
+  });
+
+  afterEach(function() {
+    mixedObject.destroy();
+  });
+
+  describe('#isStorePersisted', function() {
+    it('should throw error if not overrided in mixed object', function() {
+      var errorThrown = false;
+      try {
+        mixedObject.get('isStorePersisted');
+      } catch (e) {
+        errorThrown = true;
+      } finally {
+        expect(errorThrown).to.be.true;
+      }
+    });
+    it('should not throw error if overrided in mixed object', function() {
+      var errorThrown = false;
+      mixedObject.reopen({
+        isStorePersisted: function() {
+          return true;
+        }.property()
+      });
+      try {
+        mixedObject.get('isStorePersisted');
+      } catch (e) {
+        errorThrown = true;
+      } finally {
+        expect(errorThrown).to.be.false;
+      }
+    });
+  });
+
+  describe('#initilizeKDCStoreProperties', function() {
+    [
+      {
+        isStorePersisted: true,
+        e: {
+          isEditable: true,
+          hintMessage: Em.I18n.t('admin.kerberos.credentials.store.hint.supported')
+        },
+        message: 'Persistent store available, config should be editable, and appropriate hint shown'
+      },
+      {
+        isStorePersisted: false,
+        e: {
+          isEditable: false,
+          hintMessage: Em.I18n.t('admin.kerberos.credentials.store.hint.not.supported')
+        },
+        message: 'Only temporary store available, config should be disabled, and appropriate hint shown'
+      }
+    ].forEach(function(test) {
+      it(test.message, function() {
+        var configs = [],
+            config;
+        mixedObject.reopen({
+          isStorePersisted: function() {
+            return test.isStorePersisted;
+          }.property()
+        });
+        mixedObject.initilizeKDCStoreProperties(configs);
+        config = configs.findProperty('name', 'persist_credentials');
+        Em.keys(test.e).forEach(function(key) {
+          assert.equal(Em.get(config, key), test.e[key], 'validate attribute: ' + key);
+        });
+      });
+    });
+  });
+
+  describe('#createKDCCredentials', function() {
+    var createConfig = function(name, value) {
+      return App.ServiceConfigProperty.create({
+        name: name,
+        value: value
+      });
+    };
+    [
+      {
+        configs: [
+          createConfig('admin_password', 'admin'),
+          createConfig('admin_principal', 'admin/admin'),
+          createConfig('persist_credentials', 'true')
+        ],
+        e: [
+          'testName',
+          'kdc.admin.credential',
+          {
+            type: 'persisted',
+            key: 'admin',
+            principal: 'admin/admin'
+          }
+        ],
+        message: 'Save Admin credentials checkbox checked, credentials should be saved as `persisted`'
+      },
+      {
+        configs: [
+          createConfig('admin_password', 'admin'),
+          createConfig('admin_principal', 'admin/admin'),
+          createConfig('persist_credentials', 'false')
+        ],
+        e: [
+          'testName',
+          'kdc.admin.credential',
+          {
+            type: 'temporary',
+            key: 'admin',
+            principal: 'admin/admin'
+          }
+        ],
+        message: 'Save Admin credentials checkbox un-checked, credentials should be saved as `temporary`'
+      },
+      {
+        configs: [
+          createConfig('admin_password', 'admin'),
+          createConfig('admin_principal', 'admin/admin'),
+          createConfig('persist_credentials', 'false')
+        ],
+        e: [
+          'testName',
+          'kdc.admin.credential',
+          {
+            type: 'temporary',
+            key: 'admin',
+            principal: 'admin/admin'
+          }
+        ],
+        credentialWasSaved: true,
+        message: 'Save Admin credentials checkbox checked, credential was saved, credentials should be saved as `temporary`, #updateKDCCredentials should be called'
+      }
+    ].forEach(function(test) {
+      it(test.message, function() {
+        sinon.stub(App, 'get').withArgs('clusterName').returns('testName');
+        sinon.stub(credentialsUtils, 'createCredentials', function() {
+          if (test.credentialWasSaved) {
+            return $.Deferred().reject().promise();
+          } else {
+            return $.Deferred().resolve().promise();
+          }
+        });
+        if (test.credentialWasSaved) {
+          sinon.stub(credentialsUtils, 'updateCredentials', function() {
+            return $.Deferred().resolve().promise();
+          });
+        }
+
+        mixedObject.reopen({
+          isStorePersisted: function() {
+            return true;
+          }.property()
+        });
+        mixedObject.createKDCCredentials(test.configs);
+        assert.isTrue(credentialsUtils.createCredentials.calledOnce, 'credentialsUtils#createCredentials called');
+        assert.deepEqual(credentialsUtils.createCredentials.args[0], test.e, 'credentialsUtils#createCredentials called with correct arguments');
+        credentialsUtils.createCredentials.restore();
+        if (test.credentialWasSaved) {
+          assert.isTrue(credentialsUtils.updateCredentials.calledOnce, 'credentialUtils#updateCredentials called');
+          assert.deepEqual(credentialsUtils.updateCredentials.args[0], test.e, 'credentialUtils#updateCredentials called with correct arguments');
+          credentialsUtils.updateCredentials.restore();
+        }
+        App.get.restore();
+      });
+    });
+  });
+
+});