1   /*
2    *   Copyright 2004 The Apache Software Foundation
3    *
4    *   Licensed under the Apache License, Version 2.0 (the "License");
5    *   you may not use this file except in compliance with the License.
6    *   You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *   Unless required by applicable law or agreed to in writing, software
11   *   distributed under the License is distributed on an "AS IS" BASIS,
12   *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *   See the License for the specific language governing permissions and
14   *   limitations under the License.
15   *
16   */
17  package org.apache.ldap.server.authz;
18  
19  
20  import org.apache.ldap.common.message.LockableAttributesImpl;
21  import org.apache.ldap.common.message.LockableAttributeImpl;
22  import org.apache.ldap.common.name.LdapName;
23  import org.apache.ldap.common.exception.LdapNameNotFoundException;
24  import org.apache.ldap.common.exception.LdapNoPermissionException;
25  
26  import javax.naming.NamingException;
27  import javax.naming.Name;
28  import javax.naming.NamingEnumeration;
29  import javax.naming.directory.*;
30  import java.util.Map;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  
34  
35  /***
36   * Tests whether or not authorization around search, list and lookup operations
37   * work properly.
38   *
39   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
40   * @version $Rev$
41   */
42  public class SearchAuthorizationTest extends AbstractAuthorizationTest
43  {
44      /***
45       * The search results of tests are added to this map via put (<String, SearchResult>)
46       * the map is also cleared before each search test.  This allows further inspections
47       * of the results for more specific test cases.
48       */
49      private Map results = new HashMap();
50  
51      /***
52       * Generates a set of simple organizationalUnit entries where the
53       * ou of the entry returned is the index of the entry in the array.
54       *
55       * @param count the number of entries to produce
56       * @return an array of entries with length = count
57       */
58      private Attributes[] getTestNodes( final int count )
59      {
60          Attributes[] attributes = new Attributes[count];
61          for ( int ii = 0; ii < count; ii++ )
62          {
63              attributes[ii] = new LockableAttributesImpl();
64              Attribute oc = new LockableAttributeImpl( "objectClass" );
65              oc.add( "top" );
66              oc.add( "organizationalUnit" );
67              attributes[ii].put( oc );
68              Attribute ou = new LockableAttributeImpl( "ou" );
69              ou.add( String.valueOf( ii ) );
70              ou.add( "testEntry" );
71              attributes[ii].put( ou );
72              attributes[ii].put( "telephoneNumber", String.valueOf( count ) );
73          }
74  
75          return attributes;
76      }
77  
78  
79      private void recursivelyAddSearchData( Name parent, Attributes[] children, final int sizeLimit, int[] count )
80              throws NamingException
81      {
82          Name[] childRdns = new Name[children.length];
83          for ( int ii = 0; ii < children.length && count[0] < sizeLimit; ii++ )
84          {
85              Name childRdn = new LdapName();
86              childRdn.addAll( parent );
87              childRdn.add( "ou=" + ii );
88              childRdns[ii] = childRdn;
89              sysRoot.createSubcontext( childRdn, children[ii] );
90              count[0]++;
91          }
92  
93          if ( count[0] >= sizeLimit )
94          {
95              return;
96          }
97  
98          for ( int ii = 0; ii < children.length && count[0] < sizeLimit; ii++ )
99          {
100             recursivelyAddSearchData( childRdns[ii], children, sizeLimit, count );
101         }
102     }
103 
104 
105     /***
106      * Starts creating nodes under a parent with a set number of children.  First
107      * a single node is created under the parent.  Thereafter a number of children
108      * determined by the branchingFactor is added.  Until a sizeLimit is reached
109      * descendants are created this way in a breath first recursive descent.
110      *
111      * @param parent the parent under which the first node is created
112      * @param branchingFactor
113      * @param sizelimit
114      * @return the immediate child node created under parent which contains the subtree
115      * @throws NamingException
116      */
117     private Name addSearchData( Name parent, int branchingFactor, int sizelimit ) throws NamingException
118     {
119         parent = ( Name ) parent.clone();
120         parent.add( "ou=tests" );
121         sysRoot.createSubcontext( parent, getTestNodes(1)[0] );
122         recursivelyAddSearchData( parent, getTestNodes( branchingFactor ), sizelimit, new int[] { 1 } );
123         return parent;
124     }
125 
126 
127     /***
128      * Recursively deletes all entries including the base specified.
129      *
130      * @param rdn the relative dn from ou=system of the entry to delete recursively
131      * @throws NamingException if there are problems deleting entries
132      */
133     private void recursivelyDelete( Name rdn ) throws NamingException
134     {
135         NamingEnumeration results = sysRoot.search( rdn, "(objectClass=*)", new SearchControls() );
136         while ( results.hasMore() )
137         {
138             SearchResult result = ( SearchResult ) results.next();
139             Name childRdn = new LdapName( result.getName() );
140             childRdn.remove( 0 );
141             recursivelyDelete( childRdn );
142         }
143         sysRoot.destroySubcontext( rdn );
144     }
145 
146 
147     /***
148      * Performs a single level search as a specific user on newly created data and checks
149      * that result set count is 3.  The basic (objectClass=*) filter is used.
150      *
151      * @param uid the uid RDN attribute value for the user under ou=users,ou=system
152      * @param password the password of the user
153      * @return true if the search succeeds as expected, false otherwise
154      * @throws NamingException if there are problems conducting the search
155      */
156     private boolean checkCanSearchAs( String uid, String password ) throws NamingException
157     {
158         return checkCanSearchAs( uid, password, "(objectClass=*)", null, 3 );
159     }
160 
161 
162     /***
163      * Performs a single level search as a specific user on newly created data and checks
164      * that result set count is equal to a user specified amount.  The basic
165      * (objectClass=*) filter is used.
166      *
167      * @param uid the uid RDN attribute value for the user under ou=users,ou=system
168      * @param password the password of the user
169      * @param resultSetSz the expected size of the results
170      * @return true if the search succeeds as expected, false otherwise
171      * @throws NamingException if there are problems conducting the search
172      */
173     private boolean checkCanSearchAs( String uid, String password, int resultSetSz ) throws NamingException
174     {
175         return checkCanSearchAs( uid, password, "(objectClass=*)", null, resultSetSz );
176     }
177 
178 
179     /***
180      * Performs a search as a specific user on newly created data and checks
181      * that result set count is equal to a user specified amount.  The basic
182      * (objectClass=*) filter is used.
183      *
184      * @param uid the uid RDN attribute value for the user under ou=users,ou=system
185      * @param password the password of the user
186      * @param resultSetSz the expected size of the results
187      * @return true if the search succeeds as expected, false otherwise
188      * @throws NamingException if there are problems conducting the search
189      */
190     private boolean checkCanSearchAs( String uid, String password, SearchControls cons, int resultSetSz )
191             throws NamingException
192     {
193         return checkCanSearchAs( uid, password, "(objectClass=*)", cons, resultSetSz );
194     }
195 
196 
197     /***
198      * Performs a search as a specific user on newly created data and checks
199      * that result set count is equal to a user specified amount.
200      *
201      * @param uid the uid RDN attribute value for the user under ou=users,ou=system
202      * @param password the password of the user
203      * @param filter the search filter to use
204      * @param resultSetSz the expected size of the results
205      * @return true if the search succeeds as expected, false otherwise
206      * @throws NamingException if there are problems conducting the search
207      */
208     private boolean checkCanSearchAs( String uid, String password, String filter,
209                                       SearchControls cons, int resultSetSz ) throws NamingException
210     {
211         if ( cons == null )
212         {
213             cons = new SearchControls();
214         }
215 
216         Name base = addSearchData( new LdapName(), 3, 10 );
217         Name userDn = new LdapName( "uid="+uid+",ou=users,ou=system" );
218         try
219         {
220             results.clear();
221             DirContext userCtx = getContextAs( userDn, password );
222             NamingEnumeration list = userCtx.search( base, filter, cons );
223             int counter = 0;
224             while ( list.hasMore() )
225             {
226                 SearchResult result = ( SearchResult ) list.next();
227                 results.put( result.getName(), result );
228                 counter++;
229             }
230             return counter == resultSetSz;
231         }
232         catch ( LdapNoPermissionException e )
233         {
234             return false;
235         }
236         finally
237         {
238             recursivelyDelete( base );
239         }
240     }
241 
242 
243     /***
244      * Adds an entryACI to specified entry below ou=system and runs a search.  Then it
245      * checks to see the result size is correct.
246      *
247      * @param uid the uid RDN attribute value for the user under ou=users,ou=system
248      * @param password the password of the user
249      * @return true if the search succeeds as expected, false otherwise
250      * @throws NamingException if there are problems conducting the search
251      */
252     private boolean checkSearchAsWithEntryACI( String uid, String password, SearchControls cons, Name rdn,
253                                                String aci, int resultSetSz )
254             throws NamingException
255     {
256         if ( cons == null )
257         {
258             cons = new SearchControls();
259         }
260 
261         Name base = addSearchData( new LdapName(), 3, 10 );
262         addEntryACI( rdn, aci );
263         Name userDn = new LdapName( "uid="+uid+",ou=users,ou=system" );
264         try
265         {
266             results.clear();
267             DirContext userCtx = getContextAs( userDn, password );
268             NamingEnumeration list = userCtx.search( base, "(objectClass=*)", cons );
269             int counter = 0;
270             while ( list.hasMore() )
271             {
272                 SearchResult result = ( SearchResult ) list.next();
273                 results.put( result.getName(), result );
274                 counter++;
275             }
276             return counter == resultSetSz;
277         }
278         catch ( LdapNoPermissionException e )
279         {
280             return false;
281         }
282         finally
283         {
284             recursivelyDelete( base );
285         }
286     }
287 
288 
289     /***
290      * Checks to see that the addSearchData() and the recursiveDelete()
291      * functions in this test work properly.
292      *
293      * @throws NamingException if there is a problem with the implementation of
294      * these utility functions
295      */
296     public void testAddSearchData() throws NamingException
297     {
298         Name base = addSearchData( new LdapName(), 3, 10 );
299         SearchControls controls = new SearchControls();
300         controls.setSearchScope( SearchControls.SUBTREE_SCOPE );
301         NamingEnumeration results = sysRoot.search( base, "(objectClass=*)", controls );
302         int counter = 0;
303         while ( results.hasMore() )
304         {
305             results.next();
306             counter++;
307         }
308 
309         assertEquals( 10, counter );
310         recursivelyDelete( base );
311         try { sysRoot.lookup( base ); fail(); } catch ( LdapNameNotFoundException e ) {}
312     }
313 
314 
315     // -----------------------------------------------------------------------
316     // All or nothing search ACI rule tests
317     // -----------------------------------------------------------------------
318 
319 
320     /***
321      * Checks to make sure group membership based userClass works for add operations.
322      *
323      * @throws javax.naming.NamingException if the test encounters an error
324      */
325     public void testGrantAdministrators() throws NamingException
326     {
327         // create the non-admin user
328         createUser( "billyd", "billyd" );
329 
330         // try an add operation which should fail without any ACI
331         assertFalse( checkCanSearchAs( "billyd", "billyd" ) );
332 
333         // Gives search perms to all users in the Administrators group for
334         // entries and all attribute types and values
335         createAccessControlSubentry( "searchAdmin", "{ " +
336                 "identificationTag \"searchAci\", " +
337                 "precedence 14, " +
338                 "authenticationLevel none, " +
339                 "itemOrUserFirst userFirst: { " +
340                 "userClasses { userGroup { \"cn=Administrators,ou=groups,ou=system\" } }, " +
341                 "userPermissions { { " +
342                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
343                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
344 
345         // see if we can now add that test entry which we could not before
346         // add op should still fail since billd is not in the admin group
347         assertFalse( checkCanSearchAs( "billyd", "billyd" ) );
348 
349         // now add billyd to the Administrator group and try again
350         addUserToGroup( "billyd", "Administrators" );
351 
352         // try an add operation which should succeed with ACI and group membership change
353         assertTrue( checkCanSearchAs( "billyd", "billyd" ) );
354     }
355 
356 
357     /***
358      * Checks to make sure name based userClass works for search operations.
359      *
360      * @throws javax.naming.NamingException if the test encounters an error
361      */
362     public void testGrantSearchByName() throws NamingException
363     {
364         // create the non-admin user
365         createUser( "billyd", "billyd" );
366 
367         // try an add operation which should fail without any ACI
368         assertFalse( checkCanSearchAs( "billyd", "billyd" ) );
369 
370         // now add a subentry that enables user billyd to add an entry below ou=system
371         createAccessControlSubentry( "billydSearch", "{ " +
372                 "identificationTag \"searchAci\", " +
373                 "precedence 14, " +
374                 "authenticationLevel none, " +
375                 "itemOrUserFirst userFirst: { " +
376                 "userClasses { name { \"uid=billyd,ou=users,ou=system\" } }, " +
377                 "userPermissions { { " +
378                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
379                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
380 
381         // should work now that billyd is authorized by name
382         assertTrue( checkCanSearchAs( "billyd", "billyd" ) );
383     }
384 
385 
386     /***
387      * Checks to make sure subtree based userClass works for search operations.
388      *
389      * @throws javax.naming.NamingException if the test encounters an error
390      */
391     public void testGrantSearchBySubtree() throws NamingException
392     {
393         // create the non-admin user
394         createUser( "billyd", "billyd" );
395 
396         // try an add operation which should fail without any ACI
397         assertFalse( checkCanSearchAs( "billyd", "billyd" ) );
398 
399         // now add a subentry that enables user billyd to add an entry below ou=system
400         createAccessControlSubentry( "billySearchBySubtree", "{ " +
401                 "identificationTag \"searchAci\", " +
402                 "precedence 14, " +
403                 "authenticationLevel none, " +
404                 "itemOrUserFirst userFirst: { " +
405                 "userClasses { subtree { { base \"ou=users,ou=system\" } } }, " +
406                 "userPermissions { { " +
407                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
408                 "grantsAndDenials {  grantRead, grantReturnDN, grantBrowse } } } } }" );
409 
410         // should work now that billyd is authorized by the subtree userClass
411         assertTrue( checkCanSearchAs( "billyd", "billyd" ) );
412     }
413 
414 
415     /***
416      * Checks to make sure <b>allUsers</b> userClass works for search operations.
417      *
418      * @throws javax.naming.NamingException if the test encounters an error
419      */
420     public void testGrantSearchAllUsers() throws NamingException
421     {
422         // create the non-admin user
423         createUser( "billyd", "billyd" );
424 
425         // try an search operation which should fail without any ACI
426         assertFalse( checkCanSearchAs( "billyd", "billyd" ) );
427 
428         // now add a subentry that enables anyone to search an entry below ou=system
429         createAccessControlSubentry( "anybodySearch", "{ " +
430                 "identificationTag \"searchAci\", " +
431                 "precedence 14, " +
432                 "authenticationLevel none, " +
433                 "itemOrUserFirst userFirst: { " +
434                 "userClasses { allUsers }, " +
435                 "userPermissions { { " +
436                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
437                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
438 
439         // see if we can now search that tree which we could not before
440         // should work now with billyd now that all users are authorized
441         assertTrue( checkCanSearchAs( "billyd", "billyd" ) );
442     }
443 
444 
445     // -----------------------------------------------------------------------
446     //
447     // -----------------------------------------------------------------------
448 
449 
450     /***
451      * Checks to make sure search does not return entries not assigned the
452      * perscriptiveACI and that it does not fail with an exception.
453      *
454      * @throws javax.naming.NamingException if the test encounters an error
455      */
456     public void testSelectiveGrantsAllUsers() throws NamingException
457     {
458         // create the non-admin user
459         createUser( "billyd", "billyd" );
460 
461         // try an add operation which should fail without any ACI
462         SearchControls cons = new SearchControls();
463         cons.setSearchScope( SearchControls.SUBTREE_SCOPE );
464         assertFalse( checkCanSearchAs( "billyd", "billyd", cons, 4 ) );
465 
466         // now add a subentry that enables anyone to add an entry below ou=system
467         // down two more rdns for DNs of a max size of 3
468         createAccessControlSubentry( "anybodySearch",
469                 "{ maximum 2 }",
470                 "{ " +
471                 "identificationTag \"searchAci\", " +
472                 "precedence 14, " +
473                 "authenticationLevel none, " +
474                 "itemOrUserFirst userFirst: { " +
475                 "userClasses { allUsers }, " +
476                 "userPermissions { { " +
477                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
478                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
479 
480         // see if we can now add that test entry which we could not before
481         // should work now with billyd now that all users are authorized
482         assertTrue( checkCanSearchAs( "billyd", "billyd", cons, 4 ) );
483     }
484 
485 
486     /***
487      * Checks to make sure attributeTypes are not present when permissions are
488      * not given for reading them and their values.
489      *
490      * @throws javax.naming.NamingException if the test encounters an error
491      */
492     public void testHidingAttributes() throws NamingException
493     {
494         // create the non-admin user
495         createUser( "billyd", "billyd" );
496 
497         // try an add operation which should fail without any ACI
498         SearchControls cons = new SearchControls();
499         cons.setSearchScope( SearchControls.SUBTREE_SCOPE );
500         assertFalse( checkCanSearchAs( "billyd", "billyd", cons, 4 ) );
501 
502         // now add a subentry that enables anyone to search an entry below ou=system
503         // down two more rdns for DNs of a max size of 3.  It only grants access to
504         // the ou and objectClass attributes however.
505         createAccessControlSubentry( "excluseTelephoneNumber",
506                 "{ maximum 2 }",
507                 "{ " +
508                 "identificationTag \"searchAci\", " +
509                 "precedence 14, " +
510                 "authenticationLevel none, " +
511                 "itemOrUserFirst userFirst: { " +
512                 "userClasses { allUsers }, " +
513                 "userPermissions { { " +
514                 "protectedItems {entry, allAttributeValues { ou, objectClass } }, " +
515                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
516 
517         // see if we can now add that search and find 4 entries
518         assertTrue( checkCanSearchAs( "billyd", "billyd", cons, 4 ) );
519 
520         // check to make sure the telephoneNumber attribute is not present in results
521         Iterator list = results.values().iterator();
522         while ( list.hasNext() )
523         {
524             SearchResult result = ( SearchResult ) list.next();
525             assertNull( result.getAttributes().get( "telephoneNumber" ) );
526         }
527 
528         // delete the subentry to test more general rule's inclusion of telephoneNumber
529         deleteAccessControlSubentry( "excluseTelephoneNumber" );
530 
531         // now add a subentry that enables anyone to search an entry below ou=system
532         // down two more rdns for DNs of a max size of 3.  This time we should be able
533         // to see the telephoneNumber attribute
534         createAccessControlSubentry( "includeAllAttributeTypesAndValues",
535                 "{ maximum 2 }",
536                 "{ " +
537                 "identificationTag \"searchAci\", " +
538                 "precedence 14, " +
539                 "authenticationLevel none, " +
540                 "itemOrUserFirst userFirst: { " +
541                 "userClasses { allUsers }, " +
542                 "userPermissions { { " +
543                 "protectedItems {entry, allUserAttributeTypesAndValues }, " +
544                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
545 
546         // again we should find four entries
547         assertTrue( checkCanSearchAs( "billyd", "billyd", cons, 4 ) );
548 
549         // check now to make sure the telephoneNumber attribute is present in results
550         list = results.values().iterator();
551         while ( list.hasNext() )
552         {
553             SearchResult result = ( SearchResult ) list.next();
554             assertNotNull( result.getAttributes().get( "telephoneNumber" ) );
555         }
556     }
557 
558 
559     /***
560      * Checks to make sure specific attribute values are not present when
561      * read permission is denied.
562      *
563      * @throws javax.naming.NamingException if the test encounters an error
564      */
565     public void testHidingAttributeValues() throws NamingException
566     {
567         // create the non-admin user
568         createUser( "billyd", "billyd" );
569 
570         // try an add operation which should fail without any ACI
571         assertFalse( checkCanSearchAs( "billyd", "billyd", 3 ) );
572 
573         // now add a subentry that enables anyone to search an entry below ou=system
574         // down two more rdns for DNs of a max size of 3.  It only grants access to
575         // the ou and objectClass attributes however.
576         createAccessControlSubentry( "excluseOUValue",
577                 "{ maximum 2 }",
578                 "{ " +
579                 "identificationTag \"searchAci\", " +
580                 "precedence 14, " +
581                 "authenticationLevel none, " +
582                 "itemOrUserFirst userFirst: { " +
583                 "userClasses { allUsers }, " +
584                 "userPermissions { { " +
585                 "protectedItems {entry, attributeType { ou }, allAttributeValues { objectClass }, attributeValue { ou=0, ou=1, ou=2 } }, " +
586                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
587 
588         // see if we can now add that search and find 4 entries
589         assertTrue( checkCanSearchAs( "billyd", "billyd", 3 ) );
590 
591         // check to make sure the ou attribute value "testEntry" is not present in results
592         Iterator list = results.values().iterator();
593         while ( list.hasNext() )
594         {
595             SearchResult result = ( SearchResult ) list.next();
596             assertFalse( result.getAttributes().get( "ou" ).contains( "testEntry" ) );
597         }
598 
599         // delete the subentry to test more general rule's inclusion of all values
600         deleteAccessControlSubentry( "excluseOUValue" );
601 
602         // now add a subentry that enables anyone to search an entry below ou=system
603         // down two more rdns for DNs of a max size of 3.  This time we should be able
604         // to see the telephoneNumber attribute
605         createAccessControlSubentry( "includeAllAttributeTypesAndValues",
606                 "{ maximum 2 }",
607                 "{ " +
608                 "identificationTag \"searchAci\", " +
609                 "precedence 14, " +
610                 "authenticationLevel none, " +
611                 "itemOrUserFirst userFirst: { " +
612                 "userClasses { allUsers }, " +
613                 "userPermissions { { " +
614                 "protectedItems {entry, allUserAttributeTypesAndValues }, " +
615                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
616 
617         // again we should find four entries
618         assertTrue( checkCanSearchAs( "billyd", "billyd", 3 ) );
619 
620         // check now to make sure the telephoneNumber attribute is present in results
621         list = results.values().iterator();
622         while ( list.hasNext() )
623         {
624             SearchResult result = ( SearchResult ) list.next();
625             assertTrue( result.getAttributes().get( "ou" ).contains( "testEntry" ) );
626         }
627     }
628 
629 
630     /***
631      * Adds a perscriptiveACI to allow search, tests for success, then adds entryACI
632      * to deny read, browse and returnDN to a specific entry and checks to make sure
633      * that entry cannot be accessed via search as a specific user.
634      *
635      * @throws NamingException if the test is broken
636      */
637     public void testPerscriptiveGrantWithEntryDenial() throws NamingException
638     {
639         // create the non-admin user
640         createUser( "billyd", "billyd" );
641 
642         // now add an entryACI denies browse, read and returnDN to a specific entry
643         String aci = "{ " +
644                 "identificationTag \"denyAci\", " +
645                 "precedence 14, " +
646                 "authenticationLevel none, " +
647                 "itemOrUserFirst userFirst: { " +
648                 "userClasses { allUsers }, " +
649                 "userPermissions { { " +
650                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
651                 "grantsAndDenials { denyRead, denyReturnDN, denyBrowse } } } } }";
652 
653         // try a search operation which should fail without any prescriptive ACI
654         SearchControls cons = new SearchControls();
655         cons.setSearchScope( SearchControls.SUBTREE_SCOPE );
656         LdapName rdn = new LdapName( "ou=tests" );
657         assertFalse( checkSearchAsWithEntryACI( "billyd", "billyd", cons, rdn, aci, 9 ) );
658 
659         // now add a subentry that enables anyone to search below ou=system
660         createAccessControlSubentry( "anybodySearch", "{ " +
661                 "identificationTag \"searchAci\", " +
662                 "precedence 14, " +
663                 "authenticationLevel none, " +
664                 "itemOrUserFirst userFirst: { " +
665                 "userClasses { allUsers }, " +
666                 "userPermissions { { " +
667                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
668                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
669 
670         // see if we can now search the tree which we could not before
671         // should work with billyd now that all users are authorized
672         // we should NOT see the entry we are about to deny access to
673         assertTrue( checkSearchAsWithEntryACI( "billyd", "billyd", cons, rdn, aci, 9 ) );
674         assertNull( results.get( "ou=tests,ou=system" ) );
675 
676         // try without the entry ACI .. just perscriptive and see ou=tests,ou=system
677         assertTrue( checkCanSearchAs( "billyd", "billyd", cons, 10 ) );
678         assertNotNull( results.get( "ou=tests,ou=system" ) );
679     }
680 
681 
682     /***
683      * Adds a perscriptiveACI to allow search, tests for success, then adds entryACI
684      * to deny read, browse and returnDN to a specific entry and checks to make sure
685      * that entry cannot be accessed via search as a specific user.  Here the
686      * precidence of the ACI is put to the test.
687      *
688      * @throws NamingException if the test is broken
689      */
690     public void testPerscriptiveGrantWithEntryDenialWithPrecidence() throws NamingException
691     {
692         // create the non-admin user
693         createUser( "billyd", "billyd" );
694 
695         // now add an entryACI denies browse, read and returnDN to a specific entry
696         String aci = "{ " +
697                 "identificationTag \"denyAci\", " +
698                 "precedence 14, " +
699                 "authenticationLevel none, " +
700                 "itemOrUserFirst userFirst: { " +
701                 "userClasses { allUsers }, " +
702                 "userPermissions { { " +
703                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
704                 "grantsAndDenials { denyRead, denyReturnDN, denyBrowse } } } } }";
705 
706         // try a search operation which should fail without any prescriptive ACI
707         SearchControls cons = new SearchControls();
708         cons.setSearchScope( SearchControls.SUBTREE_SCOPE );
709         LdapName rdn = new LdapName( "ou=tests" );
710         assertFalse( checkSearchAsWithEntryACI( "billyd", "billyd", cons, rdn, aci, 9 ) );
711 
712         // now add a subentry that enables anyone to search below ou=system
713         createAccessControlSubentry( "anybodySearch", "{ " +
714                 "identificationTag \"searchAci\", " +
715                 "precedence 15, " +
716                 "authenticationLevel none, " +
717                 "itemOrUserFirst userFirst: { " +
718                 "userClasses { allUsers }, " +
719                 "userPermissions { { " +
720                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
721                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
722 
723         // see if we can now search the tree which we could not before
724         // should work with billyd now that all users are authorized
725         // we should also see the entry we are about to deny access to
726         // we see it because the precidence of the grant is greater
727         // than the precedence of the denial
728         assertTrue( checkSearchAsWithEntryACI( "billyd", "billyd", cons, rdn, aci, 10 ) );
729         assertNotNull( results.get( "ou=tests,ou=system" ) );
730 
731         // now add an entryACI denies browse, read and returnDN to a specific entry
732         // but this time the precedence will be higher than that of the grant
733         aci = "{ " +
734                 "identificationTag \"denyAci\", " +
735                 "precedence 16, " +
736                 "authenticationLevel none, " +
737                 "itemOrUserFirst userFirst: { " +
738                 "userClasses { allUsers }, " +
739                 "userPermissions { { " +
740                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
741                 "grantsAndDenials { denyRead, denyReturnDN, denyBrowse } } } } }";
742 
743         // see if we can now search the tree which we could not before
744         // should work with billyd now that all users are authorized
745         // we should NOT see the entry we are about to deny access to
746         // we do NOT see it because the precidence of the grant is less
747         // than the precedence of the denial - so the denial wins
748         assertTrue( checkSearchAsWithEntryACI( "billyd", "billyd", cons, rdn, aci, 9 ) );
749         assertNull( results.get( "ou=tests,ou=system" ) );
750     }
751 
752 
753     /***
754      * Performs an object level search on the specified subentry relative to ou=system as a specific user.
755      *
756      * @param uid the uid RDN attribute value of the user to perform the search as
757      * @param password the password of the user
758      * @param rdn the relative name to the subentry under the ou=system AP
759      * @return the single search result if access is allowed or null
760      * @throws NamingException if the search fails w/ exception other than no permission
761      */
762     private SearchResult checkCanSearhSubentryAs( String uid, String password, Name rdn ) throws NamingException
763     {
764         DirContext userCtx = getContextAs( new LdapName( "uid="+uid+",ou=users,ou=system" ), password );
765         SearchControls cons = new SearchControls();
766         cons.setSearchScope( SearchControls.OBJECT_SCOPE );
767         SearchResult result = null;
768         NamingEnumeration list = null;
769 
770         try
771         {
772             list = userCtx.search( rdn, "(objectClass=*)", cons );
773             if ( list.hasMore() )
774             {
775                 result = ( SearchResult ) list.next();
776                 list.close();
777                 return result;
778             }
779         }
780         catch ( LdapNoPermissionException e )
781         {
782         }
783         finally
784         {
785             if ( list != null ) { list.close(); }
786         }
787 
788         return result;
789     }
790 
791 
792     public void testSubentryAccess() throws NamingException
793     {
794         // create the non-admin user
795         createUser( "billyd", "billyd" );
796 
797         // now add a subentry that enables anyone to search below ou=system
798         createAccessControlSubentry( "anybodySearch", "{ " +
799                 "identificationTag \"searchAci\", " +
800                 "precedence 14, " +
801                 "authenticationLevel none, " +
802                 "itemOrUserFirst userFirst: { " +
803                 "userClasses { allUsers }, " +
804                 "userPermissions { { " +
805                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
806                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse } } } } }" );
807 
808         // check and see if we can access the subentry now
809         assertNotNull( checkCanSearhSubentryAs( "billyd", "billyd", new LdapName( "cn=anybodySearch" ) ) );
810 
811         // now add a denial to prevent all users except the admin from accessing the subentry
812         addSubentryACI( "{ " +
813                 "identificationTag \"searchAci\", " +
814                 "precedence 14, " +
815                 "authenticationLevel none, " +
816                 "itemOrUserFirst userFirst: { " +
817                 "userClasses { allUsers }, " +
818                 "userPermissions { { " +
819                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
820                 "grantsAndDenials { denyRead, denyReturnDN, denyBrowse } } } } }" );
821 
822         // now we should not be able to access the subentry with a search
823         assertNull( checkCanSearhSubentryAs( "billyd", "billyd", new LdapName( "cn=anybodySearch" ) ) );
824     }
825 
826 
827     public void testGetMatchedName() throws  NamingException
828     {
829         // create the non-admin user
830         createUser( "billyd", "billyd" );
831 
832         // now add a subentry that enables anyone to search/lookup and disclose on error
833         // below ou=system, with the exclusion of ou=groups and everything below it
834         createAccessControlSubentry( "selectiveDiscloseOnError",
835                 "{ specificExclusions { chopBefore:\"ou=groups\" } }",
836                 "{ " +
837                 "identificationTag \"searchAci\", " +
838                 "precedence 14, " +
839                 "authenticationLevel none, " +
840                 "itemOrUserFirst userFirst: { " +
841                 "userClasses { allUsers }, " +
842                 "userPermissions { { " +
843                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
844                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse, grantDiscloseOnError } } } } }" );
845 
846         // get a context as the user and try a lookup of a non-existant entry under ou=groups,ou=system
847         DirContext userCtx = getContextAs( new LdapName( "uid=billyd,ou=users,ou=system" ), "billyd" );
848         try
849         {
850             userCtx.lookup( "cn=blah,ou=groups" );
851         }
852         catch( NamingException e )
853         {
854             Name matched = e.getResolvedName();
855 
856             // we should not see ou=groups,ou=system for the remaining name
857             assertEquals( matched.toString(), "ou=system" );
858         }
859 
860         // now delete and replace subentry with one that does not excluse ou=groups,ou=system
861         deleteAccessControlSubentry( "selectiveDiscloseOnError" );
862         createAccessControlSubentry( "selectiveDiscloseOnError",
863                 "{ " +
864                 "identificationTag \"searchAci\", " +
865                 "precedence 14, " +
866                 "authenticationLevel none, " +
867                 "itemOrUserFirst userFirst: { " +
868                 "userClasses { allUsers }, " +
869                 "userPermissions { { " +
870                 "protectedItems {entry, allUserAttributeTypesAndValues}, " +
871                 "grantsAndDenials { grantRead, grantReturnDN, grantBrowse, grantDiscloseOnError } } } } }" );
872 
873         // now try a lookup of a non-existant entry under ou=groups,ou=system again
874         try
875         {
876             userCtx.lookup( "cn=blah,ou=groups" );
877         }
878         catch( NamingException e )
879         {
880             Name matched = e.getResolvedName();
881 
882             // we should not see ou=groups,ou=system for the remaining name
883             assertEquals( matched.toString(), "ou=groups,ou=system" );
884         }
885     }
886 }