On 06/14/2016 09:41 PM, Pavel Vomacka wrote:


On 05/13/2016 06:56 PM, Petr Vobornik wrote:
On 04/26/2016 04:23 PM, Pavel Vomacka wrote:
Self-NACK for patches 0027, 28, 29, 30 - used incorrect policy. I also attach all patches which were not changed - it is easier to get the whole patchset.

On 04/26/2016 02:02 PM, Pavel Vomacka wrote:
I forgot to mention that my patches requires patches from :
https://www.redhat.com/archives/freeipa-devel/2016-April/msg00209.html


On 04/26/2016 01:33 PM, Pavel Vomacka wrote:
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

Great stuff, couple comments below.

We can discuss some items in person. Not everything needs to be done.

I didn't run it, just reading the code.

Patch 0018:

1. Nit pick: When a value should be boolean, then following method won't
make sure that dropdown_menu won't be e.g. an object.
  +    that.dropdown_menu = spec.dropdown_menu || false;

I would prefer:
  +    that.dropdown_menu = !!spec.dropdown_menu;

Which retypes it to boolean. If default should be true (not this case) then:
   that.dropdown_menu = spec.dropdown_menu !== undefined ?
!!spec.dropdown_menu : true;

Also the interface is very specific. It says that the child widget will
have dropdown menu. What if the actions won't be in dropdown menu but,
e.g., some overlay menu.

Imho the interface should be:

  that.custom_actions = !!spec.custom_actions;

Than the child object would have define,e.g., :

    action_object get_custom_actions()

Interface of action_object would be e.g.:
    get_items()
    set_items(items)
    enable_item(name)
    disable_item(name)

Dropdown menu would have to define these methods.

Patch 0019:

1. Shouldn't disable_item or enable_item automatically rerender the items?

2. The rerender, used in later patches. Imo it should do only:

   if (this.ul_node) {
      construct.empty(this.ul_node);
      this._render_items(this.items);
   }

Or just re-render the one item.
   $( "li[data-name=" + item.name +"]", this.ul_node ).replaceWith(
this._render_item(item));

