From 51a4e42dd777661addd4f2fed1654ee978e8a4d7 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 3 May 2022 11:54:47 +0300 Subject: [PATCH] External IdP: add Web UI to manage IdP references Signed-off-by: Alexander Bokovoy Reviewed-By: Francisco Trivino Reviewed-By: Sumit Bose Reviewed-By: Francisco Trivino Reviewed-By: Sumit Bose --- install/ui/src/freeipa/app.js | 1 + install/ui/src/freeipa/host.js | 4 + install/ui/src/freeipa/idp.js | 296 ++++++++++++++++++ .../ui/src/freeipa/navigation/menu_spec.js | 1 + install/ui/src/freeipa/policy.js | 11 + install/ui/src/freeipa/serverconfig.js | 3 +- install/ui/src/freeipa/service.js | 4 + install/ui/src/freeipa/stageuser.js | 3 +- install/ui/src/freeipa/user.js | 14 +- install/ui/test/data/i18n_messages.json | 1 + ipaserver/plugins/internal.py | 11 + 11 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 install/ui/src/freeipa/idp.js diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js index 9e0007528..f98a96002 100644 --- a/install/ui/src/freeipa/app.js +++ b/install/ui/src/freeipa/app.js @@ -58,6 +58,7 @@ define([ './topology', './user', './vault', + './idp', 'dojo/domReady!' ],function(app_container) { return app_container; diff --git a/install/ui/src/freeipa/host.js b/install/ui/src/freeipa/host.js index c4c1e0491..c5d8dca6e 100644 --- a/install/ui/src/freeipa/host.js +++ b/install/ui/src/freeipa/host.js @@ -136,6 +136,10 @@ return { { label: '@i18n:authtype.type_hardened', value: 'hardened' + }, + { + label: '@i18n:authtype.type_idp', + value: 'idp' } ], tooltip: { diff --git a/install/ui/src/freeipa/idp.js b/install/ui/src/freeipa/idp.js new file mode 100644 index 000000000..ada09c075 --- /dev/null +++ b/install/ui/src/freeipa/idp.js @@ -0,0 +1,296 @@ +// Copyright (C) 2022 FreeIPA Contributors see COPYING for license + +define([ + 'dojo/on', + './ipa', + './jquery', + './menu', + './phases', + './reg', + './text', + './details', + './search', + './entity', + './dialogs/password' + ], + function(on, IPA, $, menu, phases, reg, text) { + +/** + * IdP module + * @class + * @singleton + */ +var idp = IPA.idp = {}; + +// Templates are hardcoded in idp_add class +// and cannot be retrieved with an API call +// Structure below references template names +// and additional fields per template +idp.templates = [ + { value: 'keycloak', + label: text.get('@i18n:objects.idp.template_keycloak'), + fields: ['ipaidporg', 'ipaidpbaseurl']}, + { value: 'google', + label: text.get('@i18n:objects.idp.template_google'), + fields: []}, + { value: 'github', + label: text.get('@i18n:objects.idp.template_github'), + fields: []}, + { value: 'microsoft', + label: text.get('@i18n:objects.idp.template_microsoft'), + fields: ['ipaidporg']}, + { value: 'okta', + label: text.get('@i18n:objects.idp.template_okta'), + fields: ['ipaidporg', 'ipaidpbaseurl']} +]; + + +var make_spec = function() { +return { + name: 'idp', + enable_test: function() { + return true; + }, + facets: [ + { + $type: 'search', + columns: [ + 'cn', + 'ipaidpclientid', + 'ipaidpscope', + 'description' + ] + }, + { + $type: 'details', + sections: [ + { + name: 'idpclient', + label: '@i18n:objects.idp.label_idpclient', + fields: [ + 'cn', + 'ipaidpclientid', + { + $type: 'password', + name: 'ipaidpclientsecret', + flags: ['w_if_no_aci'] + }, + ] + }, + { + name: 'idp', + label: '@i18n:objects.idp.label_idp', + fields: [ + 'ipaidpscope', + 'ipaidpsub', + 'ipaidpauthendpoint', + 'ipaidpdevauthendpoint', + 'ipaidptokenendpoint', + 'ipaidpuserinfoendpoint', + 'ipaidpkeysendpoint', + 'ipaidpissuerurl' + ] + } + ], + actions: [ + { + $type: 'password', + dialog: { + password_name: 'ipaidpclientsecret' + } + } + ], + header_actions: ['password'] + } + ], + adder_dialog: { + title: '@i18n:objects.idp.add', + policies: [ + IPA.add_idp_policy + ], + sections: [ + { + name: 'idpclientsetup', + label: '@i18n:objects.idp.label_idpclient', + fields: [ + 'cn', + 'ipaidpclientid', + { + $type: 'password', + name: 'ipaidpclientsecret' + }, + { + $type: 'password', + name: 'ipaidpclientsecret_verify', + label: '@i18n:objects.idp.verify_secret', + flags: ['no_command'], + required: false, + validators: [{ + $type: 'same_password', + other_field: 'ipaidpclientsecret' + }] + } + ] + }, + { + name: 'idpsetup', + label: '@i18n:objects.idp.label_idp', + fields: [ + { + name: 'type', + label: 'Provider type', + $type: 'radio', + flags: ['no_command'], + layout: 'vertical', + default_value: 'template', + options: [ + { + value: 'template', + label: 'Pre-defined IdP template', + }, + { + value: 'custom', + label: 'Custom IdP definition', + } + ] + }, + { + label: '@i18n:idp.provider', + name: 'ipaidpprovider', + $type: 'select', + options: IPA.create_options(idp.templates) + }, + { + name: 'ipaidporg', + label: '@i18n:objects.idp.ipaidporg', + metadata: '@mc-opt:idp_add:ipaidporg' + }, + { + name: 'ipaidpbaseurl', + label: '@i18n:objects.idp.ipaidpbaseurl', + metadata: '@mc-opt:idp_add:ipaidpbaseurl' + }, + 'ipaidpscope', + 'ipaidpsub', + 'ipaidpauthendpoint', + 'ipaidpdevauthendpoint', + 'ipaidptokenendpoint', + 'ipaidpuserinfoendpoint', + 'ipaidpkeysendpoint' + ] + }, + ] + }, + deleter_dialog: { + title: '@i18n:objects.idp.remove' + } +};}; + +IPA.add_idp_policy = function() { + // Custom provider fields + // need to be hidden for a chosen template + // and show for the custom provider + var custom_fields = [ + 'ipaidpauthendpoint', + 'ipaidpdevauthendpoint', + 'ipaidptokenendpoint', + 'ipaidpuserinfoendpoint', + 'ipaidpkeysendpoint' + ]; + + // Template may require an additional field + // Make it required and visible in that case + var template_fields = [ + 'ipaidporg', + 'ipaidpbaseurl' + ]; + + var that = IPA.facet_policy(); + + that.init = function() { + // Handle choice of either pre-defined or custom IdP + var type_f = that.container.fields.get_field('type'); + on(type_f, 'value-change', that.on_type_change); + + // Handle choice of a pre-defined IdP to show/remove additional fields + var prov_f = that.container.fields.get_field('ipaidpprovider'); + on(prov_f, 'value-change', that.on_prov_change); + }; + + that.on_type_change = function() { + var type_f = that.container.fields.get_field('type'); + var mode = type_f.get_value()[0]; + var show_custom = true; + + if (mode === 'template') show_custom = false; + + // For custom template we show custom fields + // and mark all of them required and passed to the RPC + // If show_custom is false, the opposite happens + custom_fields.forEach(fname => { + widget_f = that.container.fields.get_field(fname); + widget_f.set_required(show_custom); + widget_f.set_enabled(show_custom); + widget_f.widget.set_visible(show_custom); + }); + + // For template fields we show them if custom aren't shown + template_fields.forEach(fname => { + widget_f = that.container.fields.get_field(fname); + widget_f.set_enabled(!show_custom); + widget_f.widget.set_visible(!show_custom); + }); + + widget_f = that.container.fields.get_field('ipaidpprovider'); + widget_f.set_required(!show_custom); + widget_f.set_enabled(!show_custom); + widget_f.widget.set_visible(!show_custom); +}; + + that.on_prov_change = function() { + var prov_f = that.container.fields.get_field('ipaidpprovider'); + var value = prov_f.get_value()[0]; + + // First, clear template fields from the previous provider choice + template_fields.forEach(fname => { + widget_f = that.container.fields.get_field(fname); + widget_f.widget.set_visible(false); + widget_f.set_required(false); + widget_f.set_enabled(false); + }); + + // Second, enable and get required template-specific fields + idp.templates.forEach(idp_v => { + if (idp_v['value'] == value) { + idp_v['fields'].forEach(fname => { + widget_f = that.container.fields.get_field(fname); + widget_f.set_required(true); + widget_f.set_enabled(true); + widget_f.widget.set_visible(true); + }); + } + } + + ) + }; + + return that; +}; + +/** + * IdP specification object + */ +idp.spec = make_spec(); + +/** + * Register IdP entity + */ +idp.register = function() { + var e = reg.entity; + e.register({type: 'idp', spec: idp.spec}); +}; + +phases.on('registration', idp.register); + +return idp; +}); diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js index a205dfade..41f01a728 100644 --- a/install/ui/src/freeipa/navigation/menu_spec.js +++ b/install/ui/src/freeipa/navigation/menu_spec.js @@ -185,6 +185,7 @@ var nav = {}; }, { entity: 'otptoken' }, { entity: 'radiusproxy' }, + { entity: 'idp' }, { entity: 'certmaprule', facet: 'search', diff --git a/install/ui/src/freeipa/policy.js b/install/ui/src/freeipa/policy.js index c8fc26f61..fa2028a52 100644 --- a/install/ui/src/freeipa/policy.js +++ b/install/ui/src/freeipa/policy.js @@ -166,7 +166,18 @@ return { name: 'krbauthindmaxticketlife_hardened', acl_param: 'krbauthindmaxticketlife', measurement_unit: 'seconds' + }, + { + name: 'krbauthindmaxrenewableage_idp', + acl_param: 'krbauthindmaxrenewableage', + measurement_unit: 'seconds' + }, + { + name: 'krbauthindmaxticketlife_idp', + acl_param: 'krbauthindmaxticketlife', + measurement_unit: 'seconds' } + ] } ], diff --git a/install/ui/src/freeipa/serverconfig.js b/install/ui/src/freeipa/serverconfig.js index 9de0bab4b..145b0e21b 100644 --- a/install/ui/src/freeipa/serverconfig.js +++ b/install/ui/src/freeipa/serverconfig.js @@ -112,7 +112,8 @@ return { { label: '@i18n:authtype.type_radius', value: 'radius' }, { label: '@i18n:authtype.type_otp', value: 'otp' }, { label: '@i18n:authtype.type_pkinit', value: 'pkinit' }, - { label: '@i18n:authtype.type_hardened', value: 'hardened' } + { label: '@i18n:authtype.type_hardened', value: 'hardened' }, + { label: '@i18n:authtype.type_idp', value: 'idp' } ], tooltip: { title: '@i18n:authtype.config_tooltip', diff --git a/install/ui/src/freeipa/service.js b/install/ui/src/freeipa/service.js index 47881a0a8..ab79c894f 100644 --- a/install/ui/src/freeipa/service.js +++ b/install/ui/src/freeipa/service.js @@ -146,6 +146,10 @@ return { { label: '@i18n:authtype.type_hardened', value: 'hardened' + }, + { + label: '@i18n:authtype.type_idp', + value: 'idp' } ], tooltip: { diff --git a/install/ui/src/freeipa/stageuser.js b/install/ui/src/freeipa/stageuser.js index 851a4a9e5..8330a19ab 100644 --- a/install/ui/src/freeipa/stageuser.js +++ b/install/ui/src/freeipa/stageuser.js @@ -171,7 +171,8 @@ return { { label: '@i18n:authtype.type_radius', value: 'radius' }, { label: '@i18n:authtype.type_otp', value: 'otp' }, { label: '@i18n:authtype.type_pkinit', value: 'pkinit' }, - { label: '@i18n:authtype.type_hardened', value: 'hardened' } + { label: '@i18n:authtype.type_hardened', value: 'hardened' }, + { label: '@i18n:authtype.type_idp', value: 'idp' } ], tooltip: '@i18n:authtype.user_tooltip' }, diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js index 6a56320c5..a580db035 100644 --- a/install/ui/src/freeipa/user.js +++ b/install/ui/src/freeipa/user.js @@ -239,7 +239,8 @@ return { { label: '@i18n:authtype.type_radius', value: 'radius' }, { label: '@i18n:authtype.type_otp', value: 'otp' }, { label: '@i18n:authtype.type_pkinit', value: 'pkinit' }, - { label: '@i18n:authtype.type_hardened', value: 'hardened' } + { label: '@i18n:authtype.type_hardened', value: 'hardened' }, + { label: '@i18n:authtype.type_idp', value: 'idp' } ], tooltip: { title: '@i18n:authtype.user_tooltip', @@ -256,6 +257,17 @@ return { { name: 'ipatokenradiususername', flags: ['w_if_no_aci'] + }, + { + $type: 'entity_select', + name: 'ipaidpconfiglink', + flags: ['w_if_no_aci'], + other_entity: 'idp', + other_field: 'cn' + }, + { + name: 'ipaidpsub', + flags: ['w_if_no_aci'] } ] }, diff --git a/install/ui/test/data/i18n_messages.json b/install/ui/test/data/i18n_messages.json index fda1d8bf9..49d288326 100644 --- a/install/ui/test/data/i18n_messages.json +++ b/install/ui/test/data/i18n_messages.json @@ -60,6 +60,7 @@ "type_radius": "RADIUS", "type_pkinit": "PKINIT", "type_hardened": "Hardened Password (by SPAKE or FAST)", + "type_idp": "External Identity Provider", "user_tooltip": "

Per-user setting, overwrites the global setting if any option is checked.

Password + Two-factor: LDAP and Kerberos allow authentication with either one of the authentication types but Kerberos uses pre-authentication method which requires to use armor ccache.

RADIUS with another type: Kerberos always use RADIUS, but LDAP never does. LDAP only recognize the password and two-factor authentication options.

" }, "buttons": { diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py index 29e09f006..5ffa7a281 100644 --- a/ipaserver/plugins/internal.py +++ b/ipaserver/plugins/internal.py @@ -191,6 +191,7 @@ class i18n_messages(Command): "type_radius": _("RADIUS"), "type_pkinit": _("PKINIT"), "type_hardened": _("Hardened Password (by SPAKE or FAST)"), + "type_idp": _("External Identity Provider"), "type_disabled": _("Disable per-user override"), "user_tooltip": _("

Per-user setting, overwrites the global setting if any option is checked.

Password + Two-factor: LDAP and Kerberos allow authentication with either one of the authentication types but Kerberos uses pre-authentication method which requires to use armor ccache.

RADIUS with another type: Kerberos always use RADIUS, but LDAP never does. LDAP only recognize the password and two-factor authentication options.

"), }, @@ -1091,6 +1092,16 @@ class i18n_messages(Command): "Remove host groups from host group '${primary_key}'" ), }, + "idp": { + "template_keycloak": _("Keycloak or Red Hat SSO"), + "template_google": _("Google"), + "template_github": _("Github"), + "template_microsoft": _("Microsoft or Azure"), + "template_okta": _("Okta"), + "label_idpclient": _("OAuth 2.0 client details"), + "label_idp": _("Identity provider details"), + "verify_secret": _("Verify secret"), + }, "idoverrideuser": { "anchor_label": _("User to override"), "anchor_tooltip": _("Enter trusted or IPA user login. Note: search doesn't list users from trusted domains."),