Hello,

the attached patches add support for more certificates and ability to add and remove certificates. Fixes these two tickets:
https://fedorahosted.org/freeipa/ticket/5108
https://fedorahosted.org/freeipa/ticket/5381

These patches add ability to view, get, download, revoke, restore and delete each certificate directly from user/host/service details page. There is also button for adding new certificates.

There is one known issue, that after page save action is performed some data disappear (includes certificates). This issue has a ticket already: https://fedorahosted.org/freeipa/ticket/5776

--
Pavel^3 Vomacka
From 7a0f73dd7f103b3ac4f4d28af2f0cb6654a6d95f Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 10:22:30 +0200
Subject: [PATCH 01/13] Add support for dropdown menu in multivalued widget

Every single widget which is in multivalued widget can now have action dropdown menu
and the delete button is included in this action dropdown menu.

Part of this ticket:
https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/widget.js | 62 +++++++++++++++++++++++++++++-----------
 1 file changed, 46 insertions(+), 16 deletions(-)

diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index fc2d6ef0bf9fd9361a88c9bf7523077739dc615f..8f7a4413273d1471964fd4a08d2e4c7c944fb811 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -987,6 +987,7 @@ IPA.multivalued_widget = function(spec) {
     that.undo_control;
     that.initialized = true;
     that.updating = false;
+    that.dropdown_menu = spec.dropdown_menu || false;
 
     that.rows = [];
 
@@ -1165,23 +1166,39 @@ IPA.multivalued_widget = function(spec) {
             that.emit('error-show', { source: that });
         });
 
+        var remove_row = function() {
+            that.remove_row(row);
+        };
+
         var remove_link_visible = !(row.is_new || !that.is_writable());