3. in future for loops write:
   for (var i=0, l=this.items.length; i<l; i++) {
instead of
   for (var i=0; i<this.items.length; i++) {

It's defensive style when you don't know if computing length is O(1) or
O(n) or how big is n. In this instance it doesn't probably matter.

Patch 0020:

1. Working widgets inherits from IPA.widget so why do you redefine
'name', 'facet', 'entity'

2. why to you introduce 'base_cls' and 'other_cls' when there are
'base_css_class' and 'css_class'?

3. typo in comment 'opaciti'

4. We can ignore IE < 9, I'm not sure if you avoid using some features
because of IE9.

5. _normalize_bg_color_css could be moved to util.js But I wonder if we
need it. rgba is supported in 95% of browsers and definitely in all
supported by IPA (chrome, firefox).

Patch 0021:

1. The original code is not great - RPC code should be agnostic to
display layer. Ideally it would not call methods like
IPA.hide_activity_icon(); and IPA.display_activity_icon();

I would rather avoid adding more of such things there and would refactor
now, when it is easy.

IMO the rpc class could use Evented mixin and have:
  4 local events: start, end, success, error
  2 global topics: rpc-start, rpc-end

Local events will be new - will replace the notify_activity_start and
notify_activity_end. For convenience, spec object could accept a
handlers in similar fashion as now. The handlers would be registered to
the events.

Global topics would replace current IPA.hide_activity_icon(); and
IPA.display_activity_icon();

App class would subscribe to the global events and to stuff (hide|show)
accordingly. Example of such subscription:
topic.subscribe('phase-error', lang.hitch(this, this.on_phase_error));

Then hide_activity_icon could be something like notify_global with
default to true which can be suppressed by setting it to false.

What do you think?

Patch 0022:

1. Leftover line:
   var sn = record[i].serial_number;

Patch 0023:

1. move the css code from ipa.css to layout.less?

2. Did you mean "display: table"?
   display: cert-table;

3. Why are create_layout, create_row and create_cell private methods of
create_content? It would make sense to create a table dialog mixin
(similar as confirm mixin) and use the same code in all cert dialogs(if
applicable).

4. 'table-head' class is used in every cell. Why is it call 'head' when
it is in fact in all cells?

Patch 0024:

Will require changes described in patch 21(rpc).

1. should the methods have rather params: serial_number and
revocation_reason, instead of certificate(maybe ok) and dialog(not ok)?
That way it would be more general/reusable.

Patch 0025: ACK

Patch 0026:

1. Add link use exactly the same code as in multivalued_widget. The code
should be moved to method: create_add_link(container) and then reused.
new_row would be overridedn and it would call open_addcert_dialog or
open_addcert_dialog renamed to new_row.

2. it can have a spec default:
   spec.dropdown_menu = spec.dropdown_menu !== undefined ? true :
spec.dropdown_menu;

And then it doesn't have to be defined in patches 28, 29, 30.


Patch 0027:

1. Why is download action name a 'that.data_url' ?

2. the dropdown_menu -> custom_actions change

3. "that.certificate &&" is not required it will be always true
         that.certificate = $.extend({}, certificate);

if (that.certificate && that.certificate.revoked !== 'undefined') {
             that.get_cert_revoke_status();
         }

4. update_link is weird name for a method which updates data in a table

5. "that.update_link = function(r)", "r" shouldn't be there

6. Following doesn't look future proof:
"""
     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;
"""
Would check it with Fraser

7. in handle_revocation_reason, useless call:
   var dd_menu = that.get_dropdown_menu();

8. in compose_dialog_title, there is a check for `cert.subject` if true
it sets `cn`, `o`. But if false then the values are `undefined` which
are then used in the r.replace calls. Is it intended?

Patch 0028:
Looks OK, "dropdown_menu: true" doesn't have to be there as mentioned
earlier

Will test later.

Patch 0029:

1. What about extending value_state_evaluator with adapter and param
property. So that it would use standard adapter by default and every
child could use it's own? IMO other parts of UI could use it as well.
The reason why it doesn't use adapter already is that adapters were
introduced later.

It would simplify both definitions + the definitions in patch 30.

Patch 0030:

Same as patch 0029.
Thank you for the review. I fixed everthing, only rpc code isn't changed. I will do it later in separate patch.

These patches requires jcholast's patches 551.2, 552.2, 623, 624 (which require ftweedal's Sub-CA patches) and WebUI patches for Sub-CAs (because of download functionality).

Edited and rebased patches are attached.

--
Pavel^3 Vomacka


I moved big part of certs_widget code to more general custom_command_multivalued_widget. This change will be useful in future.

Updated patches attached. It would be nice to apply patch 57 between patches 25 and 26, but it should be possible to apply it on the top.

--
Pavel^3 Vomacka
From 3d9113a63005817b8f9e1e25b924e358dd3344e3 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 26 Apr 2016 08:44:03 +0200
Subject: [PATCH 10/16] 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 | 165 ++++++++++++++++++++++++----------
 install/ui/src/freeipa/field.js       |  19 ++++
 install/ui/test/data/ipa_init.json    |   1 +
 ipaserver/plugins/internal.py         |   1 +
 4 files changed, 138 insertions(+), 48 deletions(-)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 16a6b4bdf58ca0ea6d133775b1dae6e41f5cf4b0..d20cf8289e0500d026a679173c8b3b9dab993b1b 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -113,6 +113,25 @@ 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) {
+        match = match[2].replace(/\s*/g, '');
+        return $.trim(match);
+    }
+
+    text = text.replace(/\s*/g, '');
+    return $.trim(text);
+};
+
 IPA.cert.pem_format_base64 = function(text) {
     /*
      * Input is assumed to be base64 possibly with embedded whitespace.
@@ -1101,57 +1120,108 @@ 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';
-
-    var that = IPA.input_widget(spec);
-    that.certs_visible = false;
-
-    that.create = function(container) {
-
-        that.widget_create(container);
-        that.content_el = $('<div>').appendTo(container);
+    spec.child_spec = spec.child_spec || {
+        $factory: IPA.cert.cert_widget,
+        css_class: 'certificate-widget',
+        facet: spec.facet
     };
 
-    that.create_status = function(name, text, icon) {
-
-        var status = $('<label/>', {
-            'class': 'certificate-status'
-        });
-
-        $('<i/>', {
-            'class': icon
-        }).appendTo(status);
-
-        status.append(" " + text);
-
-        return status;
-    };
-
-    that.create_certs = function() {
-
-        that.content_el.empty();
-        var l = that.certificates.length;
-
-        if (l && that.certs_visible) {
-            for (var i=0; i<l; i++) {
-                $('<div/>', {
-                    'class': 'certificate',
-                    text: that.certificates[i]
-                }).appendTo(that.content_el);
-            }
-            $('<div/>').append(
-                IPA.button({
-                    name: 'hide',
-                    label: '@i18n:buttons.hide',
-                    click: function() {
-                        that.certs_visible = false;
-                        that.create_certs();
+    spec.item_name = 'cert';
+
+    spec.custom_actions = spec.custom_actions === undefined ? true :
+        spec.custom_actions;
+
+    spec.adder_dialog_spec = {
+        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
                     }
-                })).
-            appendTo(that.content_el);
+                ],
+                layout:
+                {
+                    $factory: widget_mod.fluid_layout,
+                    widget_cls: 'col-sm-12',
+                    label_cls: 'col-sm-6 control-label'
+                }
+            }
+        ]
+    };
+
+    var that = IPA.custom_command_multivalued_widget(spec);
+
+    that.create_remove_options = function(row) {
+        var blob = row.widget.save();
+        var options = {
+            usercertificate: blob
+        };
+
+        return options;
+    };
+
+    /**
+     * Called on success of remove command. Override point.
+     */
+    that.on_success_remove = function(data, text_status, xhr) {
+        that.facet.refresh();
+        that.facet.certificate_updated.notify();
+        IPA.notify_success(data.result.summary);
+    };
+
+    that.create_add_options = function() {
+        var blob = that.adder_dialog.get_field('new_cert').get_value()[0];
+        blob = IPA.cert.get_base64(blob);
+        var options = {
+            usercertificate: blob
+        };
+
+        return options;
+    };
+
+    that.on_success_add = function(data, text_status, xhr) {
+        that.facet.refresh();
+        that.facet.certificate_updated.notify();
+        IPA.notify_success(data.result.summary);
+        that.adder_dialog.close();
+    };
+
+    that.create_remove_dialog_title = function(row) {
+        var title = row.widget.compose_dialog_title();
+
+        return title;
+    };
+
+    that.create_remove_dialog_message = function(row) {
+        var sn = row.widget.certificate.serial_number;
+        var message = text.get('@i18n:actions.delete_confirm');
+        message = message.replace('${object}',
+            text.get('@i18n:objects.cert.delete_cert_end') + sn);
+
+        return message;
+    };
+
+    return that;
+};
         }
 
         if (!l) {
@@ -1604,10 +1674,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 3f7e1d1b48efdbf60c6472458f6b4622cbea5232..f94bcc30699201793d80f0347c7f960438db46c4 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -1268,6 +1268,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 have only one value are tranformed this way:
  *  property1: {"__base64__": "value1"} => property1: "value1",
@@ -1553,6 +1571,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);
diff --git a/install/ui/test/data/ipa_init.json b/install/ui/test/data/ipa_init.json
index 8a2e2010a23e3d70dfdae97393a2212c8e9c7b7b..95cfe3e5e7ebabd3d62b62b06a7bb22004313a54 100644
--- a/install/ui/test/data/ipa_init.json
+++ b/install/ui/test/data/ipa_init.json
@@ -241,6 +241,7 @@
                             "cessation_of_operation": "Cessation of Operation",
                             "common_name": "Common Name",
                             "download": "Download",
