From 92a72515c6a6b77a4f811bdd19a3436c8609b002 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Fri, 6 Jun 2008 20:27:27 +0000 Subject: Add support for adding, removing entries, and arbitrary attributes --- Pivot.py | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 281 insertions(+), 60 deletions(-) (limited to 'Pivot.py') diff --git a/Pivot.py b/Pivot.py index fe865cd..12cdfb7 100644 --- a/Pivot.py +++ b/Pivot.py @@ -1,17 +1,20 @@ #!/usr/bin/env python -import ldap, ldap.dn, ldap.filter -import Backend, sets +import sys, os, sets +import ldap, ldap.dn, ldap.filter, ldif +import Backend HOST = "ldap://localhost:3890" -BINDDN = "cn=root,dc=fam" +ROOTDN = "cn=root,dc=fam" PASSWORD = "barn" BASE = "dc=fam" REF_ATTRIBUTE = "member" KEY_ATTRIBUTE = "uid" TAG_ATTRIBUTE = "memberOf" +ACCESS_ATTRIBUTE = "access" +FILENAME = "/tmp/pivot.ldif" -OBJECT_CLASS = "groupOfNames" +OBJECT_CLASS = "group" DN_ATTRIBUTE = "cn" # hasSubordinates: TRUE @@ -20,7 +23,7 @@ SCOPE_BASE = "0" SCOPE_ONE = "1" SCOPE_SUB = "2" -class Storage: +class Lookups: def __init__(self, url): self.url = url self.ldap = None @@ -32,7 +35,7 @@ class Storage: self.ldap.unbind() self.ldap = ldap.initialize(self.url) try: - self.ldap.simple_bind_s(BINDDN, PASSWORD) + self.ldap.simple_bind_s(ROOTDN, PASSWORD) except ldap.LDAPError, ex: raise Backend.Error(Backend.OPERATIONS_ERROR, "Couldn't do internal authenticate: %s" % ex.args[0]["desc"]) @@ -75,6 +78,88 @@ class Storage: self.__connect(True) self.modify(dn, mods, retries - 1) +class Storage: + + def __init__(self, filename): + self.filename = filename + self.entries = { } + self.load() + + def load(self): + if not os.path.exists(self.filename): + return + input = open(self.filename, 'r') + reader = ldif.LDIFRecordList(input) + reader.parse() + input.close() + self.entries = { } + for (dn, entry) in reader.all_records: + self.entries[dn] = entry + + def save(self): + output = open(self.filename, 'w') + print >> output, "# Overwritten automatically, do not edit\n" + + writer = ldif.LDIFWriter(output) + for (dn, entry) in self.entries.items(): + if (entry): + print + print dn + print repr(entry) + print + writer.unparse(dn, entry) + output.close() + + def __entry_for_dn(self, dn): + if not self.entries.has_key(dn): + self.entries[dn] = { } + return self.entries[dn] + + def store(self, dn, attribute, value): + if value is None: + return + entry = self.__entry_for_dn(dn) + if not entry.has_key(attribute): + entry[attribute] = [ ] + if value not in entry[attribute]: + entry[attribute].append(value) + + def remove(self, dn, attribute, value = None): + entry = self.__entry_for_dn(dn) + if entry.has_key(attribute): + if value is None: + del entry[attribute] + elif value in entry[attribute]: + entry[attribute] = [val for val in entry[attribute] if val != value] + + def has(self, dn, attribute, value = None): + entry = self.__entry_for_dn(dn) + if not entry.has_key(attribute): + return False + if value is None: + return True + return value in entry[attribute] + + def retrieve(self, dn, attribute): + entry = self.__entry_for_dn(dn) + if not entry.has_key(attribute): + return [ ] + return entry[attribute][:] # copy + + def list_attributes(self, dn): + entry = self.__entry_for_dn(dn) + return entry.keys()[:] # copy + + def list_dns(self): + return self.entries.keys()[:] # copy + + def exists(self, dn): + return dn in self.entries.keys() + + def delete(self, dn): + if self.entries.has_key(dn): + del self.entries[dn] + class Static: def __init__(self, func): @@ -89,7 +174,7 @@ class Tags: if not force and self.tags is not None: return try: - results = self.database.storage.search(BASE, "(%s=*)" % TAG_ATTRIBUTE, [TAG_ATTRIBUTE]) + results = self.database.lookups.search(BASE, "(%s=*)" % TAG_ATTRIBUTE, [TAG_ATTRIBUTE]) tags = { } for (dn, entry) in results: @@ -143,8 +228,16 @@ class Tags: from_database = Static(from_database) -def is_parsed_dn_parent (dn, parent): - if len(dn) <= len(parent): +def parse_dn(dn): + try: + return ldap.dn.str2dn(dn) + except: + raise Backend.Error(Backend.PROTOCOL_ERROR, "Invalid dn: %s" % dn) + +def is_parsed_dn_parent(dn, parent): + print dn + print parent + if len(dn) != len(parent) + 1: return False # Go backwards and validate each parent for i in range(-1, -1 - len(parent)): @@ -156,11 +249,9 @@ def is_parsed_dn_parent (dn, parent): class Database(Backend.Database): def __init__(self, suffix): Backend.Database.__init__(self, suffix) - self.storage = Storage(HOST) - try: - self.suffix_dn = ldap.dn.str2dn(self.suffix) - except ValueError: - raise Backend.Error(Backend.Error.PROTOCOL_ERROR, "invalid suffix dn") + self.lookups = Lookups(HOST) + self.suffix_dn = parse_dn(self.suffix) + self.storage = Storage(FILENAME) def __search_tag_keys(self, tags): @@ -176,7 +267,7 @@ class Database(Backend.Database): try: # Search for all those guys - results = self.storage.search(BASE, filter, [ KEY_ATTRIBUTE ]) + results = self.lookups.search(BASE, filter, [ KEY_ATTRIBUTE ]) except ldap.LDAPError, ex: raise Backend.Error(Backend.OPERATIONS_ERROR, @@ -192,7 +283,7 @@ class Database(Backend.Database): try: # Do the actual search - results = self.storage.search(BASE, filter) + results = self.lookups.search(BASE, filter) except ldap.LDAPError, ex: raise Backend.Error(Backend.OPERATIONS_ERROR, @@ -201,7 +292,7 @@ class Database(Backend.Database): return [dn for (dn, entry) in results if dn] - def __build_root_entry(self, tags, any_attrs = True): + def __build_root_entry(self, tags, with_attrs = True): attrs = { "objectClass" : [ "top" ], "hasSubordinates" : [ ] @@ -213,13 +304,13 @@ class Database(Backend.Database): attrs[typ].append(val) # Note that we don't access 'tags' unless attributes requested - if any_attrs: + if with_attrs: attrs["hasSubordinates"].append(tags and "TRUE" or "FALSE") return (self.suffix, attrs) - def __build_pivot_entry(self, tags, any_attrs = True): + def __build_pivot_entry(self, tags, with_attrs = True): attrs = { REF_ATTRIBUTE : [ ], "objectClass" : [ OBJECT_CLASS ], @@ -238,68 +329,118 @@ class Database(Backend.Database): dn = ldap.dn.dn2str(dn) # Get out all the attributes - if any_attrs: + if with_attrs: for key in self.__search_tag_keys(tags): attrs[REF_ATTRIBUTE].append(key) return (dn, attrs) + def __build_storage_entry(self, parsed_dn, with_attrs = True): + attrs = { } + + # Build up DN relevant attrs + for (typ, val, num) in parsed_dn[0]: + if not attrs.has_key(typ): + attrs[typ] = [ ] + attrs[typ].append(val) + + # All other storage attributes retrieved later if necessary + return (ldap.dn.dn2str(parsed_dn), attrs) + - def __limit_results(self, args, results): + def __complete_results(self, args, entries): # TODO: Support sizelimit # TODO: Support a filter # Only return the attribute names? - if args["attrsonly"] == "1": - for (dn, attrs) in results: - for attr in attrs: - attrs[attr] = [ "" ] + only_names = (args["attrsonly"] == "1") + which_attrs = args["attrs"] + all_attrs = (which_attrs == "all" or + which_attrs == "*" or + which_attrs == "+") + which_attrs = which_attrs.split(" ") - # Only return these attributes? - which = args["attrs"] - if which != "all" and which != "*" and which != "+": - which = which.split(" ") - for (dn, attrs) in results: - for attr in attrs.keys(): - if attr not in which: - del attrs[attr] + # Convert results from our map to a list with (dn, entry) tuples + results = [ ] + for (dn, entry) in entries.items(): - def search(self, dn, args): - results = [] + # Retrieve extra value names + extra = self.storage.list_attributes(dn) - try: - parsed = ldap.dn.str2dn(dn) - except: - raise Backend.Error(Backend.PROTOCOL_ERROR, "Invalid dn in search: %s" % dn) + # Only return attribute names + if only_names: + for attr in extra: + entry[attr] = [ "" ] + for attr in entry: + entry[attr] = [ "" ] + + # Return extra attribute names and values + else: + for attr in extra: + values = self.storage.retrieve(dn, attr) + if entry.has_key(attr): + entry[attr].extend(values) + else: + entry[attr] = values + # Remove all duplicates + entry[attr] = list(set(entry[attr])) + + # Limit to the attributes requested + if not all_attrs: + for attr in entry.keys(): + if attr not in which_attrs: + del entry[attr] + + results.append((dn, entry)) + + return results + + + def search(self, dn, args): + results = { } + parsed = parse_dn(dn) # Arguments sent scope = args["scope"] or SCOPE_BASE - any_attrs = len(args["attrs"].strip()) > 0 + with_attrs = len(args["attrs"].strip()) > 0 # Start at the root if parsed == self.suffix_dn: tags = Tags.from_database(self) if scope == SCOPE_BASE or scope == SCOPE_SUB: - results.append(self.__build_root_entry(tags, any_attrs)) + (dn, entry) = self.__build_root_entry(tags, with_attrs) + results[dn] = entry if scope == SCOPE_ONE or scope == SCOPE_SUB: # Process each tag individually, by default for (tag, typ) in tags.items(): - results.append(self.__build_pivot_entry({ tag : typ }, any_attrs)) + (child, entry) = self.__build_pivot_entry({ tag : typ }, with_attrs) + results[child] = entry + # Process all extra storage items + for child in self.storage.list_dns(): + if child not in results: + (child, entry) = self.__build_storage_entry(parse_dn(child)) + results[child] = entry # Start at a tag - elif is_parsed_dn_parent (parsed, self.suffix_dn): + elif is_parsed_dn_parent(parsed, self.suffix_dn): tags = Tags.from_parsed_dn(parsed) if scope == SCOPE_BASE or scope == SCOPE_SUB: - results.append(self.__build_pivot_entry(tags, any_attrs)) + (dn, entry) = self.__build_pivot_entry(tags, with_attrs) + results[dn] = entry + + # Something in the database + elif self.storage.exists(dn): + if scope == SCOPE_BASE or scope == SCOPE_SUB: + (dn, entry) = self.__build_storage_entry(parsed, with_attrs) + results[dn] = entry # We don't have that base else: raise Backend.Error(Backend.NO_SUCH_OBJECT, "DN '%s' does not exist" % dn) - self.__limit_results(args, results) - return results + return self.__complete_results(args, results) def __build_key_mods(self, key, tags, op, mods): @@ -313,23 +454,90 @@ class Database(Backend.Database): for tag in tags: mods[dn][1].append((op, TAG_ATTRIBUTE, tag)) - def modify(self, dn, mods): - try: - parsed = ldap.dn.str2dn(dn) - except: - raise Backend.Error(Backend.PROTOCOL_ERROR, "Invalid dn in modify: %s" % dn) + def __check_write_access(self, dn): + if self.binddn == ROOTDN: + return True + return self.storage.has(dn, ACCESS_ATTRIBUTE, self.binddn) - if dn == self.suffix: - raise Backend.Error(Backend.INSUFFICIENT_ACCESS, - "Cannot modify root dn of pivot area: %s" % dn) - if not is_parsed_dn_parent (parsed, self.suffix_dn): - raise Backend.Error(Backend.NO_SUCH_OBJECT, - "DN '%s' does not exist" % dn) + def add(self, dn, entry): + + parsed = parse_dn(dn) + tags = Tags.from_parsed_dn(parsed) + + if parsed == self.suffix_dn: + raise Backend.Error(Backend.ALREADY_EXISTS, "This entry already exists: %s" % dn) + if not is_parsed_dn_parent(parsed, self.suffix_dn): + raise Backend.Error(Backend.NO_SUCH_OBJECT, "Parent of '%s' does not exist or is not valid" % dn) + if self.storage.exists(dn): + raise Backend.Error(Backend.ALREADY_EXISTS, "This entry already exists: %s" % dn) + if len(self.__search_tag_keys(tags)): + raise Backend.Error(Backend.ALREADY_EXISTS, "This entry already exists: %s" % dn) + + # Everyone has implicit access to create a new group + + # Convert into a modify change set + mods = [] + for (attr, values) in entry.items(): + for value in values: + mods.append((ldap.MOD_ADD, attr, value)) + + # Add an access attribute for the creator + if self.binddn and not ACCESS_ATTRIBUTE in entry : + mods.append((ldap.MOD_ADD, ACCESS_ATTRIBUTE, self.binddn)) + + # Make the actual changes + self.__change(parsed, mods, tags) + + # Save extra attributes to storage + self.storage.save() + + + def delete(self, dn, args): + parsed = parse_dn(dn) + tags = Tags.from_parsed_dn(parsed) + + if parsed == self.suffix_dn: + raise Backend.Error(Backend.NOT_ALLOWED_ON_NONLEAF, "Cannot delete the root entry: %s" % dn) + if not is_parsed_dn_parent(parsed, self.suffix_dn): + raise Backend.Error(Backend.NO_SUCH_OBJECT, "Entry does not exist: %s" % dn) + + if not self.__check_write_access(dn): + raise Backend.Error(Backend.INSUFFICIENT_ACCESS, "Access denied to delete entry: %s" % dn) + + mods = [] + mods.append((ldap.MOD_DELETE, REF_ATTRIBUTE, None)) + + # Make the actual changes + self.__change(parsed, mods, tags) + # Delete extra attributes from storage + self.storage.delete(dn) + self.storage.save() + + + def modify(self, dn, mods): + parsed = parse_dn(dn) tags = Tags.from_parsed_dn(parsed) + if dn == self.suffix: + raise Backend.Error(Backend.INSUFFICIENT_ACCESS, "Cannot modify root dn of pivot area: %s" % dn) + if not is_parsed_dn_parent (parsed, self.suffix_dn): + raise Backend.Error(Backend.NO_SUCH_OBJECT, "DN '%s' does not exist" % dn) + + if not self.__check_write_access(dn): + raise Backend.Error(Backend.INSUFFICIENT_ACCESS, "Access denied to modify entry: %s" % dn) + + # Make the actual changes + self.__change(parsed, mods, tags) + + # Save extra attributes to storage + self.storage.save() + + + def __change(self, parsed, mods, tags): + add_keys = sets.Set() remove_keys = sets.Set() remove_all = False @@ -337,9 +545,9 @@ class Database(Backend.Database): # Parse out all the adds and removes for (op, attr, value) in mods: + # Process access attributes later if attr != REF_ATTRIBUTE: - raise Backend.Error(Backend.CONSTRAINT_VIOLATION, - "Cannot modify '%s' attribute" % attr) + continue if op == ldap.MOD_ADD: if value: @@ -380,7 +588,7 @@ class Database(Backend.Database): for (dn, (key, mod)) in keys_and_mods_by_dn.items(): try: print dn, mod - self.storage.modify(dn, mod) + self.lookups.modify(dn, mod) except (ldap.TYPE_OR_VALUE_EXISTS, ldap.NO_SUCH_ATTRIBUTE): continue except ldap.NO_SUCH_OBJECT: @@ -394,4 +602,17 @@ class Database(Backend.Database): raise Backend.Error(Backend.CONSTRAINT_VIOLATION, "Couldn't change %s for %s" % (KEY_ATTRIBUTE, ", ".join(errors))) + # Process other attributes now + dn = ldap.dn.dn2str(parsed) + for (op, attr, value) in mods: + if attr == REF_ATTRIBUTE: + continue + if op == ldap.MOD_ADD: + self.storage.store(dn, attr, value) + elif op == ldap.MOD_REPLACE: + self.storage.remove(dn, attr) + self.storage.add(dn, attr, value) + elif op == ldap.MOD_DELETE: + self.storage.remove(dn, attr, value) + -- cgit v1.2.3