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