+                            "delete_cert_end": "the certificate with serial number ",
                             "expires_on": "Expires On",
                             "fingerprints": "Fingerprints",
                             "find_issuedon_from": "Issued on from",
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
index d5fdd080f0e29dba2b7089fc04914349a53ec133..a60b853e957cfb9629edad68e82d60f495e6872c 100644
--- a/ipaserver/plugins/internal.py
+++ b/ipaserver/plugins/internal.py
@@ -378,6 +378,7 @@ class i18n_messages(Command):
                 "cessation_of_operation": _("Cessation of Operation"),
                 "common_name": _("Common Name"),
                 "download": _("Download"),
+                "delete_cert_end": _("the certificate with serial number "),
                 "expires_on": _("Expires On"),
                 "find_issuedon_from": _("Issued on from"),
                 "find_issuedon_to": _("Issued on to"),
-- 
2.5.5

From e883b6954d828555218286eb0ae59105afa42ef8 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 26 Apr 2016 12:28:45 +0200
Subject: [PATCH 11/16] 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          |  18 ++
 install/ui/src/freeipa/certificate.js | 315 ++++++++++++++++++++++++++++++----
 install/ui/test/data/ipa_init.json    |   6 +
 ipaserver/plugins/internal.py         |   6 +
 4 files changed, 316 insertions(+), 29 deletions(-)

