URL: https://github.com/freeipa/freeipa/pull/1379
Author: tiran
 Title: #1379: Prevent set_directive from clobbering other keys / safe 
directive setter
Action: opened

PR body:
"""
This PR combines @frasertweedale PR #1347 with a safe directive setter 
(https://pagure.io/freeipa/issue/7312)

## Original PR message

`set_directive` only looks for a prefix of the line matching the
given directive (key).  If a directive is encountered for which the
given key is prefix, it will be vanquished.

This occurs in the case of `{ca,kra}.sslserver.cert[req]`; the
`cert` directive gets updated after certificate renewal, and the
`certreq` directive gets clobbered.  This can cause failures later
on during KRA installation, and possibly cloning.

Match the whole directive to avoid this issue.

Fixes: https://pagure.io/freeipa/issue/7288

-----

Cause: corner case.

How to test:

1. ensure `ca.sslserver.certreq=<base64 CSR>` exists in `ca/CS.cfg`.
2. resubmit Certmonger tracking request for `Server-Cert cert-pki-ca` Dogtag 
system cert.
3. verify that `ca.sslserver.certreq=<base64 CSR>` still exists in `ca/CS.cfg`.

## safe DirectiveSetter

The new context manager ``DirectiveSetter`` avoids several possible issues that 
can lead to destroyed configuration files.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/1379/head:pr1379
git checkout pr1379
From 80fdfc8554196aeba406980fc129f711f6b92b35 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Sat, 21 Nov 2020 08:47:57 +1100
Subject: [PATCH 1/5] Prevent set_directive from clobbering other keys

`set_directive` only looks for a prefix of the line matching the
given directive (key).  If a directive is encountered for which the
given key is prefix, it will be vanquished.

This occurs in the case of `{ca,kra}.sslserver.cert[req]`; the
`cert` directive gets updated after certificate renewal, and the
`certreq` directive gets clobbered.  This can cause failures later
on during KRA installation, and possibly cloning.

Match the whole directive to avoid this issue.

Fixes: https://pagure.io/freeipa/issue/7288
---
 ipaserver/install/cainstance.py     | 2 +-
 ipaserver/install/dogtaginstance.py | 2 +-
 ipaserver/install/installutils.py   | 6 +++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 8e0e69888a..859a7f901d 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -952,7 +952,7 @@ def __enable_crl_publish(self):
         installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.enable', 'true', quotes=False, separator='=')
         installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.mapper', 'NoMap', quotes=False, separator='=')
         installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.pluginName', 'Rule', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.predicate=', '', quotes=False, separator='')
+        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.predicate', '', quotes=False, separator='=')
         installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.publisher', 'FileBaseCRLPublisher', quotes=False, separator='=')
         installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.type', 'crl', quotes=False, separator='=')
 
diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py
index bcc9265de9..67edaf511c 100644
--- a/ipaserver/install/dogtaginstance.py
+++ b/ipaserver/install/dogtaginstance.py
@@ -214,7 +214,7 @@ def enable_client_auth_to_db(self, config):
                 separator='=')
             # Remove internaldb password as is not needed anymore
             installutils.set_directive(paths.PKI_TOMCAT_PASSWORD_CONF,
-                                       'internaldb', None)
+                                       'internaldb', None, separator='=')
 
     def uninstall(self):
         if self.is_installed():
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 92e0d8fa8c..645493186b 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -441,7 +441,7 @@ def set_directive(filename, directive, value, quotes=True, separator=' '):
 
     A value of None means to drop the directive.
 
-    This has only been tested with nss.conf
+    Does not tolerate (or put) spaces around the separator.
 
     :param filename: input filename
     :param directive: directive name
@@ -450,7 +450,7 @@ def set_directive(filename, directive, value, quotes=True, separator=' '):
         any existing double quotes are first escaped to avoid
         unparseable directives.
     :param separator: character serving as separator between directive and
-        value
+        value.  Correct value required even when dropping a directive.
     """
 
     new_directive_value = ""
