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 */
020package org.apache.directory.server.ldap.handlers.extended;
021
022
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.Set;
026
027import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyRequest;
028import org.apache.directory.api.ldap.extras.extended.pwdModify.PasswordModifyRequest;
029import org.apache.directory.api.ldap.extras.extended.pwdModify.PasswordModifyResponse;
030import org.apache.directory.api.ldap.extras.extended.pwdModify.PasswordModifyResponseImpl;
031import org.apache.directory.api.ldap.model.constants.SchemaConstants;
032import org.apache.directory.api.ldap.model.entry.Attribute;
033import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
034import org.apache.directory.api.ldap.model.entry.DefaultModification;
035import org.apache.directory.api.ldap.model.entry.Entry;
036import org.apache.directory.api.ldap.model.entry.Modification;
037import org.apache.directory.api.ldap.model.entry.ModificationOperation;
038import org.apache.directory.api.ldap.model.entry.Value;
039import org.apache.directory.api.ldap.model.exception.LdapException;
040import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
041import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
042import org.apache.directory.api.ldap.model.exception.LdapOperationException;
043import org.apache.directory.api.ldap.model.message.Control;
044import org.apache.directory.api.ldap.model.message.ModifyRequest;
045import org.apache.directory.api.ldap.model.message.ModifyRequestImpl;
046import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
047import org.apache.directory.api.ldap.model.name.Dn;
048import org.apache.directory.api.ldap.model.password.PasswordUtil;
049import org.apache.directory.api.util.Strings;
050import org.apache.directory.server.core.api.CoreSession;
051import org.apache.directory.server.core.api.DirectoryService;
052import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
053import org.apache.directory.server.core.shared.DefaultCoreSession;
054import org.apache.directory.server.ldap.ExtendedOperationHandler;
055import org.apache.directory.server.ldap.LdapServer;
056import org.apache.directory.server.ldap.LdapSession;
057import org.apache.mina.core.session.IoSession;
058import org.slf4j.Logger;
059import org.slf4j.LoggerFactory;
060
061
062/**
063 * An handler to manage PwdModifyRequest. Users can send a pwdModify request
064 * for their own passwords, or for another people password. Only admin can
065 * change someone else password without having to provide the original password.
066 *
067 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
068 */
069public class PwdModifyHandler implements ExtendedOperationHandler<PasswordModifyRequest, PasswordModifyResponse>
070{
071    private static final Logger LOG = LoggerFactory.getLogger( PwdModifyHandler.class );
072    public static final Set<String> EXTENSION_OIDS;
073
074    static
075    {
076        Set<String> set = new HashSet<>( 2 );
077        set.add( PasswordModifyRequest.EXTENSION_OID );
078        set.add( PasswordModifyResponse.EXTENSION_OID );
079        EXTENSION_OIDS = Collections.unmodifiableSet( set );
080    }
081
082
083    /**
084     * {@inheritDoc}
085     */
086    public String getOid()
087    {
088        return PasswordModifyRequest.EXTENSION_OID;
089    }
090
091
092    /**
093     * Modify the user's credentials.
094     * 
095     * We will need to modify the userPassword attribute, accordingly to a few rules:
096     * - if the old password is present, we should verify it's valid. if not, we return an error
097     * - if the old password is absent, we are modifying the password of the current used.
098     * - if the new password is absent, we will return an error. The RFC says that we could
099     * generate a random password, but that would be dangerous to do so.
100     * - if the new password already exists, we simply return not changing anything 
101     * - otherwise, we just remove the old password from the list of passwords (we may have 
102     * more than one) and add the new password. This is done with a REPLACE operation (Modify)
103     */
104    private void modifyUserPassword( CoreSession userSession, Entry userEntry, Dn userDn, 
105        byte[] oldPassword, byte[] newPassword, PasswordModifyRequest req )
106    {
107        IoSession ioSession = ( ( DefaultCoreSession ) userSession ).getIoSession();
108
109        if ( newPassword == null )
110        {
111            // We don't support password generation on ApacheDS
112            writeResult( ioSession, req, ResultCodeEnum.UNWILLING_TO_PERFORM, 
113                "Cannot change a password for user " + userDn + ", exception : null new password" );
114
115            return;
116        }
117        
118        // Get the user password attribute
119        Attribute userPassword = userEntry.get( SchemaConstants.USER_PASSWORD_AT );
120        
121        if ( userPassword == null )
122        {
123            // We can't modify the password
124            writeResult( ioSession, req, ResultCodeEnum.UNWILLING_TO_PERFORM, 
125                "Cannot change a password for user " + userDn + ", the user has no existing password" );
126
127            return;
128        }
129        
130        if ( userPassword.contains( newPassword ) )
131        {
132           // Ok, we are done : just return success
133            PasswordModifyResponseImpl pmrl = new PasswordModifyResponseImpl(
134                req.getMessageId(), ResultCodeEnum.SUCCESS );
135
136            Control ppolicyControl = req.getControl( PasswordPolicyRequest.OID );
137
138            if ( ppolicyControl != null )
139            {
140                pmrl.addControl( ppolicyControl );
141            }
142
143            ioSession.write( pmrl );
144
145            return;
146        }
147        
148        if ( oldPassword == null )
149        {
150            // We are modifying the password on behalf of another user. ACI will
151            // protect such modification if it's not allowed. In any case, we just 
152            // modify the existing userPassword attribute, adding the password
153            ModifyRequest modifyRequest = new ModifyRequestImpl();
154            modifyRequest.setName( userDn );
155
156            Control ppolicyControl = req.getControl( PasswordPolicyRequest.OID );
157            
158            if ( ppolicyControl != null )
159            {
160                modifyRequest.addControl( ppolicyControl );
161            }
162            
163            try
164            {
165                Modification modification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
166                    userPassword.getAttributeType(), newPassword );
167    
168                modifyRequest.addModification( modification );
169                ResultCodeEnum errorCode = null;
170                String errorMessage = null;
171
172                try
173                {
174                    userSession.modify( modifyRequest );
175
176                    if ( LOG.isDebugEnabled() )
177                    {
178                        LOG.debug( "Password modified for user {}", userDn );
179                    }
180
181                    // Ok, all done
182                    PasswordModifyResponseImpl pmrl = new PasswordModifyResponseImpl(
183                        req.getMessageId(), ResultCodeEnum.SUCCESS );
184
185                    ppolicyControl = modifyRequest.getResultResponse().getControl( PasswordPolicyRequest.OID );
186
187                    if ( ppolicyControl != null )
188                    {
189                        pmrl.addControl( ppolicyControl );
190                    }
191
192                    ioSession.write( pmrl );
193
194                    return;
195                }
196                catch ( LdapOperationException loe )
197                {
198                    errorCode = loe.getResultCode();
199                    errorMessage = loe.getMessage();
200                }
201                catch ( LdapException le )
202                {
203                    // this exception means something else must be wrong
204                    errorCode = ResultCodeEnum.OTHER;
205                    errorMessage = le.getMessage();
206                }
207
208                // We can't modify the password
209                LOG.error( "Cannot modify the password for user {}, exception : {}", userDn, errorMessage );
210                PasswordModifyResponseImpl errorPmrl = new PasswordModifyResponseImpl(
211                    req.getMessageId(), errorCode, "Cannot modify the password for user "
212                        + userDn + ", exception : " + errorMessage );
213
214                ppolicyControl = modifyRequest.getResultResponse().getControl( PasswordPolicyRequest.OID );
215
216                if ( ppolicyControl != null )
217                {
218                    errorPmrl.addControl( ppolicyControl );
219                }
220
221                ioSession.write( errorPmrl );
222                
223                return;
224            }
225            catch ( LdapInvalidAttributeValueException liave )
226            {
227                // Nothing to do, this will never be a problem
228            }
229        }
230        else
231        {
232            // We are changing the password of the current user, check the password
233            boolean valid = false;
234            Attribute modifiedPassword = new DefaultAttribute( userPassword.getAttributeType() );
235            
236            for ( Value value : userPassword )
237            {
238                if ( !valid )
239                {
240                    valid = PasswordUtil.compareCredentials( oldPassword, value.getBytes() );
241                }
242                
243                try
244                {
245                    if ( valid )
246                    {
247                        modifiedPassword.add( newPassword );
248                    }
249                    else
250                    { 
251                        modifiedPassword.add( value );
252                    }
253                }
254                catch ( LdapInvalidAttributeValueException e )
255                {
256                    // Nothing to do, this will never be a problem
257                }
258            }
259            
260            // At this point, we have what is needed to modify the password, if the oldPassword
261            // was valid
262            if ( valid )
263            {
264                ModifyRequest modifyRequest = new ModifyRequestImpl();
265                modifyRequest.setName( userDn );
266
267                Control ppolicyControl = req.getControl( PasswordPolicyRequest.OID );
268                
269                if ( ppolicyControl != null )
270                {
271                    modifyRequest.addControl( ppolicyControl );
272                }
273                
274                Modification modification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
275                    modifiedPassword );
276
277                modifyRequest.addModification( modification );
278
279                ResultCodeEnum errorCode = null;
280                String errorMessage = null;
281
282                try
283                {
284                    userSession.modify( modifyRequest );
285
286                    if ( LOG.isDebugEnabled() )
287                    {
288                        LOG.debug( "Password modified for user {}", userDn );
289                    }
290
291                    // Ok, all done
292                    PasswordModifyResponseImpl pmrl = new PasswordModifyResponseImpl(
293                        req.getMessageId(), ResultCodeEnum.SUCCESS );
294
295                    ppolicyControl = modifyRequest.getResultResponse().getControl( PasswordPolicyRequest.OID );
296
297                    if ( ppolicyControl != null )
298                    {
299                        pmrl.addControl( ppolicyControl );
300                    }
301
302                    ioSession.write( pmrl );
303
304                    return;
305                }
306                catch ( LdapOperationException loe )
307                {
308                    errorCode = loe.getResultCode();
309                    errorMessage = loe.getMessage();
310                }
311                catch ( LdapException le )
312                {
313                    // this exception means something else must be wrong
314                    errorCode = ResultCodeEnum.OTHER;
315                    errorMessage = le.getMessage();
316                }
317
318                // We can't modify the password
319                LOG.error( "Cannot modify the password for user {}, exception : {}", userDn, errorMessage );
320                PasswordModifyResponseImpl errorPmrl = new PasswordModifyResponseImpl(
321                    req.getMessageId(), errorCode, "Cannot modify the password for user "
322                        + userDn + ", exception : " + errorMessage );
323
324                ppolicyControl = modifyRequest.getResultResponse().getControl( PasswordPolicyRequest.OID );
325
326                if ( ppolicyControl != null )
327                {
328                    errorPmrl.addControl( ppolicyControl );
329                }
330
331                ioSession.write( errorPmrl );
332                
333                return;
334            }
335            else
336            {
337                // Too bad, the old password is invalid
338                writeResult( ioSession, req, ResultCodeEnum.INVALID_CREDENTIALS, 
339                    "Cannot change a password for user " + userDn + ", invalid credentials" );
340
341                return;
342            }
343        }
344    }
345
346    
347    private void writeResult( LdapSession requestor, PasswordModifyRequest req, ResultCodeEnum error, String errorMessage )
348    {
349        writeResult( requestor.getIoSession(), req, error, errorMessage );
350
351    }
352
353    
354    private void writeResult( IoSession ioSession, PasswordModifyRequest req, ResultCodeEnum error, String errorMessage )
355    {
356        LOG.error( errorMessage );
357        ioSession.write( new PasswordModifyResponseImpl(
358            req.getMessageId(), error, errorMessage ) );
359
360    }
361    
362    
363    private Entry getModifiedEntry( LdapSession requestor, PasswordModifyRequest req, Dn entryDn )
364    {
365        try
366        {
367            Entry modifiedEntry = requestor.getCoreSession().lookup( entryDn, SchemaConstants.ALL_ATTRIBUTES_ARRAY );
368            
369            if ( modifiedEntry == null )
370            {
371                // The entry does not exist, we can't modify its password
372                writeResult( requestor, req, ResultCodeEnum.NO_SUCH_OBJECT, 
373                    "The entry does not exist, we can't modify its password" );
374                return null;
375            }
376            else
377            {
378                return modifiedEntry;
379            }
380        }
381        catch ( Exception le )
382        {
383            // The entry does not exist, we can't modify its password
384            writeResult( requestor, req, ResultCodeEnum.NO_SUCH_OBJECT, 
385                "The entry does not exist, we can't modify its password" );
386            return null;
387        }
388    }
389    
390    
391    private void processAuthenticatedPasswordModify( LdapSession requestor, PasswordModifyRequest req,
392        Dn userDn )
393    {
394        byte[] oldPassword = req.getOldPassword();
395        byte[] newPassword = req.getNewPassword();
396
397        // We are already bound. Fetch the entry which we want to modify
398        Entry modifiedEntry = null;
399        
400        Dn principalDn = requestor.getCoreSession().getEffectivePrincipal().getDn();
401
402        LOG.debug( "User {} trying to modify password of user {}", principalDn, userDn );
403        
404        
405        // First, check that the userDn is null : we can't change the password of someone else
406        // except if we are admin
407        if ( ( userDn != null ) && ( !userDn.equals( principalDn ) ) )
408        {
409            // Are we admin ?
410            if ( requestor.getCoreSession().isAdministrator() )
411            {
412                modifiedEntry = getModifiedEntry( requestor, req, userDn );
413                
414                if ( modifiedEntry == null )
415                {
416                    return;
417                }
418                
419                // We are administrator, we can try to modify the user's credentials
420                modifyUserPassword( requestor.getCoreSession(), modifiedEntry, userDn, oldPassword, newPassword, req );
421            }
422            else
423            {
424                // No : error
425                writeResult( requestor, req, ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, 
426                    "Non-admin user cannot access another user's password to modify it" );
427            }
428        }
429        else
430        {
431            // We are trying to modify our own password
432            modifiedEntry = getModifiedEntry( requestor, req, principalDn );
433
434            if ( modifiedEntry == null )
435            {
436                return;
437            }
438
439            modifyUserPassword( requestor.getCoreSession(), modifiedEntry, principalDn, oldPassword, newPassword, req );
440        }
441    }
442    
443
444    /**
445     * {@inheritDoc}
446     */
447    public void handleExtendedOperation( LdapSession requestor, PasswordModifyRequest req ) throws Exception
448    {
449        LOG.debug( "Password modification requested" );
450
451        // Grab the adminSession, we might need it later
452        DirectoryService service = requestor.getLdapServer().getDirectoryService();
453        CoreSession adminSession = service.getAdminSession();
454        String userIdentity = Strings.utf8ToString( req.getUserIdentity() );
455        Dn userDn = null;
456
457        if ( !Strings.isEmpty( userIdentity ) )
458        {
459            try
460            {
461                userDn = service.getDnFactory().create( userIdentity );
462            }
463            catch ( LdapInvalidDnException lide )
464            {
465                // The userIdentity is not a DN : return with an error code.
466                writeResult( requestor, req, ResultCodeEnum.INVALID_DN_SYNTAX, 
467                    "The user DN is invalid : " + userDn );
468
469                return;
470            }
471        }
472
473        byte[] oldPassword = req.getOldPassword();
474        byte[] newPassword = req.getNewPassword();
475
476        // First check if the user is bound or not
477        if ( requestor.isAuthenticated() )
478        {
479            processAuthenticatedPasswordModify( requestor, req, userDn );
480        }
481        else
482        {
483            // The user is not authenticated : we have to use the provided userIdentity
484            // and the oldPassword to check if the user is present
485            BindOperationContext bindContext = new BindOperationContext( adminSession );
486            bindContext.setDn( userDn );
487            bindContext.setCredentials( oldPassword );
488
489            try
490            {
491                service.getOperationManager().bind( bindContext );
492            }
493            catch ( LdapException le )
494            {
495                // We can't bind with the provided information : we thus can't
496                // change the password...
497                requestor.getIoSession().write( new PasswordModifyResponseImpl(
498                    req.getMessageId(), ResultCodeEnum.INVALID_CREDENTIALS ) );
499
500                return;
501            }
502
503            // Ok, we were able to bind using the userIdentity and the password. Let's
504            // modify the password now
505            modifyUserPassword( requestor.getCoreSession(), bindContext.getEntry(), userDn, oldPassword, newPassword, req );
506        }
507    }
508
509
510    /**
511     * {@inheritDoc}
512     */
513    public Set<String> getExtensionOids()
514    {
515        return EXTENSION_OIDS;
516    }
517
518
519    /**
520     * {@inheritDoc}
521     */
522    public void setLdapServer( LdapServer ldapServer )
523    {
524    }
525}