summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStef Walter <stef@thewalter.net>2008-06-06 20:27:27 +0000
committerStef Walter <stef@thewalter.net>2008-06-06 20:27:27 +0000
commit92a72515c6a6b77a4f811bdd19a3436c8609b002 (patch)
tree447019bd91125d55c139dd225794f4c0880b17e5
parent089fc82089164ba70b3c9cb6d439e44f88adeaae (diff)
Add support for adding, removing entries, and arbitrary attributes
-rw-r--r--Pivot.py341
1 files changed, 281 insertions, 60 deletions
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)
+