@@ -465,7 +465,7 @@ def set_directive(filename, directive, value, quotes=True, separator=' '):
     fd = open(filename)
     newfile = []
     for line in fd:
-        if line.lstrip().startswith(directive):
+        if re.match(r'\s*{}'.format(re.escape(directive + separator)), line):
             valueset = True
             if value is not None:
                 newfile.append(new_directive_value)

From 37209443dd39a399ac6538ea5d71dd304ea7454f Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Thu, 30 Nov 2017 12:00:53 +1100
Subject: [PATCH 2/5] pep8: reduce line lengths in
 CAInstance.__enable_crl_publish

Part of: https://pagure.io/freeipa/issue/7288
---
 ipaserver/install/cainstance.py | 71 ++++++++++++++++++++++++-----------------
 1 file changed, 41 insertions(+), 30 deletions(-)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 859a7f901d..49dfcd1373 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -928,52 +928,63 @@ def __enable_crl_publish(self):
 
         https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Certificate_System/8.0/html/Admin_Guide/Setting_up_Publishing.html
         """
-        caconfig = paths.CA_CS_CFG_PATH
 
-        publishdir = self.prepare_crl_publish_dir()
+        def put(k, v):
+            installutils.set_directive(
+                paths.CA_CS_CFG_PATH, k, v, quotes=False, separator='=')
 
         # Enable file publishing, disable LDAP
-        installutils.set_directive(caconfig, 'ca.publish.enable', 'true', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.ldappublish.enable', 'false', quotes=False, separator='=')
+        put('ca.publish.enable', 'true')
+        put('ca.publish.ldappublish.enable', 'false')
 
         # Create the file publisher, der only, not b64
-        installutils.set_directive(caconfig, 'ca.publish.publisher.impl.FileBasedPublisher.class','com.netscape.cms.publish.publishers.FileBasedPublisher', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.crlLinkExt', 'bin', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.directory', publishdir, quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.latestCrlLink', 'true', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.pluginName', 'FileBasedPublisher', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.timeStamp', 'LocalTime', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.zipCRLs', 'false', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.zipLevel', '9', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.Filename.b64', 'false', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.publisher.instance.FileBaseCRLPublisher.Filename.der', 'true', quotes=False, separator='=')
+        put('ca.publish.publisher.impl.FileBasedPublisher.class',
+            'com.netscape.cms.publish.publishers.FileBasedPublisher')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.crlLinkExt',
+            'bin')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.directory',
+            self.prepare_crl_publish_dir())
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.latestCrlLink',
+            'true')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.pluginName',
+            'FileBasedPublisher')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.timeStamp',
+            'LocalTime')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.zipCRLs',
+            'false')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.zipLevel', '9')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.Filename.b64',
+            'false')
+        put('ca.publish.publisher.instance.FileBaseCRLPublisher.Filename.der',
+            'true')
 
         # The publishing rule
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.enable', 'true', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.mapper', 'NoMap', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.pluginName', 'Rule', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.predicate', '', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.publisher', 'FileBaseCRLPublisher', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.FileCrlRule.type', 'crl', quotes=False, separator='=')
+        put('ca.publish.rule.instance.FileCrlRule.enable', 'true')
+        put('ca.publish.rule.instance.FileCrlRule.mapper', 'NoMap')
+        put('ca.publish.rule.instance.FileCrlRule.pluginName', 'Rule')
+        put('ca.publish.rule.instance.FileCrlRule.predicate', '')
+        put('ca.publish.rule.instance.FileCrlRule.publisher',
+            'FileBaseCRLPublisher')
+        put('ca.publish.rule.instance.FileCrlRule.type', 'crl')
 
         # Now disable LDAP publishing
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.LdapCaCertRule.enable', 'false', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.LdapCrlRule.enable', 'false', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.LdapUserCertRule.enable', 'false', quotes=False, separator='=')
-        installutils.set_directive(caconfig, 'ca.publish.rule.instance.LdapXCertRule.enable', 'false', quotes=False, separator='=')
+        put('ca.publish.rule.instance.LdapCaCertRule.enable', 'false')
+        put('ca.publish.rule.instance.LdapCrlRule.enable', 'false')
+        put('ca.publish.rule.instance.LdapUserCertRule.enable', 'false')
+        put('ca.publish.rule.instance.LdapXCertRule.enable', 'false')
 
         # If we are the initial master then we are the CRL generator, otherwise
         # we point to that master for CRLs.
         if not self.clone:
             # These next two are defaults, but I want to be explicit that the
             # initial master is the CRL generator.
-            installutils.set_directive(caconfig, 'ca.crl.MasterCRL.enableCRLCache', 'true', quotes=False, separator='=')
-            installutils.set_directive(caconfig, 'ca.crl.MasterCRL.enableCRLUpdates', 'true', quotes=False, separator='=')
-            installutils.set_directive(caconfig, 'ca.listenToCloneModifications', 'true', quotes=False, separator='=')
+            put('ca.crl.MasterCRL.enableCRLCache', 'true')
+            put('ca.crl.MasterCRL.enableCRLUpdates', 'true')
+            put('ca.listenToCloneModifications', 'true')
         else:
-            installutils.set_directive(caconfig, 'ca.crl.MasterCRL.enableCRLCache', 'false', quotes=False, separator='=')
-            installutils.set_directive(caconfig, 'ca.crl.MasterCRL.enableCRLUpdates', 'false', quotes=False, separator='=')
-            installutils.set_directive(caconfig, 'ca.listenToCloneModifications', 'false', quotes=False, separator='=')
+            put('ca.crl.MasterCRL.enableCRLCache', 'false')
+            put('ca.crl.MasterCRL.enableCRLUpdates', 'false')
+            put('ca.listenToCloneModifications', 'false')
 
     def uninstall(self):
         # just eat state

From 8dc451afed33c68b1fa43ba638f85d592281c8b8 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 5 Dec 2017 13:43:04 +1100
Subject: [PATCH 3/5] installutils: refactor set_directive

To separate concerns and make it easier to test set_directive,
extract function ``set_directive_lines`` to do the line-wise
search/replace, leaving ``set_directive`` to deal with the file
handling.

Part of: https://pagure.io/freeipa/issue/7288
---
 ipaserver/install/installutils.py | 56 +++++++++++++++++++++++----------------
 1 file changed, 33 insertions(+), 23 deletions(-)

diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 645493186b..60d6c37b0d 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -452,34 +452,44 @@ def set_directive(filename, directive, value, quotes=True, separator=' '):
     :param separator: character serving as separator between directive and
         value.  Correct value required even when dropping a directive.
     """
