URL: https://github.com/freeipa/freeipa/pull/433
Author: LiptonB
 Title: #433: csrgen: Allow some certificate fields to be specified by the user
Action: opened

PR body:
"""
These patches allow CSR generation rules to contain a "prompt," which will 
cause data to be requested from the user and interpolated into the CSR.

The second commit runs the prompt through gettext. As I asked about 
[here](https://www.redhat.com/archives/freeipa-devel/2016-August/msg00823.html),
 I'm not sure if this is useful because the prompt strings in the rule files 
won't be recognized as translatable. But I decided to include the commit for 
discussion.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/433/head:pr433
git checkout pr433
From 96f4e25a4770bd2076390301adcee53d55086fa2 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Thu, 28 Jul 2016 16:21:44 -0400
Subject: [PATCH 1/3] csrgen: Implement fields that prompt user for data

Allows some data to be user-specified rather than coming out of the
database. The provided data can be formatted with jinja2 rules just as
database values can.

https://fedorahosted.org/freeipa/ticket/4899
---
 install/share/csrgen/Makefile.am                   |  1 +
 .../share/csrgen/rules/dataEmailUserSpecified.json | 16 ++++++++++
 ipaclient/csrgen.py                                | 36 ++++++++++++++++++++--
 ipaclient/plugins/csrgen.py                        |  9 ++++--
 ipatests/test_ipaclient/test_csrgen.py             | 15 ++++-----
 5 files changed, 66 insertions(+), 11 deletions(-)
 create mode 100644 install/share/csrgen/rules/dataEmailUserSpecified.json

diff --git a/install/share/csrgen/Makefile.am b/install/share/csrgen/Makefile.am
index 12c62c4..ad4412e 100644
--- a/install/share/csrgen/Makefile.am
+++ b/install/share/csrgen/Makefile.am
@@ -10,6 +10,7 @@ ruledir = $(IPA_DATA_DIR)/csrgen/rules
 rule_DATA =				\
 	rules/dataDNS.json		\
 	rules/dataEmail.json		\
+	rules/dataEmailUserSpecified.json	\
 	rules/dataHostCN.json		\
 	rules/dataUsernameCN.json	\
 	rules/dataSubjectBase.json	\
diff --git a/install/share/csrgen/rules/dataEmailUserSpecified.json b/install/share/csrgen/rules/dataEmailUserSpecified.json
new file mode 100644
index 0000000..3fb2fb1
--- /dev/null
+++ b/install/share/csrgen/rules/dataEmailUserSpecified.json
@@ -0,0 +1,16 @@
+{
+  "rules": [
+    {
+      "helper": "openssl",
+      "template": "email = {{userdata.email}}"
+    },
+    {
+      "helper": "certutil",
+      "template": "email:{{userdata.email|quote}}"
+    }
+  ],
+  "options": {
+    "data_source": "userdata.email",
+    "prompt": "Email address"
+  }
+}
diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
index 96100ae..2c1c5fc 100644
--- a/ipaclient/csrgen.py
+++ b/ipaclient/csrgen.py
@@ -345,8 +345,9 @@ class CSRGenerator(object):
     def __init__(self, rule_provider):
         self.rule_provider = rule_provider
 
-    def csr_script(self, principal, config, profile_id, helper):
-        render_data = {'subject': principal, 'config': config}
+    def csr_script(self, principal, config, userdata, profile_id, helper):
+        render_data = {
+            'subject': principal, 'config': config, 'userdata': userdata}
 
         formatter = self.FORMATTERS[helper]()
         rules = self.rule_provider.rules_for_profile(profile_id, helper)
@@ -360,3 +361,34 @@ def csr_script(self, principal, config, profile_id, helper):
                 'Template error when formatting certificate data'))
 
         return script
+
+    def get_user_prompts(self, profile_id, helper):
+        prompts = {}
+        syntax_rules = []
+        rules = self.rule_provider.rules_for_profile(profile_id, helper)
+
+        for field_mapping in rules:
+            for rule in field_mapping.data_rules:
+                if 'prompt' in rule.options:
+                    try:
+                        var = rule.options['data_source']
+                    except KeyError:
+                        raise errors.CertificateMappingError(reason=_(
+                            'Certificate mapping rule %(rule)s has a prompt'
+                            ' but no data_source set') % {'rule': rule.name})
+                    if var in prompts:
+                        raise errors.CertificateMappingError(reason=_(
+                            'More than one data rule in this profile prompts'
+                            ' for the %(item)s data item') % {'item': var})
+                    var_parts = var.split('.')
+                    if len(var_parts) != 2 or var_parts[0] != 'userdata':
+                        raise errors.CertificateMappingError(
+                            reason=_(
+                                'Format of variable name in rule %(rule)s is'
+                                ' incorrect. Rules that prompt for data must'
+                                ' use a variable "userdata.<var_name>"') %
+                            {'rule': rule.name})
+
+                    prompts[var_parts[1]] = rule.options['prompt']
+
+        return prompts
diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
index 0669a47..d480946 100644
--- a/ipaclient/plugins/csrgen.py
+++ b/ipaclient/plugins/csrgen.py
@@ -82,6 +82,9 @@ def execute(self, *args, **options):
         if not backend.isconnected():
             backend.connect()
 
+        generator = CSRGenerator(FileRuleProvider())
+        prompts = generator.get_user_prompts(profile_id, helper)
+
         try:
             if principal.is_host:
                 principal_obj = api.Command.host_show(
@@ -98,10 +101,12 @@ def execute(self, *args, **options):
         principal_obj = principal_obj['result']
         config = api.Command.config_show()['result']
 
-        generator = CSRGenerator(FileRuleProvider())
+        userdata = {}
+        for name, prompt in prompts.items():
+            userdata[name] = self.Backend.textui.prompt(prompt)
 
         script = generator.csr_script(
-            principal_obj, config, profile_id, helper)
+            principal_obj, config, userdata, profile_id, helper)
 
         result = {}
         if 'out' in options:
diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py
index 556f8e0..b056042 100644
--- a/ipatests/test_ipaclient/test_csrgen.py
+++ b/ipatests/test_ipaclient/test_csrgen.py
@@ -197,7 +197,8 @@ def test_userCert_OpenSSL(self, generator):
             ],
         }
 
-        script = generator.csr_script(principal, config, 'userCert', 'openssl')
+        script = generator.csr_script(
+            principal, config, {}, 'userCert', 'openssl')
         with open(os.path.join(
                 CSR_DATA_DIR, 'scripts', 'userCert_openssl.sh')) as f:
             expected_script = f.read()
@@ -215,7 +216,7 @@ def test_userCert_Certutil(self, generator):
         }
 
         script = generator.csr_script(
-            principal, config, 'userCert', 'certutil')
+            principal, config, {}, 'userCert', 'certutil')
 
         with open(os.path.join(
                 CSR_DATA_DIR, 'scripts', 'userCert_certutil.sh')) as f:
@@ -235,7 +236,7 @@ def test_caIPAserviceCert_OpenSSL(self, generator):
         }
 
         script = generator.csr_script(
-            principal, config, 'caIPAserviceCert', 'openssl')
+            principal, config, {}, 'caIPAserviceCert', 'openssl')
         with open(os.path.join(
                 CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_openssl.sh')) as f:
             expected_script = f.read()
@@ -254,7 +255,7 @@ def test_caIPAserviceCert_Certutil(self, generator):
         }
 
         script = generator.csr_script(
-            principal, config, 'caIPAserviceCert', 'certutil')
+            principal, config, {}, 'caIPAserviceCert', 'certutil')
         with open(os.path.join(
                 CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_certutil.sh')) as f:
             expected_script = f.read()
@@ -270,7 +271,7 @@ def test_optionalAttributeMissing(self, generator):
         generator = IdentityCSRGenerator(rule_provider)
 
         script = generator.csr_script(
-            principal, {}, 'example', 'identity')
+            principal, {}, {}, 'example', 'identity')
         assert script == '\n'
 
     def test_twoDataRulesOneMissing(self, generator):
@@ -282,7 +283,7 @@ def test_twoDataRulesOneMissing(self, generator):
             'data2', '{{subject.uid}}', {'data_source': 'subject.uid'}))
         generator = IdentityCSRGenerator(rule_provider)
 
-        script = generator.csr_script(principal, {}, 'example', 'identity')
+        script = generator.csr_script(principal, {}, {}, 'example', 'identity')
         assert script == ',testuser\n'
 
     def test_requiredAttributeMissing(self):
@@ -295,4 +296,4 @@ def test_requiredAttributeMissing(self):
 
         with pytest.raises(errors.CSRTemplateError):
             _script = generator.csr_script(
-                principal, {}, 'example', 'identity')
+                principal, {}, {}, 'example', 'identity')

From 23ef577f4c6d860d4ba4faeedb25621ba62ce261 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 29 Aug 2016 12:36:17 -0400
Subject: [PATCH 2/3] csrgen: Run user prompts through gettext before
 displaying

Currently doesn't change anything because the strings are not
translated.  Need to find a way to include them in the translation
files.

https://fedorahosted.org/freeipa/ticket/4899
---
 ipaclient/csrgen.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py
index 2c1c5fc..3800c8d 100644
--- a/ipaclient/csrgen.py
+++ b/ipaclient/csrgen.py
@@ -389,6 +389,6 @@ def get_user_prompts(self, profile_id, helper):
                                 ' use a variable "userdata.<var_name>"') %
                             {'rule': rule.name})
 
-                    prompts[var_parts[1]] = rule.options['prompt']
+                    prompts[var_parts[1]] = _(rule.options['prompt'])
 
         return prompts

From a5f19d013e02762dbe6d4529b274fc08a37055f7 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Wed, 14 Sep 2016 13:16:51 -0400
Subject: [PATCH 3/3] tests: Add tests for handling of user-specified data

https://fedorahosted.org/freeipa/ticket/4899
---
 ipatests/test_ipaclient/test_csrgen.py | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py
index b056042..e3ebdbe 100644
--- a/ipatests/test_ipaclient/test_csrgen.py
+++ b/ipatests/test_ipaclient/test_csrgen.py
@@ -7,6 +7,7 @@
 
 from ipaclient import csrgen
 from ipalib import errors
+from ipalib.text import _
 
 BASE_DIR = os.path.dirname(__file__)
 CSR_DATA_DIR = os.path.join(BASE_DIR, 'data', 'test_csrgen')
@@ -297,3 +298,27 @@ def test_requiredAttributeMissing(self):
         with pytest.raises(errors.CSRTemplateError):
             _script = generator.csr_script(
                 principal, {}, {}, 'example', 'identity')
+
+    def test_get_user_prompts(self):
+        rule_provider = StubRuleProvider()
+        rule_provider.data_rule.options = {
+            'data_source': 'userdata.nickname', 'prompt': "Nickname"}
+        generator = IdentityCSRGenerator(rule_provider)
+
+        prompts = generator.get_user_prompts('example', 'identity')
+
+        expected_prompts = {'nickname': _('Nickname')}
+        assert prompts == expected_prompts
+
+    def test_userdata_included(self):
+        principal = {'uid': 'testuser'}
+        userdata = {'nickname': 'mynick'}
+        rule_provider = StubRuleProvider()
+        rule_provider.data_rule.template = 'nickname:{{userdata.nickname}}'
+        rule_provider.data_rule.options = {
+            'data_source': 'userdata.nickname', 'prompt': "Nickname"}
+        generator = IdentityCSRGenerator(rule_provider)
+
+        script = generator.csr_script(
+            principal, {}, userdata, 'example', 'identity')
+        expected_script = 'nickname:mynick'
-- 
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