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