+    st = os.stat(filename)
+    with open(filename, 'r') as f:
+        lines = list(f)  # read the whole file
+        new_lines = set_directive_lines(
+            quotes, separator, directive, value, lines)
+    with open(filename, 'w') as f:
+        # don't construct the whole string; write line-wise
+        for line in new_lines:
+            f.write(line)
+    os.chown(filename, st.st_uid, st.st_gid)  # reset perms
 
-    new_directive_value = ""
-    if value is not None:
-        value_to_set = quote_directive_value(value, '"') if quotes else value
 
-        new_directive_value = "".join(
-            [directive, separator, value_to_set, '\n'])
+def set_directive_lines(quotes, separator, k, v, lines):
+    """Set a name/value pair in a configuration (iterable of lines).
 
-    valueset = False
-    st = os.stat(filename)
-    fd = open(filename)
-    newfile = []
-    for line in fd:
-        if re.match(r'\s*{}'.format(re.escape(directive + separator)), line):
-            valueset = True
-            if value is not None:
-                newfile.append(new_directive_value)
+    Replaces the value of the key if found, otherwise adds it at
+    end.  If value is ``None``, remove the key if found.
+
+    Takes an iterable of lines (with trailing newline).
+    Yields lines (with trailing newline).
+
+    """
+    new_line = ""
+    if v is not None:
+        v_quoted = quote_directive_value(v, '"') if quotes else v
+        new_line = ''.join([k, separator, v_quoted, '\n'])
+
+    found = False
+    for line in lines:
+        if re.match(r'\s*{}'.format(re.escape(k + separator)), line):
+            found = True
+            if v is not None:
+                yield new_line
         else:
