def _dummy_modify(connect_spec, dn, directives): assert dn in db e = db[dn] for op, attr, vals in directives: if op == 'add': assert len(vals) existing_vals = e.setdefault(attr, OrderedSet()) for val in vals: assert val not in existing_vals existing_vals.add(val) elif op == 'delete': assert attr in e existing_vals = e[attr] assert len(existing_vals) if not len(vals): del e[attr] continue for val in vals: assert val in existing_vals existing_vals.remove(val) if not len(existing_vals): del e[attr] elif op == 'replace': e.pop(attr, None) e[attr] = OrderedSet(vals) else: raise ValueError() return True
def dummy_modify(self, connect_spec, dn, directives): assert dn in self.db e = self.db[dn] for op, attr, vals in directives: if op == "add": assert vals existing_vals = e.setdefault(attr, OrderedSet()) for val in vals: assert val not in existing_vals existing_vals.add(val) elif op == "delete": assert attr in e existing_vals = e[attr] assert existing_vals if not vals: del e[attr] continue for val in vals: assert val in existing_vals existing_vals.remove(val) if not existing_vals: del e[attr] elif op == "replace": e.pop(attr, None) e[attr] = OrderedSet(vals) else: raise ValueError() return True
def _toset(thing): '''helper to convert various things to a set This enables flexibility in what users provide as the list of LDAP entry attribute values. Note that the LDAP spec prohibits duplicate values in an attribute. RFC 2251 states that: "The order of attribute values within the vals set is undefined and implementation-dependent, and MUST NOT be relied upon." However, OpenLDAP have an X-ORDERED that is used in the config schema. Using sets would mean we can't pass ordered values and therefore can't manage parts of the OpenLDAP configuration, hence the use of OrderedSet. Sets are also good for automatically removing duplicates. None becomes an empty set. Iterables except for strings have their elements added to a new set. Non-None scalars (strings, numbers, non-iterable objects, etc.) are added as the only member of a new set. ''' if thing is None: return OrderedSet() if isinstance(thing, six.string_types): return OrderedSet((thing, )) # convert numbers to strings so that equality checks work # (LDAP stores numbers as strings) try: return OrderedSet((str(x) for x in thing)) except TypeError: return OrderedSet((str(thing), ))
def _dummy_change(connect_spec, dn, before, after): assert before != after assert len(before) assert len(after) assert dn in db e = db[dn] assert e == before all_attrs = OrderedSet() all_attrs.update(before) all_attrs.update(after) directives = [] for attr in all_attrs: if attr not in before: assert attr in after assert len(after[attr]) directives.append(('add', attr, after[attr])) elif attr not in after: assert attr in before assert len(before[attr]) directives.append(('delete', attr, ())) else: assert len(before[attr]) assert len(after[attr]) to_del = before[attr] - after[attr] if len(to_del): directives.append(('delete', attr, to_del)) to_add = after[attr] - before[attr] if len(to_add): directives.append(('add', attr, to_add)) return _dummy_modify(connect_spec, dn, directives)
def dummy_change(self, connect_spec, dn, before, after): assert before != after assert before assert after assert dn in self.db e = self.db[dn] assert e == before all_attrs = OrderedSet() all_attrs.update(before) all_attrs.update(after) directives = [] for attr in all_attrs: if attr not in before: assert attr in after assert after[attr] directives.append(("add", attr, after[attr])) elif attr not in after: assert attr in before assert before[attr] directives.append(("delete", attr, ())) else: assert before[attr] assert after[attr] to_del = before[attr] - after[attr] if to_del: directives.append(("delete", attr, to_del)) to_add = after[attr] - before[attr] if to_add: directives.append(("add", attr, to_add)) return self.dummy_modify(connect_spec, dn, directives)
def _dummy_add(connect_spec, dn, attributes): assert dn not in db assert len(attributes) db[dn] = {} for attr, vals in six.iteritems(attributes): assert len(vals) db[dn][attr] = OrderedSet(vals) return True
def _complex_db(): return { 'dnfoo': { 'attrfoo1': OrderedSet(( 'valfoo1.1', 'valfoo1.2', )), 'attrfoo2': OrderedSet(('valfoo2.1', )), }, 'dnbar': { 'attrbar1': OrderedSet(( 'valbar1.1', 'valbar1.2', )), 'attrbar2': OrderedSet(('valbar2.1', )), }, }
def _dummy_add(connect_spec, dn, attributes): assert dn not in db assert attributes db[dn] = {} for attr, vals in attributes.items(): assert vals db[dn][attr] = OrderedSet(vals) return True
def _complex_db(): return { "dnfoo": { "attrfoo1": OrderedSet(( "valfoo1.1", "valfoo1.2", )), "attrfoo2": OrderedSet(("valfoo2.1", )), }, "dnbar": { "attrbar1": OrderedSet(( "valbar1.1", "valbar1.2", )), "attrbar2": OrderedSet(("valbar2.1", )), }, }
def no_change_complex_db(db): db.db = { "dnfoo": { "attrfoo1": OrderedSet(( b"valfoo1.1", b"valfoo1.2", )), "attrfoo2": OrderedSet((b"valfoo2.1", )), }, "dnbar": { "attrbar1": OrderedSet(( b"valbar1.1", b"valbar1.2", )), "attrbar2": OrderedSet((b"valbar2.1", )), }, } return db
def _update_entry(entry, status, directives): """Update an entry's attributes using the provided directives :param entry: A dict mapping each attribute name to a set of its values :param status: A dict holding cross-invocation status (whether delete_others is True or not, and the set of mentioned attributes) :param directives: A dict mapping directive types to directive-specific state """ for directive, state in directives.items(): if directive == "delete_others": status["delete_others"] = state continue for attr, vals in state.items(): status["mentioned_attributes"].add(attr) vals = _toset(vals) if directive == "default": if vals and (attr not in entry or not entry[attr]): entry[attr] = vals elif directive == "add": vals.update(entry.get(attr, OrderedSet())) if vals: entry[attr] = vals elif directive == "delete": existing_vals = entry.pop(attr, OrderedSet()) if vals: existing_vals -= vals if existing_vals: entry[attr] = existing_vals elif directive == "replace": entry.pop(attr, None) if vals: entry[attr] = vals else: raise ValueError("unknown directive: " + directive)
def _update_entry(entry, status, directives): '''Update an entry's attributes using the provided directives :param entry: A dict mapping each attribute name to a set of its values :param status: A dict holding cross-invocation status (whether delete_others is True or not, and the set of mentioned attributes) :param directives: A dict mapping directive types to directive-specific state ''' for directive, state in six.iteritems(directives): if directive == 'delete_others': status['delete_others'] = state continue for attr, vals in six.iteritems(state): status['mentioned_attributes'].add(attr) vals = _toset(vals) if directive == 'default': if len(vals) and (attr not in entry or not len(entry[attr])): entry[attr] = vals elif directive == 'add': vals.update(entry.get(attr, ())) if len(vals): entry[attr] = vals elif directive == 'delete': existing_vals = entry.pop(attr, OrderedSet()) if len(vals): existing_vals -= vals if len(existing_vals): entry[attr] = existing_vals elif directive == 'replace': entry.pop(attr, None) if len(vals): entry[attr] = vals else: raise ValueError('unknown directive: ' + directive)
def _test_helper(self, init_db, expected_ret, replace, delete_others=False): _init_db(copy.deepcopy(init_db)) old = _dump_db() new = _dump_db() expected_db = copy.deepcopy(init_db) for dn, attrs in six.iteritems(replace): for attr, vals in six.iteritems(attrs): if len(vals): new.setdefault(dn, {})[attr] = list(OrderedSet(vals)) expected_db.setdefault(dn, {})[attr] = OrderedSet(vals) elif dn in expected_db: new[dn].pop(attr, None) expected_db[dn].pop(attr, None) if not len(expected_db.get(dn, {})): new.pop(dn, None) expected_db.pop(dn, None) if delete_others: dn_to_delete = OrderedSet() for dn, attrs in six.iteritems(expected_db): if dn in replace: to_delete = OrderedSet() for attr, vals in six.iteritems(attrs): if attr not in replace[dn]: to_delete.add(attr) for attr in to_delete: del attrs[attr] del new[dn][attr] if not len(attrs): dn_to_delete.add(dn) for dn in dn_to_delete: del new[dn] del expected_db[dn] name = 'ldapi:///' expected_ret['name'] = name expected_ret.setdefault('result', True) expected_ret.setdefault('comment', 'Successfully updated LDAP entries') expected_ret.setdefault( 'changes', dict(((dn, { 'old': dict((attr, vals) for attr, vals in six.iteritems(old[dn]) if vals != new.get(dn, {}).get(attr, ())) if dn in old else None, 'new': dict((attr, vals) for attr, vals in six.iteritems(new[dn]) if vals != old.get(dn, {}).get(attr, ())) if dn in new else None }) for dn in replace if old.get(dn, {}) != new.get(dn, {})))) entries = [{ dn: [{ 'replace': attrs }, { 'delete_others': delete_others }] } for dn, attrs in six.iteritems(replace)] actual = salt.states.ldap.managed(name, entries) self.assertDictEqual(expected_ret, actual) self.assertDictEqual(expected_db, db)
def _process_entries(l, entries): """Helper for managed() to process entries and return before/after views Collect the current database state and update it according to the data in :py:func:`managed`'s ``entries`` parameter. Return the current database state and what it will look like after modification. :param l: the LDAP connection object :param entries: the same object passed to the ``entries`` parameter of :py:func:`manage` :return: an ``(old, new)`` tuple that describes the current state of the entries and what they will look like after modification. Each item in the tuple is an OrderedDict that maps an entry DN to another dict that maps an attribute name to a set of its values (it's a set because according to the LDAP spec, attribute value ordering is unspecified and there can't be duplicates). The structure looks like this: {dn1: {attr1: set([val1])}, dn2: {attr1: set([val2]), attr2: set([val3, val4])}} All of an entry's attributes and values will be included, even if they will not be modified. If an entry mentioned in the entries variable doesn't yet exist in the database, the DN in ``old`` will be mapped to an empty dict. If an entry in the database will be deleted, the DN in ``new`` will be mapped to an empty dict. All value sets are non-empty: An attribute that will be added to an entry is not included in ``old``, and an attribute that will be deleted frm an entry is not included in ``new``. These are OrderedDicts to ensure that the user-supplied entries are processed in the user-specified order (in case there are dependencies, such as ACL rules specified in an early entry that make it possible to modify a later entry). """ old = OrderedDict() new = OrderedDict() for entries_dict in entries: for dn, directives_seq in entries_dict.items(): # get the old entry's state. first check to see if we've # previously processed the entry. olde = new.get(dn, None) if olde is None: # next check the database results = __salt__["ldap3.search"](l, dn, "base") if len(results) == 1: attrs = results[dn] olde = { attr: OrderedSet(attrs[attr]) for attr in attrs if len(attrs[attr]) } else: # nothing, so it must be a brand new entry assert len(results) == 0 olde = {} old[dn] = olde # copy the old entry to create the new (don't do a simple # assignment or else modifications to newe will affect # olde) newe = copy.deepcopy(olde) new[dn] = newe # process the directives entry_status = { "delete_others": False, "mentioned_attributes": set(), } for directives in directives_seq: _update_entry(newe, entry_status, directives) if entry_status["delete_others"]: to_delete = set() for attr in newe: if attr not in entry_status["mentioned_attributes"]: to_delete.add(attr) for attr in to_delete: del newe[attr] return old, new
def _test_helper(self, init_db, expected_ret, replace, delete_others=False): _init_db(copy.deepcopy(init_db)) old = _dump_db() new = _dump_db() expected_db = copy.deepcopy(init_db) for dn, attrs in replace.items(): for attr, vals in attrs.items(): vals = [to_bytes(val) for val in vals] if vals: new.setdefault(dn, {})[attr] = list(OrderedSet(vals)) expected_db.setdefault(dn, {})[attr] = OrderedSet(vals) elif dn in expected_db: new[dn].pop(attr, None) expected_db[dn].pop(attr, None) if not expected_db.get(dn, {}): new.pop(dn, None) expected_db.pop(dn, None) if delete_others: dn_to_delete = OrderedSet() for dn, attrs in expected_db.items(): if dn in replace: to_delete = OrderedSet() for attr, vals in attrs.items(): if attr not in replace[dn]: to_delete.add(attr) for attr in to_delete: del attrs[attr] del new[dn][attr] if not attrs: dn_to_delete.add(dn) for dn in dn_to_delete: del new[dn] del expected_db[dn] name = "ldapi:///" expected_ret["name"] = name expected_ret.setdefault("result", True) expected_ret.setdefault("comment", "Successfully updated LDAP entries") expected_ret.setdefault( "changes", { dn: { "old": { attr: vals for attr, vals in old[dn].items() if vals != new.get(dn, {}).get(attr, ()) } if dn in old else None, "new": { attr: vals for attr, vals in new[dn].items() if vals != old.get(dn, {}).get(attr, ()) } if dn in new else None, } for dn in replace if old.get(dn, {}) != new.get(dn, {}) }, ) entries = [ {dn: [{"replace": attrs}, {"delete_others": delete_others}]} for dn, attrs in replace.items() ] actual = salt.states.ldap.managed(name, entries) self.assertDictEqual(expected_ret, actual) self.assertDictEqual(expected_db, db)
def _test_helper_add(db, expected_ret, add_items, delete_others=False): old = db.dump_db() new = db.dump_db() expected_db = copy.deepcopy(db.db) for dn, attrs in add_items.items(): for attr, vals in attrs.items(): vals = [to_bytes(val) for val in vals] vals.extend(old.get(dn, {}).get(attr, OrderedSet())) vals.sort() if vals: new.setdefault(dn, {})[attr] = list(OrderedSet(vals)) expected_db.setdefault(dn, {})[attr] = OrderedSet(vals) elif dn in expected_db: new[dn].pop(attr, None) expected_db[dn].pop(attr, None) if not expected_db.get(dn, {}): new.pop(dn, None) expected_db.pop(dn, None) if delete_others: dn_to_delete = OrderedSet() for dn, attrs in expected_db.items(): if dn in add_items: to_delete = OrderedSet() for attr, vals in attrs.items(): if attr not in add_items[dn]: to_delete.add(attr) for attr in to_delete: del attrs[attr] del new[dn][attr] if not attrs: dn_to_delete.add(dn) for dn in dn_to_delete: del new[dn] del expected_db[dn] name = "ldapi:///" expected_ret["name"] = name expected_ret.setdefault("result", True) expected_ret.setdefault("comment", "Successfully updated LDAP entries") expected_ret.setdefault( "changes", { dn: { "old": { attr: vals for attr, vals in old[dn].items() if vals != new.get(dn, {}).get(attr, ()) } if dn in old else None, "new": { attr: vals for attr, vals in new[dn].items() if vals != old.get(dn, {}).get(attr, ()) } if dn in new else None, } for dn in add_items if old.get(dn, {}) != new.get(dn, {}) }, ) entries = [{ dn: [{ "add": attrs }, { "delete_others": delete_others }] } for dn, attrs in add_items.items()] actual = salt.states.ldap.managed(name, entries) assert expected_ret == actual assert expected_db == db.db