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.kerberos;
21  
22  
23  import org.apache.directory.server.core.DefaultDirectoryService;
24  import org.apache.directory.server.core.DirectoryService;
25  import org.apache.directory.server.core.entry.ServerEntry;
26  import org.apache.directory.server.core.integ.IntegrationUtils;
27  import org.apache.directory.server.core.integ.Level;
28  import org.apache.directory.server.core.integ.annotations.ApplyLdifs;
29  import org.apache.directory.server.core.integ.annotations.CleanupLevel;
30  import org.apache.directory.server.core.integ.annotations.Factory;
31  import org.apache.directory.server.core.interceptor.Interceptor;
32  import org.apache.directory.server.core.kerberos.PasswordPolicyInterceptor;
33  import org.apache.directory.server.core.partition.Partition;
34  import org.apache.directory.server.xdbm.Index;
35  import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex;
36  import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
37  import org.apache.directory.server.integ.LdapServerFactory;
38  import org.apache.directory.server.integ.SiRunner;
39  import org.apache.directory.server.ldap.LdapService;
40  import org.apache.directory.server.ldap.handlers.bind.MechanismHandler;
41  import org.apache.directory.server.ldap.handlers.bind.cramMD5.CramMd5MechanismHandler;
42  import org.apache.directory.server.ldap.handlers.bind.digestMD5.DigestMd5MechanismHandler;
43  import org.apache.directory.server.ldap.handlers.bind.gssapi.GssapiMechanismHandler;
44  import org.apache.directory.server.ldap.handlers.bind.ntlm.NtlmMechanismHandler;
45  import org.apache.directory.server.ldap.handlers.bind.plain.PlainMechanismHandler;
46  import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
47  import org.apache.directory.server.protocol.shared.SocketAcceptor;
48  import org.apache.directory.shared.ldap.constants.SupportedSaslMechanisms;
49  import org.apache.mina.util.AvailablePortFinder;
50  import org.junit.After;
51  import org.junit.Before;
52  import org.junit.Test;
53  import org.junit.runner.RunWith;
54  import static org.junit.Assert.fail;
55  import static org.junit.Assert.assertTrue;
56  import static org.junit.Assert.assertFalse;
57  
58  import javax.naming.NamingException;
59  import javax.naming.directory.Attribute;
60  import javax.naming.directory.Attributes;
61  import javax.naming.directory.BasicAttribute;
62  import javax.naming.directory.BasicAttributes;
63  import javax.naming.directory.DirContext;
64  import javax.naming.directory.InitialDirContext;
65  
66  import java.util.HashMap;
67  import java.util.HashSet;
68  import java.util.Hashtable;
69  import java.util.List;
70  import java.util.Map;
71  import java.util.Set;
72  
73  
74  /**
75   * An {@link AbstractServerTest} testing the (@link {@link PasswordPolicyInterceptor}.
76   * 
77   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
78   * @version $Rev$, $Date$
79   */
80  @RunWith ( SiRunner.class ) 
81  @CleanupLevel ( Level.CLASS )
82  @Factory ( PasswordPolicyServiceIT.Factory.class )
83  @ApplyLdifs( {
84      // Entry #0
85      "dn: dc=example,dc=com\n" +
86      "dc: example\n" +
87      "objectClass: top\n" +
88      "objectClass: domain\n\n"
89      }
90  )
91  public class PasswordPolicyServiceIT 
92  {
93      private DirContext ctx;
94      private DirContext users;
95  
96  
97      public static LdapService ldapService;
98  
99      
100     public static class Factory implements LdapServerFactory
101     {
102         public LdapService newInstance() throws Exception
103         {
104             DirectoryService service = new DefaultDirectoryService();
105             IntegrationUtils.doDelete( service.getWorkingDirectory() );
106             service.getChangeLog().setEnabled( true );
107             service.setAllowAnonymousAccess( false );
108             service.setShutdownHookEnabled( false );
109 
110             Set<Partition> partitions = new HashSet<Partition>();
111             JdbmPartition partition = new JdbmPartition();
112             partition.setId( "example" );
113             partition.setSuffix( "dc=example,dc=com" );
114 
115             Set<Index<?,ServerEntry>> indexedAttrs = new HashSet<Index<?,ServerEntry>>();
116             indexedAttrs.add( new JdbmIndex<String,ServerEntry>( "ou" ) );
117             indexedAttrs.add( new JdbmIndex<String,ServerEntry>( "dc" ) );
118             indexedAttrs.add( new JdbmIndex<String,ServerEntry>( "objectClass" ) );
119             partition.setIndexedAttributes( indexedAttrs );
120 
121             partitions.add( partition );
122             service.setPartitions( partitions );
123 
124             List<Interceptor> list = service.getInterceptors();
125             list.add( new PasswordPolicyInterceptor() );
126             service.setInterceptors( list );
127             
128             // change the working directory to something that is unique
129             // on the system and somewhere either under target directory
130             // or somewhere in a temp area of the machine.
131 
132             LdapService ldapService = new LdapService();
133             ldapService.setDirectoryService( service );
134             ldapService.setSocketAcceptor( new SocketAcceptor( null ) );
135             ldapService.setIpPort( AvailablePortFinder.getNextAvailable( 1024 ) );
136             ldapService.setAllowAnonymousAccess( false );
137             ldapService.addExtendedOperationHandler( new StoredProcedureExtendedOperationHandler() );
138 
139             // Setup SASL Mechanisms
140             
141             Map<String, MechanismHandler> mechanismHandlerMap = new HashMap<String,MechanismHandler>();
142             mechanismHandlerMap.put( SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler() );
143 
144             CramMd5MechanismHandler cramMd5MechanismHandler = new CramMd5MechanismHandler();
145             mechanismHandlerMap.put( SupportedSaslMechanisms.CRAM_MD5, cramMd5MechanismHandler );
146 
147             DigestMd5MechanismHandler digestMd5MechanismHandler = new DigestMd5MechanismHandler();
148             mechanismHandlerMap.put( SupportedSaslMechanisms.DIGEST_MD5, digestMd5MechanismHandler );
149 
150             GssapiMechanismHandler gssapiMechanismHandler = new GssapiMechanismHandler();
151             mechanismHandlerMap.put( SupportedSaslMechanisms.GSSAPI, gssapiMechanismHandler );
152 
153             NtlmMechanismHandler ntlmMechanismHandler = new NtlmMechanismHandler();
154             mechanismHandlerMap.put( SupportedSaslMechanisms.NTLM, ntlmMechanismHandler );
155             mechanismHandlerMap.put( SupportedSaslMechanisms.GSS_SPNEGO, ntlmMechanismHandler );
156 
157             ldapService.setSaslMechanismHandlers( mechanismHandlerMap );
158             ldapService.setSaslHost( "localhost" );
159             
160             return ldapService;
161         }
162     }
163     
164     
165     /**
166      * Set up a partition for EXAMPLE.COM, add the {@link PasswordPolicyInterceptor}
167      * interceptor, and create a users subcontext.
168      */
169     @Before
170     public void setUp() throws Exception
171     {
172         Attributes attrs;
173         Hashtable<String, String> env = new Hashtable<String, String>();
174         env.put( "java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory" );
175         env.put( "java.naming.provider.url", "ldap://localhost:" + ldapService.getIpPort() + "/dc=example,dc=com" );
176         env.put( "java.naming.security.principal", "uid=admin,ou=system" );
177         env.put( "java.naming.security.credentials", "secret" );
178         env.put( "java.naming.security.authentication", "simple" );
179         ctx = new InitialDirContext( env );
180 
181         attrs = getOrgUnitAttributes( "users" );
182         users = ctx.createSubcontext( "ou=users", attrs );
183     }
184 
185     
186     /**
187      * Tests that passwords that are too short are properly rejected. 
188      */
189     @Test
190     public void testLength()
191     {
192         Attributes attrs = getPersonAttributes( "Nelson", "Horatio Nelson", "hnelson", "HN1" );
193         try
194         {
195             users.createSubcontext( "uid=hnelson", attrs );
196             fail( "Shouldn't have gotten here." );
197         }
198         catch ( NamingException ne )
199         {
200             assertTrue( ne.getMessage().contains( "length too short" ) );
201             assertFalse( ne.getMessage().contains( "insufficient character mix" ) );
202             assertFalse( ne.getMessage().contains( "contains portions of username" ) );
203         }
204     }
205 
206 
207     /**
208      * Tests that passwords with insufficient character mix are properly rejected. 
209      */
210     @Test
211     public void testCharacterMix()
212     {
213         Attributes attrs = getPersonAttributes( "Nelson", "Horatio Nelson", "hnelson", "secret" );
214         try
215         {
216             users.createSubcontext( "uid=hnelson", attrs );
217             fail( "Shouldn't have gotten here." );
218         }
219         catch ( NamingException ne )
220         {
221             assertFalse( ne.getMessage().contains( "length too short" ) );
222             assertTrue( ne.getMessage().contains( "insufficient character mix" ) );
223             assertFalse( ne.getMessage().contains( "contains portions of username" ) );
224         }
225     }
226 
227 
228     /**
229      * Tests that passwords that contain substrings of the username are properly rejected. 
230      */
231     @Test
232     public void testContainsUsername()
233     {
234         Attributes attrs = getPersonAttributes( "Nelson", "Horatio Nelson", "hnelson", "A1nelson" );
235         try
236         {
237             users.createSubcontext( "uid=hnelson", attrs );
238             fail( "Shouldn't have gotten here." );
239         }
240         catch ( NamingException ne )
241         {
242             assertFalse( ne.getMessage().contains( "length too short" ) );
243             assertFalse( ne.getMessage().contains( "insufficient character mix" ) );
244             assertTrue( ne.getMessage().contains( "contains portions of username" ) );
245         }
246     }
247 
248 
249     /**
250      * Tests that passwords with insufficient character mix and that are too
251      * short are properly rejected. 
252      */
253     @Test
254     public void testCharacterMixAndLength()
255     {
256         Attributes attrs = getPersonAttributes( "Nelson", "Horatio Nelson", "hnelson", "hi" );
257         try
258         {
259             users.createSubcontext( "uid=hnelson", attrs );
260             fail( "Shouldn't have gotten here." );
261         }
262         catch ( NamingException ne )
263         {
264             assertTrue( ne.getMessage().contains( "length too short" ) );
265             assertTrue( ne.getMessage().contains( "insufficient character mix" ) );
266             assertFalse( ne.getMessage().contains( "contains portions of username" ) );
267         }
268     }
269 
270 
271     /**
272      * Tests that passwords that are too short and that contain substrings of
273      * the username are properly rejected.
274      */
275     @Test
276     public void testLengthAndContainsUsername()
277     {
278         Attributes attrs = getPersonAttributes( "Bush", "William Bush", "wbush", "bush1" );
279         try
280         {
281             users.createSubcontext( "uid=wbush", attrs );
282             fail( "Shouldn't have gotten here." );
283         }
284         catch ( NamingException ne )
285         {
286             assertTrue( ne.getMessage().contains( "length too short" ) );
287             assertFalse( ne.getMessage().contains( "insufficient character mix" ) );
288             assertTrue( ne.getMessage().contains( "contains portions of username" ) );
289         }
290     }
291 
292 
293     /**
294      * Tests that passwords with insufficient character mix and that contain substrings of
295      * the username are properly rejected.
296      */
297     @Test
298     public void testCharacterMixAndContainsUsername()
299     {
300         Attributes attrs = getPersonAttributes( "Nelson", "Horatio Nelson", "hnelson", "hnelson" );
301         try
302         {
303             users.createSubcontext( "uid=hnelson", attrs );
304             fail( "Shouldn't have gotten here." );
305         }
306         catch ( NamingException ne )
307         {
308             assertFalse( ne.getMessage().contains( "length too short" ) );
309             assertTrue( ne.getMessage().contains( "insufficient character mix" ) );
310             assertTrue( ne.getMessage().contains( "contains portions of username" ) );
311         }
312     }
313 
314 
315     /**
316      * Tests that passwords with insufficient character mix and that are too
317      * short and that contain substrings of the username are properly rejected.
318      */
319     @Test
320     public void testCharacterMixAndLengthAndContainsUsername()
321     {
322         Attributes attrs = getPersonAttributes( "Bush", "William Bush", "wbush", "bush" );
323         try
324         {
325             users.createSubcontext( "uid=wbush", attrs );
326             fail( "Shouldn't have gotten here." );
327         }
328         catch ( NamingException ne )
329         {
330             assertTrue( ne.getMessage().contains( "length too short" ) );
331             assertTrue( ne.getMessage().contains( "insufficient character mix" ) );
332             assertTrue( ne.getMessage().contains( "contains portions of username" ) );
333         }
334     }
335 
336 
337     /**
338      * Tear down.
339      */
340     @After
341     public void tearDown() throws Exception
342     {
343         ctx.close();
344         ctx = null;
345     }
346 
347 
348     /**
349      * Convenience method for creating a person.
350      */
351     protected Attributes getPersonAttributes( String sn, String cn, String uid, String userPassword )
352     {
353         Attributes attrs = new BasicAttributes( true );
354         Attribute ocls = new BasicAttribute( "objectClass" );
355         ocls.add( "top" );
356         ocls.add( "person" ); // sn $ cn
357         ocls.add( "inetOrgPerson" ); // uid
358         attrs.put( ocls );
359         attrs.put( "cn", cn );
360         attrs.put( "sn", sn );
361         attrs.put( "uid", uid );
362         attrs.put( "userPassword", userPassword );
363 
364         return attrs;
365     }
366 
367 
368     /**
369      * Convenience method for creating an organizational unit.
370      */
371     protected Attributes getOrgUnitAttributes( String ou )
372     {
373         Attributes attrs = new BasicAttributes( true );
374         Attribute ocls = new BasicAttribute( "objectClass" );
375         ocls.add( "top" );
376         ocls.add( "organizationalUnit" );
377         attrs.put( ocls );
378         attrs.put( "ou", ou );
379 
380         return attrs;
381     }
382 }