-            newfile.append(line)
-    fd.close()
-    if not valueset:
-        if value is not None:
-            newfile.append(new_directive_value)
+            yield line
 
-    fd = open(filename, "w")
-    fd.write("".join(newfile))
-    fd.close()
-    os.chown(filename, st.st_uid, st.st_gid) # reset perms
+    if not found and v is not None:
+        yield new_line
 
 
 def get_directive(filename, directive, separator=' '):

From df7b4b2da744aebc4947359b81e1d6074582d55c Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 5 Dec 2017 15:00:18 +1100
Subject: [PATCH 4/5] Add tests for installutils.set_directive

Part of: https://pagure.io/freeipa/issue/7288
---
 .../test_install/test_installutils.py              | 57 ++++++++++++++++++++++
 1 file changed, 57 insertions(+)
 create mode 100644 ipatests/test_ipaserver/test_install/test_installutils.py

diff --git a/ipatests/test_ipaserver/test_install/test_installutils.py b/ipatests/test_ipaserver/test_install/test_installutils.py
new file mode 100644
index 0000000000..cc8fd3cf3f
--- /dev/null
+++ b/ipatests/test_ipaserver/test_install/test_installutils.py
@@ -0,0 +1,57 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors.  See COPYING for license
+#
+
+import os
+import tempfile
+
+from ipaserver.install import installutils
+
+EXAMPLE_CONFIG = [
+    'foo=1\n',
+    'foobar=2\n',
+]
+
+
+class test_set_directive_lines(object):
+    def test_remove_directive(self):
+        lines = installutils.set_directive_lines(
+            False, '=', 'foo', None, EXAMPLE_CONFIG)
+        assert list(lines) == ['foobar=2\n']
+
+    def test_add_directive(self):
+        lines = installutils.set_directive_lines(
+            False, '=', 'baz', '4', EXAMPLE_CONFIG)
+        assert list(lines) == ['foo=1\n', 'foobar=2\n', 'baz=4\n']
+
+    def test_set_directive_does_not_clobber_suffix_key(self):
+        lines = installutils.set_directive_lines(
+            False, '=', 'foo', '3', EXAMPLE_CONFIG)
+        assert list(lines) == ['foo=3\n', 'foobar=2\n']
+
+
+class test_set_directive(object):
+    def test_set_directive(self):
+        """Check that set_directive writes the new data and preserves mode."""
+        fd, filename = tempfile.mkstemp()
+        try:
+            os.close(fd)
+            stat_pre = os.stat(filename)
+
+            with open(filename, 'w') as f:
+                for line in EXAMPLE_CONFIG:
+                    f.write(line)
+
+            installutils.set_directive(filename, 'foo', '3', False, '=')
+
+            stat_post = os.stat(filename)
+            with open(filename, 'r') as f:
+                lines = list(f)
+
+            assert lines == ['foo=3\n', 'foobar=2\n']
+            assert stat_pre.st_mode == stat_post.st_mode
+            assert stat_pre.st_uid == stat_post.st_uid
+            assert stat_pre.st_gid == stat_post.st_gid
+
+        finally:
+            os.remove(filename)

From 8f257bb84d3a916f79d236a52e8fad4ea77a5b9f Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Fri, 8 Dec 2017 12:38:41 +0100
Subject: [PATCH 5/5] Add safe DirectiveSetter context manager

installutils.set_directive() is both inefficient and potentially
dangerous. It does not ensure that the whole file is written and
properly synced to disk. In worst case it could lead to partially
written or destroyed config files.

