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}