diff --git a/install/ui/less/widgets.less b/install/ui/less/widgets.less
index b68161b65ef856d731778ef97044b6cdfeaca4ca..7345ebc96050413d120dd6c2d138089a8d7e90ed 100644
--- a/install/ui/less/widgets.less
+++ b/install/ui/less/widgets.less
@@ -134,13 +134,31 @@
 // Certificate Widget
 
 .certificate-widget {
+    padding: 5px 5px 5px 5px;
+    border: 1px dashed #DDD;
     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.5;
+        font-size: 150%;
+    }
 }
 
 // Working widget
diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index d20cf8289e0500d026a679173c8b3b9dab993b1b..5e45c2f3b6a5fbfa977c35e39dbdf7fdf27c8a1b 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 = {};
 
@@ -1222,40 +1225,294 @@ 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);
+    IPA.table_mixin().apply(that);
+
+    that.certificate = null;
+
+    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 = that.create_layout().appendTo(that.container);
+
+        var tr = that.create_row().appendTo(that.table_layout);
+        that.create_header_cell('@i18n:objects.cert.serial_number', ':')
+            .appendTo(tr);
+        that.cert_sn = that.create_cell('', '', 'cert-value').appendTo(tr);
+
+        tr = that.create_row().appendTo(that.table_layout);
+        that.create_header_cell('@i18n:objects.cert.issued_by', ':')
+            .appendTo(tr);
+        that.cert_issuer = that.create_cell('', '', 'cert-value').appendTo(tr);
+
+        tr = that.create_row().appendTo(that.table_layout);
+        that.create_header_cell('@i18n:objects.cert.valid_from', ':')
+            .appendTo(tr);
+        that.cert_valid_from = that.create_cell('', '', 'cert-value')
+            .appendTo(tr);
+
+        tr = that.create_row().appendTo(that.table_layout);
+        that.create_header_cell('@i18n:objects.cert.valid_to', ':')
+            .appendTo(tr);
+        that.cert_valid_to = that.create_cell('', '', 'cert-value')
+            .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: 'download',
+                    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: 'remove_hold',
+                    label: text.get('@i18n:buttons.remove_hold'),
+                    disabled: true,
+                    handler: that.perform_remove_hold
                 }
-            }).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.get_custom_actions = function() {
+        return that.dropdown;
+    };
+
+    that.update_displayed_data = function() {
+
+        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) {
+            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.handle_revocation_reason(cert.revocation_reason);
+    };
+
+    that.toggle_revoked_note = function(show) {
+        if (show) {
+            that.revoke_note.css('display', 'block');
+        }
+        else {
+            that.revoke_note.css('display', 'none');
+        }
+    };
+
+    that.handle_revocation_reason = function(reason) {
+        // Skip certificates which are not issued by ipa's CA
+        if (that.certificate.revoked === undefined) return;
+
+        var dd_menu = that.get_custom_actions();
+
+        if (reason && reason === 6) {
+            dd_menu.enable_item('remove_hold');
+            dd_menu.disable_item('revoke');
+            that.toggle_revoked_note(true);
+        }
+        else if (reason === null || reason === undefined) {
+            dd_menu.enable_item('revoke');
+            dd_menu.disable_item('remove_hold');
+        }
+        else if (typeof reason === 'number' && reason >= 0 &&
+            reason < IPA.cert.CRL_REASON.length) {
+                dd_menu.disable_item('revoke');
+                that.toggle_revoked_note(true);
         }
     };
 
     that.update = function(values) {
-        that.certificates = values;
-        that.create_certs();
+
+        var certificate = values[0];
+
+        if (!certificate ) certificate = {};
+
+        that.certificate = certificate;
+
+        that.update_displayed_data();
+    };
+
+    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;
+        }
+        else {
+            cn = o = text.get('@i18n:objects.cert.unspecified');
+        }
+
+        var r = text.get('@i18n:objects.cert.view_certificate');
+        r = r.replace('${entity}', cn);
+        r = r.replace('${primary_key}', o);
+
+        return r;
     };
 