The new DirectiveSetter context manager wraps everything under an easy
to use interface.

https://pagure.io/freeipa/issue/7312

Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 ipaserver/install/cainstance.py                    | 109 ++++++++++-----------
 ipaserver/install/installutils.py                  |  65 +++++++++++-
 .../test_install/test_installutils.py              |  51 ++++++++++
 3 files changed, 167 insertions(+), 58 deletions(-)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 49dfcd1373..6af5ee90a9 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -929,62 +929,61 @@ def __enable_crl_publish(self):
         https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Certificate_System/8.0/html/Admin_Guide/Setting_up_Publishing.html
         """
 
-        def put(k, v):
-            installutils.set_directive(
-                paths.CA_CS_CFG_PATH, k, v, quotes=False, separator='=')
+        with installutils.DirectiveSetter(paths.CA_CS_CFG_PATH,
+                                          quotes=False, separator='=') as ds:
 
-        # Enable file publishing, disable LDAP
-        put('ca.publish.enable', 'true')
-        put('ca.publish.ldappublish.enable', 'false')
-
-        # Create the file publisher, der only, not b64
-        put('ca.publish.publisher.impl.FileBasedPublisher.class',
-            'com.netscape.cms.publish.publishers.FileBasedPublisher')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.crlLinkExt',
-            'bin')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.directory',
-            self.prepare_crl_publish_dir())
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.latestCrlLink',
-            'true')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.pluginName',
-            'FileBasedPublisher')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.timeStamp',
-            'LocalTime')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.zipCRLs',
-            'false')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.zipLevel', '9')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.Filename.b64',
-            'false')
-        put('ca.publish.publisher.instance.FileBaseCRLPublisher.Filename.der',
-            'true')
-
-        # The publishing rule
-        put('ca.publish.rule.instance.FileCrlRule.enable', 'true')
-        put('ca.publish.rule.instance.FileCrlRule.mapper', 'NoMap')
-        put('ca.publish.rule.instance.FileCrlRule.pluginName', 'Rule')
-        put('ca.publish.rule.instance.FileCrlRule.predicate', '')
-        put('ca.publish.rule.instance.FileCrlRule.publisher',
-            'FileBaseCRLPublisher')
-        put('ca.publish.rule.instance.FileCrlRule.type', 'crl')
-
-        # Now disable LDAP publishing
-        put('ca.publish.rule.instance.LdapCaCertRule.enable', 'false')
-        put('ca.publish.rule.instance.LdapCrlRule.enable', 'false')
-        put('ca.publish.rule.instance.LdapUserCertRule.enable', 'false')
-        put('ca.publish.rule.instance.LdapXCertRule.enable', 'false')
-
-        # If we are the initial master then we are the CRL generator, otherwise
-        # we point to that master for CRLs.
-        if not self.clone:
-            # These next two are defaults, but I want to be explicit that the
-            # initial master is the CRL generator.
-            put('ca.crl.MasterCRL.enableCRLCache', 'true')
-            put('ca.crl.MasterCRL.enableCRLUpdates', 'true')
-            put('ca.listenToCloneModifications', 'true')
-        else:
-            put('ca.crl.MasterCRL.enableCRLCache', 'false')
-            put('ca.crl.MasterCRL.enableCRLUpdates', 'false')
-            put('ca.listenToCloneModifications', 'false')
+            # Enable file publishing, disable LDAP
+            ds.set('ca.publish.enable', 'true')
+            ds.set('ca.publish.ldappublish.enable', 'false')
+
+            # Create the file publisher, der only, not b64
+            ds.set(
+                'ca.publish.publisher.impl.FileBasedPublisher.class',
+                'com.netscape.cms.publish.publishers.FileBasedPublisher'
+            )
+            prefix = 'ca.publish.publisher.instance.FileBaseCRLPublisher.'
+            ds.set(prefix + 'crlLinkExt', 'bin')
+            ds.set(prefix + 'directory', self.prepare_crl_publish_dir())
+            ds.set(prefix + 'latestCrlLink', 'true')
+            ds.set(prefix + 'pluginName', 'FileBasedPublisher')
+            ds.set(prefix + 'timeStamp', 'LocalTime')
+            ds.set(prefix + 'zipCRLs', 'false')
+            ds.set(prefix + 'zipLevel', '9')
+            ds.set(prefix + 'Filename.b64', 'false')
+            ds.set(prefix + 'Filename.der', 'true')
+
+            # The publishing rule
+            ds.set('ca.publish.rule.instance.FileCrlRule.enable', 'true')
+            ds.set('ca.publish.rule.instance.FileCrlRule.mapper', 'NoMap')
+            ds.set('ca.publish.rule.instance.FileCrlRule.pluginName', 'Rule')
+            ds.set('ca.publish.rule.instance.FileCrlRule.predicate', '')
+            ds.set(
+                'ca.publish.rule.instance.FileCrlRule.publisher',
+                'FileBaseCRLPublisher'
+            )
+            ds.set('ca.publish.rule.instance.FileCrlRule.type', 'crl')
+
+            # Now disable LDAP publishing
+            ds.set('ca.publish.rule.instance.LdapCaCertRule.enable', 'false')
+            ds.set('ca.publish.rule.instance.LdapCrlRule.enable', 'false')
+            ds.set(
+                'ca.publish.rule.instance.LdapUserCertRule.enable',
+                'false'
+            )
+            ds.set('ca.publish.rule.instance.LdapXCertRule.enable', 'false')
+
+            # If we are the initial master then we are the CRL generator,
+            # otherwise we point to that master for CRLs.
+            if not self.clone:
+                # These next two are defaults, but I want to be explicit
+                # that the initial master is the CRL generator.
+                ds.set('ca.crl.MasterCRL.enableCRLCache', 'true')
+                ds.set('ca.crl.MasterCRL.enableCRLUpdates', 'true')
+                ds.set('ca.listenToCloneModifications', 'true')
+            else:
+                ds.set('ca.crl.MasterCRL.enableCRLCache', 'false')
+                ds.set('ca.crl.MasterCRL.enableCRLUpdates', 'false')
+                ds.set('ca.listenToCloneModifications', 'false')
 
     def uninstall(self):
         # just eat state
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 60d6c37b0d..4184a86bd6 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -25,6 +25,7 @@
 import socket
 import getpass
 import gssapi
+import io
 import ldif
 import os
 import re
@@ -32,6 +33,7 @@
 import sys
 import tempfile
 import shutil
+import stat  # pylint: disable=bad-python3-import
 import traceback
 import textwrap
 from contextlib import contextmanager
@@ -435,6 +437,60 @@ def unquote_directive_value(value, quote_char):
 
     return unescaped_value
 
+SENTINEL = object()
+
+class DirectiveSetter(object):
+    def __init__(self, filename, quotes=True, separator=' '):
+        self.filename = os.path.abspath(filename)
+        self.quotes = quotes
+        self.separator = separator
+        self.lines = None
+        self.stat = None
+
+    def __enter__(self):
+        with open(self.filename) as f:
+            self.stat = os.fstat(f.fileno())
+            self.lines = list(f)
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if exc_type is not None:
+            return
+        directory, prefix = os.path.split(self.filename)
+        # use tempfile in same directory to have atomic rename
+        fd, name = tempfile.mkstemp(prefix=prefix, dir=directory, text=True)
+        with io.open(fd, mode='w', closefd=True) as f:
+            for line in self.lines:
+                if not isinstance(line, six.text_type):
+                    line = line.decode('utf-8')
+                f.write(line)
+            self.lines = None
+            os.fchmod(f.fileno(), stat.S_IMODE(self.stat.st_mode))
+            os.fchown(f.fileno(), self.stat.st_uid, self.stat.st_gid)
+            self.stat = None
+            # flush and sync tempfile inode
+            f.flush()
+            os.fsync(f.fileno())
+
+        # rename file and sync directory inode
+        os.rename(name, self.filename)
+        dirfd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY)
+        try:
+            os.fsync(dirfd)
+        finally:
+            os.close(dirfd)
+
+    def set(self, directive, value, quotes=SENTINEL, separator=SENTINEL):
+        if quotes is SENTINEL:
+            quotes = self.quotes
+        if separator is SENTINEL:
+            separator = self.separator
+        # materialize lines
+        # set_directive_lines() modify item, shrink or enlage line count
+        self.lines = list(set_directive_lines(
+            quotes, separator, directive, value, self.lines
+        ))
+
 
 def set_directive(filename, directive, value, quotes=True, separator=' '):
     """Set a name/value pair directive in a configuration file.