-        row.remove_link = $('<button/>', {
-            name: 'remove',
-            'class': 'btn btn-default',
-            title: text.get('@i18n:buttons.remove'),
-            html: text.get('@i18n:buttons.remove'),
-            click: function () {
-                that.remove_row(row);
-                return false;
+
+        if (!that.dropdown_menu) {
+            row.remove_link = $('<button/>', {
+                name: 'remove',
+                'class': 'btn btn-default',
+                title: text.get('@i18n:buttons.remove'),
+                html: text.get('@i18n:buttons.remove'),
+                click: function () {
+                    remove_row();
+                    return false;
+                }
+            });
+
+            if (row.widget.input_group_btn) {
+                // A little hack to make delete button part of row widget
+                row.remove_link.appendTo(row.widget.input_group_btn);
+            } else {
+                row.remove_link.appendTo(row.container);
             }
-        });
-
-        if (row.widget.input_group_btn) {
-            // A little hack to make delete button part of row widget
-            row.remove_link.appendTo(row.widget.input_group_btn);
         } else {
-            row.remove_link.appendTo(row.container);
+            row.remove_link = {
+                name: 'remove',
+                label: text.get('@i18n:buttons.remove'),
+                handler: remove_row
+            };
+
+            row.widget.get_dropdown_menu().items.push(row.remove_link);
+            row.widget.get_dropdown_menu().render();
         }
 
         if (row.is_new) {
@@ -1202,10 +1219,23 @@ IPA.multivalued_widget = function(spec) {
 
     that.toggle_remove_link = function(row, show) {
         if (show) {
-            row.remove_link.show();
+            if (that.dropdown_menu) {
+                row.widget.get_dropdown_menu().enable_item('remove');
+                row.widget.get_dropdown_menu().render();
+            }
+            else {
+                row.remove_link.show();
+            }
         } else {
-            row.remove_link.hide();
+            if (that.dropdown_menu) {
+                row.widget.get_dropdown_menu().disable_item('remove');
+                row.widget.get_dropdown_menu().render();
+            }
+            else {
+                row.remove_link.hide();
+            }
         }
+
         if (row.widget.update_input_group_state) {
             row.widget.update_input_group_state();
         }
-- 
2.5.5

From ce67e7eaff751a540a9cf900c7e12abd738e1fab Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 10:27:10 +0200
Subject: [PATCH 02/13] Extends functionality of DropdownWidget

Adds methods which are able to enable and disable options according to the name of option.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/widgets/DropdownWidget.js | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/install/ui/src/freeipa/widgets/DropdownWidget.js b/install/ui/src/freeipa/widgets/DropdownWidget.js
index 1f925a80a09782274226e7faf32c0c370fd32e04..c927da0b77f70b8948ec3f66e34eaa03033918b9 100644
--- a/install/ui/src/freeipa/widgets/DropdownWidget.js
+++ b/install/ui/src/freeipa/widgets/DropdownWidget.js
@@ -142,6 +142,25 @@ define(['dojo/_base/declare',
             return this.dom_node;
         },
 
+        disable_item: function(item_name) {
+            var item = this._find_item(item_name);
+            if (item) item.disabled = true;
+        },
+
+        enable_item: function(item_name) {
+            var item = this._find_item(item_name);
+            if (item) item.disabled = false;
+        },
+
+        _find_item: function(item_name) {
+            for (var i=0; i<this.items.length; i++) {
+                if (this.items[i].name && this.items[i].name == item_name) {
+                    return this.items[i];
+                }
+            }
+            return null;
+        },
+
         _render_toggle: function(container) {
 
             this.toggle_node = construct.create('a', {
-- 
2.5.5

From 5f350a1a90363aca4181b4d3b6f1153faa0f9b29 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 10:31:07 +0200
Subject: [PATCH 03/13] Add working widget

This widget can be used as notification that some other widget is working.
It shows spinner and cover the other widget by specified color.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/less/widgets.less     |  14 +++++
 install/ui/src/freeipa/widget.js | 111 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 125 insertions(+)

diff --git a/install/ui/less/widgets.less b/install/ui/less/widgets.less
index 0f9bc8c171d5c35d955c6b55d2e95ee105c35159..bcc12ac56f19af870168b6bf34ab4c6f4ccfe6be 100644
--- a/install/ui/less/widgets.less
+++ b/install/ui/less/widgets.less
@@ -143,6 +143,20 @@
     }
 }
 
+// Working widget
+
+.working-widget {
+    position: absolute;
+    display: none;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    .spinner {
+        align-self: center;
+    }
+}
+
+
 // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=409254
 tbody:empty { display: none; }
 
diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index 8f7a4413273d1471964fd4a08d2e4c7c944fb811..6531e52350dd69f509e4ebfbd3761038fd06b147 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -325,6 +325,117 @@ IPA.widget = function(spec) {
 };
 
 /**
+ * Working widget which contains spinner and can be used while some other
+ * widget is working.
+ *
+ * @class
+ * @param {Object} spec
+ */
+IPA.working_widget = function(spec) {
+
+    spec = spec || {};
+
+    var that = IPA.widget();
+
+    /**
+     * @property {string} name
+     */
+    that.name = spec.name;
+
+    /**
+     * @property {Object} facet
+     */
+    that.facet = spec.facet;
+
+    /**
+     * @property {Object} entity
+     */
+    that.entity = spec.entity;
+
+    /**
+     * Classes which will be added to the whole widget.
+     *
+     * @property {string} class
+     */
+    that.base_cls = spec.base_cls || 'working-widget';
+
+    /**
+     * @property {string} name
+     */
+    that.other_cls = spec.other_cls || '';
+
+    /**
+     * Patternfly class name which defines size of spinner. Possible values:
+     * ''|'spinner-lg'|'spinner-sm'|'spinner-xs'
+     *
+     * @property {string} class_name
+     */
+    that.spinner_size_cls = spec.spinner_size_cls || 'spinner-sm';
+
+    /**
+     * Color of background of the widget. Defined by array
+     * [RRR,GGG,BBB,opacity] where RRR,GGG,BBB are integers from 0-255.
+     * The opaciti value is optional.
+     *
+     * @property {Array} color definition
+     */
+    that.bg_color = spec.bg_color || [255,255,255,0.7];
+
+    /**
+     * Z-index of this widget.
+     *
+     * @property {number}
+     */
+    that.z_index = spec.z_index || 99;
+
+    that._normalize_bg_color_css = function() {
+        var property = 'background: ';
+        var basic = '#';
+        var older = 'rgb(';
+        var modern = 'rgba(';
+        var r = that.bg_color[0];
+        var g = that.bg_color[1];
+        var b = that.bg_color[2];
+        var op = that.bg_color[3];
+
+        // handle situations where opacity is not set.
+        op = op || 1;
+
+        basic = basic + parseInt(r, 16) + parseInt(g, 16) + parseInt(b, 16) + ';';
+        older = older + r + ', ' + g + ', ' + b + ');';
+        modern = modern + r + ', ' + g + ', ' + b + ', ' + op + ');';
+        return property + basic + property + older + property + modern;
+    };
+
+    // display:flex is not supported in < IE9
+    that.create = function(container) {
+        var bg_color = that._normalize_bg_color_css();
+
+        that.spin_div = $('<div />', {
+            'class': that.base_cls + ' ' + that.other_cls,
+            style: bg_color + 'z-index: ' + that.z_index + ';'
+        });
+        that.spinner = $('<div />', {
+            'class': 'spinner ' + that.spinner_size_cls
+        }).appendTo(that.spin_div);
+
+        that.spin_div.appendTo(container);
+
+        that.on('hide-spinner', function() {
+            that.spin_div.fadeOut();
+        });
+
+        that.on('display-spinner', function() {
+            that.spin_div.fadeIn();
+            that.spin_div.css('display', 'flex');
+            that.spin_div.css('display', '-webkit-flex');
+        });
+    };
+
+    return that;
+};
+
+/**
  * Base class for input gathering widgets.
  * @class
  * @extends IPA.widget
-- 
2.5.5

From 83e7502ce3cecab674f659eee86578564e4a70c6 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 10:34:06 +0200
Subject: [PATCH 04/13] Add ability to turn off activity icon

By specifying correct attribute when creating command it turn off showing activity icon
when webui waits for response from the server.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/rpc.js | 46 +++++++++++++++++++++++++++++++++++++++----
 1 file changed, 42 insertions(+), 4 deletions(-)

diff --git a/install/ui/src/freeipa/rpc.js b/install/ui/src/freeipa/rpc.js
index 9e684a7daccfc4cd327d256844be6dbf1efe6533..a185585f4176658e299e7e92434522c936cc36b4 100644
--- a/install/ui/src/freeipa/rpc.js
+++ b/install/ui/src/freeipa/rpc.js
@@ -96,6 +96,29 @@ rpc.command = function(spec) {
      */
     that.retry = typeof spec.retry == 'undefined' ? true : spec.retry;
 
+    /**
+     * Allow turning off the activity icon.
+     *
+     * @property {Boolean} show=true
+     */
+    that.hide_activity_icon = spec.hide_activity_icon || false;
+
+    /**
+     * Allow set function which will be called when the activity of the command
+     * starts. Works only when 'activity_icon' property is set to false
+     *
+     * @property {Function}
+     */
+    that.notify_activity_start = spec.notify_activity_start || null;
+
+    /**
+     * Allow set function which will be called when the activity of the command
+     * ends. Works only when 'activity_icon' property is set to false
+     *
+     * @property {Function}
+     */
+    that.notify_activity_end = spec.notify_activity_end || null;
+
     /** @property {string} error_message Default error message */
     that.error_message = text.get(spec.error_message || '@i18n:dialogs.batch_error_message', 'Some operations failed.');
 
@@ -198,6 +221,15 @@ rpc.command = function(spec) {
         }
     };
 
+    that.handle_notify_execution_end = function() {
+        if (that.hide_activity_icon) {
+            if (that.notify_activity_end) that.notify_activity_end();
+        }
+        else {
+            IPA.hide_activity_icon();
+        }
+    };
+
     /**
      * Execute the command.
      *
@@ -280,7 +312,7 @@ rpc.command = function(spec) {
          */
         function error_handler(xhr, text_status, error_thrown) {
 
-            IPA.hide_activity_icon();
+            that.handle_notify_execution_end();
 
             if (xhr.status === 401) {
                 error_handler_auth(xhr, text_status, error_thrown);
@@ -360,7 +392,7 @@ rpc.command = function(spec) {
                 });
 
             } else {
-                IPA.hide_activity_icon();
+                that.handle_notify_execution_end();
 
                 var ajax = this;
                 var failed = that.get_failed(that, data.result, text_status, xhr);
@@ -413,7 +445,13 @@ rpc.command = function(spec) {
             error: error_handler_login
         };
 
-        IPA.display_activity_icon();
+        if (that.hide_activity_icon) {
+            if (that.notify_activity_start) that.notify_activity_start();
+        }
+        else {
+            IPA.display_activity_icon();
+        }
+
         $.ajax(that.request);
         return deferred.promise;
     };
@@ -1005,4 +1043,4 @@ rpc.extract_objects = function(values) {
 };
 
 return rpc;
-});
\ No newline at end of file
+});
-- 
2.5.5

From b7619219a1165a59e21462aab62665783a6aab59 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 10:45:49 +0200
Subject: [PATCH 05/13] Add Object adapter

Object adapter changes data to more useful format. Single value is reachable
as single value, property with more values is transformed to array.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/field.js | 41 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index fdf925e3d7adc62622e0c9e5e1440eefbdac807b..1f65098355adaee8e332d9f95aebb301d96d0ee1 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -1259,6 +1259,46 @@ field.SshKeysAdapter = declare([field.Adapter], {
     }
 });
 
+
+/**
+ * ObjectAdapter is basic adapter which converts object to more usable format.
+ * All properties which has only one value are tranformed this way:
+ *  property1: {"__base64__": "value1"} => property1: "value1",
+ *  property2: {"value2"} => property2: "value2",
+ * Works for __base64__ as well as for __datetime__ and __dns_name__
+ *
+ * In case that the property has more values, then they are returned as array.
+ *
+ * @class
+ * @extends field.Adapter
+ */
+field.ObjectAdapter = declare([field.Adapter], {
+
+    normalize_object: function(obj) {
+        for (var property in obj) {
+            if (obj.hasOwnProperty(property)) {
+                obj[property] = rpc.extract_objects([obj[property]]);
+                if (obj[property].length == 1) {
+                    obj[property] = obj[property][0];
+                }
+            }
+        }
+    },
+
+    load: function(data) {
+
+        var record = this.get_record(data);
+
+        for (var i=0; i<record.length; i++) {
+            var sn = record[i].serial_number;
+            this.normalize_object(record[i]);
+        }
+
+        return record;
+    }
+});
+
+
 /**
  * Field for enabling/disabling entity
  *
@@ -1529,6 +1569,7 @@ field.register = function() {
     v.register('same_password', field.same_password_validator);
 
     l.register('adapter', field.Adapter);
+    l.register('object_adapter', field.ObjectAdapter);
 };
 phases.on('registration', field.register);
 
-- 
2.5.5

From 7861fda13b8c1afac3cca3dbeb9bf8b49cbd572c Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 10:50:38 +0200
Subject: [PATCH 06/13] Refactored certificate view and restore dialog

Removed old layout created using tables. Now table layout is made by div
and modern css styling.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/ipa.css                    |  19 +++
 install/ui/src/freeipa/certificate.js | 227 ++++++++++++++++++----------------
 install/ui/test/data/ipa_init.json    |   1 +
 ipalib/plugins/internal.py            |   1 +
 4 files changed, 140 insertions(+), 108 deletions(-)

diff --git a/install/ui/ipa.css b/install/ui/ipa.css
index f419eb224252aa03eaf4b25bb03435f4c9a6de9b..982fb7579536539a327c7a0e9e110a0fa82fdc3e 100644
--- a/install/ui/ipa.css
+++ b/install/ui/ipa.css
@@ -555,3 +555,22 @@ table.scrollable tbody {
     font-weight: bold;
     font-size: 1.1em;
 }
+
+/* --- Table layout created by CSS --- */
+.table-layout {
+    display: cert-table;
+}
+.table-row {
+    display: table-row;
+}
+.table-row .table-cell {
+    display: table-cell;
+    vertical-align: middle;
+}
+.table-row .table-cell.table-head {
+    padding: 0 5px 0 0;
+}
+
+.break-words {
+    word-break: break-all;
+}
diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index ae05ebb3d45974cd1df50c16e19d0ab9fd27a19b..65ecbfc92fb1cd3675dd1b05f75cc4097cb47737 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -234,22 +234,35 @@ IPA.cert.revoke_dialog = function(spec) {
 
     that.create_content = function() {
 
-        var table = $('<table/>').appendTo(that.container);
+        var create_layout = function() {
+            return $('<div />', {
+                'class': 'table-layout'
+            });
+        };
 
-        var tr = $('<tr/>').appendTo(table);
+        var create_row = function() {
+            return $('<div />', {
+                'class': 'table-row'
+            });
+        };
 
-        var td = $('<td/>').appendTo(tr);
-        td.append(text.get('@i18n:objects.cert.note')+':');
+        var create_cell = function(str, suffix) {
+            suffix = suffix || '';
+            return $('<div />', {
+                'class': 'table-cell table-head',
+                text: text.get(str) + suffix
+            });
+        };
 
-        td = $('<td/>').appendTo(tr);
-        td.append(text.get('@i18n:objects.cert.revoke_confirmation'));
+        var table = create_layout().appendTo(that.container);
 
-        tr = $('<tr/>').appendTo(table);
+        var tr = create_row().appendTo(table);
+        var td = create_cell('@i18n:objects.cert.note', ':').appendTo(tr);
+        td = create_cell('@i18n:objects.cert.revoke_confirmation').appendTo(tr);
 
-        td = $('<td/>').appendTo(tr);
-        td.append(text.get('@i18n:objects.cert.reason')+':');
-
-        td = $('<td/>').appendTo(tr);
+        tr = create_row().appendTo(table);
+        td = create_cell('@i18n:objects.cert.reason', ':').appendTo(tr);
+        td = create_cell().appendTo(tr);
 
         that.select = $('<select/>').appendTo(td);
         for (var i=0; i<IPA.cert.CRL_REASON.length; i++) {
@@ -282,6 +295,7 @@ IPA.cert.view_dialog = function(spec) {
     that.expires_on = spec.certificate.valid_not_after || '';
     that.md5_fingerprint = spec.certificate.md5_fingerprint || '';
     that.sha1_fingerprint = spec.certificate.sha1_fingerprint || '';
+    that.sha256_fingerprint = spec.certificate.sha256_fingerprint || '';
 
     that.create_button({
         name: 'close',
@@ -293,103 +307,100 @@ IPA.cert.view_dialog = function(spec) {
 
     that.create_content = function() {
 
-        var table = $('<table/>').appendTo(that.container);
-
-        var tr = $('<tr/>').appendTo(table);
-        $('<td/>', {
-            'colspan': 2,
-            'html': '<h3>'+text.get('@i18n:objects.cert.issued_to')+'</h3>'
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.common_name')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.subject.cn
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.organization')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.subject.o
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.organizational_unit')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.subject.ou
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.serial_number')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.serial_number
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.serial_number_hex')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.serial_number_hex
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td/>', {
-            'colspan': 2,
-            'html': '<h3>'+text.get('@i18n:objects.cert.issued_by')+'</h3>'
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.common_name')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.issuer.cn
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.organization')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.issuer.o
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.organizational_unit')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.issuer.ou
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td/>', {
-            'colspan': 2,
-            'html': '<h3>'+text.get('@i18n:objects.cert.validity')+'</h3>'
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.issued_on')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.issued_on
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.expires_on')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.expires_on
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td/>', {
-            'colspan': 2,
-            'html': '<h3>'+text.get('@i18n:objects.cert.fingerprints')+'</h3>'
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.sha1_fingerprint')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.sha1_fingerprint
-        }).appendTo(tr);
-
-        tr = $('<tr/>').appendTo(table);
-        $('<td>'+text.get('@i18n:objects.cert.md5_fingerprint')+':</td>').appendTo(tr);
-        $('<td/>', {
-            text: that.md5_fingerprint
-        }).appendTo(tr);
+        var create_title = function(str) {
+            return $('<h3 />', {
+                text: text.get(str)
+            });
+        };
+
+        var create_layout = function() {
+            return $('<div />', {
+                'class': 'table-layout'
+            });
+        };
+
+        var create_row = function() {
+            return $('<div />', {
+                'class': 'table-row'
+            });
+        };
+
+        var create_cell = function(str, suffix, cls) {
+            suffix = suffix || '';
+
+            return $('<div />', {
+                'class': 'table-cell table-head ' + cls,
+                text: text.get(str) + suffix
+            });
+        };
+
+        create_title('@i18n:objects.cert.issued_to').appendTo(that.container);
+
+        var table_layout = create_layout().appendTo(that.container);
+
+        var row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.common_name', ':'));
+        row.append(create_cell(that.subject.cn, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.organization', ':'));
+        row.append(create_cell(that.subject.o, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.organizational_unit', ':'));
+        row.append(create_cell(that.subject.ou, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.serial_number', ':'));
+        row.append(create_cell(that.serial_number.toString(), '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.serial_number_hex', ':'));
+        row.append(create_cell(that.serial_number_hex, '', 'break-words'));
+
+        create_title('@i18n:objects.cert.issued_by').appendTo(that.container);
+
+        table_layout = create_layout().appendTo(that.container);
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.common_name', ':'));
+        row.append(create_cell(that.issuer.cn, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.organization', ':'));
+        row.append(create_cell(that.issuer.o, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.organizational_unit', ':'));
+        row.append(create_cell(that.issuer.ou, '', 'break-words'));
+
+        create_title('@i18n:objects.cert.validity').appendTo(that.container);
+
+        table_layout = create_layout().appendTo(that.container);
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.issued_on', ':'));
+        row.append(create_cell(that.issued_on, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.expires_on', ':'));
+        row.append(create_cell(that.expires_on, '', 'break-words'));
+
+        create_title('@i18n:objects.cert.fingerprints').appendTo(that.container);
+
+        table_layout = create_layout().appendTo(that.container);
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.md5_fingerprint', ':'));
+        row.append(create_cell(that.md5_fingerprint, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.sha1_fingerprint', ':'));
+        row.append(create_cell(that.sha1_fingerprint, '', 'break-words'));
+
+        row = create_row().appendTo(table_layout);
+        row.append(create_cell('@i18n:objects.cert.sha256_fingerprint', ':'));
+        row.append(create_cell(that.sha256_fingerprint, '', 'break-words'));
     };
 
     return that;
diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json
index 1b9b69ff909a9668c1e1867008459d25d5e062a9..87d7a1961a3a7bfc014f9c91eb57376b87a07af4 100644
--- a/install/ui/test/data/ipa_init.json
+++ b/install/ui/test/data/ipa_init.json
@@ -275,6 +275,7 @@
                             "serial_number": "Serial Number",
                             "serial_number_hex": "Serial Number (hex)",
                             "sha1_fingerprint": "SHA1 Fingerprint",
+                            "sha256_fingerprint": "SHA256 Fingerprint",
                             "status": "Status",
                             "superseded": "Superseded",
                             "unspecified": "Unspecified",
diff --git a/ipalib/plugins/internal.py b/ipalib/plugins/internal.py
index 54871f76de99d92f0f23129b4d636cc4fccfbb8b..35b0ab8fc5ae1dc4311907745c25471acee3df20 100644
--- a/ipalib/plugins/internal.py
+++ b/ipalib/plugins/internal.py
@@ -419,6 +419,7 @@ class i18n_messages(Command):
                 "serial_number": _("Serial Number"),
                 "serial_number_hex": _("Serial Number (hex)"),
                 "sha1_fingerprint": _("SHA1 Fingerprint"),
+                "sha256_fingerprint": _("SHA256 Fingerprint"),
                 "status": _("Status"),
                 "superseded": _("Superseded"),
                 "unspecified": _("Unspecified"),
-- 
2.5.5

From ee63a25b2a177edbf95ee201b29f2bda1c9458fc Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 11:13:06 +0200
Subject: [PATCH 07/13] Changed the way how to handle restore and revoke
 actions

Method calling is moved to another function - these calls may be used
by another functions, not only by actions.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/certificate.js | 62 +++++++++++++++++++++++++----------
 1 file changed, 45 insertions(+), 17 deletions(-)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 65ecbfc92fb1cd3675dd1b05f75cc4097cb47737..24bc9ee7e631d03b69b7eb2e5fddd10051489ecf 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -766,6 +766,25 @@ IPA.cert.request_action = function(spec) {
     return that;
 };
 
+IPA.cert.perform_revoke = function(spec, certificate, dialog) {
+
+    spec.hide_activity_icon = spec.hide_activity_icon || false;
+
+    rpc.command({
+        entity: 'cert',
+        method: 'revoke',
+        hide_activity_icon: spec.hide_activity_icon,
+        args: [certificate.serial_number],
+        options: {
+            'revocation_reason': dialog.get_reason()
+        },
+        notify_activity_start: spec.notify_activity_start,
+        notify_activity_end: spec.notify_activity_end,
+        on_success: spec.on_success,
+        on_error: spec.on_error
+    }).execute();
+};
+
 IPA.cert.revoke_action = function(spec) {
 
     spec = spec || {};
@@ -801,26 +820,37 @@ IPA.cert.revoke_action = function(spec) {
 
     that.execute_action = function(facet) {
 
-        var certificate = facet.certificate;
-
-        rpc.command({
-            entity: 'cert',
-            method: 'revoke',
-            args: [certificate.serial_number],
-            options: {
-                'revocation_reason': that.dialog.get_reason()
-            },
+        var spec = {
             on_success: function(data, text_status, xhr) {
                 facet.refresh();
                 IPA.notify_success('@i18n:objects.cert.revoked');
                 facet.certificate_updated.notify([], that.facet);
             }
-        }).execute();
+        };
+
+        IPA.cert.perform_revoke(spec, facet.certificate, that.dialog);
     };
 
     return that;
 };
 
+IPA.cert.perform_restore = function(spec, certificate) {
+
+    spec.hide_activity_icon = spec.hide_activity_icon || false;
+
+    rpc.command({
+        entity: 'cert',
+        method: 'remove_hold',
+        hide_activity_icon: spec.hide_activity_icon,
+        args: [certificate.serial_number],
+        notify_activity_start: spec.notify_activity_start,
+        notify_activity_end: spec.notify_activity_end,
+        on_success: spec.on_success,
+        on_error: spec.on_error
+    }).execute();
+};
+
+
 IPA.cert.restore_action = function(spec) {
 
     spec = spec || {};
@@ -858,18 +888,16 @@ IPA.cert.restore_action = function(spec) {
 
     that.execute_action = function(facet) {
 
-        var certificate = facet.certificate;
-
-        rpc.command({
-            entity: 'cert',
-            method: 'remove_hold',
-            args: [certificate.serial_number],
+        var spec = {
             on_success: function(data, text_status, xhr) {
                 facet.refresh();
                 IPA.notify_success('@i18n:objects.cert.restored');
                 facet.certificate_updated.notify([], that.facet);
             }
-        }).execute();
+        };
+
+        IPA.cert.perform_restore(spec, facet.certificate);
+
     };
 
     return that;
-- 
2.5.5

From 528a5cad6b89d531409c9c0d54673810cfcf3f0f Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 12:12:01 +0200
Subject: [PATCH 08/13] Remove old useless actions - get and view

These two actions are not available any more. So that code is never called.

https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/certificate.js | 68 -----------------------------------
 1 file changed, 68 deletions(-)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 24bc9ee7e631d03b69b7eb2e5fddd10051489ecf..c3e093ab6face8490a33a51e0a091d4f9eb66125 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -626,72 +626,6 @@ IPA.cert.is_enabled = function() {
     return !!IPA.ca_enabled;
 };
 
-IPA.cert.view_action = function(spec) {
-
-    spec = spec || {};
-    spec.name = spec.name || 'view_cert';
-    spec.label = spec.label || '@i18n:objects.cert.view_certificate_btn';
-    spec.enable_cond = spec.enable_cond || ['has_certificate'];
-
-    var that = IPA.action(spec);
-    that.entity_label = spec.entity_label;
-
-    that.execute_action = function(facet) {
-
-        var certificate = facet.certificate;
-        if (!certificate) that.facet.refresh();
-
-        var entity_label = that.entity_label || facet.entity.metadata.label_singular;
-        var entity_name = certificate.entity_info.name;
-
-        var title = text.get('@i18n:objects.cert.view_certificate');
-        title = title.replace('${entity}', entity_label);
-        title = title.replace('${primary_key}', entity_name);
-
-        var dialog = IPA.cert.view_dialog({
-            title: title,
-            certificate: certificate
-        });
-
-        dialog.open();
-    };
-
-    return that;
-};
-
-IPA.cert.get_action = function(spec) {
-
-    spec = spec || {};
-    spec.name = spec.name || 'get_cert';
-    spec.label = spec.label || '@i18n:objects.cert.get_certificate';
-    spec.enable_cond = spec.enable_cond || ['has_certificate'];
-
-    var that = IPA.action(spec);
-    that.entity_label = spec.entity_label;
-
-    that.execute_action = function(facet) {
-
-        var certificate = facet.certificate;
-        if (!certificate) that.facet.refresh();
-
-        var entity_label = that.entity_label || facet.entity.metadata.label_singular;
-        var entity_name = certificate.entity_info.name;
-
-        var title = text.get('@i18n:objects.cert.view_certificate');
-        title = title.replace('${entity}', entity_label);
-        title = title.replace('${primary_key}', entity_name);
-
-        var dialog = IPA.cert.download_dialog({
-            title: title,
-            certificate: certificate.certificate
-        });
-
-        dialog.open();
-    };
-
-    return that;
-};
-
 IPA.cert.request_action = function(spec) {
 
     spec = spec || {};
@@ -1564,8 +1498,6 @@ exp.register = function() {
     f.register('revocation_reason', IPA.revocation_reason_field);
     w.register('revocation_reason', IPA.text_widget);
 
-    a.register('cert_view', IPA.cert.view_action);
-    a.register('cert_get', IPA.cert.get_action);
     a.register('cert_request', IPA.cert.request_action);
     a.register('cert_revoke', IPA.cert.revoke_action);
     a.register('cert_restore', IPA.cert.restore_action);
-- 
2.5.5

From 64d5b38ef0082ea4e3a6f167378acf4f6d496c3a Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 26 Apr 2016 08:44:03 +0200
Subject: [PATCH 09/13] Add widget for showing multiple certificates

Certs widget is based on multivalued widget and adds ability to add new certificate
and delete it. Each line is cert_widget.

https://fedorahosted.org/freeipa/ticket/5108
https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/certificate.js | 233 +++++++++++++++++++++++++++++-----
 install/ui/src/freeipa/field.js       |  19 +++
 2 files changed, 218 insertions(+), 34 deletions(-)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index c3e093ab6face8490a33a51e0a091d4f9eb66125..78d0d830421329d58ff51d813c122d60f14be8f6 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -111,6 +111,22 @@ IPA.cert.parse_dn = function(dn) {
     return result;
 };
 
+IPA.cert.get_base64 = function(text) {
+    /*
+     * Input is assumed to be base64 or PEM formatted certificate.
+     * The function just cuts the '-----BEGIN CERTIFICATE----' and
+     * '-----END CERTIFICATE-----' strings if they are present.
+     * Returns only base64 blob.
+     */
+
+    var match = IPA.cert.PEM_CERT_REGEXP.exec(text);
+
+    if (match) {
+        return $.trim(match[2]);
+    }
+    return $.trim(text);
+};
+
 IPA.cert.pem_format_base64 = function(text) {
     /*
      * Input is assumed to be base64 possibly with embedded whitespace.
@@ -993,57 +1009,207 @@ IPA.cert.status_field = function(spec) {
     return that;
 };
 
-IPA.cert.cert_widget = function(spec) {
+
+/**
+ * Certificates widget
+ *
+ * Multivalued widget with certificate widget instead of text widget.
+ *
+ * @class
+ * @extends IPA.multivalued_widget
+ */
+IPA.cert.certs_widget = function(spec) {
 
     spec = spec || {};
-    spec.css_class = spec.css_class || 'certificate-widget';
+    spec.child_spec = {
+        $factory: IPA.cert.cert_widget,
+        css_class: 'certificate-widget',
+        facet: spec.facet
+    };
 
-    var that = IPA.input_widget(spec);
-    that.certs_visible = false;
+    var that = IPA.multivalued_widget(spec);
 
     that.create = function(container) {
 
         that.widget_create(container);
-        that.content_el = $('<div>').appendTo(container);
+
+        that.create_error_link(container);
+
+        that.add_link = $('<button/>', {
+            name: 'add',
+            'class': 'btn btn-default',
+            title: text.get('@i18n:buttons.add'),
+            html: text.get('@i18n:buttons.add'),
+            click: function() {
+                that.open_addcert_dialog();
+                return false;
+            }
+        }).appendTo(container);
     };
 
-    that.create_status = function(name, text, icon) {
 
-        var status = $('<label/>', {
-            'class': 'certificate-status'
-        });
+    that.remove_row = function(row) {
+
+        var perform_remove = function() {
+            that.perform_remove(row);
+        };
+
+        var sn = row.widget.certificate.serial_number;
+        var title = row.widget.compose_dialog_title();
+        var message = text.get('@i18n:actions.delete_confirm');
+        message = message.replace('${object}',
+            'the certificate with serial number ' + sn);
+
+        var spec = {
+            title: title,
+            message: message,
+            on_ok: perform_remove
+        };
+
+        that.create_confirmation_dialog(spec);
+    };
+
+    that.create_confirmation_dialog = function(spec) {
+        spec = spec || {};
+        var confirm_dialog = IPA.confirm_dialog(spec);
+
+        if (that.facet.is_dirty()) {
+            var dialog = IPA.dirty_dialog({
+                facet: that.facet
+            });
+
+            dialog.callback = function() {
+                confirm_dialog.open();
+            };
+            dialog.open();
+
+        } else {
+            confirm_dialog.open();
+        }
+    };
 
-        $('<i/>', {
-            'class': icon
-        }).appendTo(status);
+    that.perform_remove = function(row) {
+        var facet = that.facet;
+        var pkey = facet.get_pkey();
+        var blob = row.widget.save();
 
-        status.append(" " + text);
+        var command = rpc.command({
+            entity: facet.entity.name,
+            method: 'remove_cert',
+            args: [pkey],
+            options: {
+                usercertificate: blob
+            },
+            on_success: function(data) {
+                facet.refresh();
+                IPA.notify_success(data.result.summary);
+            },
+            on_error: function(data) {
+                IPA.notify(data.result.summary, 'error');
+            }
+        });
 
-        return status;
+        command.execute();
     };
 
-    that.create_certs = function() {
+    that.add = function(dialog) {
+        var value = dialog.get_field('new_cert').get_value();
+        value = IPA.cert.get_base64(value);
 
-        that.content_el.empty();
-        var l = that.certificates.length;
+        var facet = that.facet;
+        var pkey = facet.get_pkey();
 
-        if (l && that.certs_visible) {
-            for (var i=0; i<l; i++) {
-                $('<div/>', {
-                    'class': 'certificate',
-                    text: that.certificates[i]
-                }).appendTo(that.content_el);
+        var command = rpc.command({
+            entity: facet.entity.name,
+            method: 'add_cert',
+            args: [pkey],
+            options: {
+                usercertificate: value
+            },
+            on_success: function(data) {
+                facet.refresh();
+                IPA.notify_success(data.result.summary);
+                dialog.close();
+            },
+            on_error: function(data) {
+                dialog.focus_first_element();
             }
-            $('<div/>').append(
-                IPA.button({
-                    name: 'hide',
-                    label: '@i18n:buttons.hide',
-                    click: function() {
-                        that.certs_visible = false;
-                        that.create_certs();
+        });
+
+        command.execute();
+    };
+
+    that.open_addcert_dialog = function() {
+        var add_dialog = that.create_addcert_dialog();
+
+        if (that.facet.is_dirty()) {
+            var dialog = IPA.dirty_dialog({
+                facet: that.facet
+            });
+
+            dialog.callback = function() {
+                add_dialog.open();
+            };
+            dialog.open();
+
+        } else {
+            add_dialog.open();
+        }
+
+    };
+
+    that.create_addcert_dialog = function() {
+        var dialog = IPA.dialog({
+            name: 'cert-add-dialog',
+            title: '@i18n:objects.cert.new_certificate',
+            sections: [
+                {
+                    show_header: false,
+                    fields: [
+                        {
+                            $type: 'textarea',
+                            name: 'new_cert',
+                            label: '@i18n:objects.cert.new_cert_format',
+                            required: true,
+                            rows: 15
+                        }
+                    ],
+                    layout:
+                    {
+                        $factory: widget_mod.fluid_layout,
+                        widget_cls: 'col-sm-12',
+                        label_cls: 'col-sm-6 control-label'
                     }
-                })).
-            appendTo(that.content_el);
+                }
+            ]
+        });
+
+        dialog.create_button({
+            name: 'add',
+            label: '@i18n:buttons.add',
+            click: function() {
+                if (!dialog.validate()) {
+                    widget_mod.focus_invalid(dialog);
+                }
+                else {
+                    that.add(dialog);
+                }
+            }
+        });
+
+        dialog.create_button({
+            name: 'cancel',
+            label: '@i18n:buttons.cancel',
+            click: function() {
+                dialog.close();
+            }
+        });
+
+        return dialog;
+    };
+
+    return that;
+};
         }
 
         if (!l) {
@@ -1491,10 +1657,9 @@ exp.register = function() {
     var f = reg.field;
     var a = reg.action;
 
-    w.register('certificate', IPA.cert.cert_widget);
+    w.register('certs', IPA.cert.certs_widget);
     w.register('certificate_status', IPA.cert.status_widget);
     f.register('certificate_status', IPA.cert.status_field);
-
     f.register('revocation_reason', IPA.revocation_reason_field);
     w.register('revocation_reason', IPA.text_widget);
 
diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index 1f65098355adaee8e332d9f95aebb301d96d0ee1..e8c878cbccb9f27980501d799f06c383dda2148e 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -1261,6 +1261,24 @@ field.SshKeysAdapter = declare([field.Adapter], {
 
 
 /**
+ * Used along with cert widget
+ *
+ * @class
+ * @alternateClassName IPA.certs_field
+ * @extends IPA.field
+ */
+field.certs_field = IPA.certs_field = function(spec) {
+
+    spec = spec || {};
+
+    spec.adapter = spec.field_adapter || {};
+
+    var that = IPA.field(spec);
+
+    return that;
+};
+
+/**
  * ObjectAdapter is basic adapter which converts object to more usable format.
  * All properties which has only one value are tranformed this way:
  *  property1: {"__base64__": "value1"} => property1: "value1",
@@ -1547,6 +1565,7 @@ field.register = function() {
     var v = reg.validator;
     var l = reg.adapter;
 
+    f.register('certs', field.certs_field);
     f.register('checkbox', field.checkbox_field);
     f.register('checkboxes', field.field);
     f.register('combobox', field.field);
-- 
2.5.5

From 2a15e60eaa287eee7330a57bbd78926b370a0177 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 26 Apr 2016 12:28:45 +0200
Subject: [PATCH 10/13] Add certificate widget

The certificate widget is used for each certificate in certs_widget. It allows to
view, get, download, revoke and restore certificate.

https://fedorahosted.org/freeipa/ticket/5108
https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/less/widgets.less          |  17 ++
 install/ui/src/freeipa/certificate.js | 401 +++++++++++++++++++++++++++++++---
 install/ui/test/data/ipa_init.json    |   4 +
 ipalib/plugins/internal.py            |   4 +
 4 files changed, 397 insertions(+), 29 deletions(-)

diff --git a/install/ui/less/widgets.less b/install/ui/less/widgets.less
index bcc12ac56f19af870168b6bf34ab4c6f4ccfe6be..4190b6b57c22260cb2e185f0262d14aa5775df8d 100644
--- a/install/ui/less/widgets.less
+++ b/install/ui/less/widgets.less
@@ -134,13 +134,30 @@
 // Certificate Widget
 
 .certificate-widget {
+    padding: 0;
     label {
         padding-right: 10px;
     }
+    .cert-value {
+        padding-left: 5px;
+    }
     .certificate {
         word-wrap: break-word;
         padding-bottom: 10px;
     }
+    .dropdown {
+        float:right;
+    }
+    ul.dropdown-menu {
+        min-width: 100px;
+    }
+    .watermark {
+        position: absolute;
+        bottom: 0;
+        right: 0;
+        opacity:0.2;
+        font-size: 150%;
+    }
 }
 
 // Working widget
diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 0c6b61cc963f8329c16d8085684217727103a932..49ff850b348fced39cc09aa5072fd0d107c67209 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -21,7 +21,9 @@
 
 define([
     'dojo/_base/lang',
+    'dojo/on',
     './builder',
+    './datetime',
     './metadata',
     './ipa',
     './jquery',
@@ -31,10 +33,11 @@ define([
     './rpc',
     './text',
     './widget',
+    './widgets/DropdownWidget',
     './dialog'],
     function(
-        lang, builder, metadata_provider, IPA, $, menu,
-        phases, reg, rpc, text, widget_mod) {
+        lang, on, builder, datetime, metadata_provider, IPA, $, menu,
+        phases, reg, rpc, text, widget_mod, DropdownWidget) {
 
 var exp = IPA.cert = {};
 
@@ -1210,40 +1213,380 @@ IPA.cert.certs_widget = function(spec) {
 
     return that;
 };
-        }
-
-        if (!l) {
-            that.content_el.append(that.create_status(
-                'missing',
-                text.get('@i18n:objects.cert.missing'),
-                'fa fa-warning'));
-        }
-
-        if (l && !that.certs_visible) {
-
-            var msg = text.get('@i18n:objects.cert.present');
-            msg = msg.replace('${count}', l);
-            that.content_el.append(
-                that.create_status('present', msg, 'fa fa-check'));
-
-            IPA.button({
-                name: 'show',
-                label: '@i18n:buttons.show',
-                click: function() {
-                    that.certs_visible = true;
-                    that.create_certs();
+
+/**
+ * certificate widget
+ *
+ * @class
+ * @extends IPA.input_widget
+ */
+IPA.cert.cert_widget = function(spec) {
+
+    spec = spec || {};
+
+    var that = IPA.input_widget(spec);
+
+    that.certificate = null;
+    that.ipa_issuer_subjbase = IPA.server_config.ipacertificatesubjectbase[0];
+    that.ipa_issuer_commonname = 'Certificate Authority';
+
+    that.create = function(container) {
+
+        that.widget_create(container);
+
+        that.container = container;
+        that.container.addClass('cert-container col-sm-12');
+
+        var spinner_spec = {
+            name: 'working-notification'
+        };
+
+        that.spinner = IPA.working_widget(spinner_spec);
+        that.spinner.create(that.container);
+
+        that.cert_subject = $('<div />', {
+            style: 'font-weight: bold;',
+            text: ''
+        }).appendTo(that.container);
+
+        that.table_layout = $('<div />', {
+            'class': 'table-layout'
+        }).appendTo(that.container);
+
+        var tr = $('<div />', {
+            'class': 'table-row'
+        }).appendTo(that.table_layout);
+        $('<div />', {
+            'class': 'table-cell table-head',
+            text: text.get('@i18n:objects.cert.serial_number') + ':'
+        }).appendTo(tr);
+        that.cert_sn = $('<div />', {
+            'class': 'table-cell cert-value',
+            text: ''
+        }).appendTo(tr);
+
+        tr = $('<div />', {
+            'class': 'table-row'
+        }).appendTo(that.table_layout);
+        $('<div />', {
+            'class': 'table-cell table-head',
+            text: text.get('@i18n:objects.cert.issued_by') + ':'
+        }).appendTo(tr);
+        that.cert_issuer = $('<div />', {
+            'class': 'table-cell cert-value',
+            text: ''
+        }).appendTo(tr);
+
+        tr = $('<div />', {
+            'class': 'table-row'
+        }).appendTo(that.table_layout);
+        $('<div />', {
+            'class': 'table-cell table-head',
+            text: text.get('Valid from') + ':'
+        }).appendTo(tr);
+        that.cert_valid_from = $('<div />', {
+            'class': 'table-cell cert-value',
+            text: ''
+        }).appendTo(tr);
+
+        tr = $('<div />', {
+            'class': 'table-row'
+        }).appendTo(that.table_layout);
+        $('<div />', {
+            'class': 'table-cell table-head',
+            text: text.get('Valid to') + ':'
+        }).appendTo(tr);
+        that.cert_valid_to = $('<div />', {
+            'class': 'table-cell cert-value',
+            text: ''
+        }).appendTo(tr);
+
+        that.dropdown = builder.build(null, {
+            $ctor: DropdownWidget,
+            toggle_text: text.get('@i18n:actions.title'),
+            toggle_class: 'btn btn-default dropdown-toggle',
+            toggle_icon: 'caret',
+            right_aligned: true,
+            name: 'cert-actions',
+            'class': 'dropdown cert-actions',
+            items: [
+                {
+                    name: 'view',
+                    label: text.get('@i18n:buttons.view'),
+                    handler: that.open_view_dialog
+                },
+                {
+                    name: 'get',
+                    label: text.get('@i18n:buttons.get'),
+                    handler: that.open_get_dialog
+                },
+                {
+                    name: that.data_url,
+                    label: text.get('@i18n:buttons.download'),
+                    handler: that.perform_download
+                },
+                {
+                    name: 'revoke',
+                    label: text.get('@i18n:buttons.revoke'),
+                    disabled: true,
+                    handler: that.open_revoke_dialog
+                },
+                {
+                    name: 'restore',
+                    label: text.get('@i18n:buttons.restore'),
+                    disabled: true,
+                    handler: that.perform_restore
                 }
-            }).appendTo(that.content_el);
+            ]
+        });
+
+        on(that.dropdown, 'item-click', function(item) {
+            if (!item.disabled && item.handler) {
+                item.handler();
+            }
+        });
+
+        that.container.append(that.dropdown.render());
+        that.table_layout.appendTo(that.container);
+
+        that.create_error_link(that.container);
+    };
+
+    that.create_data_uri = function() {
+        if (!that.certificate || !that.certificate.certificate) return;
+
+        var format = 'data:,';
+        var uri_new_line = '%0A';
+        var cert = that.certificate.certificate;
+
+        that.data_uri = IPA.cert.pem_format_base64(cert);
+        that.data_uri = IPA.cert.pem_cert_format(that.data_uri);
+        that.data_uri = format + that.data_uri.replace(/\n/g, uri_new_line);
+    };
+
+    that.get_dropdown_menu = function() {
+        return that.dropdown;
+    };
+
+    that.update_link = function(r) {
+
+        that.revoke_note = $('<div />', {
+            text: text.get('@i18n:objects.cert.revoked_status'),
+            style: 'display: none',
+            'class': 'watermark'
+        }).appendTo(that.container);
+
+        var cert = that.certificate;
+
+        if (cert.certificate) {
+            that.cert_subject.text(IPA.cert.parse_dn(cert.subject).cn);
+            that.cert_sn.text(cert.serial_number);
+            that.cert_issuer.text(IPA.cert.parse_dn(cert.issuer).cn);
+            that.cert_valid_from.text(cert.valid_not_before);
+            that.cert_valid_to.text(cert.valid_not_after);
         }
     };
 
+    that.toggle_revoked_note = function(show) {
+        if (show) {
+            that.revoke_note.css('display', 'block');
+        }
+        else {
+            that.revoke_note.css('display', 'none');
+        }
+    };
+
+    that.get_cert_revoke_status = function() {
+        // Double check whether the certificate is issued by our CA.
+        var issuer = 'CN=' + that.ipa_issuer_commonname + ',' +
+            that.ipa_issuer_subjbase;
+        if (that.certificate.issuer !== issuer) return;
+
+        rpc.command({
+            entity: 'cert',
+            method: 'show',
+            // turn off the activity widget for this command
+            hide_activity_icon: true,
+            args: [that.certificate.serial_number],
+            on_success: function(data, text_status, xhr) {
+                var result = data.result.result;
+                that.handle_revocation_reason(result.revocation_reason);
+            },
+            notify_activity_end: function() {
+                that.spinner.emit('hide-spinner');
+            },
+            notify_activity_start: function() {
+                that.spinner.emit('display-spinner');
+            }
+        }).execute();
+    };
+
+    that.handle_revocation_reason = function(reason) {
+        var dd_menu = that.get_dropdown_menu();
+
+        if (reason && reason === 6) {
+            that.dropdown.enable_item('restore');
+            that.dropdown.disable_item('revoke');
+            that.toggle_revoked_note(true);
+        }
+        else if (reason === null || reason === undefined) {
+            that.dropdown.enable_item('revoke');
+            that.dropdown.disable_item('restore');
+        }
+        else if (typeof reason === 'number' && reason >= 0 &&
+            reason < IPA.cert.CRL_REASON.length) {
+                that.dropdown.disable_item('revoke');
+                that.toggle_revoked_note(true);
+        }
+
+        that.dropdown.render();
+    };
+
     that.update = function(values) {
-        that.certificates = values;
-        that.create_certs();
+
+        var certificate = values[0];
+
+        if (!certificate.certificate || certificate.certificate === '') {
+            certificate = {};
+        }
+        else {
+            var sn_hex = '0x' + certificate.serial_number.toString(16);
+            certificate.serial_number_hex = sn_hex;
+
+            var nb = datetime.parse(certificate.valid_not_before);
+            var na = datetime.parse(certificate.valid_not_after);
+
+            // check whether time is already parser - used while reverting page
+            if (na) certificate.valid_not_after = na.toString();
+            if (nb) certificate.valid_not_before = nb.toString();
+        }
+
+        that.certificate = $.extend({}, certificate);
+
+        if (that.certificate && that.certificate.revoked !== 'undefined') {
+            that.get_cert_revoke_status();
+        }
+
+        that.create_data_uri();
+        that.update_link();
+    };
+
+    that.save = function() {
+        return that.certificate.certificate;
+    };
+
+    that.compose_dialog_title = function() {
+        var cert = that.certificate;
+        var cn, o;
+
+        if (cert.subject) {
+            cn = IPA.cert.parse_dn(cert.subject).cn;
+            o = IPA.cert.parse_dn(cert.subject).o;
+        }
+
+        var r = text.get('@i18n:objects.cert.view_certificate');
+        r = r.replace('${entity}', cn);
+        r = r.replace('${primary_key}', o);
+
+        return r;
+    };
+
+    that.open_view_dialog = function() {
+
+        var spec = {
+            title: that.compose_dialog_title(),
+            certificate: that.certificate
+        };
+
+        var dialog = IPA.cert.view_dialog(spec);
+        dialog.open();
+    };
+
+    that.open_get_dialog = function() {
+        var spec = {
+            title: that.compose_dialog_title(),
+            certificate: that.certificate.certificate
+        };
+
+        var dialog = IPA.cert.download_dialog(spec);
+        dialog.open();
+    };
+
+    that.perform_download = function() {
+        var a = document.createElement("a");
+        // Adding own click function as workaround for Firefox
+        a.click = function() {
+           var evt = this.ownerDocument.createEvent('MouseEvents');
+           evt.initMouseEvent('click', true, true, this.ownerDocument.defaultView,
+                1, 0, 0, 0, 0, false, false, false, false, 0, null);
+           this.dispatchEvent(evt);
+        };
+        a.download = 'cert.pem';
+        a.href = that.data_uri;
+
+        a.click();
     };
 
-    that.clear = function() {
-        that.content_el.empty();
+
+
+    that.open_revoke_dialog = function() {
+        var spec = {
+            title: that.compose_dialog_title(),
+            message: '@i18n:objects.cert.revoke_confirmation',
+            ok_label: '@i18n:buttons.revoke',
+            on_ok: function() {
+
+                var command_spec = {
+                    hide_activity_icon: true,
+                    notify_activity_end: function() {
+                        that.spinner.emit('hide-spinner');
+                    },
+                    notify_activity_start: function() {
+                        that.spinner.emit('display-spinner');
+                    },
+                    on_success: function() {
+                        var reason = parseInt(dialog.get_reason(), 10);
+                        that.handle_revocation_reason(reason);
+                        that.facet.on_update.notify();
+                        IPA.notify_success('@i18n:objects.cert.revoked');
+                    }
+                };
+
+                IPA.cert.perform_revoke(command_spec, that.certificate, dialog);
+            }
+        };
+
+        var dialog = IPA.cert.revoke_dialog(spec);
+        dialog.open();
+    };
+
+    that.perform_restore = function() {
+        var spec = {
+            title: that.compose_dialog_title(),
+            message: '@i18n:objects.cert.restore_confirmation',
+            ok_label: '@i18n:buttons.restore',
+            on_ok: function () {
+                var command_spec = {
+                    hide_activity_icon: true,
+                    notify_activity_end: function() {
+                        that.spinner.emit('hide-spinner');
+                    },
+                    notify_activity_start: function() {
+                        that.spinner.emit('display-spinner');
+                    },
+                    on_success: function() {
+                        that.toggle_revoked_note();
+                        that.handle_revocation_reason();
+                        that.facet.on_update.notify();
+                        IPA.notify_success('@i18n:objects.cert.restored');
+                    }
+                };
+                IPA.cert.perform_restore(command_spec, that.certificate);
+            }
+        };
+
+        var dialog = IPA.confirm_dialog(spec);
+        dialog.open();
     };
 
     return that;
diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json
index 942c8b59f8b9353544287533246984ed0d99d51a..89a4fa32b01d8d7d991a9cf6d861ca654d5b9863 100644
--- a/install/ui/test/data/ipa_init.json
+++ b/install/ui/test/data/ipa_init.json
@@ -68,6 +68,8 @@
                         "cancel": "Cancel",
                         "close": "Close",
                         "disable": "Disable",
+                        "download": "Download",
+                        "download_title": "Download certificate as PEM formatted file.",
                         "edit": "Edit",
                         "enable": "Enable",
                         "filter": "Filter",
@@ -255,6 +257,7 @@
                             "md5_fingerprint": "MD5 Fingerprint",
                             "missing": "No Valid Certificate",
                             "new_certificate": "New Certificate",
+                            "new_cert_format": "Certificate in base64 or PEM format",
                             "note": "Note",
                             "organization": "Organization",
                             "organizational_unit": "Organizational Unit",
@@ -273,6 +276,7 @@
                             "revoke_certificate_simple": "Revoke Certificate",
                             "revoke_confirmation": "To confirm your intention to revoke this certificate, select a reason from the pull-down list, and click the \"Revoke\" button.",
                             "revoked": "Certificate Revoked",
+                            "revoked_status": "REVOKED",
                             "serial_number": "Serial Number",
                             "serial_number_hex": "Serial Number (hex)",
                             "sha1_fingerprint": "SHA1 Fingerprint",
diff --git a/ipalib/plugins/internal.py b/ipalib/plugins/internal.py
index b90af3168d17d8ffa024f58e3d8b7380b113e9bb..b273cdaf20a636e44fd23b9ce75d06a76037dca4 100644
--- a/ipalib/plugins/internal.py
+++ b/ipalib/plugins/internal.py
@@ -211,6 +211,8 @@ class i18n_messages(Command):
             "cancel": _("Cancel"),
             "close": _("Close"),
             "disable": _("Disable"),
+            "download": _("Download"),
+            "download_title": _("Download certificate as PEM formatted file."),
             "edit": _("Edit"),
             "enable": _("Enable"),
             "filter": _("Filter"),
@@ -399,6 +401,7 @@ class i18n_messages(Command):
                 "md5_fingerprint": _("MD5 Fingerprint"),
                 "missing": _("No Valid Certificate"),
                 "new_certificate": _("New Certificate"),
+                "new_cert_format": _("Certificate in base64 or PEM format"),
                 "note": _("Note"),
                 "organization": _("Organization"),
                 "organizational_unit": _("Organizational Unit"),
@@ -417,6 +420,7 @@ class i18n_messages(Command):
                 "revoke_certificate_simple": _("Revoke Certificate"),
                 "revoke_confirmation": _("To confirm your intention to revoke this certificate, select a reason from the pull-down list, and click the \"Revoke\" button."),
                 "revoked": _("Certificate Revoked"),
+                "revoked_status": _("REVOKED"),
                 "serial_number": _("Serial Number"),
                 "serial_number_hex": _("Serial Number (hex)"),
                 "sha1_fingerprint": _("SHA1 Fingerprint"),
-- 
2.5.5

From 7eb4754f56e4880a2094a6a49bc307043de0888b Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 12:57:39 +0200
Subject: [PATCH 11/13] Add new certificates widget to the user details page

https://fedorahosted.org/freeipa/ticket/5108
https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/user.js | 43 +++++++++++++++++++++++++++++++++++++++---
 1 file changed, 40 insertions(+), 3 deletions(-)

diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
index a9727f57d69e7126d87707f541786ffab4d0c999..fc0044d8c170be222b4dc5ac68e38a05cd0c27ba 100644
--- a/install/ui/src/freeipa/user.js
+++ b/install/ui/src/freeipa/user.js
@@ -61,7 +61,19 @@ return {
     name: 'user',
     policies: [
         IPA.search_facet_update_policy,
-        IPA.details_facet_update_policy
+        IPA.details_facet_update_policy,
+        {
+            $factory: IPA.facet_update_policy,
+            source_facet: 'details',
+            dest_entity: 'cert',
+            dest_facet: 'search'
+        },
+        {
+            $factory: IPA.facet_update_policy,
+            source_facet: 'details',
+            dest_entity: 'cert',
+            dest_facet: 'details'
+        }
     ],
     facets: [
         {
@@ -176,8 +188,13 @@ return {
                             label: '@i18n:objects.sshkeystore.keys'
                         },
                         {
-                            $type: 'certificate',
-                            name: 'usercertificate'
+                            $type: 'certs',
+                            field_adapter: {
+                                $type: 'object_adapter',
+                                result_index: 3
+                            },
+                            dropdown_menu: true,
+                            label: '@i18n:objects.cert.certificates'
                         },
                         {
                             $type: 'checkboxes',
@@ -539,14 +556,34 @@ IPA.user.details_facet = function(spec, no_init) {
 
         batch.add_command(krbtpolicy_command);
 
+        var certificates = rpc.command({
+            entity: 'basecert',
+            method: 'find',
+            retry: false,
+            options: {
+                user: [ pkey ],
+                check_revocation: true
+            }
+        });
+
+        batch.add_command(certificates);
+
         return batch;
     };
 
+
     if (!no_init) that.init_details_facet();
 
     return that;
 };
 
+IPA.user.cert_update_policy = function(spec) {
+
+    spec = spec || {};
+    spec.event = spec.event || 'certificate_updated';
+    return IPA.facet_update_policy(spec);
+};
+
 /**
  * @member user
  * Makes user association facets read-only in self service
-- 
2.5.5

From 7f55236253b623e7c0889ed0d0308c813211256c Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 12:58:22 +0200
Subject: [PATCH 12/13] Add new certificates widget to the host details page

https://fedorahosted.org/freeipa/ticket/5108
https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/host.js | 96 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 88 insertions(+), 8 deletions(-)

diff --git a/install/ui/src/freeipa/host.js b/install/ui/src/freeipa/host.js
index 764e551b40a00d3a35ea4e8ec99de9164bc97be3..aa2f0c81d7aca90140930bf1a381ec8b600d066f 100644
--- a/install/ui/src/freeipa/host.js
+++ b/install/ui/src/freeipa/host.js
@@ -19,7 +19,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-define(['./ipa',
+define(['./builder',
+        './ipa',
         './jquery',
         './phases',
         './reg',
@@ -30,7 +31,7 @@ define(['./ipa',
         './association',
         './entity',
         './certificate'],
-    function(IPA, $, phases, reg, rpc, text) {
+    function(builder, IPA, $, phases, reg, rpc, text) {
 
 var exp = IPA.host = {};
 
@@ -41,13 +42,13 @@ return {
         IPA.search_facet_update_policy,
         IPA.details_facet_update_policy,
         {
-            $factory: IPA.cert.cert_update_policy,
+            $factory: IPA.facet_update_policy,
             source_facet: 'details',
             dest_entity: 'cert',
             dest_facet: 'details'
         },
         {
-            $factory: IPA.cert.cert_update_policy,
+            $factory: IPA.facet_update_policy,
             source_facet: 'details',
             dest_entity: 'cert',
             dest_facet: 'search'
@@ -140,8 +141,13 @@ return {
                     name: 'certificate',
                     fields: [
                         {
-                            $type: 'certificate',
-                            name: 'usercertificate'
+                            $type: 'certs',
+                            field_adapter: {
+                                $type: 'object_adapter',
+                                result_index: 1
+                            },
+                            dropdown_menu: true,
+                            label: '@i18n:objects.cert.certificates'
                         }
                     ]
                 },
@@ -270,8 +276,16 @@ return {
                 'request_cert'],
             state: {
                 evaluators: [
-                    IPA.host.has_password_evaluator,
-                    IPA.host.has_keytab_evaluator,
+                    {
+                        $factory: IPA.host.has_password_evaluator,
+                        param: 'has_password',
+                        adapter: { $type: 'batch', result_index: 0 }
+                    },
+                    {
+                        $factory: IPA.host.has_keytab_evaluator,
+                        param: 'has_keytab',
+                        adapter: { $type: 'batch', result_index: 0 }
+                    },
                     IPA.host.userpassword_acl_evaluator,
                     IPA.host.krbprincipalkey_acl_evaluator,
                     IPA.cert.certificate_evaluator
@@ -363,6 +377,31 @@ IPA.host.details_facet = function(spec, no_init) {
     that.certificate_loaded = IPA.observer();
     that.certificate_updated = IPA.observer();
 
+    that.create_refresh_command = function() {
+        var pkey = that.get_pkey();
+
+        var batch = rpc.batch_command({
+            name: 'host_details_refresh'
+        });
+
+        var host_command = that.details_facet_create_refresh_command();
+        batch.add_command(host_command);
+
+        var certificates = rpc.command({
+            entity: 'basecert',
+            method: 'find',
+            retry: false,
+            options: {
+                host: [ pkey ]
+            }
+        });
+
+        batch.add_command(certificates);
+
+        return batch;
+
+    };
+
     that.get_refresh_command_name = function() {
         return that.entity.name+'_show_'+that.get_pkey();
     };
@@ -803,6 +842,26 @@ IPA.host.has_keytab_evaluator = function(spec) {
     spec.representation = spec.representation || 'has_keytab';
 
     var that = IPA.value_state_evaluator(spec);
+    that.param = spec.param || 'has_keytab';
+    that.adapter = builder.build('adapter', { $type: 'adapter'}, { context: that });
+
+    that.on_event = function(data) {
+
+        var old_state, value, loaded_value;
+
+        old_state = that.state;
+        value = that.normalize_value(that.value);
+        that.state = [];
+
+        loaded_value = that.adapter.load(data, spec.attribute);
+
+        if(!IPA.array_diff(value, loaded_value)) {
+            that.state.push(that.get_state_text());
+        }
+
+        that.notify_on_change(old_state);
+    };
+
     return that;
 };
 
@@ -885,6 +944,27 @@ IPA.host.has_password_evaluator = function(spec) {
     spec.representation = spec.representation || 'has_password';
 
     var that = IPA.value_state_evaluator(spec);
+    that.param = spec.param || 'has_password';
+    that.adapter = builder.build('adapter', { $type: 'adapter'}, { context: that });
+
+    that.on_event = function(data) {
+
+        var old_state, value, loaded_value;
+
+        old_state = that.state;
+        value = that.normalize_value(that.value);
+        that.state = [];
+
+        loaded_value = that.adapter.load(data, spec.attribute);
+
+        if(!IPA.array_diff(value, loaded_value)) {
+            that.state.push(that.get_state_text());
+        }
+
+        that.notify_on_change(old_state);
+    };
+
+
     return that;
 };
 
-- 
2.5.5

From 70580d92fbecedccfc2ede9b23e8a4a333d9c923 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Fri, 22 Apr 2016 12:59:30 +0200
Subject: [PATCH 13/13] Add new certificates widget to the service details page

https://fedorahosted.org/freeipa/ticket/5108
https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/service.js | 66 +++++++++++++++++++++++++++++++++++----
 1 file changed, 60 insertions(+), 6 deletions(-)

diff --git a/install/ui/src/freeipa/service.js b/install/ui/src/freeipa/service.js
index f1f8d951e415e9768aab433e28da852a732bc8ba..604d8d515242b05f5ffce3489762edbc53c5810f 100644
--- a/install/ui/src/freeipa/service.js
+++ b/install/ui/src/freeipa/service.js
@@ -21,6 +21,7 @@
 define([
     'dojo/_base/declare',
     './field',
+    './builder',
     './ipa',
     './jquery',
     './phases',
@@ -31,7 +32,7 @@ define([
     './search',
     './association',
     './entity'],
-        function(declare, field_mod, IPA, $, phases, reg, rpc, text) {
+        function(declare, field_mod, builder, IPA, $, phases, reg, rpc, text) {
 
 var exp =IPA.service = {};
 
@@ -42,13 +43,13 @@ return {
         IPA.search_facet_update_policy,
         IPA.details_facet_update_policy,
         {
-            $factory: IPA.cert.cert_update_policy,
+            $factory: IPA.facet_update_policy,
             source_facet: 'details',
             dest_entity: 'cert',
             dest_facet: 'details'
         },
         {
-            $factory: IPA.cert.cert_update_policy,
+            $factory: IPA.facet_update_policy,
             source_facet: 'details',
             dest_entity: 'cert',
             dest_facet: 'search'
@@ -134,8 +135,13 @@ return {
                     name: 'certificate',
                     fields: [
                         {
-                            $type: 'certificate',
-                            name: 'usercertificate'
+                            $type: 'certs',
+                            field_adapter: {
+                                $type: 'object_adapter',
+                                result_index: 1
+                            },
+                            dropdown_menu: true,
+                            label: '@i18n:objects.cert.certificates'
                         }
                     ]
                 },
@@ -234,7 +240,11 @@ return {
             ],
             state: {
                 evaluators: [
-                    IPA.service.has_keytab_evaluator,
+                    {
+                        $factory: IPA.host.has_keytab_evaluator,
+                        param: 'has_keytab',
+                        adapter: { $type: 'batch', result_index: 0 }
+                    },
                     IPA.service.krbprincipalkey_acl_evaluator,
                     IPA.cert.certificate_evaluator
                 ]
@@ -304,6 +314,30 @@ IPA.service.details_facet = function(spec, no_init) {
     that.certificate_loaded = IPA.observer();
     that.certificate_updated = IPA.observer();
 
+    that.create_refresh_command = function() {
+        var pkey = that.get_pkey();
+
+        var batch = rpc.batch_command({
+            name: 'services_details_refresh'
+        });
+
+        var service_command = that.details_facet_create_refresh_command();
+        batch.add_command(service_command);
+
+        var certificates = rpc.command({
+            entity: 'basecert',
+            method: 'find',
+            retry: false,
+            options: {
+                service: [ pkey ]
+            }
+        });
+
+        batch.add_command(certificates);
+
+        return batch;
+    };
+
     if (!no_init) that.init_details_facet();
 
     return that;
@@ -524,6 +558,26 @@ IPA.service.has_keytab_evaluator = function(spec) {
     spec.representation = spec.representation || 'has_keytab';
 
     var that = IPA.value_state_evaluator(spec);
+    that.param = spec.param || 'has_keytab';
+    that.adapter = builder.build('adapter', { $type: 'adapter'}, { context: that });
+
+    that.on_event = function(data) {
+
+        var old_state, value, loaded_value;
+
+        old_state = that.state;
+        value = that.normalize_value(that.value);
+        that.state = [];
+
+        loaded_value = that.adapter.load(data, spec.attribute);
+
+        if(!IPA.array_diff(value, loaded_value)) {
+            that.state.push(that.get_state_text());
+        }
+
+        that.notify_on_change(old_state);
+    };
+
     return that;
 };
 
-- 
2.5.5

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to