From bc056cda2dc9f55371064430a72b3667b5c29344 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 16 Mar 2009 17:38:48 -0400 Subject: Update the ACI class to be more robust and the beginnings of an ACI plugin The ACI plugin is really meant for developers to help manage the ACIs. It may or may not be shipped. If it is it will be disabled by default. It is very much a shoot-in-foot problem waiting to happen. --- ipalib/aci.py | 251 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 87 deletions(-) (limited to 'ipalib/aci.py') diff --git a/ipalib/aci.py b/ipalib/aci.py index 9dde767c0..a9219f8dc 100755 --- a/ipalib/aci.py +++ b/ipalib/aci.py @@ -29,6 +29,16 @@ ACIPat = re.compile(r'\s*(\(.*\)+)\s*\(version\s+3.0\s*;\s*acl\s+\"(.*)\"\s*;\s* # Break the permissions/bind_rules out PermPat = re.compile(r'(\w+)\s*\((.*)\)\s+(.*)') +# Break the bind rule out +BindPat = re.compile(r'([a-zA-Z0-9;\.]+)\s*(\!?=)\s*(.*)') + +# Don't allow arbitrary attributes to be set in our __setattr__ implementation. +OBJECTATTRS = ["name", "orig_acistr", "target", "action", "permissions", + "bindrule"] +ACTIONS = ["allow", "deny"] + +PERMISSIONS = ["read", "write", "add", "delete", "search", "compare", + "selfwrite", "proxy", "all"] class ACI: """ @@ -36,23 +46,13 @@ class ACI: entry in LDAP. Has methods to parse an ACI string and export to an ACI String. """ - - # Don't allow arbitrary attributes to be set in our __setattr__ implementation. - _objectattrs = ["name", "orig_acistr", "target", "action", "permissions", - "bindrule"] - - __actions = ["allow", "deny"] - - __permissions = ["read", "write", "add", "delete", "search", "compare", - "selfwrite", "proxy", "all"] - def __init__(self,acistr=None): self.name = None self.orig_acistr = acistr self.target = {} self.action = "allow" self.permissions = ["write"] - self.bindrule = None + self.bindrule = {} if acistr is not None: self._parse_acistr(acistr) @@ -70,73 +70,21 @@ class ACI: """An alias for export_to_string()""" return self.export_to_string() - def __getattr__(self, name): - """ - Backward compatibility for the old ACI class. - - The following extra attributes are available: - - - source_group - - dest_group - - attrs - """ - if name == 'source_group': - group = '' - dn = self.bindrule.split('=',1) - if dn[0] == "groupdn": - group = self._remove_quotes(dn[1]) - if group.startswith("ldap:///"): - group = group[8:] - return group - if name == 'dest_group': - group = self.target.get('targetfilter', '') - if group: - g = group.split('=',1)[1] - if g.endswith(')'): - g = g[:-1] - return g - return '' - if name == 'attrs': - return self.target.get('targetattr', None) - raise AttributeError, "object has no attribute '%s'" % name - - def __setattr__(self, name, value): - """ - Backward compatibility for the old ACI class. - - The following extra attributes are available: - - source_group - - dest_group - - attrs - """ - if name == 'source_group': - self.__dict__['bindrule'] = 'groupdn="ldap:///%s"' % value - elif name == 'dest_group': - if value.startswith('('): - self.__dict__['target']['targetfilter'] = 'memberOf=%s' % value - else: - self.__dict__['target']['targetfilter'] = '(memberOf=%s)' % value - elif name == 'attrs': - self.__dict__['target']['targetattr'] = value - elif name in self._objectattrs: - self.__dict__[name] = value - else: - raise AttributeError, "object has no attribute '%s'" % name - def export_to_string(self): """Output a Directory Server-compatible ACI string""" self.validate() aci = "" for t in self.target: - if isinstance(self.target[t], list): + op = self.target[t]['operator'] + if isinstance(self.target[t]['expression'], list): target = "" - for l in self.target[t]: + for l in self.target[t]['expression']: target = target + l + " || " target = target[:-4] - aci = aci + "(%s=\"%s\")" % (t, target) + aci = aci + "(%s %s \"%s\")" % (t, op, target) else: - aci = aci + "(%s=\"%s\")" % (t, self.target[t]) - aci = aci + "(version 3.0;acl \"%s\";%s (%s) %s" % (self.name, self.action, ",".join(self.permissions), self.bindrule) + ";)" + aci = aci + "(%s %s \"%s\")" % (t, op, self.target[t]['expression']) + aci = aci + "(version 3.0;acl \"%s\";%s (%s) %s %s \"%s\"" % (self.name, self.action, ",".join(self.permissions), self.bindrule['keyword'], self.bindrule['operator'], self.bindrule['expression']) + ";)" return aci def _remove_quotes(self, s): @@ -154,13 +102,18 @@ class ACI: l = [] var = False + op = "=" for token in lexer: # We should have the form (a = b)(a = b)... if token == "(": var = lexer.next().strip() operator = lexer.next() if operator != "=" and operator != "!=": - raise SyntaxError('No operator in target, got %s' % operator) + # Peek at the next char before giving up + operator = operator + lexer.next() + if operator != "=" and operator != "!=": + raise SyntaxError("No operator in target, got '%s'" % operator) + op = operator val = lexer.next().strip() val = self._remove_quotes(val) end = lexer.next() @@ -169,10 +122,14 @@ class ACI: if var == 'targetattr': # Make a string of the form attr || attr || ... into a list - t = re.split('[\W]+', val) - self.target[var] = t + t = re.split('[^a-zA-Z0-9;\*]+', val) + self.target[var] = {} + self.target[var]['operator'] = op + self.target[var]['expression'] = t else: - self.target[var] = val + self.target[var] = {} + self.target[var]['operator'] = op + self.target[var]['expression'] = val def _parse_acistr(self, acistr): acimatch = ACIPat.match(acistr) @@ -184,8 +141,8 @@ class ACI: if not bindperms or len(bindperms.groups()) < 3: raise SyntaxError, "malformed ACI" self.action = bindperms.group(1) - self.permissions = bindperms.group(2).split(',') - self.bindrule = bindperms.group(3) + self.permissions = bindperms.group(2).replace(' ','').split(',') + self.set_bindrule(bindperms.group(3)) def validate(self): """Do some basic verification that this will produce a @@ -196,7 +153,7 @@ class ACI: if not isinstance(self.permissions, list): raise SyntaxError, "permissions must be a list" for p in self.permissions: - if not p.lower() in self.__permissions: + if not p.lower() in PERMISSIONS: raise SyntaxError, "invalid permission: '%s'" % p if not self.name: raise SyntaxError, "name must be set" @@ -204,14 +161,100 @@ class ACI: raise SyntaxError, "name must be a string" if not isinstance(self.target, dict) or len(self.target) == 0: raise SyntaxError, "target must be a non-empty dictionary" + if not isinstance(self.bindrule, dict): + raise SyntaxError, "bindrule must be a dictionary" + if not self.bindrule.get('operator') or not self.bindrule.get('keyword') or not self.bindrule.get('expression'): + raise SyntaxError, "bindrule is missing a component" + return True + + def set_target_filter(self, filter, operator="="): + self.target['targetfilter'] = {} + if not filter.startswith("("): + filter = "(" + filter + ")" + self.target['targetfilter']['expression'] = filter + self.target['targetfilter']['operator'] = operator + + def set_target_attr(self, attr, operator="="): + if not isinstance(attr, list): + attr = [attr] + self.target['targetattr'] = {} + self.target['targetattr']['expression'] = attr + self.target['targetattr']['operator'] = operator + + def set_target(self, target, operator="="): + assert target.startswith("ldap:///") + self.target['target'] = {} + self.target['target']['expression'] = target + self.target['target']['operator'] = operator + + def set_bindrule(self, bindrule): + match = BindPat.match(bindrule) + if not match or len(match.groups()) < 3: + raise SyntaxError, "malformed bind rule" + self.set_bindrule_keyword(match.group(1)) + self.set_bindrule_operator(match.group(2)) + self.set_bindrule_expression(match.group(3).replace('"','')) + + def set_bindrule_keyword(self, keyword): + self.bindrule['keyword'] = keyword + + def set_bindrule_operator(self, operator): + self.bindrule['operator'] = operator + + def set_bindrule_expression(self, expression): + self.bindrule['expression'] = expression + + def isequal(self, b): + """ + Compare the current ACI to another one to see if they are + the same. + + returns True if equal, False if not. + """ + try: + if self.name != b.name: + return False + + if set(self.permissions) != set(b.permissions): + return False + + if self.bindrule.get('keyword') != b.bindrule.get('keyword'): + return False + if self.bindrule.get('operator') != b.bindrule.get('operator'): + return False + if self.bindrule.get('expression') != b.bindrule.get('expression'): + return False + + if self.target.get('targetfilter',{}).get('expression') != b.target.get('targetfilter',{}).get('expression'): + return False + if self.target.get('targetfilter',{}).get('operator') != b.target.get('targetfilter',{}).get('operator'): + return False + + if set(self.target.get('targetattr',{}).get('expression')) != set(b.target.get('targetattr',{}).get('expression')): + return False + if self.target.get('targetattr',{}).get('operator') != b.target.get('targetattr',{}).get('operator'): + return False + + if self.target.get('target',{}).get('expression') != b.target.get('target',{}).get('expression'): + return False + if self.target.get('target',{}).get('operator') != b.target.get('target',{}).get('operator'): + return False + + except Exception: + # If anything throws up then they are not equal + return False + + # We got this far so lets declare them the same return True def extract_group_cns(aci_list, client): - """Extracts all the cn's from a list of aci's and returns them as a hash - from group_dn to group_cn. + """ + Extracts all the cn's from a list of aci's and returns them as a hash + from group_dn to group_cn. - It first tries to cheat by looking at the first rdn for the - group dn. If that's not cn for some reason, it looks up the group.""" + It first tries to cheat by looking at the first rdn for the + group dn. If that's not cn for some reason, it looks up the group. + """ group_dn_to_cn = {} for aci in aci_list: for dn in (aci.source_group, aci.dest_group): @@ -231,15 +274,49 @@ def extract_group_cns(aci_list, client): return group_dn_to_cn if __name__ == '__main__': - # Pass in an ACI as a string - a = ACI('(targetattr="title")(targetfilter="(memberOf=cn=bar,cn=groups,cn=accounts ,dc=example,dc=com)")(version 3.0;acl "foobar";allow (write) groupdn="ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com";)') +# a = ACI('(targetattr="title")(targetfilter="(memberOf=cn=bar,cn=groups,cn=accounts ,dc=example,dc=com)")(version 3.0;acl "foobar";allow (write) groupdn="ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com";)') +# print a +# a = ACI('(target="ldap:///uid=bjensen,dc=example,dc=com")(targetattr=*) (version 3.0;acl "aci1";allow (write) userdn="ldap:///self";)') +# print a +# a = ACI(' (targetattr = "givenName || sn || cn || displayName || title || initials || loginShell || gecos || homePhone || mobile || pager || facsimileTelephoneNumber || telephoneNumber || street || roomNumber || l || st || postalCode || manager || secretary || description || carLicense || labeledURI || inetUserHTTPURL || seeAlso || employeeType || businessCategory || ou")(version 3.0;acl "Self service";allow (write) userdn = "ldap:///self";)') +# print a + + a = ACI('(target="ldap:///uid=*,cn=users,cn=accounts,dc=example,dc=com")(version 3.0;acl "add_user";allow (add) groupdn="ldap:///cn=add_user,cn=taskgroups,dc=example,dc=com";)') + print a + print "---" + + a = ACI('(targetattr=member)(target="ldap:///cn=ipausers,cn=groups,cn=accounts,dc=example,dc=com")(version 3.0;acl "add_user_to_default_group";allow (write) groupdn="ldap:///cn=add_user_to_default_group,cn=taskgroups,dc=example,dc=com";)') + print a + print "---" + + a = ACI('(targetattr!=member)(target="ldap:///cn=ipausers,cn=groups,cn=accounts,dc=example,dc=com")(version 3.0;acl "add_user_to_default_group";allow (write) groupdn="ldap:///cn=add_user_to_default_group,cn=taskgroups,dc=example,dc=com";)') print a + print "---" + + a = ACI('(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "change_password"; allow (write) groupdn = "ldap:///cn=change_password,cn=taskgroups,dc=example,dc=com";)') + print a + print "---" - # Create an ACI in pieces a = ACI() - a.name ="foobar" - a.source_group="cn=foo,cn=groups,dc=example,dc=org" - a.dest_group="cn=bar,cn=groups,dc=example,dc=org" - a.attrs = ['title'] + a.name ="foo" + a.set_target_attr(['title','givenname'], "!=") +# a.set_bindrule("groupdn = \"ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com\"") + a.set_bindrule_keyword("groupdn") + a.set_bindrule_operator("=") + a.set_bindrule_expression ("\"ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com\"") a.permissions = ['read','write','add'] print a + + b = ACI() + b.name ="foo" + b.set_target_attr(['givenname','title'], "!=") + b.set_bindrule_keyword("groupdn") + b.set_bindrule_operator("=") + b.set_bindrule_expression ("\"ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com\"") + b.permissions = ['add','read','write'] + print b + + print a.isequal(b) + + a = ACI('(targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)') + print a -- cgit