-    that.clear = function() {
-        that.content_el.empty();
+    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 data_uri = IPA.cert.create_data_uri(that.certificate.certificate);
+        IPA.cert.perform_download(data_uri);
+    };
+
+    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.certificate_updated.notify();
+                        IPA.notify_success('@i18n:objects.cert.revoked');
+                    }
+                };
+
+                var sn = that.certificate.serial_number;
+                var revocation_reason = dialog.get_reason();
+                IPA.cert.perform_revoke(command_spec, sn, revocation_reason);
+            }
+        };
+
+        var dialog = IPA.cert.revoke_dialog(spec);
+        dialog.open();
+    };
+
+    that.perform_remove_hold = function() {
+        var spec = {
+            title: that.compose_dialog_title(),
+            message: '@i18n:objects.cert.remove_certificate_hold_confirmation',
+            ok_label: '@i18n:buttons.remove_hold',
+            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.certificate_updated.notify();
+                        IPA.notify_success('@i18n:objects.cert.hold_removed');
+                    }
+                };
+
+                var sn =  that.certificate.serial_number;
+                IPA.cert.perform_remove_hold(command_spec, sn);
+            }
+        };
+
+        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 95cfe3e5e7ebabd3d62b62b06a7bb22004313a54..e1e3512c9e2746f553e3f59e8ad5f98aabee80b8 100644
--- a/install/ui/test/data/ipa_init.json
+++ b/install/ui/test/data/ipa_init.json
@@ -69,6 +69,8 @@
                         "cancel": "Cancel",
                         "close": "Close",
                         "disable": "Disable",
+                        "download": "Download",
+                        "download_title": "Download certificate as PEM formatted file.",
                         "edit": "Edit",
                         "enable": "Enable",
                         "filter": "Filter",
@@ -267,6 +269,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",
@@ -286,6 +289,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",
@@ -294,6 +298,8 @@
                             "superseded": "Superseded",
                             "unspecified": "Unspecified",
                             "valid": "Valid Certificate Present",
+                            "valid_from": "Valid from",
+                            "valid_to": "Valid to",
                             "validity": "Validity",
                             "view_certificate": "Certificate for ${entity} ${primary_key}",
                             "view_certificate_btn": "View Certificate"
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
index a60b853e957cfb9629edad68e82d60f495e6872c..1d69de7ddcb926145c2ddf6f6adda35f269eea8e 100644
--- a/ipaserver/plugins/internal.py
+++ b/ipaserver/plugins/internal.py
@@ -205,6 +205,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"),
@@ -404,6 +406,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"),
@@ -423,6 +426,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"),
@@ -431,6 +435,8 @@ class i18n_messages(Command):
                 "superseded": _("Superseded"),
                 "unspecified": _("Unspecified"),
                 "valid": _("Valid Certificate Present"),
+                "valid_from": _("Valid from"),
+                "valid_to": _("Valid to"),
                 "validity": _("Validity"),
                 "view_certificate": _("Certificate for ${entity} ${primary_key}"),
                 "view_certificate_btn": _("View Certificate"),
-- 
2.5.5

From c0ed0339ca1c1a438cbad063f7a15deef9a00717 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 14 Jun 2016 19:30:06 +0200
Subject: [PATCH 15/16] Updated certificates table

All certificates which are not issued by IPA CA are grey and not clickable. That's
because these certificates are not maintained by IPA CA.

Part of: https://fedorahosted.org/freeipa/ticket/5381
---
 install/ui/src/freeipa/certificate.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/install/ui/src/freeipa/certificate.js b/install/ui/src/freeipa/certificate.js
