001/* 002 * Copyright 2011-2025 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2011-2025 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * 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, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2011-2025 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.listener; 037 038 039 040import java.security.SecureRandom; 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.List; 045 046import com.unboundid.asn1.ASN1OctetString; 047import com.unboundid.ldap.sdk.Control; 048import com.unboundid.ldap.sdk.DN; 049import com.unboundid.ldap.sdk.Entry; 050import com.unboundid.ldap.sdk.ExtendedRequest; 051import com.unboundid.ldap.sdk.ExtendedResult; 052import com.unboundid.ldap.sdk.LDAPException; 053import com.unboundid.ldap.sdk.Modification; 054import com.unboundid.ldap.sdk.ModificationType; 055import com.unboundid.ldap.sdk.ResultCode; 056import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedRequest; 057import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedResult; 058import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl; 059import com.unboundid.util.Debug; 060import com.unboundid.util.NotMutable; 061import com.unboundid.util.NotNull; 062import com.unboundid.util.StaticUtils ; 063import com.unboundid.util.ThreadLocalSecureRandom; 064import com.unboundid.util.ThreadSafety; 065import com.unboundid.util.ThreadSafetyLevel; 066 067import static com.unboundid.ldap.listener.ListenerMessages.*; 068 069 070 071/** 072 * This class provides an implementation of an extended operation handler for 073 * the in-memory directory server that can be used to process the password 074 * modify extended operation as defined in 075 * <A HREF="http://www.ietf.org/rfc/rfc3062.txt">RFC 3062</A>. 076 */ 077@NotMutable() 078@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 079public final class PasswordModifyExtendedOperationHandler 080 extends InMemoryExtendedOperationHandler 081{ 082 /** 083 * Creates a new instance of this extended operation handler. 084 */ 085 public PasswordModifyExtendedOperationHandler() 086 { 087 // No initialization is required. 088 } 089 090 091 092 /** 093 * {@inheritDoc} 094 */ 095 @Override() 096 @NotNull() 097 public String getExtendedOperationHandlerName() 098 { 099 return "Password Modify"; 100 } 101 102 103 104 /** 105 * {@inheritDoc} 106 */ 107 @Override() 108 @NotNull() 109 public List<String> getSupportedExtendedRequestOIDs() 110 { 111 return Collections.singletonList( 112 PasswordModifyExtendedRequest.PASSWORD_MODIFY_REQUEST_OID); 113 } 114 115 116 117 /** 118 * {@inheritDoc} 119 */ 120 @Override() 121 @NotNull() 122 public ExtendedResult processExtendedOperation( 123 @NotNull final InMemoryRequestHandler handler, 124 final int messageID, 125 @NotNull final ExtendedRequest request) 126 { 127 // This extended operation handler supports the no operation control. If 128 // any other control is present, then reject it if it's critical. 129 boolean noOperation = false; 130 for (final Control c : request.getControls()) 131 { 132 if (c.getOID().equalsIgnoreCase(NoOpRequestControl.NO_OP_REQUEST_OID)) 133 { 134 noOperation = true; 135 } 136 else if (c.isCritical()) 137 { 138 return new ExtendedResult(messageID, 139 ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, 140 ERR_PW_MOD_EXTOP_UNSUPPORTED_CONTROL.get(c.getOID()), 141 null, null, null, null, null); 142 } 143 } 144 145 146 // Decode the request. 147 final PasswordModifyExtendedRequest pwModRequest; 148 try 149 { 150 pwModRequest = new PasswordModifyExtendedRequest(request); 151 } 152 catch (final LDAPException le) 153 { 154 Debug.debugException(le); 155 return new ExtendedResult(messageID, le.getResultCode(), 156 le.getDiagnosticMessage(), le.getMatchedDN(), le.getReferralURLs(), 157 null, null, null); 158 } 159 160 161 // Get the elements of the request. 162 final String userIdentity = pwModRequest.getUserIdentity(); 163 final byte[] oldPWBytes = pwModRequest.getOldPasswordBytes(); 164 final byte[] newPWBytes = pwModRequest.getNewPasswordBytes(); 165 166 167 // Determine the DN of the target user. 168 final DN targetDN; 169 if (userIdentity == null) 170 { 171 targetDN = handler.getAuthenticatedDN(); 172 } 173 else 174 { 175 // The user identity should generally be a DN, but we'll also allow an 176 // authorization ID. 177 final String lowerUserIdentity = StaticUtils.toLowerCase(userIdentity); 178 if (lowerUserIdentity.startsWith("dn:") || 179 lowerUserIdentity.startsWith("u:")) 180 { 181 try 182 { 183 targetDN = handler.getDNForAuthzID(userIdentity); 184 } 185 catch (final LDAPException le) 186 { 187 Debug.debugException(le); 188 return new PasswordModifyExtendedResult(messageID, 189 le.getResultCode(), le.getMessage(), le.getMatchedDN(), 190 le.getReferralURLs(), null, le.getResponseControls()); 191 } 192 } 193 else 194 { 195 try 196 { 197 targetDN = new DN(userIdentity); 198 } 199 catch (final LDAPException le) 200 { 201 Debug.debugException(le); 202 return new PasswordModifyExtendedResult(messageID, 203 ResultCode.INVALID_DN_SYNTAX, 204 ERR_PW_MOD_EXTOP_CANNOT_PARSE_USER_IDENTITY.get(userIdentity), 205 null, null, null, null); 206 } 207 } 208 } 209 210 if ((targetDN == null) || targetDN.isNullDN()) 211 { 212 return new PasswordModifyExtendedResult(messageID, 213 ResultCode.UNWILLING_TO_PERFORM, ERR_PW_MOD_NO_IDENTITY.get(), 214 null, null, null, null); 215 } 216 217 final Entry userEntry = handler.getEntry(targetDN); 218 if (userEntry == null) 219 { 220 return new PasswordModifyExtendedResult(messageID, 221 ResultCode.UNWILLING_TO_PERFORM, 222 ERR_PW_MOD_EXTOP_CANNOT_GET_USER_ENTRY.get(targetDN.toString()), 223 null, null, null, null); 224 } 225 226 227 // Make sure that the server is configured with at least one password 228 // attribute. 229 final List<String> passwordAttributes = handler.getPasswordAttributes(); 230 if (passwordAttributes.isEmpty()) 231 { 232 return new PasswordModifyExtendedResult(messageID, 233 ResultCode.UNWILLING_TO_PERFORM, ERR_PW_MOD_EXTOP_NO_PW_ATTRS.get(), 234 null, null, null, null); 235 } 236 237 238 // If an old password was provided, then validate it. If not, then 239 // determine whether it is acceptable for no password to have been given. 240 if (oldPWBytes == null) 241 { 242 if (handler.getAuthenticatedDN().isNullDN()) 243 { 244 return new PasswordModifyExtendedResult(messageID, 245 ResultCode.UNWILLING_TO_PERFORM, 246 ERR_PW_MOD_EXTOP_NO_AUTHENTICATION.get(), null, null, null, null); 247 } 248 } 249 else 250 { 251 final List<InMemoryDirectoryServerPassword> passwordList = 252 handler.getPasswordsInEntry(userEntry, 253 pwModRequest.getRawOldPassword()); 254 if (passwordList.isEmpty()) 255 { 256 return new PasswordModifyExtendedResult(messageID, 257 ResultCode.INVALID_CREDENTIALS, null, null, null, null, null); 258 } 259 } 260 261 262 // If no new password was provided, then generate a random password to use. 263 final byte[] pwBytes; 264 final ASN1OctetString genPW; 265 if (newPWBytes == null) 266 { 267 final SecureRandom random = ThreadLocalSecureRandom.get(); 268 final byte[] pwAlphabet = StaticUtils.getBytes( 269 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); 270 pwBytes = new byte[8]; 271 for (int i=0; i < pwBytes.length; i++) 272 { 273 pwBytes[i] = pwAlphabet[random.nextInt(pwAlphabet.length)]; 274 } 275 genPW = new ASN1OctetString(pwBytes); 276 } 277 else 278 { 279 genPW = null; 280 pwBytes = newPWBytes; 281 } 282 283 284 // Construct the set of modifications to apply to the user entry. Iterate 285 // through the passwords 286 287 final List<InMemoryDirectoryServerPassword> existingPasswords = 288 handler.getPasswordsInEntry(userEntry, null); 289 final ArrayList<Modification> mods = 290 new ArrayList<>(existingPasswords.size()+1); 291 if (existingPasswords.isEmpty()) 292 { 293 mods.add(new Modification(ModificationType.REPLACE, 294 passwordAttributes.get(0), pwBytes)); 295 } 296 else 297 { 298 final HashSet<String> usedPWAttrs = new HashSet<>( 299 StaticUtils.computeMapCapacity(existingPasswords.size())); 300 for (final InMemoryDirectoryServerPassword p : existingPasswords) 301 { 302 final String attr = StaticUtils.toLowerCase(p.getAttributeName()); 303 if (usedPWAttrs.isEmpty()) 304 { 305 usedPWAttrs.add(attr); 306 mods.add(new Modification(ModificationType.REPLACE, 307 p.getAttributeName(), pwBytes)); 308 } 309 else if (! usedPWAttrs.contains(attr)) 310 { 311 usedPWAttrs.add(attr); 312 mods.add(new Modification(ModificationType.REPLACE, 313 p.getAttributeName())); 314 } 315 } 316 } 317 318 319 // If the no operation request control was provided, then return an 320 // appropriate result now. 321 if (noOperation) 322 { 323 return new PasswordModifyExtendedResult(messageID, 324 ResultCode.NO_OPERATION, INFO_PW_MOD_EXTOP_NO_OP.get(), null, null, 325 genPW, null); 326 } 327 328 329 // Attempt to modify the user password. 330 try 331 { 332 handler.modifyEntry(userEntry.getDN(), mods); 333 return new PasswordModifyExtendedResult(messageID, ResultCode.SUCCESS, 334 null, null, null, genPW, null); 335 } 336 catch (final LDAPException le) 337 { 338 Debug.debugException(le); 339 return new PasswordModifyExtendedResult(messageID, le.getResultCode(), 340 ERR_PW_MOD_EXTOP_CANNOT_CHANGE_PW.get(userEntry.getDN(), 341 le.getMessage()), 342 le.getMatchedDN(), le.getReferralURLs(), null, null); 343 } 344 } 345}