@@ -455,8 +511,10 @@ def set_directive(filename, directive, value, quotes=True, separator=' '):
     st = os.stat(filename)
     with open(filename, 'r') as f:
         lines = list(f)  # read the whole file
-        new_lines = set_directive_lines(
-            quotes, separator, directive, value, lines)
+        # materialize new list
+        new_lines = list(set_directive_lines(
+            quotes, separator, directive, value, lines
+        ))
     with open(filename, 'w') as f:
         # don't construct the whole string; write line-wise
         for line in new_lines:
@@ -480,8 +538,9 @@ def set_directive_lines(quotes, separator, k, v, lines):
         new_line = ''.join([k, separator, v_quoted, '\n'])
 
     found = False
+    matcher = re.compile(r'\s*{}'.format(re.escape(k + separator)))
     for line in lines:
-        if re.match(r'\s*{}'.format(re.escape(k + separator)), line):
+        if matcher.match(line):
             found = True
             if v is not None:
                 yield new_line
diff --git a/ipatests/test_ipaserver/test_install/test_installutils.py b/ipatests/test_ipaserver/test_install/test_installutils.py
index cc8fd3cf3f..8dbc7c4c2f 100644
--- a/ipatests/test_ipaserver/test_install/test_installutils.py
+++ b/ipatests/test_ipaserver/test_install/test_installutils.py
@@ -3,8 +3,11 @@
 #
 
 import os