index 5e45c2f3b6a5fbfa977c35e39dbdf7fdf27c8a1b..f5af329c055fbdb39cd59450ee9fcece0f01dcd4 100755
--- a/install/ui/src/freeipa/certificate.js
+++ b/install/ui/src/freeipa/certificate.js
@@ -1655,6 +1655,7 @@ return {
             disable_facet_tabs: false,
             tabs_in_sidebar: true,
             tab_label: '@i18n:tabs.cert',
+            row_enabled_attribute: 'status',
             facet_groups: [exp.facet_group],
             facet_group: 'certificates',
             pagination: false,
@@ -1844,6 +1845,11 @@ IPA.cert.search_facet = function(spec) {
         return command;
     };
 
+    that.table.setup_column = function(column, div, record) {
+        var supress_link = record.status === undefined;
+        column.setup(div, record, supress_link);
+    };
+
     // parent method only sets expired flag when filter change, it doesn't
     // expect that option can change -> set expire flag for every search
     that.find = function() {
-- 
2.5.5

From 69681d67602baf1d18efaab175306d57437b43b6 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Mon, 20 Jun 2016 10:32:12 +0200
Subject: [PATCH 09/16] Add new custom command multivalued widget

Add general class for multivalued widget which uses special commands which
are performed immediately.

Part of: https://fedorahosted.org/freeipa/ticket/5108
---
 install/ui/src/freeipa/widget.js | 292 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 292 insertions(+)

diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index 157806cd0d5679c3102d256fbb72d40999eed0a4..4b7c1152420f2dc04990f53d3004b13bbf463755 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -1503,6 +1503,296 @@ IPA.multivalued_widget = function(spec) {
     return that;
 };
 
+
+/**
+ * Multivalued widget which allows to perform add and remove using commands
+ * like 'entity_{add|remove}_item', i.e. 'user_add_cert', etc.
+ *
+ * @class
+ * @extends IPA.multivalued_widget
+ */
+IPA.custom_command_multivalued_widget = function(spec) {
+
+    spec = spec || {};
+
+    spec.spec_child = spec.spec_child || {};
+
+    var that = IPA.multivalued_widget(spec);
+
+    that.item_name = spec.item_name || '';
+
+    that.adder_dialog_spec = spec.adder_dialog_spec;
+    that.remove_dialog_spec = spec.remove_dialog_spec;
+
+    /**
+     * Called on success of add command. Override point.
+     */
+    that.on_success_add = function(data, text_status, xhr) {
+        that.facet.refresh();
+        IPA.notify_success(data.result.summary);
+        that.adder_dialog.close();
+    };
+
+    /**
+     * Called on error of add command. Override point.
+     */
+    that.on_error_add = function(data) {
+        that.adder_dialog.focus_first_element();
+    };
+
+    /**
+     * Called on success of remove command. Override point.
+     */
+    that.on_success_remove = function(data, text_status, xhr) {
+        that.facet.refresh();
+        IPA.notify_success(data.result.summary);
+    };
+
+    /**
+     * Called on error of remove command. Override point.
+     */
+    that.on_error_remove = function(data) {
+        IPA.notify(data.result.summary, 'error');
+    };
+
+    /**
+     * Checks whether the facet doesn't need 'Save' or 'Revert' before
+     * refreshing.
+     */
+    that.handle_dirty_facet_dialog = function(dialog) {
+        if (that.facet.is_dirty()) {
+            var dirty_dialog = IPA.dirty_dialog({
+                facet: that.facet
+            });
+
+            dirty_dialog.callback = function() {
+                dialog.open();
+            };
+            dirty_dialog.open();
+
+        } else {
+            dialog.open();
+        }
+    };
+
+    /* On widget's Add button click */
+    that.new_row = function() {
+        that.open_adder_dialog();
+    };
+
+    that.open_adder_dialog = function() {
+        that.create_adder_dialog();
+
+        that.handle_dirty_facet_dialog(that.adder_dialog);
+    };
+
+    /**
+     * New adder dialog is stored in that.adder_dialog.
+     */
+    that.create_adder_dialog = function() {
+        var spec = that.adder_dialog_spec || {
+            name: 'custom-add-dialog'
+        };
+
+        that.adder_dialog = IPA.dialog(spec);
+        that.adder_dialog.create_button({
+            name: 'add',
+            label: '@i18n:buttons.add',
+            click: function() {
+                if (!that.adder_dialog.validate()) {
+                    exp.focus_invalid(that.adder_dialog);
+                }
+                else {
+                    that.add(that.adder_dialog);
+                }
+            }
+        });
+
+        that.adder_dialog.create_button({
+            name: 'cancel',
+            label: '@i18n:buttons.cancel',
+            click: function() {
+                that.adder_dialog.close();
+            }
+        });
+    };
+
+    /* on button 'Add' on adder dialog click */
+    that.add = function() {
+        var command = that.create_add_command();
+        command.execute();
+    };
+
+    /* Function is called after clicking on widget's 'Delete' button */
+    that.remove_row = function(row) {
+        that.open_remove_dialog(row);
+    };
+
+    that.open_remove_dialog = function(row) {
+        that.create_remove_dialog(row);
+
+        that.handle_dirty_facet_dialog(that.remove_dialog);
+    };
+
+    /**
+     * Create remove dialog title. Override point.
+     *
+     * @param {Object} row
+     * @return {String} title
+     */
+    that.create_remove_dialog_title = function(row) {
+        var title = text.get('@i18n:dialogs.confirmation');
+
+        return title;
+    };
+
+    /**
+     * Create remove dialog message. Override point.
+     *
+     * @param {Object} row
+     * @return {String} title
+     */
+    that.create_remove_dialog_message = function(row) {
+        var message = text.get('@i18n:search.delete_confirm');
+
+        return message;
+    };
+
+    /**
+     * New remove dialog is stored in that.remove_dialog.
+     */
+    that.create_remove_dialog = function(row) {
+        var perform_remove = function() {
+            that.perform_remove(row);
+        };
+
+        var title = that.create_remove_dialog_title(row);
+        var message = that.create_remove_dialog_message(row);
+
+        var spec = that.remove_dialog_spec || {
+            title: title,
+            message: message,
+            on_ok: perform_remove
+        };
+
+        that.remove_dialog = IPA.confirm_dialog(spec);
+    };
+
+    that.perform_remove = function(row) {
+        var command = that.create_remove_command(row);
+        command.execute();
+    };
+
+    /**
+     * Compose remove command. Override point
+     *
+     * @param {Object} row
+     * @return {Object} command
+     */
+    that.create_remove_command = function(row) {
+        var method = that.create_remove_method(row);
+        var args = that.create_remove_args(row);
+        var options = that.create_remove_options(row);
+
+        var command = rpc.command({
+            entity: that.facet.entity.name,
+            method: method,
+            args: args,
+            options: options,
+            on_success: that.on_success_remove,
+            on_error: that.on_error_remove
+        });
+
+        return command;
+    };
+
+    /**
+     * Compose remove method. Override point
+     *
+     * @param {Object} row
+     * @return {String} method
+     */
+    that.create_remove_method = function(row) {
+        return 'remove_' + that.item_name;
+    };
+
+    /**
+     * Compose args for remove command. Override point
+     *
+     * @param {Object} row
+     * @return {Array} args
+     */
+    that.create_remove_args = function(row) {
+        var pkey = that.facet.get_pkey();
+        return [pkey];
+    };
+
+    /**
+     * Compose options for remove command. Override point
+     *
+     * @param {Object} row
+     * @return {Object} options
+     */
+    that.create_remove_options = function(row) {
+        var options = {};
+
+        return options;
+    };
+
+    /**
+     * Compose add command
+     *
+     * @return {Object} command
+     */
+    that.create_add_command = function() {
+        var method = that.create_add_method();
+        var args = that.create_add_args();
+        var options = that.create_add_options();
+
+        var command = rpc.command({
+            entity: that.facet.entity.name,
+            method: method,
+            args: args,
+            options: options,
+            on_success: that.on_success_add,
+            on_error: that.on_error_add
+        });
+
+        return command;
+    };
+
+    /**
+     * Compose method for add command. Override point.
+     * @return {String} method
+     */
+    that.create_add_method = function() {
+        return 'add_' + that.item_name;
+    };
+
+    /**
+     * Compose args for add command. Override point
+     * @return {Array} args
+     */
+    that.create_add_args = function() {
+        var pkey = that.facet.get_pkey();
+
+        return [pkey];
+    };
+
+    /**
+     * Compose options for add command. Override point
+     * @return {Object} options
+     */
+    that.create_add_options = function() {
+        var options = {};
+
+        return options;
+    };
+
+    return that;
+};
+
+
 /**
  * Option widget base
  *
@@ -6755,6 +7045,8 @@ exp.register = function() {
     w.register('html', IPA.html_widget);
     w.register('link', IPA.link_widget);
     w.register('multivalued', IPA.multivalued_widget);
+    w.register('custom_command_multivalued',
+        IPA.custom_command_multivalued_widget);
     w.register('password', IPA.password_widget);
     w.register('radio', IPA.radio_widget);
     w.register('select', IPA.select_widget);
-- 
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