diff options
-rwxr-xr-x | ipalib/aci.py | 251 | ||||
-rw-r--r-- | ipalib/plugins/aci.py | 438 |
2 files changed, 602 insertions, 87 deletions
diff --git a/ipalib/aci.py b/ipalib/aci.py index 9dde767c..a9219f8d 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 diff --git a/ipalib/plugins/aci.py b/ipalib/plugins/aci.py new file mode 100644 index 00000000..6eb48264 --- /dev/null +++ b/ipalib/plugins/aci.py @@ -0,0 +1,438 @@ +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2009 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Frontend plugins for managing DS ACIs +""" + +from ipalib import api, crud, errors2 +from ipalib import Object, Command # Plugin base classes +from ipalib import Str, Flag, Int # Parameter types +from ipalib.aci import ACI + +def make_aci(current, aciname, kw): + try: + taskgroup = api.Command['taskgroup_show'](kw['taskgroup']) + except errors2.NotFound: + # The task group doesn't exist, let's be helpful and add it + tgkw = {'description':aciname} + taskgroup = api.Command['taskgroup_add'](kw['taskgroup'], **tgkw) + + a = ACI(current) + a.name = aciname + a.permissions = kw['permissions'].replace(' ','').split(',') + a.set_bindrule("groupdn = \"ldap:///%s\"" % taskgroup['dn']) + if kw.get('attrs', None): + a.set_target_attr(kw['attrs'].split()) + if kw.get('type', None): + a.set_target_attr(kw['attrs'].split()) + if kw.get('memberof', None): + group = api.Command['group_show'](kw['memberof']) + a.set_target_filter("memberOf=%s" % group['dn'].decode('UTF-8')) + return a + +def search_by_name(acis, aciname): + """ + Find an aci using the name field. + + Must be an exact match of the entire name. + """ + for a in acis: + try: + t = ACI(a) + if t.name == aciname: + return str(t) + except SyntaxError, e: + # FIXME: need to log syntax errors, ignore for now + pass + + raise errors2.NotFound() + +def search_by_attr(acis, attrlist): + """ + Find an aci by targetattr. + + Returns an ACI list of all acis the attribute appears in. + """ + results = [] + for a in acis: + try: + t = ACI(a) + for attr in attrlist: + attr = attr.lower() + for v in t.target['targetattr'].get('expression'): + if attr == v.lower(): + results.append(str(t)) + except SyntaxError, e: + # FIXME: need to log syntax errors, ignore for now + pass + + if results: + return results + + raise errors2.NotFound() + +def search_by_taskgroup(acis, tgdn): + """ + Find an aci by taskgroup. This searches the ACI bind rule. + + Returns an ACI list of all acis that match. + """ + results = [] + for a in acis: + try: + t = ACI(a) + if t.bindrule['expression'] == "ldap:///" + tgdn: + results.append(str(t)) + except SyntaxError, e: + # FIXME: need to log syntax errors, ignore for now + pass + + if results: + return results + + raise errors2.NotFound() + +def search_by_perm(acis, permlist): + """ + Find an aci by permissions + + Returns an ACI list of all acis the permission appears in. + """ + results = [] + for a in acis: + try: + t = ACI(a) + for perm in permlist: + if perm.lower() in t.permissions: + results.append(str(t)) + except SyntaxError, e: + # FIXME: need to log syntax errors, ignore for now + pass + + if results: + return results + + raise errors2.NotFound() + +def search_by_memberof(acis, memberoffilter): + """ + Find an aci by memberof + + Returns an ACI list of all acis that has a matching memberOf as a + targetfilter. + """ + results = [] + memberoffilter = memberoffilter.lower() + for a in acis: + try: + t = ACI(a) + try: + if memberoffilter == t.target['targetfilter'].get('expression').lower(): + results.append(str(t)) + except KeyError: + pass + except SyntaxError, e: + # FIXME: need to log syntax errors, ignore for now + pass + + if results: + return results + + raise errors2.NotFound() + +class aci(Object): + """ + ACI object. + """ + takes_params = ( + Str('aciname', + doc='Name of ACI', + primary_key=True, + ), + Str('taskgroup', + doc='Name of taskgroup this ACI grants access to', + ), + Str('permissions', + doc='Permissions to grant: read, write', + ), + Str('attrs?', + doc='Comma-separated list of attributes', + ), + Str('type?', + doc='type of IPA object: user, group, host', + ), + Str('memberof?', + doc='member of a group', + ), + Str('filter?', + doc='A legal LDAP filter (ou=Engineering)', + ), + Str('subtree?', + doc='A subtree to apply the ACI to', + ), + ) +api.register(aci) + + +class aci_add(crud.Create): + """ + Add a new aci. + """ + + def execute(self, aciname, **kw): + """ + Execute the aci-add operation. + + Returns the entry as it will be created in LDAP. + + :param aciname: The name of the ACI being added. + :param kw: Keyword arguments for the other LDAP attributes. + """ + assert 'aciname' not in kw + ldap = self.api.Backend.ldap + + newaci = make_aci(None, aciname, kw) + + currentaci = ldap.retrieve(self.api.env.basedn, ['aci']) + + acilist = currentaci.get('aci') + for a in acilist: + try: + b = ACI(a) + if newaci.isequal(b): + raise errors2.DuplicateEntry() + except SyntaxError: + pass + acilist.append(str(newaci)) + kwupdate = {'aci': acilist} + + return ldap.update(currentaci.get('dn'), **kwupdate) + +api.register(aci_add) + + +class aci_del(crud.Delete): + 'Delete an existing aci.' + """ + Remove an aci by name. + """ + + def execute(self, aciname, **kw): + """ + Execute the aci-del operation. + + :param aciname: The name of the ACI being added. + :param kw: unused + """ + assert 'aciname' not in kw + ldap = self.api.Backend.ldap + + currentaci = ldap.retrieve(self.api.env.basedn, ['aci']) + acilist = currentaci.get('aci') + a = search_by_name(acilist, aciname) + i = acilist.index(str(a)) + del acilist[i] + + kwupdate = {'aci': acilist} + + return ldap.update(currentaci.get('dn'), **kwupdate) + + def output_for_cli(self, textui, result, aciname): + """ + Output result of this command to command line interface. + """ + textui.print_plain('Deleted aci "%s"' % aciname) + +api.register(aci_del) + + +class aci_mod(crud.Update): + 'Edit an existing aci.' + def execute(self, aciname, **kw): + return "Not implemented" + def output_for_cli(self, textui, result, aciname, **options): + textui.print_plain(result) +api.register(aci_mod) + + +class aci_find(crud.Search): + 'Search for a aci.' + takes_options = ( + Str('bindrule?', + doc='The bindrule (e.g. ldap:///self)' + ), + Flag('and?', + doc='Consider multiple options to be \"and\" so all are required.') + ) + def execute(self, term, **kw): + ldap = self.api.Backend.ldap + currentaci = ldap.retrieve(self.api.env.basedn, ['aci']) + currentaci = currentaci.get('aci') + results = [] + + # aciname + if kw.get('aciname'): + try: + a = search_by_name(currentaci, kw.get('aciname')) + results = [a] + if kw.get('and'): + currentaci = results + except errors2.NotFound: + if kw.get('and'): + results = [] + currentaci = [] + pass + + # attributes + if kw.get('attrs'): + try: + attrs = kw.get('attrs') + attrs = attrs.replace(' ','').split(',') + a=search_by_attr(currentaci, attrs) + if kw.get('and'): + results = a + currentaci = results + else: + results = results + a + except errors2.NotFound: + if kw.get('and'): + results = [] + currentaci = [] + pass + + # taskgroup + if kw.get('taskgroup'): + try: + tg = api.Command['taskgroup_show'](kw.get('taskgroup')) + except errors2.NotFound: + # FIXME, need more precise error + raise + try: + a=search_by_taskgroup(currentaci, tg.get('dn')) + if kw.get('and'): + results = a + currentaci = results + else: + results = results + a + except errors2.NotFound: + if kw.get('and'): + results = [] + currentaci = [] + pass + + # permissions + if kw.get('permissions'): + try: + permissions = kw.get('permissions') + permissions = permissions.replace(' ','').split(',') + a=search_by_perm(currentaci, permissions) + if kw.get('and'): + results = a + currentaci = results + else: + results = results + a + except errors2.NotFound: + if kw.get('and'): + results = [] + currentaci = [] + pass + + # memberOf + if kw.get('memberof'): + try: + group = api.Command['group_show'](kw['memberof']) + memberof = "(memberOf=%s)" % group['dn'].decode('UTF-8') + a=search_by_memberof(currentaci, memberof) + results = results + a + if kw.get('and'): + currentaci = results + except errors2.NotFound: + if kw.get('and'): + results = [] + currentaci = [] + pass + +# TODO +# --type=STR type of IPA object: user, group, host +# --filter=STR A legal LDAP filter (ou=Engineering) +# --subtree=STR A subtree to apply the ACI to +# --bindrule=STR A subtree to apply the ACI to + + # Make sure we have no dupes in the list + results = list(set(results)) + + # the first entry contains the count + counter = len(results) + return [counter] + results + + def output_for_cli(self, textui, result, term, **options): + counter = result[0] + acis = result[1:] + if counter == 0 or len(acis) == 0: + textui.print_plain("No entries found") + return + textui.print_name(self.name) + for a in acis: + textui.print_plain(a) + textui.print_count(acis, '%d acis matched') + +api.register(aci_find) + + +class aci_show(crud.Retrieve): + 'Examine an existing aci.' + def execute(self, aciname, **kw): + """ + Execute the aci-show operation. + + Returns the entry + + :param uid: The login name of the user to retrieve. + :param kw: unused + """ + ldap = self.api.Backend.ldap + currentaci = ldap.retrieve(self.api.env.basedn, ['aci']) + + a = search_by_name(currentaci.get('aci'), aciname) + return str(a) + + def output_for_cli(self, textui, result, aciname, **options): + textui.print_plain(result) + +api.register(aci_show) + + +class aci_showall(Command): + 'Examine all existing acis.' + def execute(self): + """ + Execute the aci-show operation. + + Returns the entry + + :param uid: The login name of the user to retrieve. + :param kw: unused + """ + ldap = self.api.Backend.ldap + return ldap.retrieve(self.api.env.basedn, ['aci']) + def output_for_cli(self, textui, result, **options): + textui.print_entry(result) + +api.register(aci_showall) |