001package org.apache.archiva.redback.authentication;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.archiva.redback.configuration.UserConfiguration;
023import org.apache.archiva.redback.policy.AccountLockedException;
024import org.apache.archiva.redback.policy.MustChangePasswordException;
025import org.apache.archiva.redback.users.User;
026import org.apache.archiva.redback.users.UserManager;
027import org.apache.archiva.redback.users.UserManagerException;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030import org.springframework.context.ApplicationContext;
031import org.springframework.stereotype.Service;
032
033import javax.annotation.PostConstruct;
034import javax.inject.Inject;
035import javax.inject.Named;
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.Collections;
039import java.util.HashMap;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Spliterator;
044import java.util.Spliterators;
045import java.util.concurrent.atomic.AtomicReference;
046import java.util.stream.Collectors;
047import java.util.stream.Stream;
048import java.util.stream.StreamSupport;
049
050
051/**
052 * DefaultAuthenticationManager: the goal of the authentication manager is to act as a conduit for
053 * authentication requests into different authentication schemes
054 * <p>
055 * For example, the default implementation can be configured with any number of authenticators and will
056 * sequentially try them for an authenticated result.  This allows you to have the standard user/pass
057 * auth procedure followed by authentication based on a known key for 'remember me' type functionality.
058 *
059 * @author: Jesse McConnell
060 */
061@Service( "authenticationManager" )
062public class DefaultAuthenticationManager
063    implements AuthenticationManager
064{
065
066    private static final Logger log = LoggerFactory.getLogger( DefaultAuthenticationManager.class );
067
068    final private AtomicReference<List<Authenticator>> authenticators = new AtomicReference<>( );
069    final private AtomicReference<Map<String, AuthenticatorControl>> controls = new AtomicReference<>( );
070    final private AtomicReference<Map<String, AuthenticatorControl>> modfiedControls = new AtomicReference<>( );
071    private Map<String, Authenticator> availableAuthenticators;
072
073    @Inject
074    private ApplicationContext applicationContext;
075
076    @Inject
077    @Named("userConfiguration#default")
078    private UserConfiguration userConfiguration;
079
080    @Inject
081    @Named( "userManager#default" )
082    private UserManager userManager;
083
084    @SuppressWarnings( "unchecked" )
085    @PostConstruct
086    public void initialize( )
087    {
088        this.availableAuthenticators =
089            applicationContext.getBeansOfType( Authenticator.class ).values( ).stream( ).collect( Collectors.toMap( a -> a.getId( ), authenticator -> authenticator ) );
090        this.modfiedControls.set( new HashMap<>( ) );
091        initializeOrder(  );
092    }
093
094    private void initializeOrder() {
095        Stream<AuthenticatorControl> controlStream = initControls( );
096        final List<Authenticator> authenticators = new ArrayList<>( );
097        final Map<String, AuthenticatorControl> controls = new LinkedHashMap<>( );
098        controlStream.forEachOrdered( control -> {
099            authenticators.add( this.availableAuthenticators.get( control.getName( ) ) );
100            controls.put( control.getName( ), control );
101        } );
102        this.authenticators.set( authenticators );
103        this.controls.set( controls );
104    }
105
106    Map<String, AuthenticatorControl> getConfigControls( )
107    {
108        return new HashMap<>( );
109    }
110
111    Map<String, Authenticator> getAvailableAuthenticators() {
112        return this.availableAuthenticators;
113    }
114
115    Map<String, AuthenticatorControl> getModifiedControls() {
116        return this.modfiedControls.get();
117    }
118
119    void setModfiedControls(Map<String, AuthenticatorControl> newControls) {
120        this.modfiedControls.set( newControls );
121    }
122
123    Map<String, AuthenticatorControl> getControlMap() {
124        return controls.get();
125    }
126
127
128    private Stream<AuthenticatorControl> initControls( )
129    {
130        Collection<Authenticator> authenticators = getAvailableAuthenticators().values();
131        Map<String, AuthenticatorControl> nondefault = getModifiedControls( );
132        Map<String, AuthenticatorControl> configControlMap = getConfigControls( );
133        Spliterator<Authenticator> spliterator = Spliterators.spliterator( authenticators, Spliterator.NONNULL );
134        return StreamSupport.stream( spliterator, false ).map( authenticator -> {
135            final String id = authenticator.getId( );
136            return nondefault.containsKey( id ) ? nondefault.get(id) : (configControlMap.containsKey( id ) ? configControlMap.get( id ) : new AuthenticatorControl( id, 100, AuthenticationControl.SUFFICIENT, true ));
137        } ).sorted( Collections.reverseOrder( ) );
138    }
139
140
141    public String getId( )
142    {
143        return "Default Authentication Manager - " + this.getClass( ).getName( ) + " : managed authenticators - " +
144            knownAuthenticators( );
145    }
146
147    public AuthenticationResult authenticate( AuthenticationDataSource source )
148        throws AccountLockedException, AuthenticationException, MustChangePasswordException
149    {
150        List<Authenticator> authenticators = this.authenticators.get( );
151        if ( authenticators == null || authenticators.size( ) == 0 )
152        {
153            return ( new AuthenticationResult( false, null, new AuthenticationException(
154                "no valid authenticators, can't authenticate" ) ) );
155        }
156
157        // put AuthenticationResult exceptions in a map
158        List<AuthenticationFailureCause> authnResultErrors = new ArrayList<AuthenticationFailureCause>( );
159        for ( Authenticator authenticator : authenticators )
160        {
161            final AuthenticatorControl control = getControlMap( ).get( authenticator.getId( ) );
162            assert control != null;
163            if ( authenticator.isValid( ) && control.isActive())
164            {
165                if ( authenticator.supportsDataSource( source ) )
166                {
167                    AuthenticationResult authResult = authenticator.authenticate( source );
168                    List<AuthenticationFailureCause> authenticationFailureCauses =
169                        authResult.getAuthenticationFailureCauses( );
170
171                    if ( authResult.isAuthenticated( ) )
172                    {
173                        //olamy: as we can chain various user managers with Archiva
174                        // user manager authenticator can lock accounts in the following case :
175                        // 2 user managers: ldap and jdo.
176                        // ldap correctly find the user but cannot compare hashed password
177                        // jdo reject password so increase loginAttemptCount
178                        // now ldap bind authenticator work but loginAttemptCount has been increased.
179                        // so we restore here loginAttemptCount to 0 if in authenticationFailureCauses
180
181                        for ( AuthenticationFailureCause authenticationFailureCause : authenticationFailureCauses )
182                        {
183                            User user = authenticationFailureCause.getUser( );
184                            if ( user != null )
185                            {
186                                if ( user.getCountFailedLoginAttempts( ) > 0 )
187                                {
188                                    user.setCountFailedLoginAttempts( 0 );
189                                    if ( !userManager.isReadOnly( ) )
190                                    {
191                                        try
192                                        {
193                                            userManager.updateUser( user );
194                                        }
195                                        catch ( UserManagerException e )
196                                        {
197                                            log.debug( e.getMessage( ), e );
198                                            log.warn( "skip error updating user: {}", e.getMessage( ) );
199                                        }
200                                    }
201                                }
202                            }
203                        }
204                        return authResult;
205                    }
206
207                    if ( authenticationFailureCauses != null )
208                    {
209                        authnResultErrors.addAll( authenticationFailureCauses );
210                    }
211                    else
212                    {
213                        if ( authResult.getException( ) != null )
214                        {
215                            authnResultErrors.add(
216                                new AuthenticationFailureCause( AuthenticationConstants.AUTHN_RUNTIME_EXCEPTION,
217                                    authResult.getException( ).getMessage( ) ) );
218                        }
219                    }
220
221
222                }
223            }
224            else
225            {
226                log.warn( "Invalid authenticator found: " + authenticator.getId( ) );
227            }
228        }
229
230        return ( new AuthenticationResult( false, null, new AuthenticationException(
231            "authentication failed on authenticators: " + knownAuthenticators( ) ), authnResultErrors ) );
232    }
233
234    @Override
235    public List<AuthenticatorControl> getControls( )
236    {
237        return getControlMap().values().stream().collect( Collectors.toList());
238    }
239
240    @Override
241    public void setControls( List<AuthenticatorControl> controlList )
242    {
243        setModfiedControls( controlList.stream( ).collect( Collectors.toMap( c -> c.getName( ), c -> c ) ) );
244        initializeOrder(  );
245    }
246
247    @Override
248    public void modifyControl( AuthenticatorControl control )
249    {
250        Map<String, AuthenticatorControl> myControls = getModifiedControls( );
251        if (availableAuthenticators.containsKey( control.getName() )) {
252            myControls.put( control.getName( ), control );
253        } else {
254            log.warn( "Cannot modify control for authenticator {}. It does not exist.", control.getName( ) );
255        }
256        initializeOrder();
257    }
258
259    public List<Authenticator> getAuthenticators( )
260    {
261        return authenticators.get();
262    }
263
264    private String knownAuthenticators( )
265    {
266        StringBuilder strbuf = new StringBuilder( );
267
268        for ( Authenticator authenticator : getAuthenticators() )
269        {
270            strbuf.append( '(' ).append( authenticator.getId( ) ).append( ") " );
271        }
272
273        return strbuf.toString( );
274    }
275}