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