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