001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *  
010     *    http://www.apache.org/licenses/LICENSE-2.0
011     *  
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License. 
018     *  
019     */
020    package org.apache.directory.server.core.authn;
021    
022    
023    import java.util.ArrayList;
024    import java.util.Collection;
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.Map;
028    import java.util.Set;
029    
030    import org.apache.directory.server.core.CoreSession;
031    import org.apache.directory.server.core.DefaultCoreSession;
032    import org.apache.directory.server.core.DirectoryService;
033    import org.apache.directory.server.core.LdapPrincipal;
034    import org.apache.directory.server.core.entry.ClonedServerEntry;
035    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
036    import org.apache.directory.server.core.interceptor.BaseInterceptor;
037    import org.apache.directory.server.core.interceptor.Interceptor;
038    import org.apache.directory.server.core.interceptor.NextInterceptor;
039    import org.apache.directory.server.core.interceptor.context.AddOperationContext;
040    import org.apache.directory.server.core.interceptor.context.BindOperationContext;
041    import org.apache.directory.server.core.interceptor.context.CompareOperationContext;
042    import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
043    import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
044    import org.apache.directory.server.core.interceptor.context.GetMatchedNameOperationContext;
045    import org.apache.directory.server.core.interceptor.context.GetRootDSEOperationContext;
046    import org.apache.directory.server.core.interceptor.context.GetSuffixOperationContext;
047    import org.apache.directory.server.core.interceptor.context.ListOperationContext;
048    import org.apache.directory.server.core.interceptor.context.ListSuffixOperationContext;
049    import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
050    import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
051    import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
052    import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
053    import org.apache.directory.server.core.interceptor.context.OperationContext;
054    import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
055    import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
056    import org.apache.directory.server.i18n.I18n;
057    import org.apache.directory.shared.ldap.constants.AuthenticationLevel;
058    import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
059    import org.apache.directory.shared.ldap.exception.LdapNoPermissionException;
060    import org.apache.directory.shared.ldap.exception.LdapUnwillingToPerformException;
061    import org.apache.directory.shared.ldap.message.ResultCodeEnum;
062    import org.apache.directory.shared.ldap.name.DN;
063    import org.apache.directory.shared.ldap.util.StringTools;
064    import org.slf4j.Logger;
065    import org.slf4j.LoggerFactory;
066    
067    
068    /**
069     * An {@link Interceptor} that authenticates users.
070     *
071     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
072     * @version $Rev: 923747 $, $Date: 2010-03-16 15:09:38 +0100 (Tue, 16 Mar 2010) $
073     * @org.apache.xbean.XBean
074     */
075    public class AuthenticationInterceptor extends BaseInterceptor
076    {
077        private static final Logger LOG = LoggerFactory.getLogger( AuthenticationInterceptor.class );
078    
079        /**
080         * Speedup for logs
081         */
082        private static final boolean IS_DEBUG = LOG.isDebugEnabled();
083    
084        private Set<Authenticator> authenticators;
085        private final Map<String, Collection<Authenticator>> authenticatorsMapByType = 
086            new HashMap<String, Collection<Authenticator>>();
087    
088        private DirectoryService directoryService;
089        
090        
091        /**
092         * Creates an authentication service interceptor.
093         */
094        public AuthenticationInterceptor()
095        {
096        }
097    
098        
099        /**
100         * Registers and initializes all {@link Authenticator}s to this service.
101         */
102        public void init( DirectoryService directoryService ) throws Exception
103        {
104            this.directoryService = directoryService;
105            
106            if ( authenticators == null )
107            {
108                setDefaultAuthenticators();
109            }
110            // Register all authenticators
111            for ( Authenticator authenticator : authenticators )
112            {
113                register( authenticator, directoryService );
114            }
115        }
116    
117        
118        private void setDefaultAuthenticators()
119        {
120            Set<Authenticator> set = new HashSet<Authenticator>();
121            set.add( new AnonymousAuthenticator() );
122            set.add( new SimpleAuthenticator() );
123            set.add( new StrongAuthenticator() );
124    
125            setAuthenticators( set );
126        }
127    
128    
129        public Set<Authenticator> getAuthenticators()
130        {
131            return authenticators;
132        }
133    
134        
135        /**
136         * @param authenticators authenticators to be used by this AuthenticationInterceptor
137         * @org.apache.xbean.Property nestedType="org.apache.directory.server.core.authn.Authenticator"
138         */
139        public void setAuthenticators( Set<Authenticator> authenticators )
140        {
141            this.authenticators = authenticators;
142        }
143    
144        
145        /**
146         * Deinitializes and deregisters all {@link Authenticator}s from this service.
147         */
148        public void destroy()
149        {
150            authenticatorsMapByType.clear();
151            Set<Authenticator> copy = new HashSet<Authenticator>( authenticators );
152            authenticators = null;
153            for ( Authenticator authenticator : copy )
154            {
155                authenticator.destroy();
156            }
157        }
158    
159        
160        /**
161         * Initializes the specified {@link Authenticator} and registers it to
162         * this service.
163         *
164         * @param authenticator Authenticator to initialize and register by type
165         * @param directoryService configuration info to supply to the Authenticator during initialization
166         * @throws javax.naming.Exception if initialization fails.
167         */
168        private void register( Authenticator authenticator, DirectoryService directoryService ) throws Exception
169        {
170            authenticator.init( directoryService );
171    
172            Collection<Authenticator> authenticatorList = getAuthenticators( authenticator.getAuthenticatorType() );
173    
174            if ( authenticatorList == null )
175            {
176                authenticatorList = new ArrayList<Authenticator>();
177                authenticatorsMapByType.put( authenticator.getAuthenticatorType(), authenticatorList );
178            }
179    
180            authenticatorList.add( authenticator );
181        }
182    
183    
184        /**
185         * Returns the list of {@link Authenticator}s with the specified type.
186         *
187         * @param type type of Authenticator sought
188         * @return A list of Authenticators of the requested type or <tt>null</tt> if no authenticator is found.
189         */
190        private Collection<Authenticator> getAuthenticators( String type )
191        {
192            Collection<Authenticator> result = authenticatorsMapByType.get( type );
193    
194            if ( ( result != null ) && ( result.size() > 0 ) )
195            {
196                return result;
197            } 
198            else
199            {
200                return null;
201            }
202        }
203    
204    
205        public void add( NextInterceptor next, AddOperationContext opContext ) throws Exception
206        {
207            if ( IS_DEBUG )
208            {
209                LOG.debug( "Operation Context: {}", opContext );
210            }
211    
212            checkAuthenticated( opContext );
213            next.add( opContext );
214        }
215    
216    
217        public void delete( NextInterceptor next, DeleteOperationContext opContext ) throws Exception
218        {
219            if ( IS_DEBUG )
220            {
221                LOG.debug( "Operation Context: {}", opContext );
222            }
223    
224            checkAuthenticated( opContext );
225            next.delete( opContext );
226            invalidateAuthenticatorCaches( opContext.getDn() );
227        }
228    
229    
230        public DN getMatchedName( NextInterceptor next, GetMatchedNameOperationContext opContext ) throws Exception
231        {
232            if ( IS_DEBUG )
233            {
234                LOG.debug( "Operation Context: {}", opContext );
235            }
236    
237            checkAuthenticated( opContext );
238            return next.getMatchedName( opContext );
239        }
240    
241    
242        public ClonedServerEntry getRootDSE( NextInterceptor next, GetRootDSEOperationContext opContext ) throws Exception
243        {
244            if ( IS_DEBUG )
245            {
246                LOG.debug( "Operation Context: {}", opContext );
247            }
248    
249            checkAuthenticated( opContext );
250            return next.getRootDSE( opContext );
251        }
252    
253    
254        public DN getSuffix( NextInterceptor next, GetSuffixOperationContext opContext ) throws Exception
255        {
256            if ( IS_DEBUG )
257            {
258                LOG.debug( "Operation Context: {}", opContext );
259            }
260    
261            checkAuthenticated( opContext );
262            return next.getSuffix( opContext );
263        }
264    
265    
266        public boolean hasEntry( NextInterceptor next, EntryOperationContext opContext ) throws Exception
267        {
268            if ( IS_DEBUG )
269            {
270                LOG.debug( "Operation Context: {}", opContext );
271            }
272    
273            checkAuthenticated( opContext );
274            return next.hasEntry( opContext );
275        }
276    
277    
278        public EntryFilteringCursor list( NextInterceptor next, ListOperationContext opContext ) throws Exception
279        {
280            if ( IS_DEBUG )
281            {
282                LOG.debug( "Operation Context: {}", opContext );
283            }
284    
285            checkAuthenticated( opContext );
286            return next.list( opContext );
287        }
288    
289    
290        public Set<String> listSuffixes( NextInterceptor next, ListSuffixOperationContext opContext ) throws Exception
291        {
292            if ( IS_DEBUG )
293            {
294                LOG.debug( "Operation Context: {}", opContext );
295            }
296    
297            checkAuthenticated( opContext );
298            return next.listSuffixes( opContext );
299        }
300    
301    
302        public ClonedServerEntry lookup( NextInterceptor next, LookupOperationContext opContext ) throws Exception
303        {
304            if ( IS_DEBUG )
305            {
306                LOG.debug( "Operation Context: {}", opContext );
307            }
308    
309            checkAuthenticated( opContext );
310            return next.lookup( opContext );
311        }
312    
313        
314        private void invalidateAuthenticatorCaches( DN principalDn )
315        {
316            for ( String authMech : authenticatorsMapByType.keySet() )
317            {
318                Collection<Authenticator> authenticators = getAuthenticators( authMech );
319    
320                // try each authenticator
321                for ( Authenticator authenticator : authenticators )
322                {
323                    authenticator.invalidateCache( principalDn );
324                }
325            }
326        }
327    
328    
329        public void modify( NextInterceptor next, ModifyOperationContext opContext ) throws Exception
330        {
331            if ( IS_DEBUG )
332            {
333                LOG.debug( "Operation Context: {}", opContext );
334            }
335    
336            checkAuthenticated( opContext );
337            next.modify( opContext );
338            invalidateAuthenticatorCaches( opContext.getDn() );
339        }
340    
341    
342        public void rename( NextInterceptor next, RenameOperationContext opContext ) throws Exception
343        {
344            if ( IS_DEBUG )
345            {
346                LOG.debug( "Operation Context: {}", opContext );
347            }
348    
349            checkAuthenticated( opContext );
350            next.rename( opContext );
351            invalidateAuthenticatorCaches( opContext.getDn() );
352        }
353    
354    
355        public boolean compare( NextInterceptor next, CompareOperationContext opContext ) throws Exception
356        {
357            if ( IS_DEBUG )
358            {
359                LOG.debug( "Operation Context: {}", opContext );
360            }
361    
362            checkAuthenticated( opContext );
363            boolean result = next.compare( opContext );
364            invalidateAuthenticatorCaches( opContext.getDn() );
365            return result;
366        }
367    
368    
369        public void moveAndRename( NextInterceptor next, MoveAndRenameOperationContext opContext )
370                throws Exception
371        {
372            if ( IS_DEBUG )
373            {
374                LOG.debug( "Operation Context: {}", opContext );
375            }
376    
377            checkAuthenticated( opContext );
378            next.moveAndRename( opContext );
379            invalidateAuthenticatorCaches( opContext.getDn() );
380        }
381    
382    
383        public void move( NextInterceptor next, MoveOperationContext opContext ) throws Exception
384        {
385            if ( IS_DEBUG )
386            {
387                LOG.debug( "Operation Context: {}", opContext );
388            }
389    
390            checkAuthenticated( opContext );
391            next.move( opContext );
392            invalidateAuthenticatorCaches( opContext.getDn() );
393        }
394    
395    
396        public EntryFilteringCursor search( NextInterceptor next, SearchOperationContext opContext ) throws Exception
397        {
398            if ( IS_DEBUG )
399            {
400                LOG.debug( "Operation Context: {}", opContext );
401            }
402    
403            checkAuthenticated( opContext );
404            return next.search( opContext );
405        }
406    
407    
408        /**
409         * Check if the current operation has a valid PrincipalDN or not.
410         *
411         * @param opContext the OperationContext for this operation
412         * @param operation the operation type
413         * @throws Exception
414         */
415        private void checkAuthenticated( OperationContext operation ) throws Exception
416        {
417            if ( operation.getSession().isAnonymous() && !directoryService.isAllowAnonymousAccess() 
418                && !operation.getDn().isEmpty() )
419            {
420                LOG.error( I18n.err( I18n.ERR_5, operation.getName() ) );
421                throw new LdapNoPermissionException( I18n.err( I18n.ERR_5, operation.getName() ) );
422            }
423        }
424    
425    
426        public void bind( NextInterceptor next, BindOperationContext opContext ) throws Exception
427        {
428            if ( IS_DEBUG )
429            {
430                LOG.debug( "Operation Context: {}", opContext );
431            }
432    
433            if ( ( opContext.getSession() != null ) && ( opContext.getSession().getEffectivePrincipal() != null ) )
434            {
435                // null out the credentials
436                opContext.setCredentials( null );
437            }
438            
439            // pick the first matching authenticator type
440            AuthenticationLevel level = opContext.getAuthenticationLevel();
441            
442            if ( level == AuthenticationLevel.UNAUTHENT )
443            {
444                // This is a case where the Bind request contains a DN, but no password.
445                // We don't check the DN, we just return a UnwillingToPerform error
446                // Cf RFC 4513, chap. 5.1.2
447                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, "Cannot Bind for DN " + opContext.getDn().getName() );
448            }
449    
450            Collection<Authenticator> authenticators = getAuthenticators( level.getName() );
451    
452            if ( authenticators == null )
453            {
454                LOG.debug( "No authenticators found, delegating bind to the nexus." );
455    
456                // as a last resort try binding via the nexus
457                next.bind( opContext );
458    
459                LOG.debug( "Nexus succeeded on bind operation." );
460    
461                // bind succeeded if we got this far
462                // TODO - authentication level not being set
463                LdapPrincipal principal = new LdapPrincipal( opContext.getDn(), AuthenticationLevel.SIMPLE );
464                CoreSession session = new DefaultCoreSession( principal, directoryService );
465                opContext.setSession( session );
466    
467                // remove creds so there is no security risk
468                opContext.setCredentials( null );
469                return;
470            }
471    
472            // TODO : we should refactor that.
473            // try each authenticator
474            for ( Authenticator authenticator : authenticators )
475            {
476                try
477                {
478                    // perform the authentication
479                    LdapPrincipal principal = authenticator.authenticate( opContext );
480                    
481                    LdapPrincipal clonedPrincipal = (LdapPrincipal)(principal.clone());
482    
483                    // remove creds so there is no security risk
484                    opContext.setCredentials( null );
485                    clonedPrincipal.setUserPassword( StringTools.EMPTY_BYTES );
486    
487                    // authentication was successful
488                    CoreSession session = new DefaultCoreSession( clonedPrincipal, directoryService );
489                    opContext.setSession( session );
490    
491                    return;
492                }
493                catch ( LdapAuthenticationException e )
494                {
495                    // authentication failed, try the next authenticator
496                    if ( LOG.isInfoEnabled() )
497                    {
498                        LOG.info( "Authenticator {} failed to authenticate: {}", authenticator, opContext );
499                    }
500                }
501                catch ( Exception e )
502                {
503                    // Log other exceptions than LdapAuthenticationException
504                    if ( LOG.isWarnEnabled() )
505                    {
506                        LOG.info( "Unexpected failure for Authenticator {} : {}", authenticator, opContext );
507                    }
508                }
509            }
510    
511            if ( LOG.isInfoEnabled() )
512            {
513                LOG.info( "Cannot bind to the server " );
514            }
515    
516            DN dn = opContext.getDn();
517            String upDn = ( dn == null ? "" : dn.getName() );
518            throw new LdapAuthenticationException( I18n.err( I18n.ERR_229, upDn ) );
519        }
520    }