From 520bbd001b68bc51a79c2b4a9684fb1c12a582cd Mon Sep 17 00:00:00 2001 From: Martin Basti Date: May 11 2015 16:08:01 +0000 Subject: Server Upgrade: Allow base64 encoded values This patch allows to use base64 encoded values in update files. Double colon ('::') must be used as separator between attribute name and base64 encoded value. add:attr:: replace:attr:::: https://fedorahosted.org/freeipa/ticket/4984 Reviewed-By: Jan Cholasta --- diff --git a/install/tools/man/ipa-ldap-updater.1 b/install/tools/man/ipa-ldap-updater.1 index 9690125..6d0feb4 100644 --- a/install/tools/man/ipa-ldap-updater.1 +++ b/install/tools/man/ipa-ldap-updater.1 @@ -39,7 +39,7 @@ There are 7 keywords: * only: set an attribute to this * onlyifexist: set an attribute to this only if the entry exists * deleteentry: remove the entry - * replace: replace an existing value, format is old: new + * replace: replace an existing value, format is old::new * addifnew: add a new attribute and value only if the attribute doesn't already exist. Only works with single\-value attributes. * addifexist: add a new attribute and value only if the entry exists. This is used to update optional entries. @@ -57,6 +57,12 @@ The available template variables are: * $LIBARCH \- set to 64 on x86_64 systems to be used for plugin paths * $TIME \- an integer representation of current time +For base64 encoded values a double colon ('::') must be used between attribute and value. + +Base64 format examples: + add:binaryattr::d2UgbG92ZSBiYXNlNjQ= + replace:binaryattr::SVBBIGlzIGdyZWF0::SVBBIGlzIHJlYWxseSBncmVhdA== + A few rules: 1. Only one rule per line diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py index 58d3e72..2ea890e 100644 --- a/ipaserver/install/ldapupdate.py +++ b/ipaserver/install/ldapupdate.py @@ -22,6 +22,7 @@ # TODO # save undo files? +import base64 import sys import uuid import platform @@ -106,9 +107,31 @@ def safe_output(attr, values): return ['XXXXXXX'] * len(values) else: return 'XXXXXXXX' - else: + + if values is None: + return + + is_list = type(values) in (tuple, list) + + if is_list and None in values: return values + if not is_list: + values = [values] + + try: + all(v.decode('ascii') for v in values) + except UnicodeDecodeError: + try: + values = [base64.b64encode(v) for v in values] + except TypeError: + pass + + if not is_list: + values = values[0] + return values + + class LDAPUpdate: action_keywords = ["default", "add", "remove", "only", "onlyifexist", "deleteentry", "replace", "addifnew", "addifexist"] @@ -136,18 +159,28 @@ class LDAPUpdate: all_updates = [ { 'dn': 'cn=config,dc=example,dc=com', - 'default': ['attr1':default1'], - 'updates': ['action:attr1:value1', - 'action:attr2:value2] + 'default': [ + dict(attr='attr1', value='default1'), + ], + 'updates': [ + dict(action='action', attr='attr1', value='value1'), + dict(action='replace', attr='attr2', value=['old', 'new']), + ] }, { 'dn': 'cn=bob,ou=people,dc=example,dc=com', - 'default': ['attr3':default3'], - 'updates': ['action:attr3:value3', - 'action:attr4:value4], + 'default': [ + dict(attr='attr3', value='default3'), + ], + 'updates': [ + dict(action='action', attr='attr3', value='value3'), + dict(action='action', attr='attr4', value='value4'), + } } ] + Please notice the replace action requires two values in list + The default and update lists are "dispositions" Plugins: @@ -181,11 +214,15 @@ class LDAPUpdate: Generates this list which contain the update dictionary: [ - dict( + { 'dn': 'cn=global_policy,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', - 'updates': ['replace:krbPwdLockoutDuration:10::600', - 'replace:krbPwdMaxFailure:3::6'] - ) + 'updates': [ + dict(action='replace', attr='krbPwdLockoutDuration', + value=['10','600']), + dict(action='replace', attr='krbPwdMaxFailure', + value=['3','6']), + ] + } ] Here is another example showing how a default entry is configured: @@ -198,13 +235,14 @@ class LDAPUpdate: This generates: [ - dict( + { 'dn': 'cn=Managed Entries,cn=etc,dc=example,dc=com', - 'default': ['objectClass:nsContainer', - 'objectClass:top', - 'cn:Managed Entries' - ] - ) + 'default': [ + dict(attr='objectClass', value='nsContainer'), + dict(attr='objectClass', value='top'), + dict(attr='cn', value='Managed Entries'), + ] + } ] Note that the variable substitution in both examples has been completed. @@ -348,13 +386,52 @@ class LDAPUpdate: raise BadSyntax, "Bad formatting on line %s:%d: %s" % (data_source_name, lcount, logical_line) attr = items[1].strip() - value = items[2].strip() + # do not strip here, we need detect '::' due to base64 encoded + # values, strip may result into fake detection + value = items[2] + + # detect base64 encoding + # value which start with ':' are base64 encoded + # decode it as a binary value + if value.startswith(':'): + value = value[1:] + binary = True + else: + binary = False + value = value.strip() + + if action == 'replace': + try: + value = value.split('::', 1) + except ValueError: + raise BadSyntax( + "Bad syntax in replace on line %s:%d: %s, needs to " + "be in the format old::new in %s" % ( + data_source_name, lcount, logical_line, value) + ) + else: + value = [value] + + if binary: + for i, v in enumerate(value): + try: + value[i] = base64.b64decode(v) + except TypeError as e: + raise BadSyntax( + "Base64 encoded value %s on line %s:%d: %s is " + "incorrect (%s)" % (v, data_source_name, + lcount, logical_line, e) + ) + + if action != 'replace': + value = value[0] if action == "default": - new_value = attr + ":" + value + new_value = {'attr': attr, 'value': value} disposition = "default" else: - new_value = action + ":" + attr + ":" + value + new_value = {'action': action, "attr": attr, + 'value': value} disposition = "updates" disposition_list = update.setdefault(disposition, []) @@ -520,7 +597,9 @@ class LDAPUpdate: for item in default: # We already do syntax-parsing so this is safe - (attr, value) = item.split(':',1) + attr = item['attr'] + value = item['value'] + e = entry.get(attr) if e: # multi-valued attribute @@ -558,8 +637,11 @@ class LDAPUpdate: only = {} for update in updates: # We already do syntax-parsing so this is safe - (action, attr, update_value) = update.split(':', 2) - entry_values = entry.get(attr, []) + action = update['action'] + attr = update['attr'] + update_value = update['value'] + + entry_values = entry.raw.get(attr, []) if action == 'remove': self.debug("remove: '%s' from %s, current value %s", safe_output(attr, update_value), attr, safe_output(attr,entry_values)) try: @@ -620,11 +702,9 @@ class LDAPUpdate: # skip this update type, it occurs in __delete_entries() return None elif action == 'replace': - # value has the format "old::new" - try: - (old, new) = update_value.split('::', 1) - except ValueError: - raise BadSyntax, "bad syntax in replace, needs to be in the format old::new in %s" % update_value + # replace values were store as list + old, new = update_value + try: entry_values.remove(old) except ValueError: @@ -642,7 +722,7 @@ class LDAPUpdate: if message: self.debug("%s", message) self.debug("dn: %s", e.dn) - for a, value in e.items(): + for a, value in e.raw.items(): self.debug('%s:', a) for l in value: self.debug("\t%s", safe_output(a, l)) diff --git a/ipaserver/install/plugins/adtrust.py b/ipaserver/install/plugins/adtrust.py index 287595d..941bb63 100644 --- a/ipaserver/install/plugins/adtrust.py +++ b/ipaserver/install/plugins/adtrust.py @@ -55,14 +55,15 @@ class update_default_range(Updater): id_range_name = '%s_id_range' % self.api.env.realm id_range_size = DEFAULT_ID_RANGE_SIZE - range_entry = ['objectclass:top', - 'objectclass:ipaIDrange', - 'objectclass:ipaDomainIDRange', - 'cn:%s' % id_range_name, - 'ipabaseid:%s' % id_range_base_id, - 'ipaidrangesize:%s' % id_range_size, - 'iparangetype:ipa-local', - ] + range_entry = [ + dict(attr='objectclass', value='top'), + dict(attr='objectclass', value='ipaIDrange'), + dict(attr='objectclass', value='ipaDomainIDRange'), + dict(attr='cn', value=id_range_name), + dict(attr='ipabaseid', value=id_range_base_id), + dict(attr='ipaidrangesize', value=id_range_size), + dict(attr='iparangetype', value='ipa-local'), + ] dn = DN(('cn', '%s_id_range' % self.api.env.realm), self.api.env.container_ranges, self.api.env.basedn) @@ -129,12 +130,12 @@ class update_default_trust_view(Updater): self.api.env.basedn) default_trust_view_entry = [ - 'objectclass:top', - 'objectclass:ipaIDView', - 'cn:Default Trust View', - 'description:Default Trust View for AD users. ' - 'Should not be deleted.', - ] + dict(attr='objectclass', value='top'), + dict(attr='objectclass', value='ipaIDView'), + dict(attr='cn', value='Default Trust View'), + dict(attr='description', value='Default Trust View for AD users. ' + 'Should not be deleted.'), + ] # First, see if trusts are enabled on the server if not self.api.Command.adtrust_is_enabled()['result']: diff --git a/ipaserver/install/plugins/ca_renewal_master.py b/ipaserver/install/plugins/ca_renewal_master.py index afbf812..dae976f 100644 --- a/ipaserver/install/plugins/ca_renewal_master.py +++ b/ipaserver/install/plugins/ca_renewal_master.py @@ -99,7 +99,10 @@ class update_ca_renewal_master(Updater): dn = DN(('cn', 'CA'), ('cn', self.api.env.host), base_dn) update = { 'dn': dn, - 'updates': ['add:ipaConfigString: caRenewalMaster'], + 'updates': [ + dict(action='add', attr='ipaConfigString', + value='caRenewalMaster') + ], } return False, [update] diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py index 95c004d..aafea44 100644 --- a/ipaserver/install/plugins/dns.py +++ b/ipaserver/install/plugins/dns.py @@ -129,7 +129,8 @@ class update_dns_limits(Updater): limit_updates = [] for limit in self.limit_attributes: - limit_updates.append('only:%s:%s' % (limit, self.limit_value)) + limit_updates.append(dict(action='only', attr=limit, + value=self.limit_value)) dnsupdate = {'dn': dns_service_dn, 'updates': limit_updates} root_logger.debug("DNS: limits for service %s will be updated" % dns_service_dn) diff --git a/ipaserver/install/plugins/rename_managed.py b/ipaserver/install/plugins/rename_managed.py index f37f87d..16477cf 100644 --- a/ipaserver/install/plugins/rename_managed.py +++ b/ipaserver/install/plugins/rename_managed.py @@ -34,9 +34,9 @@ def entry_to_update(entry): for attr in entry.keys(): if isinstance(entry[attr], list): for i in xrange(len(entry[attr])): - update.append('%s:%s' % (str(attr), str(entry[attr][i]))) + update.append(dict(attr=str(attr), value=str(entry[attr][i]))) else: - update.append('%s:%s' % (str(attr), str(entry[attr]))) + update.append(dict(attr=str(attr), value=str(entry[attr]))) return update diff --git a/ipaserver/install/plugins/update_passsync.py b/ipaserver/install/plugins/update_passsync.py index a35f64e..521000c 100644 --- a/ipaserver/install/plugins/update_passsync.py +++ b/ipaserver/install/plugins/update_passsync.py @@ -65,7 +65,10 @@ class update_passync_privilege_update(Updater): root_logger.debug("PassSync user found, do update") update = {'dn': passsync_privilege_dn, - 'updates': ["add:member:'%s'" % passsync_dn]} + 'updates': [ + dict(action='add', attr='member', value=passsync_dn), + ] + } sysupgrade.set_upgrade_state('winsync', 'passsync_privilege_updated', True) return False, [update] diff --git a/ipaserver/install/plugins/update_uniqueness.py b/ipaserver/install/plugins/update_uniqueness.py index 5475f67..c162ad3 100644 --- a/ipaserver/install/plugins/update_uniqueness.py +++ b/ipaserver/install/plugins/update_uniqueness.py @@ -54,11 +54,11 @@ class update_uniqueness_plugins_to_new_syntax(Updater): plugins_dn = DN(('cn', 'plugins'), ('cn', 'config')) def __remove_update(self, update, key, value): - statement = "remove:%s:%s" % (key, value) + statement = dict(action='remove', attr=key, value=value) update.setdefault('updates', []).append(statement) def __add_update(self, update, key, value): - statement = "add:%s:%s" % (key, value) + statement = dict(action='add', attr=key, value=value) update.setdefault('updates', []).append(statement) def __subtree_style(self, entry):