URL: https://github.com/freeipa/freeipa/pull/549
Author: pvomacka
 Title: #549: T6601 certmap match
Action: opened

PR body:
"""
WebUI: add support for certmap match command. 

PR contains also certmap rule patches from pullrequest #400 (I will rebase once 
#400 will be merged) because they are necessary. It also requires PRs #398 and 
#516.


https://pagure.io/freeipa/issue/6601
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/549/head:pr549
git checkout pr549
From 8bb768e9acfd4442deb579c43f0f90cf16dafb37 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Mon, 16 Jan 2017 13:59:16 +0100
Subject: [PATCH 1/8] WebUI: Add possibility to set field always writable

If field will have set attribute 'always_writable' to true, then
'no_update' flag will be ingored. Used in command user-{add,remove}-certmap
which needs to be writable in WebUI and also needs to be omitted from
user-mod command.

Part of: https://fedorahosted.org/freeipa/ticket/6601
---
 install/ui/src/freeipa/field.js  | 43 +++++++++++++++++++++++++++++++++++++++-
 install/ui/src/freeipa/widget.js | 35 ++++++++++----------------------
 2 files changed, 52 insertions(+), 26 deletions(-)

diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index d70a778..9f287dd 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -484,7 +484,16 @@ field.field = IPA.field = function(spec) {
                 writable = false;
             }
 
-            if (that.metadata.flags && array.indexOf(that.metadata.flags, 'no_update') > -1) {
+            // In case that field has set always_writable attribute, then
+            // 'no_update' flag is ignored in WebUI. It is done because of
+            // commands like user-{add,remove}-certmap. They operate with user's
+            // attribute, which cannot be changed using user-mod, but only
+            // using command user-{add,remove}-certmap. Therefore it has set
+            // 'no_update' flag, but we need to show 'Add', 'Remove' buttons in
+            // WebUI.
+            if (that.metadata.flags &&
+                array.indexOf(that.metadata.flags, 'no_update') > -1 &&
+                !that.always_writable) {
                 writable = false;
             }
         }
@@ -1259,6 +1268,37 @@ field.certs_field = IPA.certs_field = function(spec) {
     return that;
 };
 
+
+/**
+ * Used along with custom_command_multivalued widget
+ *
+ * - by default has `w_if_no_aci` to workaround missing object class
+ * - by default has always_writable=true to workaround aci rights
+ *
+ * @class
+ * @alternateClassName IPA.custom_command_multivalued_field
+ * @extends IPA.field
+ */
+field.certmap_command_multivalued_field = function(spec) {
+
+    spec = spec || {};
+    spec.flags = spec.flags || ['w_if_no_aci'];
+
+    var that = IPA.field(spec);
+
+    /**
+     * Set field always writable in case that it is set to true
+     * @param Boolean always_writable
+     */
+    that.always_writable = spec.always_writable === undefined ? true :
+            spec.always_writable;
+
+    return that;
+};
+
+
+IPA.custom_command_multivalued_field = field.custom_command_multivalued_field;
+
 /**
  * SSH Keys Adapter
  * @class
@@ -1652,6 +1692,7 @@ field.register = function() {
     f.register('checkbox', field.checkbox_field);
     f.register('checkboxes', field.field);
     f.register('combobox', field.field);
+    f.register('certmap_multivalued', field.certmap_command_multivalued_field);
     f.register('datetime', field.datetime_field);
     f.register('enable', field.enable_field);
     f.register('entity_select', field.field);
diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index 15f0126..b7028a9 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -1534,12 +1534,8 @@ IPA.custom_command_multivalued_widget = function(spec) {
      * Called on error of add command. Override point.
      */
     that.on_error_add = function(xhr, text_status, error_thrown) {
-        that.adder_dialog.focus_first_element();
-
-        if (error_thrown.message) {
-            var msg = error_thrown.message;
-            IPA.notify(msg, 'error');
-        }
+        that.adder_dialog.show();
+        exp.focus_invalid(that.adder_dialog);
     };
 
     /**
@@ -1599,27 +1595,16 @@ IPA.custom_command_multivalued_widget = function(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);
-                }
+        spec.on_ok = function() {
+            if (!that.adder_dialog.validate()) {
+                exp.focus_invalid(that.adder_dialog);
             }
-        });
-
-        that.adder_dialog.create_button({
-            name: 'cancel',
-            label: '@i18n:buttons.cancel',
-            click: function() {
-                that.adder_dialog.close();
+            else {
+                that.add(that.adder_dialog);
             }
-        });
+        };
+
+        that.adder_dialog = IPA.custom_command_multivalued_dialog(spec);
     };
 
     /* on button 'Add' on adder dialog click */

From 2c6e870f9e58e9eb2ca5aabff0be3e2cbefdc548 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Mon, 16 Jan 2017 14:13:42 +0100
Subject: [PATCH 2/8] WebUI: Create non editable row widget for mutlivalued
 widget

Old krb-principal widget is changed to general one. And used also for
ipacertmapdata in user.

This widget make every line non-editable.

Part of: https://fedorahosted.org/freeipa/ticket/6601
---
 install/ui/src/freeipa/host.js    |  3 ++-
 install/ui/src/freeipa/service.js |  3 ++-
 install/ui/src/freeipa/user.js    |  3 ++-
 install/ui/src/freeipa/widget.js  | 29 +++++++++++++++++++----------
 4 files changed, 25 insertions(+), 13 deletions(-)

diff --git a/install/ui/src/freeipa/host.js b/install/ui/src/freeipa/host.js
index 1dfe05e..a09535c 100644
--- a/install/ui/src/freeipa/host.js
+++ b/install/ui/src/freeipa/host.js
@@ -93,7 +93,8 @@ return {
                             name: 'krbprincipalname',
                             item_name: 'principal',
                             child_spec: {
-                                $type: 'krb_principal'
+                                $type: 'non_editable_row',
+                                data_name: 'krb-principal'
                             }
                         },
                         {
diff --git a/install/ui/src/freeipa/service.js b/install/ui/src/freeipa/service.js
index 2533ad0..10f86ce 100644
--- a/install/ui/src/freeipa/service.js
+++ b/install/ui/src/freeipa/service.js
@@ -81,7 +81,8 @@ return {
                             name: 'krbprincipalname',
                             item_name: 'principal',
                             child_spec: {
-                                $type: 'krb_principal'
+                                $type: 'non_editable_row',
+                                data_name: 'krb-principal'
                             }
                         },
                         {
diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
index 628cf8e..8f78f2b 100644
--- a/install/ui/src/freeipa/user.js
+++ b/install/ui/src/freeipa/user.js
@@ -192,7 +192,8 @@ return {
                             name: 'krbprincipalname',
                             item_name: 'principal',
                             child_spec: {
-                                $type: 'krb_principal'
+                                $type: 'non_editable_row',
+                                data_name: 'krb-principal'
                             }
                         },
                         {
diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index b7028a9..17b1376 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -1792,6 +1792,8 @@ IPA.custom_command_multivalued_widget = function(spec) {
 IPA.krb_principal_multivalued_widget = function (spec) {
 
     spec = spec || {};
+    spec.child_spec = spec.child_spec || {};
+    spec.child_spec.data_name = spec.child_spec.data_name || 'krb-principal';
 
     spec.adder_dialog_spec = spec.adder_dialog_spec || {
         title: '@i18n:krbaliases.adder_title',
@@ -1812,7 +1814,7 @@ IPA.krb_principal_multivalued_widget = function (spec) {
 
     that.create_remove_dialog_message = function(row) {
         var message = text.get('@i18n:krbaliases.remove_message');
-        message = message.replace('${alias}', row.widget.principal_name);
+        message = message.replace('${alias}', row.widget.new_value);
 
         return message;
     };
@@ -1820,7 +1822,7 @@ IPA.krb_principal_multivalued_widget = function (spec) {
 
     that.create_remove_args = function(row) {
         var pkey = that.facet.get_pkey();
-        var krbprincipalname = row.widget.principal_name;
+        var krbprincipalname = row.widget.new_value;
         krbprincipalname = [ krbprincipalname ];
 
         var args = [
@@ -1847,22 +1849,27 @@ IPA.krb_principal_multivalued_widget = function (spec) {
 };
 
 /**
- * Widget which is used as row in kerberos aliases multivalued widget.
- * It contains only string where is the principal alias name and delete button.
+ * Widget which is used as row in multivalued widget. Each row is just
+ * non-editable text field.
  *
  * @class
  * @extends IPA.input_widget
  */
-IPA.krb_principal_widget = function(spec) {
+IPA.non_editable_row_widget = function(spec) {
     spec = spec || {};
 
     var that = IPA.input_widget();
 
+    /**
+     * Prefix of CSS class of each row.
+     */
+    that.data_name = spec.data_name || 'non-editable';
+
     that.create = function(container) {
         that.widget_create(container);
 
-        that.principal_text = $('<span />', {
-            'class': 'krb-principal-name',
+        that.data_text = $('<span />', {
+            'class': that.data_name + '-data',
             text: ''
         }).appendTo(container);
 
@@ -1875,19 +1882,20 @@ IPA.krb_principal_widget = function(spec) {
 
     that.update = function(value) {
 
-        var principal_name = value[0] || '';
+        var single_value = value[0] || '';
 
-        that.principal_name = principal_name;
+        that.new_value = single_value;
         that.update_text();
     };
 
     that.update_text = function() {
-        that.principal_text.text(that.principal_name);
+        that.data_text.text(that.new_value);
     };
 
     return that;
 };
 
+
 /**
  * Option widget base
  *
@@ -7161,6 +7169,7 @@ exp.register = function() {
     w.register('html', IPA.html_widget);
     w.register('link', IPA.link_widget);
     w.register('multivalued', IPA.multivalued_widget);
+    w.register('non_editable_row', IPA.non_editable_row_widget);
     w.register('custom_command_multivalued',
         IPA.custom_command_multivalued_widget);
     w.register('krb_principal_multivalued',

From 40d5ffd0c597a1b9e2bfe4b352a5e6746d876eef Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Mon, 27 Feb 2017 18:12:29 +0100
Subject: [PATCH 3/8] WebUI: Add Custom command multivalued adder dialog

Adder dialog which is used along with custom_command_multivalued_widget.
It behaivor of confirm dialog and adds fields which are necessary.

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

diff --git a/install/ui/src/freeipa/dialog.js b/install/ui/src/freeipa/dialog.js
index 8552e76..5150527 100644
--- a/install/ui/src/freeipa/dialog.js
+++ b/install/ui/src/freeipa/dialog.js
@@ -1451,6 +1451,40 @@ IPA.confirm_dialog = function(spec) {
     return that;
 };
 
+/**
+ * Custom command multivalued dialog
+ *
+ * Combines confirmation dialog which could be reopen after unsucessful command
+ * call. It also allows to define fields and widgets in the dialog.
+ *
+ * Acceptation is done by clicking on 'Add' button or hitting 'ENTER' key,
+ * refusal by clicking on 'Cancel' button or hitting 'ESCAPE' key.
+ *
+ * @class
+ * @extends IPA.form_dialog
+ */
+IPA.custom_command_multivalued_dialog = function(spec) {
+    spec = spec || {};
+
+    /**
+     * Name of confirmation button, by default set to 'Add'.
+     * @param {String} ok_label
+     */
+    spec.ok_label = spec.ok_label || '@i18n:buttons.add';
+
+    var that = IPA.form_dialog(spec);
+
+    that.close = function() {
+        that.dialog_close();
+    };
+
+    that.on_confirm = function() {
+        if (that.on_ok) that.on_ok();
+    };
+
+    return that;
+};
+
 
 /**
  *

From 8ee38cfdcf4f3f6f003547fa1925fe61e99e424e Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Mon, 16 Jan 2017 14:16:47 +0100
Subject: [PATCH 4/8] WebUI: Add certmap module

Add facets for certmaprule and certmapconfigure entities.

https://fedorahosted.org/freeipa/ticket/6601
---
 install/ui/src/freeipa/app.js                  |   1 +
 install/ui/src/freeipa/navigation/menu_spec.js |  16 +-
 install/ui/src/freeipa/plugins/certmap.js      | 381 +++++++++++++++++++++++++
 install/ui/src/freeipa/stageuser.js            |  12 +
 install/ui/src/freeipa/user.js                 |  12 +
 ipaserver/plugins/internal.py                  |  12 +
 6 files changed, 433 insertions(+), 1 deletion(-)
 create mode 100644 install/ui/src/freeipa/plugins/certmap.js

diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js
index 4eb045d..d262a64 100644
--- a/install/ui/src/freeipa/app.js
+++ b/install/ui/src/freeipa/app.js
@@ -32,6 +32,7 @@ define([
     './plugins/ca',
     './plugins/caacl',
     './plugins/certprofile',
+    './plugins/certmap',
     './dns',
     './group',
     './hbac',
diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js
index 0e717db..5f1d388 100644
--- a/install/ui/src/freeipa/navigation/menu_spec.js
+++ b/install/ui/src/freeipa/navigation/menu_spec.js
@@ -166,7 +166,21 @@ var nav = {};
                     ]
                 },
                 { entity: 'otptoken' },
-                { entity: 'radiusproxy' }
+                { entity: 'radiusproxy' },
+                {
+                    entity: 'certmaprule',
+                    facet: 'search',
+                    children: [
+                        {
+                            entity: 'certmaprule',
+                            facet: 'search'
+                        },
+                        {
+                            entity: 'certmapconfig',
+                            facet: 'details'
+                        }
+                    ]
+                }
             ]
         },
         {
diff --git a/install/ui/src/freeipa/plugins/certmap.js b/install/ui/src/freeipa/plugins/certmap.js
new file mode 100644
index 0000000..ddbc5a7
--- /dev/null
+++ b/install/ui/src/freeipa/plugins/certmap.js
@@ -0,0 +1,381 @@
+//
+// Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+//
+
+
+define([
+        'dojo/_base/lang',
+        'dojo/_base/declare',
+        'dojo/Evented',
+        'dojo/on',
+        '../navigation',
+        '../field',
+        '../ipa',
+        '../phases',
+        '../reg',
+        '../widget',
+        '../text',
+        '../util',
+        // plain imports
+        '../search',
+        '../entity'],
+            function(lang, declare, Evented, on, navigation, mod_field, IPA,
+                     phases, reg, widget_mod, text, util) {
+/**
+ * Certificate map module
+ * @class
+ */
+var certmap = IPA.certmap = {
+
+    search_facet_group: {
+        facets: {
+            certmaprule_search: 'certmaprule_search',
+            certmapconfig: 'certmapconfig_details'
+        }
+    }
+};
+
+var make_certmaprule_spec = function() {
+return {
+    name: 'certmaprule',
+    facets: [
+        {
+            $type: 'search',
+            always_request_members: true,
+            details_facet: 'details',
+            facet_groups: [certmap.search_facet_group],
+            facet_group: 'search',
+            row_enabled_attribute: 'ipaenabledflag',
+            columns: [
+                'cn',
+                {
+                    name: 'ipaenabledflag',
+                    label: '@i18n:status.label',
+                    formatter: 'boolean_status'
+                },
+                'description'
+            ],
+            actions: [
+                'batch_disable',
+                'batch_enable'
+            ],
+            control_buttons: [
+                {
+                    name: 'disable',
+                    label: '@i18n:buttons.disable',
+                    icon: 'fa-minus'
+                },
+                {
+                    name: 'enable',
+                    label: '@i18n:buttons.enable',
+                    icon: 'fa-check'
+                }
+            ]
+        },
+        {
+            $type: 'details',
+            disable_facet_tabs: true,
+            facet_groups: [certmap.search_facet_group],
+            facet_group: 'search',
+            actions: [
+                'enable',
+                'disable',
+                'delete'
+            ],
+            header_actions: ['enable', 'disable', 'delete'],
+            state: {
+                evaluators: [
+                    {
+                        $factory: IPA.enable_state_evaluator,
+                        field: 'ipaenabledflag'
+                    }
+                ]
+            },
+            sections: [
+                {
+                    name: 'details',
+                    fields: [
+                        'cn',
+                        {
+                            $type: 'textarea',
+                            name: 'description'
+                        },
+                        {
+                            name: 'ipacertmapmaprule',
+                            tooltip: {
+                                title: '@mc-opt:certmaprule_add:ipacertmapmaprule:doc'
+                            }
+                        },
+                        {
+                            name: 'ipacertmapmatchrule',
+                            tooltip: {
+                                title: '@mc-opt:certmaprule_add:ipacertmapmatchrule:doc'
+                            }
+                        },
+                        {
+                            $type: 'multivalued',
+                            name: 'associateddomain',
+                            tooltip: {
+                                title: '@mc-opt:certmaprule_add:associateddomain:doc'
+                            }
+                        },
+                        {
+                            name: 'ipacertmappriority',
+                            tooltip: {
+                                title: '@mc-opt:certmaprule_add:ipacertmappriority:doc'
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    ],
+    adder_dialog: {
+        fields: [
+            'cn',
+            {
+                name: 'ipacertmapmaprule',
+                tooltip: {
+                    title: '@mc-opt:certmaprule_add:ipacertmapmaprule:doc'
+                }
+            },
+            {
+                name: 'ipacertmapmatchrule',
+                tooltip: {
+                    title: '@mc-opt:certmaprule_add:ipacertmapmatchrule:doc'
+                }
+            },
+            {
+                $type: 'multivalued',
+                name: 'associateddomain',
+                tooltip: {
+                    title: '@mc-opt:certmaprule_add:associateddomain:doc'
+                }
+            },
+            {
+                name: 'ipacertmappriority',
+                tooltip: {
+                    title: '@mc-opt:certmaprule_add:ipacertmappriority:doc'
+                }
+            },
+            {
+                $type: 'textarea',
+                name: 'description'
+            }
+        ]
+    }
+};};
+
+
+var make_certmapconfig_spec = function() {
+return {
+    name: 'certmapconfig',
+    defines_key: false,
+    facets: [
+        {
+            $type: 'details',
+            facet_groups: [certmap.search_facet_group],
+            facet_group: 'search',
+            sections: [
+                {
+                    name: 'details',
+                    fields: [
+                        {
+                            $type: 'checkbox',
+                            name: 'ipacertmappromptusername'
+                        }
+                    ]
+                }
+            ]
+        }
+    ]
+};};
+
+
+/**
+ * Multivalued widget which is used for working with user's certmap.
+ *
+ * @class
+ * @extends IPA.custom_command_multivalued_widget
+ */
+certmap.certmap_multivalued_widget = function (spec) {
+
+    spec = spec || {};
+    spec.adder_dialog_spec = spec.adder_dialog_spec || {
+        name: 'custom-add-dialog',
+        title: '@i18n:objects.certmap.adder_title',
+        policies: [
+            {
+                $factory: IPA.multiple_choice_section_policy,
+                widget: 'type'
+            }
+        ],
+        fields: [
+            {
+                $type: 'multivalued',
+                name: 'ipacertmapdata',
+                label: '@i18n:objects.certmap.data_label',
+                widget: 'type.ipacertmapdata'
+            },
+            {
+                $type: 'multivalued',
+                name: 'certificate',
+                label: '@i18n:objects.certmap.certificate',
+                widget: 'type.certificate',
+                child_spec: {
+                    $type: 'textarea'
+                }
+            },
+            {
+                name: 'issuer',
+                label: '@i18n:objects.certmap.issuer',
+                widget: 'type.issuer'
+            },
+            {
+                name: 'subject',
+                label: '@i18n:objects.certmap.subject',
+                widget: 'type.subject'
+            }
+        ],
+        widgets: [
+            {
+                $type: 'multiple_choice_section',
+                name: 'type',
+                choices: [
+                    {
+                        name: 'data',
+                        label: '@i18n:objects.certmap.data_label',
+                        fields: ['ipacertmapdata', 'certificate'],
+                        required: [],
+                        enabled: true
+                    },
+                    {
+                        name: 'issuer_subj',
+                        label: '@i18n:objects.certmap.issuer_subject',
+                        fields: ['issuer', 'subject'],
+                        required: ['issuer', 'subject']
+                    }
+                ],
+                widgets: [
+                    {
+                        $type: 'multivalued',
+                        name: 'ipacertmapdata'
+                    },
+                    {
+                        $type: 'multivalued',
+                        name: 'certificate',
+                        child_spec: {
+                            $type: 'textarea'
+                        },
+                        tooltip: {
+                            title: '@mc-opt:user_add_certmapdata:certificate:doc'
+                        }
+                    },
+                    {
+                        name: 'issuer',
+                        tooltip: {
+                            title: '@mc-opt:user_add_certmapdata:issuer:doc'
+                        }
+                    },
+                    {
+                        name: 'subject',
+                        tooltip: {
+                            title: '@mc-opt:user_add_certmapdata:subject:doc'
+                        }
+                    }
+                ]
+            }
+        ]
+    };
+
+    var that = IPA.custom_command_multivalued_widget(spec);
+
+    that.create_remove_dialog_title = function(row) {
+        return text.get('@i18n:objects.certmap.deleter_title');
+    };
+
+    that.create_remove_dialog_message = function(row) {
+        var message = text.get('@i18n:objects.certmap.deleter_content');
+        message = message.replace('${data}', row.widget.new_value);
+
+        return message;
+    };
+
+    /**
+     * Compose options for add command.
+     * @return {Object} options
+     */
+    that.create_add_options = function() {
+        var options = {};
+        var widgets = that.adder_dialog.widgets.get_widgets();
+        var widget = widgets[0];
+        var inner_widgets = widget.widgets.get_widgets();
+
+        for (var i = 0, l = inner_widgets.length; i<l; i++) {
+            var w = inner_widgets[i];
+
+            if (w.enabled) {
+                var field = that.adder_dialog.fields.get_field(w.name);
+                var value = field.save();
+
+                if (field.name === 'issuer' || field.name === 'subject') {
+                    value = value[0];
+                }
+
+                if (!util.is_empty(value)) options[field.name] = value;
+            }
+        }
+
+        return options;
+    };
+
+
+    /**
+     * Compose options for remove command.
+     *
+     * @param {Object} row
+     * @return {Object} options
+     */
+    that.create_remove_options = function(row) {
+        var options = {};
+        var data = row.widget.new_value;
+
+        options['ipacertmapdata'] = data;
+
+        return options;
+    };
+
+    return that;
+};
+
+/**
+ * Certificat Mapping Rules entity specification object
+ * @member certmap
+ */
+certmap.certmaprule_spec = make_certmaprule_spec();
+
+/**
+ * Certificate Mapping Configuration entity specification object
+ * @member certmap
+ */
+certmap.certmapconfig_spec = make_certmapconfig_spec();
+
+
+/**
+ * Register entity
+ * @member cermap
+ */
+certmap.register = function() {
+    var e = reg.entity;
+    var w = reg.widget;
+
+    e.register({type: 'certmaprule', spec: certmap.certmaprule_spec});
+    e.register({type: 'certmapconfig', spec: certmap.certmapconfig_spec});
+    w.register('certmap_multivalued',
+                certmap.certmap_multivalued_widget);
+};
+
+phases.on('registration', certmap.register);
+
+return certmap;
+});
diff --git a/install/ui/src/freeipa/stageuser.js b/install/ui/src/freeipa/stageuser.js
index bf24491..f456189 100644
--- a/install/ui/src/freeipa/stageuser.js
+++ b/install/ui/src/freeipa/stageuser.js
@@ -147,6 +147,18 @@ return {
                             label: '@i18n:objects.sshkeystore.keys'
                         },
                         {
+                            $type: 'certmap_multivalued',
+                            name: 'ipacertmapdata',
+                            item_name: 'certmapdata',
+                            child_spec: {
+                                $type: 'non_editable_row',
+                                data_name: 'certmap'
+                            },
+                            tooltip: {
+                                title: '@mc:stageuser_add_certmapdata.doc'
+                            }
+                        },
+                        {
                             $type: 'checkboxes',
                             name: 'ipauserauthtype',
                             flags: ['w_if_no_aci'],
diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
index 8f78f2b..4bb0448 100644
--- a/install/ui/src/freeipa/user.js
+++ b/install/ui/src/freeipa/user.js
@@ -218,6 +218,18 @@ return {
                             label: '@i18n:objects.cert.certificates'
                         },
                         {
+                            $type: 'certmap_multivalued',
+                            name: 'ipacertmapdata',
+                            item_name: 'certmapdata',
+                            child_spec: {
+                                $type: 'non_editable_row',
+                                data_name: 'certmap'
+                            },
+                            tooltip: {
+                                title: '@mc:user_add_certmapdata.doc'
+                            }
+                        },
+                        {
                             $type: 'checkboxes',
                             name: 'ipauserauthtype',
                             flags: ['w_if_no_aci'],
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
index 7084d54..acd417b 100644
--- a/ipaserver/plugins/internal.py
+++ b/ipaserver/plugins/internal.py
@@ -464,6 +464,18 @@ class i18n_messages(Command):
                 "view_certificate": _("Certificate for ${entity} ${primary_key}"),
                 "view_certificate_btn": _("View Certificate"),
             },
+            "certmap": {
+                "adder_title": _("Add Certificate Mapping Data"),
+                "data_label": _("Certificate mapping data"),
+                "certificate": _("Certificate"),
+                "conf_str": _("Configuration string"),
+                "deleter_content": _("Do you want to remove certificate mapping data ${data}?"),
+                "deleter_title": _("Remove Certificate Mapping Data"),
+                "issuer": _("Issuer"),
+                "issuer_subject": _("Issuer and subject"),
+                "subject": _("Subject"),
+                "version": _("Version"),
+            },
             "config": {
                 "group": _("Group Options"),
                 "search": _("Search Options"),

From 552eeaf272ad5dea857e08c990903c02e6fad9b6 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 7 Mar 2017 21:28:32 +0100
Subject: [PATCH 5/8] WebUI: Add possibility to turn of autoload when
 details.load is called

When field on details facet has set 'autoload_value' to false, then it won't
be loaded using that.load method of details facet. That means that field
might stay unchanged even that loading of data was performed.

Part of: https://pagure.io/freeipa/issue/6601
---
 install/ui/src/freeipa/details.js | 3 ++-
 install/ui/src/freeipa/field.js   | 8 ++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/install/ui/src/freeipa/details.js b/install/ui/src/freeipa/details.js
index 9f0e632..87b355a 100644
--- a/install/ui/src/freeipa/details.js
+++ b/install/ui/src/freeipa/details.js
@@ -743,7 +743,8 @@ exp.details_facet = IPA.details_facet = function(spec, no_init) {
         var fields = that.fields.get_fields();
         for (var i=0; i<fields.length; i++) {
             var field = fields[i];
-            field.load(data);
+
+            if (field.autoload_value) field.load(data);
         }
         that.policies.post_load(data);
         that.post_load.notify([data], that);
diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index 9f287dd..4a63242 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -196,6 +196,14 @@ field.field = IPA.field = function(spec) {
     that.required = spec.required;
 
     /**
+     * Turns off loading value from command output on details pages.
+     * Used in certmap_match.
+     * @property {boolean}
+     */
+    that.autoload_value = spec.autoload_value === undefined ? true :
+                                spec.autoload_value;
+
+    /**
      * read_only is set when widget is created
      * @readonly
      * @property {boolean}

From d20cc327739533f02c94375711817fb56354bfb5 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 7 Mar 2017 21:30:00 +0100
Subject: [PATCH 6/8] WebUI: Possibility to choose object when API call returns
 list of objects

In case that API call returns array of objects which contains data, using
'object_index' attribute in adapter specification we can set which object
should be used.

It is possible to choose only one object specified by its index in array.

Part of: https://pagure.io/freeipa/issue/6601
---
 install/ui/src/freeipa/field.js | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index 4a63242..3b6b97b 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -819,6 +819,15 @@ field.Adapter = declare(null, {
     result_index: 0,
 
     /**
+     * When result of API call is an array of object this object index
+     * allows to specify exact object in array according to its position.
+     * Default value is null which means do not use object_index.
+     *
+     * @type {Number|null}
+     */
+     object_index: null,
+
+    /**
      * Name of the record which we want to extract from the result.
      * Used in dnslocations.
      * @type {String}
@@ -849,6 +858,10 @@ field.Adapter = declare(null, {
             else if (dr.results) {
                 var result = dr.results[this.result_index];
                 if (result) record = result[this.result_name];
+                var res_type = typeof record;
+                var obj_in_type = typeof this.object_index;
+                if (res_type === 'object' && obj_in_type === 'number')
+                    record = record[this.object_index];
             }
         }
         return record;

From f5cdd29d7e949ff123b0badb32e18d458f3a02f2 Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 7 Mar 2017 21:30:45 +0100
Subject: [PATCH 7/8] WebUI: Add Adapter for certmap_match result table

Result of certmap_match command is in the following format:
[{domain: 'domain1', uid:[uid11,uid12,uid13]}, {domain: 'domain2',
uid:[uid21, uid22, uid23},...]

For correct displaying in table we need to reformat it to the following:
[{domain: 'domain1', uid: 'uid11'}, {domain: 'domain1', uid: 'uid12'},...

This can be done using this Adapter.

Part of: https://pagure.io/freeipa/issue/6601
---
 install/ui/src/freeipa/field.js | 79 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 79 insertions(+)

diff --git a/install/ui/src/freeipa/field.js b/install/ui/src/freeipa/field.js
index 3b6b97b..dde2837 100644
--- a/install/ui/src/freeipa/field.js
+++ b/install/ui/src/freeipa/field.js
@@ -1462,6 +1462,84 @@ field.AlternateAttrFieldAdapter = declare([field.Adapter], {
 
 
 /**
+ * Custom adapter specifically implemented for certmap_match where it
+ * transform items in format {domain: "xxx", uid: [arrayof_uids]} to
+ * {[{domain: "xxx", uid: "uid1"}, {domain: "xxx", uid: 'uid2'}, ...]}.
+ * This is necessary for possibility to correctly display table.
+ *
+ * @class
+ * @extends field.Adapter
+ */
+field.CertMatchTransformAdapter = declare([field.Adapter], {
+
+    /**
+    * @param {Array} record
+    */
+    transform_one_record: function(record) {
+        var domain = record.domain;
+        var uids = record.uid;
+        var results = [];
+
+        for (var i=0, l=uids.length; i<l; i++) {
+            results.push({
+                domain: domain,
+                uid: uids[i]
+            });
+        }
+
+        return results;
+    },
+
+    /**
+     * Transform record to array of arrays with objects in the following format:
+     * {domain: 'xxx', uid: 'uid1'}
+     *
+     * @param {Array|Object} record
+     */
+    transform_record: function(record) {
+        if (lang.isArray(record)) {
+            for (var i=0, l=record.length; i<l; i++) {
+                record[i] = this.transform_one_record(record[i]);
+            }
+        } else {
+            record = this.transform_one_record(record);
+        }
+    },
+
+    /**
+     * Merge array of arrays of object into array of objects.
+     *
+     * @param {Array} records
+     */
+    merge_object_into_array: function(records) {
+        if (!lang.isArray(records)) return records;
+
+        var merged = [];
+        for (var i=0, l=records.length; i<l; i++) {
+            merged = merged.concat(records[i]);
+        }
+
+        return merged;
+    },
+
+    /**
+     *
+     * @param {Object} data Object which contains the record or the record
+     * @returns {Array} attribute values
+     */
+    load: function(data) {
+        var record = this.get_record(data);
+
+        this.transform_record(record);
+
+        var values = this.merge_object_into_array(record);
+
+        return values;
+    }
+});
+
+
+/**
  * Field for enabling/disabling entity
  *
  * - expects radio widget
@@ -1735,6 +1813,7 @@ field.register = function() {
     l.register('adapter', field.Adapter);
     l.register('object_adapter', field.ObjectAdapter);
     l.register('alternate_attr_field_adapter', field.AlternateAttrFieldAdapter);
+    l.register('certmatch_transform', field.CertMatchTransformAdapter);
 };
 phases.on('registration', field.register);
 

From f8d70b07b8d73822961909c17dbc1febbcc7ba8c Mon Sep 17 00:00:00 2001
From: Pavel Vomacka <pvoma...@redhat.com>
Date: Tue, 7 Mar 2017 21:31:22 +0100
Subject: [PATCH 8/8] WebUI: Add cermapmatch module

Add module which can show users which are mapped to the provided certificate.
Additionaly, the certificate is parsed and parsed information are
also displayed.

https://pagure.io/freeipa/issue/6601
---
 install/ui/src/freeipa/app.js                  |   1 +
 install/ui/src/freeipa/navigation/menu_spec.js |   4 +
 install/ui/src/freeipa/plugins/certmap.js      |   3 +-
 install/ui/src/freeipa/plugins/certmapmatch.js | 386 +++++++++++++++++++++++++
 ipaserver/plugins/internal.py                  |  12 +
 5 files changed, 405 insertions(+), 1 deletion(-)
 create mode 100644 install/ui/src/freeipa/plugins/certmapmatch.js

diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js
index d262a64..5e70dba 100644
--- a/install/ui/src/freeipa/app.js
+++ b/install/ui/src/freeipa/app.js
@@ -33,6 +33,7 @@ define([
     './plugins/caacl',
     './plugins/certprofile',
     './plugins/certmap',
+    './plugins/certmapmatch',
     './dns',
     './group',
     './hbac',
diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js
index 5f1d388..8d13da1 100644
--- a/install/ui/src/freeipa/navigation/menu_spec.js
+++ b/install/ui/src/freeipa/navigation/menu_spec.js
@@ -178,6 +178,10 @@ var nav = {};
                         {
                             entity: 'certmapconfig',
                             facet: 'details'
+                        },
+                        {
+                            label: '@i18n:objects.certmap_match.facet_label',
+                            entity: 'certmap_match'
                         }
                     ]
                 }
diff --git a/install/ui/src/freeipa/plugins/certmap.js b/install/ui/src/freeipa/plugins/certmap.js
index ddbc5a7..ecbe095 100644
--- a/install/ui/src/freeipa/plugins/certmap.js
+++ b/install/ui/src/freeipa/plugins/certmap.js
@@ -30,7 +30,8 @@ var certmap = IPA.certmap = {
     search_facet_group: {
         facets: {
             certmaprule_search: 'certmaprule_search',
-            certmapconfig: 'certmapconfig_details'
+            certmapconfig: 'certmapconfig_details',
+            certmapmatch: 'certmapmatch_details'
         }
     }
 };
diff --git a/install/ui/src/freeipa/plugins/certmapmatch.js b/install/ui/src/freeipa/plugins/certmapmatch.js
new file mode 100644
index 0000000..fcf00dd
--- /dev/null
+++ b/install/ui/src/freeipa/plugins/certmapmatch.js
@@ -0,0 +1,386 @@
+//
+// Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+//
+
+define([
+        'dojo/_base/lang',
+        'dojo/_base/declare',
+        'dojo/Evented',
+        'dojo/on',
+        '../metadata',
+        '../ipa',
+        '../phases',
+        '../reg',
+        '../rpc',
+        '../widget',
+        '../util',
+        // plain imports
+        '../search',
+        '../entity'],
+            function(lang, declare, Evented, on, metadata_provider, IPA, phases,
+                    reg, rpc, widget_mod, util ) {
+
+var certmapmatch = IPA.certmapmatch = {};
+
+var make_certmap_spec = function() {
+return {
+    name: 'certmap_match',
+    facets: [
+        {
+
+            $factory: certmapmatch.details_certmapmatch_facet,
+            disable_breadcrumb: true,
+            no_update: true,
+            name: 'cert',
+            label: "@i18n:objects.certmap_match.facet_label",
+            actions: [
+                'match', 'clear'
+            ],
+            control_buttons: [
+                {
+                    name: 'match',
+                    title: '@i18n:buttons.match_title',
+                    label: '@i18n:buttons.match',
+                    icon: 'fa-gear'
+                },
+                {
+                    name: 'clear',
+                    title: '@i18n:buttons.clear_title',
+                    label: '@i18n:buttons.clear',
+                    icon: 'fa-refresh'
+                }
+            ],
+            sections: [
+                {
+                    name: 'cert_input',
+                    label: '@i18n:objects.certmap_match.cert_for_match',
+                    fields: [
+                        {
+                            $type: 'cert_textarea',
+                            name: 'cert_textarea',
+                            label: '@i18n:objects.cert.certificate',
+                            autoload_value: false,
+                            undo: false,
+                            rows: 20,
+                            cols: 70
+                        }
+                    ]
+                },
+                {
+                    name: 'parsed_cert',
+                    label: '@i18n:objects.certmap_match.cert_data',
+                    fields: [
+                        {
+                            name: 'issuer',
+                            label: '@i18n:objects.cert.issued_by',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'subject',
+                            label: '@i18n:objects.cert.issued_to',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'serial_number',
+                            label: '@i18n:objects.cert.serial_number',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'serial_number_hex',
+                            label: '@i18n:objects.cert.serial_number_hex',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'valid_not_before',
+                            label: '@i18n:objects.cert.valid_from',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'valid_not_after',
+                            label: '@i18n:objects.cert.valid_to',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'sha1_fingerprint',
+                            label: '@i18n:objects.cert.sha1_fingerprint',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        },
+                        {
+                            name: 'sha256_fingerprint',
+                            label: '@i18n:objects.cert.sha256_fingerprint',
+                            adapter: {
+                                object_index: 0,
+                                result_index: 1
+                            },
+                            read_only: true
+                        }
+                    ]
+                },
+                {
+                    $factory: IPA.section,
+                    name: 'divider',
+                    layout_css_class: 'col-sm-12 col-sm-12',
+                    fields: []
+                },
+                {
+                    name: 'user_result_table',
+                    label: '@i18n:objects.certmap_match.matched_users',
+                    layout: {
+                        $factory: widget_mod.fluid_layout,
+                        widget_cls: "col-sm-12 col-sm-12",
+                        label_cls: "hide"
+                    },
+                    layout_css_class: 'col-md-12 col-sm-12',
+                    fields: [
+                        {
+                            $type: 'association_table',
+                            name: 'result_table',
+                            read_only: true,
+                            selectable: false,
+                            other_entity: 'user',
+                            adapter: {
+                                $type: 'certmatch_transform'
+                            },
+                            columns: [
+                                {
+                                    name: 'uid',
+                                    label: '@i18n:objects.certmap_match.userlogin'
+                                },
+                                {
+                                    name: 'domain',
+                                    label: '@i18n:objects.certmap_match.domain'
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        }
+    ]
+};};
+
+
+/**
+ * Artificial entity created from command which does not have its own entity
+ *
+ * @class certmapmatch.certmapmatch_entity
+ * @extends IPA.entity
+ */
+certmapmatch.certmapmatch_entity = function(spec) {
+    var that = IPA.entity(spec);
+
+    that.get_default_metadata = function() {
+        return metadata_provider.get('@mc:'+that.name);
+    };
+
+    return that;
+};
+
+/**
+ * Custom facet which is used for showing certmap match information
+ *
+ * @class certmapmatch.details_certmapmatch_facet
+ * @extends IPA.details_facet
+ */
+certmapmatch.details_certmapmatch_facet = function(spec) {
+
+    spec = spec || {};
+
+    var that = IPA.details_facet(spec);
+
+    that.refresh = function() {};
+
+    // always not dirty
+    that.is_dirty = function() {
+        return false;
+    };
+
+    that.get_result_table_widget = function() {
+        return that.widgets.get_widget("user_result_table.result_table");
+    };
+
+    that.update_result_table_summary = function(summary) {
+        var result_w = that.get_result_table_widget();
+        result_w.summary.text(summary);
+    };
+
+    that.clean_result = function() {
+        var result_w = that.get_result_table_widget();
+        result_w.empty();
+        that.update_result_table_summary('');
+    };
+
+    that.clean_cert_info = function() {
+        var widgets = that.widgets.get_widget('parsed_cert').widgets.get_widgets();
+
+        for (var i=0, l=widgets.length; i<l; i++) {
+            var widget = widgets[i];
+
+            widget.update();
+        }
+    };
+
+    that.obtain_cert = function() {
+        var cert_w = that.widgets.get_widget('cert_input.cert_textarea');
+
+        return cert_w.save();
+    };
+
+    that.on_cert_match = function(data) {
+        that.clean_result();
+        that.clean_cert_info();
+        var cert = that.obtain_cert();
+
+        if (util.is_empty(cert)) return;
+
+        var batch_command = rpc.batch_command({
+            name: 'certmap-match-batch',
+            show_error: false
+        });
+
+        var command = rpc.command({
+            method: 'certmap_match',
+            args: cert
+        });
+
+        batch_command.add_command(command);
+
+        command = rpc.command({
+            entity: 'cert',
+            method: 'find',
+            options: {
+                certificate: cert[0],
+                all: true
+            }
+        });
+
+        batch_command.add_command(command);
+
+        batch_command.on_success = function(data, text_status, xhr) {
+            // Error handling needs to be here because cert-find never fails,
+            // therefore batch_command always calls on_success method.
+            var certmatch_r = data.result.results[0];
+            if (certmatch_r.error === null) {
+                //no error
+                that.load(data);
+                that.update_result_table_summary(certmatch_r.summary);
+                IPA.notify_success(certmatch_r.summary);
+            } else {
+                that.update_result_table_summary(certmatch_r.error);
+                IPA.notify(certmatch_r.error, 'error');
+            }
+        };
+
+        batch_command.execute();
+    };
+
+    that.on_clear_facet = function() {
+        that.clean_result();
+        that.clean_cert_info();
+    };
+
+    that.init = function() {
+        on(that, 'cert-match', that.on_cert_match);
+        on(that, 'clear-facet', that.on_clear_facet);
+    };
+
+    return that;
+};
+
+/**
+ * Action which run certmap match.
+ *
+ * @class certmapmatch.match_action
+ * @extends IPA.object_action
+ */
+certmapmatch.match_action = function(spec) {
+    spec = spec || {};
+    spec.name = spec.name || 'match';
+
+    var that = IPA.object_action(spec);
+
+    that.execute_action = function(facet) {
+        facet.emit('cert-match');
+    };
+
+    return that;
+};
+
+
+/**
+ * Action which allows to clean whole facet.
+ *
+ * @class certmapmatch.clean_action
+ * @extends IPA.object_action
+ */
+certmapmatch.clear_action = function(spec) {
+    spec = spec || {};
+    spec.name = spec.name || 'clear';
+
+    var that = IPA.object_action(spec);
+
+    that.execute_action = function(facet) {
+        facet.emit('clear-facet');
+    };
+
+    return that;
+};
+
+/**
+ * Certificate Mapping Configuration entity specification object
+ * @member certmap
+ */
+certmapmatch.certmap_spec = make_certmap_spec();
+
+
+/**
+ * Register entity
+ * @member cermap
+ */
+certmapmatch.register = function() {
+    var e = reg.entity;
+    var f = reg.field;
+    var a = reg.action;
+
+    a.register('match', certmapmatch.match_action);
+    a.register('clear', certmapmatch.clear_action);
+    f.register('cert_textarea', certmapmatch.cert_textarea_field);
+    e.register({
+        type: 'certmap_match',
+        spec: certmapmatch.certmap_spec,
+        factory: certmapmatch.certmapmatch_entity
+    });
+};
+
+phases.on('registration', certmapmatch.register);
+
+return certmapmatch;
+});
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
index acd417b..bf0726f 100644
--- a/ipaserver/plugins/internal.py
+++ b/ipaserver/plugins/internal.py
@@ -219,6 +219,8 @@ class i18n_messages(Command):
             "apply": _("Apply"),
             "back": _("Back"),
             "cancel": _("Cancel"),
+            "clear": _("Clear"),
+            "clear_title": _("Clear result fields on the page."),
             "close": _("Close"),
             "disable": _("Disable"),
             "download": _("Download"),
@@ -230,6 +232,8 @@ class i18n_messages(Command):
             "get": _("Get"),
             "hide": _("Hide"),
             "issue": _("Issue"),
+            "match": _("Match"),
+            "match_title": _("Match users according to certificate."),
             "ok": _("OK"),
             "refresh": _("Refresh"),
             "refresh_title": _("Reload current settings from the server."),
@@ -464,6 +468,14 @@ class i18n_messages(Command):
                 "view_certificate": _("Certificate for ${entity} ${primary_key}"),
                 "view_certificate_btn": _("View Certificate"),
             },
+            "certmap_match": {
+                "cert_data": _("Certificate Data"),
+                "cert_for_match": _("Certificate For Match"),
+                "facet_label": _("Certificate Mapping Match"),
+                "domain": _("Domain"),
+                "matched_users": _("Matched Users"),
+                "userlogin": _("User Login"),
+            },
             "certmap": {
                 "adder_title": _("Add Certificate Mapping Data"),
                 "data_label": _("Certificate mapping data"),
-- 
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