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::<base64-value>
replace:attr::<old-base64-value>::<new-base64-value>

https://fedorahosted.org/freeipa/ticket/4984

Reviewed-By: Jan Cholasta <jcholast@redhat.com>
This commit is contained in:
Martin Basti 2015-05-05 15:12:12 +02:00 committed by Jan Cholasta
parent 5783d0c832
commit 520bbd001b
8 changed files with 145 additions and 51 deletions

View File

@ -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

View File

@ -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))

View File

@ -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']:

View File

@ -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]

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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):