From aa5fb90e823bc08831c524acbc3fd721b01c47c9 Mon Sep 17 00:00:00 2001 From: "Thierry bordaz (tbordaz)" Date: Wed, 15 Jan 2014 11:38:32 +0100 Subject: [PATCH] Ticket 47653 - Need a way to allow users to create entries assigned to themselves. Bug Description: Users need to be able to create, edit and delete their own entries. An entry (i.e. cn=token1_user1234,dc=example,dc=com) has an attribute (i.e. ipatokenOwner) that contains the entry DN of the user (i.e. uid=user1234,dc=example,dc=com). Being bound as 'uid=user1234,dc=example,dc=com', we should be able to any ldap operation on 'user1234' entries like cn=token1_user1234. Fix Description: It adds a BindRule: SELFDN, implemented in DS_LASSelfDnAttrEval (called by DS_LASUserAttrEval). The syntax in the aci is : = #SELFDN If the BindDN (lasinfo.clientDn) exists in entry. (lasinfo.resourceEntry[attrName]), then the aci matched https://fedorahosted.org/389/ticket/47653 Reviewed by: ? Platforms tested: F17/F19(jenkins) Flag Day: no Doc impact: no --- dirsrvtests/tickets/ticket47653MMR_test.py | 552 +++++++++++++++++++++++++++++ dirsrvtests/tickets/ticket47653_test.py | 432 ++++++++++++++++++++++ ldap/servers/plugins/acl/acl.h | 1 + ldap/servers/plugins/acl/acllas.c | 278 ++++++++++++++- 4 files changed, 1262 insertions(+), 1 deletion(-) create mode 100644 dirsrvtests/tickets/ticket47653MMR_test.py create mode 100644 dirsrvtests/tickets/ticket47653_test.py diff --git a/dirsrvtests/tickets/ticket47653MMR_test.py b/dirsrvtests/tickets/ticket47653MMR_test.py new file mode 100644 index 0000000..1f221c6 --- /dev/null +++ b/dirsrvtests/tickets/ticket47653MMR_test.py @@ -0,0 +1,552 @@ +''' +Created on Nov 7, 2013 + +@author: tbordaz +''' +import os +import sys +import time +import ldap +import logging +import socket +import time +import logging +import pytest +import re +from lib389 import DirSrv, Entry, tools +from lib389.tools import DirSrvTools +from lib389._constants import * +from lib389.properties import * +from constants import * +from lib389._constants import * + +logging.getLogger(__name__).setLevel(logging.DEBUG) +log = logging.getLogger(__name__) + +# +# important part. We can deploy Master1 and Master2 on different versions +# +installation1_prefix = None +installation2_prefix = None + +TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX +OC_NAME = 'OCticket47653' +MUST = "(postalAddress $ postalCode)" +MAY = "(member $ street)" + +OTHER_NAME = 'other_entry' +MAX_OTHERS = 10 + +BIND_NAME = 'bind_entry' +BIND_DN = 'cn=%s, %s' % (BIND_NAME, SUFFIX) +BIND_PW = 'password' + +ENTRY_NAME = 'test_entry' +ENTRY_DN = 'cn=%s, %s' % (ENTRY_NAME, SUFFIX) +ENTRY_OC = "top person %s" % OC_NAME + +def _oc_definition(oid_ext, name, must=None, may=None): + oid = "1.2.3.4.5.6.7.8.9.10.%d" % oid_ext + desc = 'To test ticket 47490' + sup = 'person' + if not must: + must = MUST + if not may: + may = MAY + + new_oc = "( %s NAME '%s' DESC '%s' SUP %s AUXILIARY MUST %s MAY %s )" % (oid, name, desc, sup, must, may) + return new_oc +class TopologyMaster1Master2(object): + def __init__(self, master1, master2): + master1.open() + self.master1 = master1 + + master2.open() + self.master2 = master2 + + +@pytest.fixture(scope="module") +def topology(request): + ''' + This fixture is used to create a replicated topology for the 'module'. + The replicated topology is MASTER1 <-> Master2. + At the beginning, It may exists a master2 instance and/or a master2 instance. + It may also exists a backup for the master1 and/or the master2. + + Principle: + If master1 instance exists: + restart it + If master2 instance exists: + restart it + If backup of master1 AND backup of master2 exists: + create or rebind to master1 + create or rebind to master2 + + restore master1 from backup + restore master2 from backup + else: + Cleanup everything + remove instances + remove backups + Create instances + Initialize replication + Create backups + ''' + global installation1_prefix + global installation2_prefix + + # allocate master1 on a given deployement + master1 = DirSrv(verbose=False) + if installation1_prefix: + args_instance[SER_DEPLOYED_DIR] = installation1_prefix + + # Args for the master1 instance + args_instance[SER_HOST] = HOST_MASTER_1 + args_instance[SER_PORT] = PORT_MASTER_1 + args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_1 + args_master = args_instance.copy() + master1.allocate(args_master) + + # allocate master1 on a given deployement + master2 = DirSrv(verbose=False) + if installation2_prefix: + args_instance[SER_DEPLOYED_DIR] = installation2_prefix + + # Args for the consumer instance + args_instance[SER_HOST] = HOST_MASTER_2 + args_instance[SER_PORT] = PORT_MASTER_2 + args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_2 + args_master = args_instance.copy() + master2.allocate(args_master) + + + # Get the status of the backups + backup_master1 = master1.checkBackupFS() + backup_master2 = master2.checkBackupFS() + + # Get the status of the instance and restart it if it exists + instance_master1 = master1.exists() + if instance_master1: + master1.stop(timeout=10) + master1.start(timeout=10) + + instance_master2 = master2.exists() + if instance_master2: + master2.stop(timeout=10) + master2.start(timeout=10) + + if backup_master1 and backup_master2: + # The backups exist, assuming they are correct + # we just re-init the instances with them + if not instance_master1: + master1.create() + # Used to retrieve configuration information (dbdir, confdir...) + master1.open() + + if not instance_master2: + master2.create() + # Used to retrieve configuration information (dbdir, confdir...) + master2.open() + + # restore master1 from backup + master1.stop(timeout=10) + master1.restoreFS(backup_master1) + master1.start(timeout=10) + + # restore master2 from backup + master2.stop(timeout=10) + master2.restoreFS(backup_master2) + master2.start(timeout=10) + else: + # We should be here only in two conditions + # - This is the first time a test involve master-consumer + # so we need to create everything + # - Something weird happened (instance/backup destroyed) + # so we discard everything and recreate all + + # Remove all the backups. So even if we have a specific backup file + # (e.g backup_master) we clear all backups that an instance my have created + if backup_master1: + master1.clearBackupFS() + if backup_master2: + master2.clearBackupFS() + + # Remove all the instances + if instance_master1: + master1.delete() + if instance_master2: + master2.delete() + + # Create the instances + master1.create() + master1.open() + master2.create() + master2.open() + + # + # Now prepare the Master-Consumer topology + # + # First Enable replication + master1.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_1) + master2.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_2) + + # Initialize the supplier->consumer + + properties = {RA_NAME: r'meTo_$host:$port', + RA_BINDDN: defaultProperties[REPLICATION_BIND_DN], + RA_BINDPW: defaultProperties[REPLICATION_BIND_PW], + RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD], + RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]} + repl_agreement = master1.agreement.create(suffix=SUFFIX, host=master2.host, port=master2.port, properties=properties) + + if not repl_agreement: + log.fatal("Fail to create a replica agreement") + sys.exit(1) + + log.debug("%s created" % repl_agreement) + + properties = {RA_NAME: r'meTo_$host:$port', + RA_BINDDN: defaultProperties[REPLICATION_BIND_DN], + RA_BINDPW: defaultProperties[REPLICATION_BIND_PW], + RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD], + RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]} + master2.agreement.create(suffix=SUFFIX, host=master1.host, port=master1.port, properties=properties) + + master1.agreement.init(SUFFIX, HOST_MASTER_2, PORT_MASTER_2) + master1.waitForReplInit(repl_agreement) + + # Check replication is working fine + master1.add_s(Entry((TEST_REPL_DN, { + 'objectclass': "top person".split(), + 'sn': 'test_repl', + 'cn': 'test_repl'}))) + loop = 0 + while loop <= 10: + try: + ent = master2.getEntry(TEST_REPL_DN, ldap.SCOPE_BASE, "(objectclass=*)") + break + except ldap.NO_SUCH_OBJECT: + time.sleep(1) + loop += 1 + + # Time to create the backups + master1.stop(timeout=10) + master1.backupfile = master1.backupFS() + master1.start(timeout=10) + + master2.stop(timeout=10) + master2.backupfile = master2.backupFS() + master2.start(timeout=10) + + # + # Here we have two instances master and consumer + # with replication working. Either coming from a backup recovery + # or from a fresh (re)init + # Time to return the topology + return TopologyMaster1Master2(master1, master2) + + +def test_ticket47653_init(topology): + """ + It adds + - Objectclass with MAY 'member' + - an entry ('bind_entry') with which we bind to test the 'SELFDN' operation + It deletes the anonymous aci + + """ + + + topology.master1.log.info("Add %s that allows 'member' attribute" % OC_NAME) + new_oc = _oc_definition(2, OC_NAME, must = MUST, may = MAY) + topology.master1.addSchema('objectClasses', new_oc) + + + # entry used to bind with + topology.master1.log.info("Add %s" % BIND_DN) + topology.master1.add_s(Entry((BIND_DN, { + 'objectclass': "top person".split(), + 'sn': BIND_NAME, + 'cn': BIND_NAME, + 'userpassword': BIND_PW}))) + + # enable acl error logging + mod = [(ldap.MOD_REPLACE, 'nsslapd-errorlog-level', str(128+8192))] # ACL + REPL + topology.master1.modify_s(DN_CONFIG, mod) + topology.master2.modify_s(DN_CONFIG, mod) + + # get read of anonymous ACI for use 'read-search' aci in SEARCH test + ACI_ANONYMOUS = "(targetattr!=\"userPassword\")(version 3.0; acl \"Enable anonymous access\"; allow (read, search, compare) userdn=\"ldap:///anyone\";)" + mod = [(ldap.MOD_DELETE, 'aci', ACI_ANONYMOUS)] + topology.master1.modify_s(SUFFIX, mod) + topology.master2.modify_s(SUFFIX, mod) + + # add dummy entries + for cpt in range(MAX_OTHERS): + name = "%s%d" % (OTHER_NAME, cpt) + topology.master1.add_s(Entry(("cn=%s,%s" % (name, SUFFIX), { + 'objectclass': "top person".split(), + 'sn': name, + 'cn': name}))) + +def test_ticket47653_add(topology): + ''' + This test ADD an entry on MASTER1 where 47653 is fixed. Then it checks that entry is replicated + on MASTER2 (even if on MASTER2 47653 is NOT fixed). Then update on MASTER2 and check the update on MASTER1 + + It checks that, bound as bind_entry, + - we can not ADD an entry without the proper SELFDN aci. + - with the proper ACI we can not ADD with 'member' attribute + - with the proper ACI and 'member' it succeeds to ADD + ''' + topology.master1.log.info("\n\n######################### ADD ######################\n") + + # bind as bind_entry + topology.master1.log.info("Bind as %s" % BIND_DN) + topology.master1.simple_bind_s(BIND_DN, BIND_PW) + + # Prepare the entry with multivalued members + entry_with_members = Entry(ENTRY_DN) + entry_with_members.setValues('objectclass', 'top', 'person', 'OCticket47653') + entry_with_members.setValues('sn', ENTRY_NAME) + entry_with_members.setValues('cn', ENTRY_NAME) + entry_with_members.setValues('postalAddress', 'here') + entry_with_members.setValues('postalCode', '1234') + members = [] + for cpt in range(MAX_OTHERS): + name = "%s%d" % (OTHER_NAME, cpt) + members.append("cn=%s,%s" % (name, SUFFIX)) + members.append(BIND_DN) + entry_with_members.setValues('member', members) + + # Prepare the entry with only one member value + entry_with_member = Entry(ENTRY_DN) + entry_with_member.setValues('objectclass', 'top', 'person', 'OCticket47653') + entry_with_member.setValues('sn', ENTRY_NAME) + entry_with_member.setValues('cn', ENTRY_NAME) + entry_with_member.setValues('postalAddress', 'here') + entry_with_member.setValues('postalCode', '1234') + member = [] + member.append(BIND_DN) + entry_with_member.setValues('member', member) + + # entry to add WITH member being BIND_DN but WITHOUT the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.master1.log.info("Try to add Add %s (aci is missing): %r" % (ENTRY_DN, entry_with_member)) + + topology.master1.add_s(entry_with_member) + except Exception as e: + topology.master1.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + + # Ok Now add the proper ACI + topology.master1.log.info("Bind as %s and add the ADD SELFDN aci" % DN_DM) + topology.master1.simple_bind_s(DN_DM, PASSWORD) + + ACI_TARGET = "(target = \"ldap:///cn=*,%s\")" % SUFFIX + ACI_TARGETFILTER = "(targetfilter =\"(objectClass=%s)\")" % OC_NAME + ACI_ALLOW = "(version 3.0; acl \"SelfDN add\"; allow (add)" + ACI_SUBJECT = " userattr = \"member#selfDN\";)" + ACI_BODY = ACI_TARGET + ACI_TARGETFILTER + ACI_ALLOW + ACI_SUBJECT + mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)] + topology.master1.modify_s(SUFFIX, mod) + + # bind as bind_entry + topology.master1.log.info("Bind as %s" % BIND_DN) + topology.master1.simple_bind_s(BIND_DN, BIND_PW) + + # entry to add WITHOUT member and WITH the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.master1.log.info("Try to add Add %s (member is missing)" % ENTRY_DN) + topology.master1.add_s(Entry((ENTRY_DN, { + 'objectclass': ENTRY_OC.split(), + 'sn': ENTRY_NAME, + 'cn': ENTRY_NAME, + 'postalAddress': 'here', + 'postalCode': '1234'}))) + except Exception as e: + topology.master1.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + # entry to add WITH memberS and WITH the ACI -> ldap.INSUFFICIENT_ACCESS + # member should contain only one value + try: + topology.master1.log.info("Try to add Add %s (with several member values)" % ENTRY_DN) + topology.master1.add_s(entry_with_members) + except Exception as e: + topology.master1.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + topology.master1.log.info("Try to add Add %s should be successful" % ENTRY_DN) + topology.master1.add_s(entry_with_member) + + # + # Now check the entry as been replicated + # + topology.master2.simple_bind_s(DN_DM, PASSWORD) + topology.master1.log.info("Try to retrieve %s from Master2" % ENTRY_DN) + loop = 0 + while loop <= 10: + try: + ent = topology.master2.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)") + break + except ldap.NO_SUCH_OBJECT: + time.sleep(1) + loop += 1 + assert loop <= 10 + + # Now update the entry on Master2 (as DM because 47653 is possibly not fixed on M2) + topology.master1.log.info("Update %s on M2" % ENTRY_DN) + mod = [(ldap.MOD_REPLACE, 'description', 'test_add')] + topology.master2.modify_s(ENTRY_DN, mod) + + topology.master1.simple_bind_s(DN_DM, PASSWORD) + loop = 0 + while loop <= 10: + try: + ent = topology.master1.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)") + if ent.hasAttr('description') and (ent.getValue('description') == 'test_add'): + break + except ldap.NO_SUCH_OBJECT: + time.sleep(1) + loop += 1 + + assert ent.getValue('description') == 'test_add' + +def test_ticket47653_modify(topology): + ''' + This test MOD an entry on MASTER1 where 47653 is fixed. Then it checks that update is replicated + on MASTER2 (even if on MASTER2 47653 is NOT fixed). Then update on MASTER2 (bound as BIND_DN). + This update may fail whether or not 47653 is fixed on MASTER2 + + It checks that, bound as bind_entry, + - we can not modify an entry without the proper SELFDN aci. + - adding the ACI, we can modify the entry + ''' + # bind as bind_entry + topology.master1.log.info("Bind as %s" % BIND_DN) + topology.master1.simple_bind_s(BIND_DN, BIND_PW) + + topology.master1.log.info("\n\n######################### MODIFY ######################\n") + + # entry to modify WITH member being BIND_DN but WITHOUT the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.master1.log.info("Try to modify %s (aci is missing)" % ENTRY_DN) + mod = [(ldap.MOD_REPLACE, 'postalCode', '9876')] + topology.master1.modify_s(ENTRY_DN, mod) + except Exception as e: + topology.master1.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + + # Ok Now add the proper ACI + topology.master1.log.info("Bind as %s and add the WRITE SELFDN aci" % DN_DM) + topology.master1.simple_bind_s(DN_DM, PASSWORD) + + ACI_TARGET = "(target = \"ldap:///cn=*,%s\")" % SUFFIX + ACI_TARGETATTR = "(targetattr = *)" + ACI_TARGETFILTER = "(targetfilter =\"(objectClass=%s)\")" % OC_NAME + ACI_ALLOW = "(version 3.0; acl \"SelfDN write\"; allow (write)" + ACI_SUBJECT = " userattr = \"member#selfDN\";)" + ACI_BODY = ACI_TARGET + ACI_TARGETATTR + ACI_TARGETFILTER + ACI_ALLOW + ACI_SUBJECT + mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)] + topology.master1.modify_s(SUFFIX, mod) + + # bind as bind_entry + topology.master1.log.info("M1: Bind as %s" % BIND_DN) + topology.master1.simple_bind_s(BIND_DN, BIND_PW) + + # modify the entry and checks the value + topology.master1.log.info("M1: Try to modify %s. It should succeeds" % ENTRY_DN) + mod = [(ldap.MOD_REPLACE, 'postalCode', '1928')] + topology.master1.modify_s(ENTRY_DN, mod) + + topology.master1.log.info("M1: Bind as %s" % DN_DM) + topology.master1.simple_bind_s(DN_DM, PASSWORD) + + topology.master1.log.info("M1: Check the update of %s" % ENTRY_DN) + ents = topology.master1.search_s(ENTRY_DN, ldap.SCOPE_BASE, 'objectclass=*') + assert len(ents) == 1 + assert ents[0].postalCode == '1928' + + + # + # Now check the update has been replicated on M2 + topology.master1.log.info("M2: Bind as %s" % DN_DM) + topology.master2.simple_bind_s(DN_DM, PASSWORD) + topology.master1.log.info("M2: Try to retrieve %s" % ENTRY_DN) + loop = 0 + while loop <= 10: + try: + ent = topology.master2.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)") + if ent.hasAttr('postalCode') and (ent.getValue('postalCode') == '1928'): + break + except ldap.NO_SUCH_OBJECT: + time.sleep(1) + loop += 1 + assert loop <= 10 + assert ent.getValue('postalCode') == '1928' + + + # Now update the entry on Master2 bound as BIND_DN (update may fail if 47653 is not fixed on M2) + topology.master1.log.info("M2: Update %s (bound as %s)" % (ENTRY_DN, BIND_DN)) + topology.master2.simple_bind_s(BIND_DN, PASSWORD) + fail = False + try: + mod = [(ldap.MOD_REPLACE, 'postalCode', '1929')] + topology.master2.modify_s(ENTRY_DN, mod) + fail = False + except ldap.INSUFFICIENT_ACCESS: + topology.master1.log.info("M2: Exception (INSUFFICIENT_ACCESS): that is fine the bug is possibly not fixed on M2") + fail = True + except Exception as e: + topology.master1.log.info("M2: Exception (not expected): %s" % type(e).__name__) + assert 0 + + if not fail: + # Check the update has been replicaed on M1 + topology.master1.log.info("M1: Bind as %s" % DN_DM) + topology.master1.simple_bind_s(DN_DM, PASSWORD) + topology.master1.log.info("M1: Check %s.postalCode=1929)" % (ENTRY_DN)) + loop = 0 + while loop <= 10: + try: + ent = topology.master1.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)") + if ent.hasAttr('postalCode') and (ent.getValue('postalCode') == '1929'): + break + except ldap.NO_SUCH_OBJECT: + time.sleep(1) + loop += 1 + assert ent.getValue('postalCode') == '1929' + +def test_ticket47653_final(topology): + topology.master1.stop(timeout=10) + topology.master2.stop(timeout=10) + +def run_isolated(): + ''' + run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..) + To run isolated without py.test, you need to + - edit this file and comment '@pytest.fixture' line before 'topology' function. + - set the installation prefix + - run this program + ''' + global installation1_prefix + global installation2_prefix + installation1_prefix = None + installation2_prefix = None + + topo = topology(True) + test_ticket47653_init(topo) + + test_ticket47653_add(topo) + test_ticket47653_modify(topo) + + test_ticket47653_final(topo) + + + + +if __name__ == '__main__': + run_isolated() + diff --git a/dirsrvtests/tickets/ticket47653_test.py b/dirsrvtests/tickets/ticket47653_test.py new file mode 100644 index 0000000..2693093 --- /dev/null +++ b/dirsrvtests/tickets/ticket47653_test.py @@ -0,0 +1,432 @@ +import os +import sys +import time +import ldap +import logging +import socket +import time +import logging +import pytest +from lib389 import DirSrv, Entry, tools +from lib389.tools import DirSrvTools +from lib389._constants import * +from lib389.properties import * +from constants import * + +log = logging.getLogger(__name__) + +installation_prefix = None + +OC_NAME = 'OCticket47653' +MUST = "(postalAddress $ postalCode)" +MAY = "(member $ street)" + +OTHER_NAME = 'other_entry' +MAX_OTHERS = 10 + +BIND_NAME = 'bind_entry' +BIND_DN = 'cn=%s, %s' % (BIND_NAME, SUFFIX) +BIND_PW = 'password' + +ENTRY_NAME = 'test_entry' +ENTRY_DN = 'cn=%s, %s' % (ENTRY_NAME, SUFFIX) +ENTRY_OC = "top person %s" % OC_NAME + +def _oc_definition(oid_ext, name, must=None, may=None): + oid = "1.2.3.4.5.6.7.8.9.10.%d" % oid_ext + desc = 'To test ticket 47490' + sup = 'person' + if not must: + must = MUST + if not may: + may = MAY + + new_oc = "( %s NAME '%s' DESC '%s' SUP %s AUXILIARY MUST %s MAY %s )" % (oid, name, desc, sup, must, may) + return new_oc + + +class TopologyStandalone(object): + def __init__(self, standalone): + standalone.open() + self.standalone = standalone + + +@pytest.fixture(scope="module") +def topology(request): + ''' + This fixture is used to standalone topology for the 'module'. + At the beginning, It may exists a standalone instance. + It may also exists a backup for the standalone instance. + + Principle: + If standalone instance exists: + restart it + If backup of standalone exists: + create/rebind to standalone + + restore standalone instance from backup + else: + Cleanup everything + remove instance + remove backup + Create instance + Create backup + ''' + global installation_prefix + + if installation_prefix: + args_instance[SER_DEPLOYED_DIR] = installation_prefix + + standalone = DirSrv(verbose=False) + + # Args for the standalone instance + args_instance[SER_HOST] = HOST_STANDALONE + args_instance[SER_PORT] = PORT_STANDALONE + args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE + args_standalone = args_instance.copy() + standalone.allocate(args_standalone) + + # Get the status of the backups + backup_standalone = standalone.checkBackupFS() + + # Get the status of the instance and restart it if it exists + instance_standalone = standalone.exists() + if instance_standalone: + # assuming the instance is already stopped, just wait 5 sec max + standalone.stop(timeout=5) + standalone.start(timeout=10) + + if backup_standalone: + # The backup exist, assuming it is correct + # we just re-init the instance with it + if not instance_standalone: + standalone.create() + # Used to retrieve configuration information (dbdir, confdir...) + standalone.open() + + # restore standalone instance from backup + standalone.stop(timeout=10) + standalone.restoreFS(backup_standalone) + standalone.start(timeout=10) + + else: + # We should be here only in two conditions + # - This is the first time a test involve standalone instance + # - Something weird happened (instance/backup destroyed) + # so we discard everything and recreate all + + # Remove the backup. So even if we have a specific backup file + # (e.g backup_standalone) we clear backup that an instance may have created + if backup_standalone: + standalone.clearBackupFS() + + # Remove the instance + if instance_standalone: + standalone.delete() + + # Create the instance + standalone.create() + + # Used to retrieve configuration information (dbdir, confdir...) + standalone.open() + + # Time to create the backups + standalone.stop(timeout=10) + standalone.backupfile = standalone.backupFS() + standalone.start(timeout=10) + + # + # Here we have standalone instance up and running + # Either coming from a backup recovery + # or from a fresh (re)init + # Time to return the topology + return TopologyStandalone(standalone) + + +def test_ticket47653_init(topology): + """ + It adds + - Objectclass with MAY 'member' + - an entry ('bind_entry') with which we bind to test the 'SELFDN' operation + It deletes the anonymous aci + + """ + + + topology.standalone.log.info("Add %s that allows 'member' attribute" % OC_NAME) + new_oc = _oc_definition(2, OC_NAME, must = MUST, may = MAY) + topology.standalone.addSchema('objectClasses', new_oc) + + + # entry used to bind with + topology.standalone.log.info("Add %s" % BIND_DN) + topology.standalone.add_s(Entry((BIND_DN, { + 'objectclass': "top person".split(), + 'sn': BIND_NAME, + 'cn': BIND_NAME, + 'userpassword': BIND_PW}))) + + # enable acl error logging + mod = [(ldap.MOD_REPLACE, 'nsslapd-errorlog-level', '128')] + topology.standalone.modify_s(DN_CONFIG, mod) + + # get read of anonymous ACI for use 'read-search' aci in SEARCH test + ACI_ANONYMOUS = "(targetattr!=\"userPassword\")(version 3.0; acl \"Enable anonymous access\"; allow (read, search, compare) userdn=\"ldap:///anyone\";)" + mod = [(ldap.MOD_DELETE, 'aci', ACI_ANONYMOUS)] + topology.standalone.modify_s(SUFFIX, mod) + + # add dummy entries + for cpt in range(MAX_OTHERS): + name = "%s%d" % (OTHER_NAME, cpt) + topology.standalone.add_s(Entry(("cn=%s,%s" % (name, SUFFIX), { + 'objectclass': "top person".split(), + 'sn': name, + 'cn': name}))) + + +def test_ticket47653_add(topology): + ''' + It checks that, bound as bind_entry, + - we can not ADD an entry without the proper SELFDN aci. + - with the proper ACI we can not ADD with 'member' attribute + - with the proper ACI and 'member' it succeeds to ADD + ''' + topology.standalone.log.info("\n\n######################### ADD ######################\n") + + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # Prepare the entry with multivalued members + entry_with_members = Entry(ENTRY_DN) + entry_with_members.setValues('objectclass', 'top', 'person', 'OCticket47653') + entry_with_members.setValues('sn', ENTRY_NAME) + entry_with_members.setValues('cn', ENTRY_NAME) + entry_with_members.setValues('postalAddress', 'here') + entry_with_members.setValues('postalCode', '1234') + members = [] + for cpt in range(MAX_OTHERS): + name = "%s%d" % (OTHER_NAME, cpt) + members.append("cn=%s,%s" % (name, SUFFIX)) + members.append(BIND_DN) + entry_with_members.setValues('member', members) + + # Prepare the entry with one member + entry_with_member = Entry(ENTRY_DN) + entry_with_member.setValues('objectclass', 'top', 'person', 'OCticket47653') + entry_with_member.setValues('sn', ENTRY_NAME) + entry_with_member.setValues('cn', ENTRY_NAME) + entry_with_member.setValues('postalAddress', 'here') + entry_with_member.setValues('postalCode', '1234') + member = [] + member.append(BIND_DN) + entry_with_member.setValues('member', member) + + # entry to add WITH member being BIND_DN but WITHOUT the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.standalone.log.info("Try to add Add %s (aci is missing): %r" % (ENTRY_DN, entry_with_member)) + + topology.standalone.add_s(entry_with_member) + except Exception as e: + topology.standalone.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + + # Ok Now add the proper ACI + topology.standalone.log.info("Bind as %s and add the ADD SELFDN aci" % DN_DM) + topology.standalone.simple_bind_s(DN_DM, PASSWORD) + + ACI_TARGET = "(target = \"ldap:///cn=*,%s\")" % SUFFIX + ACI_TARGETFILTER = "(targetfilter =\"(objectClass=%s)\")" % OC_NAME + ACI_ALLOW = "(version 3.0; acl \"SelfDN add\"; allow (add)" + ACI_SUBJECT = " userattr = \"member#selfDN\";)" + ACI_BODY = ACI_TARGET + ACI_TARGETFILTER + ACI_ALLOW + ACI_SUBJECT + mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)] + topology.standalone.modify_s(SUFFIX, mod) + + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # entry to add WITHOUT member and WITH the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.standalone.log.info("Try to add Add %s (member is missing)" % ENTRY_DN) + topology.standalone.add_s(Entry((ENTRY_DN, { + 'objectclass': ENTRY_OC.split(), + 'sn': ENTRY_NAME, + 'cn': ENTRY_NAME, + 'postalAddress': 'here', + 'postalCode': '1234'}))) + except Exception as e: + topology.standalone.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + # entry to add WITH memberS and WITH the ACI -> ldap.INSUFFICIENT_ACCESS + # member should contain only one value + try: + topology.standalone.log.info("Try to add Add %s (with several member values)" % ENTRY_DN) + topology.standalone.add_s(entry_with_members) + except Exception as e: + topology.standalone.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + topology.standalone.log.info("Try to add Add %s should be successful" % ENTRY_DN) + topology.standalone.add_s(entry_with_member) + +def test_ticket47653_search(topology): + ''' + It checks that, bound as bind_entry, + - we can not search an entry without the proper SELFDN aci. + - adding the ACI, we can search the entry + ''' + topology.standalone.log.info("\n\n######################### SEARCH ######################\n") + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # entry to search WITH member being BIND_DN but WITHOUT the ACI -> no entry returned + topology.standalone.log.info("Try to search %s (aci is missing)" % ENTRY_DN) + ents = topology.standalone.search_s(ENTRY_DN, ldap.SCOPE_BASE, 'objectclass=*') + assert len(ents) == 0 + + + # Ok Now add the proper ACI + topology.standalone.log.info("Bind as %s and add the READ/SEARCH SELFDN aci" % DN_DM) + topology.standalone.simple_bind_s(DN_DM, PASSWORD) + + ACI_TARGET = "(target = \"ldap:///cn=*,%s\")" % SUFFIX + ACI_TARGETATTR = "(targetattr = *)" + ACI_TARGETFILTER = "(targetfilter =\"(objectClass=%s)\")" % OC_NAME + ACI_ALLOW = "(version 3.0; acl \"SelfDN search-read\"; allow (read, search, compare)" + ACI_SUBJECT = " userattr = \"member#selfDN\";)" + ACI_BODY = ACI_TARGET + ACI_TARGETATTR + ACI_TARGETFILTER + ACI_ALLOW + ACI_SUBJECT + mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)] + topology.standalone.modify_s(SUFFIX, mod) + + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # entry to search with the proper aci + topology.standalone.log.info("Try to search %s should be successful" % ENTRY_DN) + ents = topology.standalone.search_s(ENTRY_DN, ldap.SCOPE_BASE, 'objectclass=*') + assert len(ents) == 1 + +def test_ticket47653_modify(topology): + ''' + It checks that, bound as bind_entry, + - we can not modify an entry without the proper SELFDN aci. + - adding the ACI, we can modify the entry + ''' + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + topology.standalone.log.info("\n\n######################### MODIFY ######################\n") + + # entry to modify WITH member being BIND_DN but WITHOUT the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.standalone.log.info("Try to modify %s (aci is missing)" % ENTRY_DN) + mod = [(ldap.MOD_REPLACE, 'postalCode', '9876')] + topology.standalone.modify_s(ENTRY_DN, mod) + except Exception as e: + topology.standalone.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + + # Ok Now add the proper ACI + topology.standalone.log.info("Bind as %s and add the WRITE SELFDN aci" % DN_DM) + topology.standalone.simple_bind_s(DN_DM, PASSWORD) + + ACI_TARGET = "(target = \"ldap:///cn=*,%s\")" % SUFFIX + ACI_TARGETATTR = "(targetattr = *)" + ACI_TARGETFILTER = "(targetfilter =\"(objectClass=%s)\")" % OC_NAME + ACI_ALLOW = "(version 3.0; acl \"SelfDN write\"; allow (write)" + ACI_SUBJECT = " userattr = \"member#selfDN\";)" + ACI_BODY = ACI_TARGET + ACI_TARGETATTR + ACI_TARGETFILTER + ACI_ALLOW + ACI_SUBJECT + mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)] + topology.standalone.modify_s(SUFFIX, mod) + + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # modify the entry and checks the value + topology.standalone.log.info("Try to modify %s. It should succeeds" % ENTRY_DN) + mod = [(ldap.MOD_REPLACE, 'postalCode', '1928')] + topology.standalone.modify_s(ENTRY_DN, mod) + + ents = topology.standalone.search_s(ENTRY_DN, ldap.SCOPE_BASE, 'objectclass=*') + assert len(ents) == 1 + assert ents[0].postalCode == '1928' + +def test_ticket47653_delete(topology): + ''' + It checks that, bound as bind_entry, + - we can not delete an entry without the proper SELFDN aci. + - adding the ACI, we can delete the entry + ''' + topology.standalone.log.info("\n\n######################### DELETE ######################\n") + + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # entry to delete WITH member being BIND_DN but WITHOUT the ACI -> ldap.INSUFFICIENT_ACCESS + try: + topology.standalone.log.info("Try to delete %s (aci is missing)" % ENTRY_DN) + topology.standalone.delete_s(ENTRY_DN) + except Exception as e: + topology.standalone.log.info("Exception (expected): %s" % type(e).__name__) + assert isinstance(e, ldap.INSUFFICIENT_ACCESS) + + # Ok Now add the proper ACI + topology.standalone.log.info("Bind as %s and add the READ/SEARCH SELFDN aci" % DN_DM) + topology.standalone.simple_bind_s(DN_DM, PASSWORD) + + ACI_TARGET = "(target = \"ldap:///cn=*,%s\")" % SUFFIX + ACI_TARGETFILTER = "(targetfilter =\"(objectClass=%s)\")" % OC_NAME + ACI_ALLOW = "(version 3.0; acl \"SelfDN delete\"; allow (delete)" + ACI_SUBJECT = " userattr = \"member#selfDN\";)" + ACI_BODY = ACI_TARGET + ACI_TARGETFILTER + ACI_ALLOW + ACI_SUBJECT + mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)] + topology.standalone.modify_s(SUFFIX, mod) + + # bind as bind_entry + topology.standalone.log.info("Bind as %s" % BIND_DN) + topology.standalone.simple_bind_s(BIND_DN, BIND_PW) + + # entry to search with the proper aci + topology.standalone.log.info("Try to delete %s should be successful" % ENTRY_DN) + topology.standalone.delete_s(ENTRY_DN) + +def test_ticket47653_final(topology): + topology.standalone.stop(timeout=10) + + + +def run_isolated(): + ''' + run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..) + To run isolated without py.test, you need to + - edit this file and comment '@pytest.fixture' line before 'topology' function. + - set the installation prefix + - run this program + ''' + global installation_prefix + installation_prefix = None + + topo = topology(True) + test_ticket47653_init(topo) + + test_ticket47653_add(topo) + test_ticket47653_search(topo) + test_ticket47653_modify(topo) + test_ticket47653_delete(topo) + + test_ticket47653_final(topo) + + +if __name__ == '__main__': + run_isolated() + diff --git a/ldap/servers/plugins/acl/acl.h b/ldap/servers/plugins/acl/acl.h index cc042d6..1a05e1f 100644 --- a/ldap/servers/plugins/acl/acl.h +++ b/ldap/servers/plugins/acl/acl.h @@ -145,6 +145,7 @@ static char* const access_str_proxy = "proxy"; #define DS_LAS_GROUP "group" #define DS_LAS_USERDN "userdn" #define DS_LAS_GROUPDN "groupdn" +#define DS_LAS_SELFDNATTR "selfdnattr" #define DS_LAS_USERDNATTR "userdnattr" #define DS_LAS_AUTHMETHOD "authmethod" #define DS_LAS_GROUPDNATTR "groupdnattr" diff --git a/ldap/servers/plugins/acl/acllas.c b/ldap/servers/plugins/acl/acllas.c index 3646fcd..ab6ff58 100644 --- a/ldap/servers/plugins/acl/acllas.c +++ b/ldap/servers/plugins/acl/acllas.c @@ -1388,6 +1388,277 @@ DS_LASUserDnAttrEval(NSErr_t *errp, char *attr_name, CmpOp_t comparator, /*************************************************************************** * +* DS_LASSelfDnAttrEval +* +* +* Input: +* attr_name The string "selfdn" - in lower case. +* comparator CMP_OP_EQ or CMP_OP_NE only +* attr_pattern A comma-separated list of users +* cachable Always set to FALSE. +* subject Subject property list +* resource Resource property list +* auth_info Authentication info, if any +* +* Returns: +* retcode The usual LAS return codes. +* +* Error Handling: +* None. +* +**************************************************************************/ +struct selfdnattr_info { + char *attr; + int result; + char *clientdn; + Acl_PBlock *aclpb; +}; +#define ACLLAS_MAX_LEVELS 10 +int +DS_LASSelfDnAttrEval(NSErr_t *errp, char *attr_name, CmpOp_t comparator, + char *attr_pattern, int *cachable, void **LAS_cookie, + PList_t subject, PList_t resource, PList_t auth_info, + PList_t global_auth) +{ + + char *n_currEntryDn = NULL; + char *s_attrName, *attrName; + char *ptr; + int matched; + int rc, len, i; + char *val; + Slapi_Attr *a; + int levels[ACLLAS_MAX_LEVELS]; + int numOflevels =0; + struct userdnattr_info info = {0}; + char *attrs[2] = { LDAP_ALL_USER_ATTRS, NULL }; + lasInfo lasinfo; + int got_undefined = 0; + + if ( 0 != (rc = __acllas_setup (errp, attr_name, comparator, 0, /* Don't allow range comparators */ + attr_pattern,cachable,LAS_cookie, + subject, resource, auth_info,global_auth, + DS_LAS_SELFDNATTR, "DS_LASSelfDnAttrEval", + &lasinfo )) ) { + return LAS_EVAL_FAIL; + } + + /* + ** The selfdnAttr syntax is + ** selfdnattr = or + ** selfdnattr = parent[0,2,4].attribute" + ** Ex: + ** selfdnattr = manager; or + ** selfdnattr = "parent[0,2,4].manager"; + ** + ** Here 0 means current level, 2 means grandfather and + ** 4 (great great grandfather) + ** + ** The function of this LAS is to compare the value of the + ** attribute in the Slapi_Entry with the "userdn". + ** + ** Ex: userdn: "cn=prasanta, o= netscape, c= us" + ** and in the Slapi_Entry the manager attribute has + ** manager = . Compare the userdn with manager.value to + ** determine the result. + ** + */ + s_attrName = attrName = slapi_ch_strdup (attr_pattern); + + /* ignore leading/trailing whitespace */ + while(ldap_utf8isspace(attrName)) LDAP_UTF8INC(attrName); + len = strlen(attrName); + ptr = attrName+len-1; + while(ptr >= attrName && ldap_utf8isspace(ptr)) { + *ptr = '\0'; + LDAP_UTF8DEC(ptr); + } + + + /* See if we have a parent[2].attr" rule */ + if (strstr(attrName, "parent[") != NULL) { + char *word, *str, *next; + + numOflevels = 0; + n_currEntryDn = slapi_entry_get_ndn ( lasinfo.resourceEntry ); + str = attrName; + + ldap_utf8strtok_r(str, "[],. ",&next); + /* The first word is "parent[" and so it's not important */ + + while ((word= ldap_utf8strtok_r(NULL, "[],.", &next)) != NULL) { + if (ldap_utf8isdigit(word)) { + while (word && ldap_utf8isspace(word)) LDAP_UTF8INC(word); + if (numOflevels < ACLLAS_MAX_LEVELS) + levels[numOflevels++] = atoi (word); + else { + /* + * Here, ignore the extra levels..it's really + * a syntax error which should have been ruled out at parse time + */ + slapi_log_error( SLAPI_LOG_FATAL, plugin_name, + "DS_LASUserDnattr: Exceeded the ATTR LIMIT:%d: Ignoring extra levels\n", + ACLLAS_MAX_LEVELS); + } + } else { + /* Must be the attr name. We can goof of by + ** having parent[1,2,a] but then you have to be + ** stupid to do that. + */ + char *p = word; + if (*--p == '.') { + attrName = word; + break; + } + } + } + info.attr = attrName; + info.clientdn = lasinfo.clientDn; + info.result = 0; + } else { + levels[0] = 0; + numOflevels = 1; + + } + + /* No attribute name specified--it's a syntax error and so undefined */ + if (attrName == NULL ) { + slapi_ch_free ( (void**) &s_attrName); + return LAS_EVAL_FAIL; + } + + slapi_log_error( SLAPI_LOG_ACL, plugin_name,"Attr:%s\n" , attrName); + matched = ACL_FALSE; + for (i=0; i < numOflevels; i++) { + if ( levels[i] == 0 ) { + Slapi_Value *sval=NULL; + const struct berval *attrVal; + int numValues = 0; + int j; + + slapi_entry_attr_find( lasinfo.resourceEntry, attrName, &a); + slapi_log_error( SLAPI_LOG_ACL, plugin_name, + "DS_LASSelfDnAttrEval: retrieve %s in resource\n", + attrName ); + if ( NULL == a ) continue; + + /* Here if the operation was a ADD, the aci evaluation should fail + * for security reason (else anyone can create an entry with the only condition + * that the entry contains 'attrName'=bindDN) + * This is an accepted risk with a new SELFDN keyword. The only "control" + * is that 'attrName' has only one value. + */ + slapi_attr_get_numvalues((const Slapi_Attr *) a, &numValues); + if (numValues != 1) { + slapi_log_error( SLAPI_LOG_ACL, plugin_name, + "DS_LASSelfDnAttrEval: fail because the retrieved %s in resource has more than one value\n", + attrName ); + got_undefined = 1; + continue; + } + j= slapi_attr_first_value ( a,&sval ); + while ( j != -1 ) { + attrVal = slapi_value_get_berval ( sval ); + /* Here if atleast 1 value matches then we are done.*/ + val = slapi_create_dn_string("%s", attrVal->bv_val); + if (NULL == val) { + slapi_log_error( SLAPI_LOG_FATAL, plugin_name, + "DS_LASSelfDnAttrEval: Invalid syntax: %s\n", + attrVal->bv_val ); + slapi_ch_free ( (void**) &s_attrName); + return LAS_EVAL_FAIL; + } + slapi_log_error( SLAPI_LOG_ACL, plugin_name, + "DS_LASSelfDnAttrEval: testing %s against bound DN %s\n", + val, lasinfo.clientDn ); + if (slapi_utf8casecmp((ACLUCHP)val, (ACLUCHP)lasinfo.clientDn ) == 0) { + char ebuf [ BUFSIZ ]; + /* Wow it matches */ + slapi_log_error( SLAPI_LOG_ACL, plugin_name, + "selfdnAttr matches(%s, %s) level (%d)\n", + val, + ACL_ESCAPE_STRING_WITH_PUNCTUATION (lasinfo.clientDn, ebuf), + 0); + matched = ACL_TRUE; + slapi_ch_free ( (void **) &val); + break; + } + slapi_ch_free ( (void**) &val); + j = slapi_attr_next_value ( a, j, &sval ); + } + } else { + char *p_dn; /* parent dn */ + + p_dn = acllas__dn_parent (n_currEntryDn, levels[i]); + if (p_dn == NULL) continue; + + /* use new search internal API */ + { + Slapi_PBlock *aPb = slapi_pblock_new (); + + /* + * This search may be chained if chaining for ACL is + * is enabled in the backend and the entry is in + * a chained backend. + */ + slapi_search_internal_set_pb ( aPb, + p_dn, + LDAP_SCOPE_BASE, + "objectclass=*", + &attrs[0], + 0, + NULL /* controls */, + NULL /* uniqueid */, + aclplugin_get_identity (ACL_PLUGIN_IDENTITY), + 0 /* actions */); + + slapi_search_internal_callback_pb(aPb, + &info /* callback_data */, + NULL/* result_callback */, + acllas__verify_client, + NULL /* referral_callback */); + slapi_pblock_destroy(aPb); + } + + /* + * Currently info.result is boolean so + * we do not need to check for ACL_DONT_KNOW + */ + if (info.result) { + matched = ACL_TRUE; + slapi_log_error( SLAPI_LOG_ACL, plugin_name, + "selfdnAttr matches at level (%d)\n", levels[i]); + } + } + if (matched == ACL_TRUE) { + break; + } + } + + slapi_ch_free ( (void **) &s_attrName); + + /* + * If no terms were undefined, then evaluate as normal. + * If there was an undefined term, but another one was TRUE, then we also evaluate + * as normal. Otherwise, the whole expression is UNDEFINED. + */ + if ( matched == ACL_TRUE || !got_undefined ) { + if (comparator == CMP_OP_EQ) { + rc = (matched == ACL_TRUE ? LAS_EVAL_TRUE : LAS_EVAL_FALSE); + } else { + rc = (matched == ACL_TRUE ? LAS_EVAL_FALSE : LAS_EVAL_TRUE); + } + } else { + rc = LAS_EVAL_FAIL; + slapi_log_error( SLAPI_LOG_ACL, plugin_name, + "Returning UNDEFINED for selfdnattr evaluation.\n"); + } + + return rc; +} + +/*************************************************************************** +* * DS_LASLdapUrlAttrEval * * @@ -3385,7 +3656,12 @@ DS_LASUserAttrEval(NSErr_t *errp, char *attr_name, CmpOp_t comparator, attrName, cachable, LAS_cookie, subject, resource, auth_info, global_auth); goto done_las; - } + } else if (0 == strncasecmp ( attrValue, "SELFDN", 6)) { + matched = DS_LASSelfDnAttrEval (errp,DS_LAS_SELFDNATTR, comparator, + attrName, cachable, LAS_cookie, + subject, resource, auth_info, global_auth); + goto done_las; + } if ( lasinfo.aclpb && ( NULL == lasinfo.aclpb->aclpb_client_entry )) { /* SD 00/16/03 pass NULL in case the req is chained */ -- 1.7.11.7