From e7beb312a593d2d09f612feb3feb37c12ca5a018 Mon Sep 17 00:00:00 2001 From: William Brown Date: Tue, 4 Aug 2015 13:30:24 +0930 Subject: [PATCH] Add aci parsing utilities, which will return an EntryAci. Can rebuild acis from dictionary data so that EntryAci objects can be edited and then saved back to ldap. Bindrules are not yet parsed, as this adds another layer of complexitiy. However the skeleton structures to parse these is in place. --- lib389/__init__.py | 14 ++-- lib389/_entry.py | 212 ++++++++++++++++++++++++++++++++++++++++++++++++ lib389/aci.py | 32 ++++++++ tests/aci_parse_test.py | 70 ++++++++++++++++ 4 files changed, 322 insertions(+), 6 deletions(-) create mode 100644 lib389/aci.py create mode 100644 tests/aci_parse_test.py diff --git a/lib389/__init__.py b/lib389/__init__.py index 519f04a..5aef9bc 100644 --- a/lib389/__init__.py +++ b/lib389/__init__.py @@ -140,12 +140,12 @@ def wrapper(f, name): return objtype, Entry(data) elif isinstance(data, list): # AD sends back these search references -# if objtype == ldap.RES_SEARCH_RESULT and \ -# isinstance(data[-1],tuple) and \ -# not data[-1][0]: -# print "Received search reference: " -# pprint.pprint(data[-1][1]) -# data.pop() # remove the last non-entry element + # if objtype == ldap.RES_SEARCH_RESULT and \ + # isinstance(data[-1],tuple) and \ + # not data[-1][0]: + # print "Received search reference: " + # pprint.pprint(data[-1][1]) + # data.pop() # remove the last non-entry element return objtype, [Entry(x) for x in data] else: @@ -343,6 +343,7 @@ class DirSrv(SimpleLDAPObject): from lib389.plugins import Plugins from lib389.tasks import Tasks from lib389.index import Index + from lib389.aci import Aci self.agreement = Agreement(self) self.replica = Replica(self) @@ -355,6 +356,7 @@ class DirSrv(SimpleLDAPObject): self.schema = Schema(self) self.plugins = Plugins(self) self.tasks = Tasks(self) + self.aci = Aci(self) def __init__(self, verbose=False, timeout=10): """ diff --git a/lib389/_entry.py b/lib389/_entry.py index b97b6e7..2bab0f9 100644 --- a/lib389/_entry.py +++ b/lib389/_entry.py @@ -212,3 +212,215 @@ class Entry(object): except MissingEntryError: log.exception("This entry should exist!") raise + + def getAcis(self): + if not self.hasAttr('aci'): + # There should be a better way to do this? Perhaps + # self search for the aci attr? + return [] + self.acis = map(lambda a: EntryAci(self, a), self.getValues('aci')) + return self.acis + +class EntryAci(object): + + # See https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/10/html/Administration_Guide/Managing_Access_Control-Bind_Rules.html + # https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/10/html/Administration_Guide/Managing_Access_Control-Creating_ACIs_Manually.html + # We seperate the keys into 3 groups, and one group that has overlap. + # This is so we can not only split the aci, but rebuild it from the dictionary + # at a later point in time. + # These are top level aci comoponent keys + _keys = [ + 'targetscope', + 'targetattrfilters', + 'targattrfilters', + 'targetfilter', + 'targetattr', + 'target', + 'version 3.0;', + ] + # These are the keys which are seperated by ; in the version 3.0 stanza. + _v3keys = [ + 'allow', + 'acl', + 'deny', + ] + # These are the keys which are used on the inside of a v3 allow statement + # We have them defined, but don't currently use them. + _v3innerkeys = [ + 'roledn', + 'userattr', + 'ip', + 'dns', + 'dayofweek', + 'timeofday', + 'authmethod', + 'userdn', + 'groupdn', + ] + # These keys values are prefixed with ldap:///, so we need to know to re-prefix + # ldap:/// onto the value when we rebuild the aci + _urlkeys = ['target', + 'userdn', + 'groupdn', + 'roledn', + ] + + def __init__(self, entry, rawaci): + """ + Breaks down an aci attribute string from 389, into a dictionary + of terms and values. These values can then be manipulated, and + subsequently rebuilt into an aci string. + """ + self.entry = entry + self._rawaci = rawaci + self.acidata = self._parse_aci(self._rawaci) + + def _format_term(self, key, value_dict): + rawaci = '' + if value_dict['equal']: + rawaci += '="' + else: + rawaci += '!="' + if key in self._urlkeys: + values = map(lambda x: 'ldap:///%s' % x, value_dict['values']) + else: + values = value_dict['values'] + for value in values[:-1]: + rawaci += "%s || " % value + rawaci += values[-1] + rawaci += '"' + return rawaci + + def getRawAci(self): + """ + This method will rebuild an aci from the contents of the acidata + dict found on the object. + + returns an aci attribute string. + + """ + # Rebuild the aci from the .acidata. + rawaci = '' + # For each key in the outer segment + ## Add a (key = val);. Depending on key format val: + for key in self._keys: + for value_dict in self.acidata[key]: + rawaci += '(%s %s)' % (key, self._format_term(key, value_dict)) + # Now create the v3.0 aci part + rawaci += "(version 3.0; " + # This could be neater ... + rawaci += 'acl "%s";' % self.acidata['acl'][0]['values'][0] + for key in ['allow', 'deny']: + if len(self.acidata[key]) > 0: + rawaci += '%s (' % key + for value in self.acidata[key][0]['values'][:-1]: + rawaci += '%s, ' % value + rawaci += '%s)' % self.acidata[key][0]['values'][-1] + rawaci += '(%s);' % self.acidata["%s_raw_bindrules" % key][0]['values'][-1] + rawaci += ")" + return rawaci + + def _find_terms(self, aci): + lbr_list = [] + rbr_list = [] + depth = 0 + for i, char in enumerate(aci): + if char == '(' and depth == 0: + lbr_list.append(i) + if char == '(': + depth += 1 + if char == ')' and depth == 1: + rbr_list.append(i) + if char == ')': + depth -= 1 + # Now build a set of terms. + terms = [] + for lb, rb in zip(lbr_list, rbr_list): + terms.append(aci[lb + 1:rb]) + return terms + + def _parse_term(self, key, term): + wdict = { 'values': [] , 'equal': True} + # Nearly all terms are = seperated + ## We make a dict that holds "equal" and an array of values + pre, val = term.split('=', 1) + val = val.replace('"', '') + if pre.strip() == '!': + wdict['equal'] = False + else: + wdict['equal'] = True + wdict['values'] = val.split('||') + if key in self._urlkeys: + ### / We could replace ldap:/// in some attrs? + wdict['values'] = map(lambda x: x.replace('ldap:///',''), wdict['values']) + wdict['values'] = map(lambda x: x.strip(), wdict['values']) + return wdict + + def _parse_bind_rules(self, subterm): + + # First, determine if there are extraneous braces wrapping the term. + subterm = subterm.strip() + if subterm[0] == '(' and subterm[-1] == ')': + subterm = subterm[1:-1] + terms = subterm.split('and') + # We could parse everything into nice structures, and then work with them. + # or we can just leave the bind rule alone, as a string. Let the human do it. + # it comes down to cost versus reward. + + return [subterm] + + def _parse_version_3_0(self, rawacipart, data): + # We have to do this because it's not the same as other term formats. + terms = [] + bindrules = [] + interms = rawacipart.split(';') + interms = map(lambda x: x.strip(), interms) + for iwork in interms: + for j in self._v3keys + self._v3innerkeys: + if iwork.startswith(j) and j == 'acl': + t = iwork.split(' ', 1)[1] + t = t.replace('"', '') + data[j].append({ 'values' : [t]}) + if iwork.startswith(j) and (j == 'allow' or j == 'deny'): + first = iwork.index('(') + 1 + second = iwork.index(')', first) + # This could likely be neater ... + data[j].append({ + 'values' : map( lambda x: x.strip(), + iwork[first:second].split(',') + ) + }) + subterm = iwork[second + 1:] + data["%s_raw_bindrules" % j].append({ + 'values' : self._parse_bind_rules(subterm) + }) + + return terms + + def _parse_aci(self, rawaci): + aci = rawaci + depth = 0 + data = { + 'rawaci': rawaci, + 'allow_raw_bindrules' : [], + 'deny_raw_bindrules' : [], + } + for k in self._keys + self._v3keys: + data[k] = [] + # We need to get a list of all the depth 0 ( and ) + terms = self._find_terms(aci) + + while len(terms) > 0: + work = terms.pop() + for k in self._keys + self._v3keys + self._v3innerkeys: + if work.startswith(k): + aci = work.replace(k, '', 1) + if k == 'version 3.0;': + #We pop more inner terms out, but we don't need to parse them "now" + # they get added to the queue + terms += self._parse_version_3_0(aci, data) + continue + data[k].append(self._parse_term(k, aci)) + break + return data + diff --git a/lib389/aci.py b/lib389/aci.py new file mode 100644 index 0000000..d72a70a --- /dev/null +++ b/lib389/aci.py @@ -0,0 +1,32 @@ +"""Aci class to help parse and create ACIs. + +You will access this via the Entry Class. +""" + +import ldap + +from lib389._constants import * +from lib389 import Entry, InvalidArgumentError + +class Aci(object): + + + def __init__(self, conn): + """ + """ + self.conn = conn + self.log = conn.log + + def list(self, basedn, scope=ldap.SCOPE_SUBTREE): + """ + List all acis in the directory server below the basedn confined by scope. + + A set of EntryAcis is returned. + """ + acis = [] + rawacientries = self.conn.search_s(basedn, scope, 'aci=*', ['aci']) + for rawacientry in rawacientries: + acis += rawacientry.getAcis() + return acis + + diff --git a/tests/aci_parse_test.py b/tests/aci_parse_test.py new file mode 100644 index 0000000..72b716b --- /dev/null +++ b/tests/aci_parse_test.py @@ -0,0 +1,70 @@ +''' +Created on Aug 3, 2015 + +@author: William Brown +''' +from lib389._constants import * +from lib389._aci import Aci +from lib389 import DirSrv,Entry +import ldap + +INSTANCE_PORT = 54321 +INSTANCE_SERVERID = 'aciparseds' +#INSTANCE_PREFIX = None + +class Test_schema(): + def setUp(self): + instance = DirSrv(verbose=False) + instance.log.debug("Instance allocated") + args = {SER_HOST: LOCALHOST, + SER_PORT: INSTANCE_PORT, + #SER_DEPLOYED_DIR: INSTANCE_PREFIX, + SER_SERVERID_PROP: INSTANCE_SERVERID + } + instance.allocate(args) + if instance.exists(): + instance.delete() + instance.create() + instance.open() + self.instance = instance + + def tearDown(self): + if self.instance.exists(): + self.instance.delete() + + def create_complex_acis(self): + gentry = Entry('cn=testgroup,%s' % DEFAULT_SUFFIX) + gentry.setValues('objectclass', 'top', 'extensibleobject') + gentry.setValues('cn', 'testgroup') + gentry.setValues('aci', + """ (targetfilter="(ou=groups)")(targetattr = "uniqueMember || member")(version 3.0; acl "Allow test aci"; allow (read, search, write) (userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)" and userdn="ldap:///dc=example,dc=com??sub?(manager=uid=wbrown,ou=managers,dc=example,dc=com) || ldap:///dc=example,dc=com??sub?(manager=uid=tbrown,ou=managers,dc=example,dc=com)" );) """, + ) + self.instance.add_s(gentry) + + def test_aci(self): + acis = self.instance.aci.list('cn=testgroup,%s' % DEFAULT_SUFFIX) + assert len(acis) == 1 + aci = acis[0] + assert aci.acidata == { + 'allow': [{'values': ['read', 'search', 'write']}], + 'target': [], 'targetattr': [{'values': ['uniqueMember', 'member'], 'equal': True}], + 'targattrfilters': [], + 'deny': [], + 'acl': [{'values': ['Allow test aci']}], + 'deny_raw_bindrules': [], + 'targetattrfilters': [], + 'allow_raw_bindrules': [{'values': ['userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)" and userdn="ldap:///dc=example,dc=com??sub?(manager=uid=wbrown,ou=managers,dc=example,dc=com) || ldap:///dc=example,dc=com??sub?(manager=uid=tbrown,ou=managers,dc=example,dc=com)" ']}], + 'targetfilter': [{'values': ['(ou=groups)'], 'equal': True}], + 'targetscope': [], + 'version 3.0;': [], + 'rawaci': ' (targetfilter="(ou=groups)")(targetattr = "uniqueMember || member")(version 3.0; acl "Allow test aci"; allow (read, search, write) (userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)" and userdn="ldap:///dc=example,dc=com??sub?(manager=uid=wbrown,ou=managers,dc=example,dc=com) || ldap:///dc=example,dc=com??sub?(manager=uid=tbrown,ou=managers,dc=example,dc=com)" );) ' + } + assert aci.getRawAci() == """(targetfilter ="(ou=groups)")(targetattr ="uniqueMember || member")(version 3.0; acl "Allow test aci";allow (read, search, write)(userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)" and userdn="ldap:///dc=example,dc=com??sub?(manager=uid=wbrown,ou=managers,dc=example,dc=com) || ldap:///dc=example,dc=com??sub?(manager=uid=tbrown,ou=managers,dc=example,dc=com)" );)""" + +if __name__ == "__main__": + test = Test_schema() + test.setUp() + test.create_complex_acis() + test.test_aci() + test.tearDown() + -- 2.4.3