+import shutil
 import tempfile
 
+import pytest
+
 from ipaserver.install import installutils
 
 EXAMPLE_CONFIG = [
@@ -13,6 +16,18 @@
 ]
 
 
+@pytest.fixture
+def tempdir(request):
+    tempdir = tempfile.mkdtemp()
+
+    def fin():
+        shutil.rmtree(tempdir)
+
+    request.addfinalizer(fin)
+    return tempdir
+
+
+
 class test_set_directive_lines(object):
     def test_remove_directive(self):
         lines = installutils.set_directive_lines(
@@ -55,3 +70,39 @@ def test_set_directive(self):
 
         finally:
             os.remove(filename)
+
+
+def test_directivesetter(tempdir):
+    filename = os.path.join(tempdir, 'example.conf')
+    with open(filename, 'w') as f:
+        for line in EXAMPLE_CONFIG:
+            f.write(line)
+
+    ds = installutils.DirectiveSetter(filename)
+    assert ds.lines is None
+    with ds:
+        assert ds.lines == EXAMPLE_CONFIG
+        ds.set('foo', '3')  # quoted, space separated, doesn't change 'foo='
+        ds.set('foobar', None, separator='=')  # remove
+        ds.set('baz', '4', False, '=')  # add
+
+    with open(filename, 'r') as f:
+        lines = list(f)
+
+    assert lines == [
+        'foo=1\n',
+        'foo "3"\n',
+        'baz=4\n'
+    ]
+
+    with installutils.DirectiveSetter(filename, True, '=') as ds:
+        ds.set('foo', '4')  # doesn't change 'foo '
+
+    with open(filename, 'r') as f:
+        lines = list(f)
+
+    assert lines == [
+        'foo="4"\n',
+        'foo "3"\n',
+        'baz=4\n'
+    ]
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org

Reply via email to