001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2011-2012 profiq s.r.o. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.opends.server.plugins; 018 019import java.io.UnsupportedEncodingException; 020import java.security.InvalidKeyException; 021import java.security.MessageDigest; 022import java.security.NoSuchAlgorithmException; 023import java.security.NoSuchProviderException; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.Set; 029import java.util.SortedSet; 030 031import javax.crypto.BadPaddingException; 032import javax.crypto.Cipher; 033import javax.crypto.IllegalBlockSizeException; 034import javax.crypto.NoSuchPaddingException; 035import javax.crypto.SecretKey; 036import javax.crypto.spec.SecretKeySpec; 037 038import org.forgerock.i18n.LocalizableMessage; 039import org.forgerock.i18n.slf4j.LocalizedLogger; 040import org.forgerock.opendj.config.server.ConfigChangeResult; 041import org.forgerock.opendj.config.server.ConfigException; 042import org.forgerock.opendj.config.server.ConfigurationChangeListener; 043import org.forgerock.opendj.ldap.ByteString; 044import org.forgerock.opendj.ldap.DN; 045import org.forgerock.opendj.ldap.ModificationType; 046import org.forgerock.opendj.ldap.ResultCode; 047import org.forgerock.opendj.ldap.schema.AttributeType; 048import org.forgerock.opendj.ldap.schema.ObjectClass; 049import org.forgerock.opendj.server.config.meta.PluginCfgDefn; 050import org.forgerock.opendj.server.config.meta.SambaPasswordPluginCfgDefn.PwdSyncPolicy; 051import org.forgerock.opendj.server.config.server.SambaPasswordPluginCfg; 052import org.opends.server.api.plugin.DirectoryServerPlugin; 053import org.opends.server.api.plugin.PluginResult; 054import org.opends.server.api.plugin.PluginType; 055import org.opends.server.controls.LDAPAssertionRequestControl; 056import org.opends.server.core.DirectoryServer; 057import org.opends.server.core.ModifyOperation; 058import org.opends.server.extensions.PasswordModifyExtendedOperation; 059import org.opends.server.protocols.internal.InternalClientConnection; 060import org.opends.server.protocols.ldap.LDAPFilter; 061import org.opends.server.types.Attribute; 062import org.opends.server.types.Attributes; 063import org.opends.server.types.Control; 064import org.opends.server.types.DirectoryException; 065import org.opends.server.types.Entry; 066import org.opends.server.types.InitializationException; 067import org.opends.server.types.Modification; 068import org.opends.server.types.RawFilter; 069import org.opends.server.types.Schema; 070import org.opends.server.types.operation.PostOperationExtendedOperation; 071import org.opends.server.types.operation.PreOperationModifyOperation; 072 073import static org.opends.messages.PluginMessages.*; 074import static org.opends.server.util.StaticUtils.*; 075 076/** 077 * The Samba password synchronization plugin implementation class. 078 * <p> 079 * This plugin synchronizes the userPassword attribute with the Samba password 080 * attribute(s) for all entries containing the specified Samba object class. 081 * <p> 082 * It handles clear-text userPassword modify operations and password modify 083 * extended operations. It does not cover the case of using pre-encoded 084 * password. 085 */ 086public final class SambaPasswordPlugin extends 087 DirectoryServerPlugin<SambaPasswordPluginCfg> implements 088 ConfigurationChangeListener<SambaPasswordPluginCfg> 089{ 090 091 /** 092 * The implementation of this algorithm has been derived from BouncyCastle.org 093 * whose license can be found at http://www.bouncycastle.org/licence.html: 094 * <p> 095 * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle 096 * (http://www.bouncycastle.org) 097 * <p> 098 * Permission is hereby granted, free of charge, to any person obtaining a 099 * copy of this software and associated documentation files (the "Software"), 100 * to deal in the Software without restriction, including without limitation 101 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 102 * and/or sell copies of the Software, and to permit persons to whom the 103 * Software is furnished to do so, subject to the following conditions: 104 * <p> 105 * The above copyright notice and this permission notice shall be included in 106 * all copies or substantial portions of the Software. 107 * <p> 108 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 109 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 110 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 111 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 112 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 113 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 114 * DEALINGS IN THE SOFTWARE. 115 */ 116 static final class MD4MessageDigest extends MessageDigest 117 { 118 /** Class is package private for testing. */ 119 private final byte[] xBuf = new byte[4]; 120 private int xBufOff; 121 private long byteCount; 122 123 private static final int DIGEST_LENGTH = 16; 124 /** IV's. */ 125 private int H1, H2, H3, H4; 126 private final int[] X = new int[16]; 127 private int xOff; 128 129 /** Round 1 left rotates. */ 130 private static final int S11 = 3; 131 private static final int S12 = 7; 132 private static final int S13 = 11; 133 private static final int S14 = 19; 134 135 /** Round 2 left rotates. */ 136 private static final int S21 = 3; 137 private static final int S22 = 5; 138 private static final int S23 = 9; 139 private static final int S24 = 13; 140 141 /** Round 3 left rotates. */ 142 private static final int S31 = 3; 143 private static final int S32 = 9; 144 private static final int S33 = 11; 145 private static final int S34 = 15; 146 147 148 149 /** Creates a new MD4 message digest algorithm. */ 150 MD4MessageDigest() 151 { 152 super("MD4"); 153 engineReset(); 154 } 155 156 157 158 @Override 159 public byte[] engineDigest() 160 { 161 final byte[] digestBytes = new byte[DIGEST_LENGTH]; 162 finish(); 163 unpackWord(H1, digestBytes, 0); 164 unpackWord(H2, digestBytes, 4); 165 unpackWord(H3, digestBytes, 8); 166 unpackWord(H4, digestBytes, 12); 167 engineReset(); 168 return digestBytes; 169 } 170 171 172 173 @Override 174 public void engineReset() 175 { 176 byteCount = 0; 177 xBufOff = 0; 178 for (int i = 0; i < xBuf.length; i++) 179 { 180 xBuf[i] = 0; 181 } 182 183 H1 = 0x67452301; 184 H2 = 0xefcdab89; 185 H3 = 0x98badcfe; 186 H4 = 0x10325476; 187 xOff = 0; 188 for (int i = 0; i != X.length; i++) 189 { 190 X[i] = 0; 191 } 192 } 193 194 195 196 @Override 197 public void engineUpdate(final byte input) 198 { 199 xBuf[xBufOff++] = input; 200 if (xBufOff == xBuf.length) 201 { 202 processWord(xBuf, 0); 203 xBufOff = 0; 204 } 205 byteCount++; 206 } 207 208 209 210 @Override 211 public void engineUpdate(final byte[] in, int inOff, int len) 212 { 213 // 214 // fill the current word 215 // 216 while (xBufOff != 0 && len > 0) 217 { 218 update(in[inOff]); 219 220 inOff++; 221 len--; 222 } 223 224 // 225 // process whole words. 226 // 227 while (len > xBuf.length) 228 { 229 processWord(in, inOff); 230 231 inOff += xBuf.length; 232 len -= xBuf.length; 233 byteCount += xBuf.length; 234 } 235 236 // 237 // load in the remainder. 238 // 239 while (len > 0) 240 { 241 update(in[inOff]); 242 243 inOff++; 244 len--; 245 } 246 } 247 248 249 250 /** 251 * F, G, H and I are the basic MD4 functions. 252 */ 253 private int F(final int u, final int v, final int w) 254 { 255 return (u & v) | (~u & w); 256 } 257 258 259 260 private void finish() 261 { 262 final long bitLength = byteCount << 3; 263 264 // 265 // add the pad bytes. 266 // 267 engineUpdate((byte) 128); 268 while (xBufOff != 0) 269 { 270 engineUpdate((byte) 0); 271 } 272 processLength(bitLength); 273 processBlock(); 274 } 275 276 277 278 private int G(final int u, final int v, final int w) 279 { 280 return (u & v) | (u & w) | (v & w); 281 } 282 283 284 285 private int H(final int u, final int v, final int w) 286 { 287 return u ^ v ^ w; 288 } 289 290 291 292 private void processBlock() 293 { 294 int a = H1; 295 int b = H2; 296 int c = H3; 297 int d = H4; 298 299 // 300 // Round 1 - F cycle, 16 times. 301 // 302 a = rotateLeft(a + F(b, c, d) + X[0], S11); 303 d = rotateLeft(d + F(a, b, c) + X[1], S12); 304 c = rotateLeft(c + F(d, a, b) + X[2], S13); 305 b = rotateLeft(b + F(c, d, a) + X[3], S14); 306 a = rotateLeft(a + F(b, c, d) + X[4], S11); 307 d = rotateLeft(d + F(a, b, c) + X[5], S12); 308 c = rotateLeft(c + F(d, a, b) + X[6], S13); 309 b = rotateLeft(b + F(c, d, a) + X[7], S14); 310 a = rotateLeft(a + F(b, c, d) + X[8], S11); 311 d = rotateLeft(d + F(a, b, c) + X[9], S12); 312 c = rotateLeft(c + F(d, a, b) + X[10], S13); 313 b = rotateLeft(b + F(c, d, a) + X[11], S14); 314 a = rotateLeft(a + F(b, c, d) + X[12], S11); 315 d = rotateLeft(d + F(a, b, c) + X[13], S12); 316 c = rotateLeft(c + F(d, a, b) + X[14], S13); 317 b = rotateLeft(b + F(c, d, a) + X[15], S14); 318 319 // 320 // Round 2 - G cycle, 16 times. 321 // 322 a = rotateLeft(a + G(b, c, d) + X[0] + 0x5a827999, S21); 323 d = rotateLeft(d + G(a, b, c) + X[4] + 0x5a827999, S22); 324 c = rotateLeft(c + G(d, a, b) + X[8] + 0x5a827999, S23); 325 b = rotateLeft(b + G(c, d, a) + X[12] + 0x5a827999, S24); 326 a = rotateLeft(a + G(b, c, d) + X[1] + 0x5a827999, S21); 327 d = rotateLeft(d + G(a, b, c) + X[5] + 0x5a827999, S22); 328 c = rotateLeft(c + G(d, a, b) + X[9] + 0x5a827999, S23); 329 b = rotateLeft(b + G(c, d, a) + X[13] + 0x5a827999, S24); 330 a = rotateLeft(a + G(b, c, d) + X[2] + 0x5a827999, S21); 331 d = rotateLeft(d + G(a, b, c) + X[6] + 0x5a827999, S22); 332 c = rotateLeft(c + G(d, a, b) + X[10] + 0x5a827999, S23); 333 b = rotateLeft(b + G(c, d, a) + X[14] + 0x5a827999, S24); 334 a = rotateLeft(a + G(b, c, d) + X[3] + 0x5a827999, S21); 335 d = rotateLeft(d + G(a, b, c) + X[7] + 0x5a827999, S22); 336 c = rotateLeft(c + G(d, a, b) + X[11] + 0x5a827999, S23); 337 b = rotateLeft(b + G(c, d, a) + X[15] + 0x5a827999, S24); 338 339 // 340 // Round 3 - H cycle, 16 times. 341 // 342 a = rotateLeft(a + H(b, c, d) + X[0] + 0x6ed9eba1, S31); 343 d = rotateLeft(d + H(a, b, c) + X[8] + 0x6ed9eba1, S32); 344 c = rotateLeft(c + H(d, a, b) + X[4] + 0x6ed9eba1, S33); 345 b = rotateLeft(b + H(c, d, a) + X[12] + 0x6ed9eba1, S34); 346 a = rotateLeft(a + H(b, c, d) + X[2] + 0x6ed9eba1, S31); 347 d = rotateLeft(d + H(a, b, c) + X[10] + 0x6ed9eba1, S32); 348 c = rotateLeft(c + H(d, a, b) + X[6] + 0x6ed9eba1, S33); 349 b = rotateLeft(b + H(c, d, a) + X[14] + 0x6ed9eba1, S34); 350 a = rotateLeft(a + H(b, c, d) + X[1] + 0x6ed9eba1, S31); 351 d = rotateLeft(d + H(a, b, c) + X[9] + 0x6ed9eba1, S32); 352 c = rotateLeft(c + H(d, a, b) + X[5] + 0x6ed9eba1, S33); 353 b = rotateLeft(b + H(c, d, a) + X[13] + 0x6ed9eba1, S34); 354 a = rotateLeft(a + H(b, c, d) + X[3] + 0x6ed9eba1, S31); 355 d = rotateLeft(d + H(a, b, c) + X[11] + 0x6ed9eba1, S32); 356 c = rotateLeft(c + H(d, a, b) + X[7] + 0x6ed9eba1, S33); 357 b = rotateLeft(b + H(c, d, a) + X[15] + 0x6ed9eba1, S34); 358 359 H1 += a; 360 H2 += b; 361 H3 += c; 362 H4 += d; 363 364 // 365 // reset the offset and clean out the word buffer. 366 // 367 xOff = 0; 368 for (int i = 0; i != X.length; i++) 369 { 370 X[i] = 0; 371 } 372 } 373 374 375 376 private void processLength(final long bitLength) 377 { 378 if (xOff > 14) 379 { 380 processBlock(); 381 } 382 383 X[14] = (int) (bitLength & 0xffffffff); 384 X[15] = (int) (bitLength >>> 32); 385 } 386 387 388 389 private void processWord(final byte[] in, final int inOff) 390 { 391 X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8) 392 | ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24); 393 394 if (xOff == 16) 395 { 396 processBlock(); 397 } 398 } 399 400 401 402 /** Rotate int x left n bits. */ 403 private int rotateLeft(final int x, final int n) 404 { 405 return (x << n) | (x >>> 32 - n); 406 } 407 408 409 410 private void unpackWord(final int word, final byte[] out, final int outOff) 411 { 412 out[outOff] = (byte) word; 413 out[outOff + 1] = (byte) (word >>> 8); 414 out[outOff + 2] = (byte) (word >>> 16); 415 out[outOff + 3] = (byte) (word >>> 24); 416 } 417 } 418 419 420 421 /** Plugin configuration object. */ 422 private SambaPasswordPluginCfg config; 423 424 /** The name of the Samba LanMan password attribute. */ 425 private static final String SAMBA_LM_PASSWORD_ATTRIBUTE_NAME = 426 "sambaLMPassword"; 427 428 /** The name of the Samba NT password attribute. */ 429 private static final String SAMBA_NT_PASSWORD_ATTRIBUTE_NAME = 430 "sambaNTPassword"; 431 432 /** The name of the Samba account object class. */ 433 private static final String SAMBA_SAM_ACCOUNT_OC_NAME = "sambaSAMAccount"; 434 435 /** The name of the Samba last password change attribute. */ 436 private static final String SAMBA_PWD_LAST_SET_NAME = "sambaPwdLastSet"; 437 438 /** Debug tracer object to log debugging information. */ 439 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 440 441 /** Password Modify Extended Operation OID. */ 442 private static final String PWMOD_EXTOP_OID = "1.3.6.1.4.1.4203.1.11.1"; 443 444 /** Magic string to be used as salt. */ 445 private static final String MAGIC_STR = "KGS!@#$%"; 446 447 /** Default timestamp provider implementation. */ 448 private static final TimeStampProvider DEFAULT_TIMESTAMP_PROVIDER = 449 new TimeStampProvider() 450 { 451 @Override 452 public long getCurrentTime() 453 { 454 return System.currentTimeMillis() / 1000L; 455 } 456 }; 457 458 /** Use the default implementation of the timestamp provider... by default. */ 459 private TimeStampProvider timeStampProvider = DEFAULT_TIMESTAMP_PROVIDER; 460 461 462 /** 463 * Add the parity to the 56-bit key converting it to 64-bit key. 464 * 465 * @param key56 466 * 56-bit key. 467 * @return 64-bit key. 468 */ 469 private static byte[] addParity(final byte[] key56) 470 { 471 final byte[] key64 = new byte[8]; 472 final int[] key7 = new int[7]; 473 final int[] key8 = new int[8]; 474 475 for (int i = 0; i < 7; i++) 476 { 477 key7[i] = key56[i] & 0xFF; 478 } 479 480 key8[0] = key7[0]; 481 key8[1] = ((key7[0] << 7) & 0xFF) | (key7[1] >> 1); 482 key8[2] = ((key7[1] << 6) & 0xFF) | (key7[2] >> 2); 483 key8[3] = ((key7[2] << 5) & 0xFF) | (key7[3] >> 3); 484 key8[4] = ((key7[3] << 4) & 0xFF) | (key7[4] >> 4); 485 key8[5] = ((key7[4] << 3) & 0xFF) | (key7[5] >> 5); 486 key8[6] = ((key7[5] << 2) & 0xFF) | (key7[6] >> 6); 487 key8[7] = key7[6] << 1; 488 489 for (int i = 0; i < 8; i++) 490 { 491 key64[i] = (byte) setOddParity(key8[i]); 492 } 493 494 return key64; 495 496 } 497 498 499 500 /** 501 * Create a LanMan hash from a clear-text password. 502 * 503 * @param password 504 * Clear-text password. 505 * @return Hex string version of the hash based on the clear-text password. 506 * @throws UnsupportedEncodingException 507 * if the <code>US-ASCII</code> coding is not available. 508 * @throws NoSuchAlgorithmException 509 * if the algorithm does not exist for the used provider. 510 * @throws InvalidKeyException 511 * if the key is inappropriate to initialize the cipher. 512 * @throws NoSuchPaddingException 513 * if the padding scheme is not available. 514 * @throws IllegalBlockSizeException 515 * if this encryption algorithm is unable to process the input data 516 * provided 517 * @throws BadPaddingException 518 * if this cipher is in decryption mode, and (un)padding has been 519 * requested, but the decrypted data is not bounded by the 520 * appropriate padding bytes 521 */ 522 private static String lmHash(final String password) 523 throws UnsupportedEncodingException, NoSuchAlgorithmException, 524 InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, 525 BadPaddingException 526 { 527 // Password has to be OEM encoded and in upper case 528 final byte[] oemPass = password.toUpperCase().getBytes("US-ASCII"); 529 530 // It shouldn't be longer then 14 bytes 531 int length = 14; 532 if (oemPass.length < length) 533 { 534 length = oemPass.length; 535 } 536 537 // The password should be divided into two 7-byte keys 538 final byte[] key1 = new byte[7]; 539 final byte[] key2 = new byte[7]; 540 if (length <= 7) 541 { 542 System.arraycopy(oemPass, 0, key1, 0, length); 543 } 544 else 545 { 546 System.arraycopy(oemPass, 0, key1, 0, 7); 547 System.arraycopy(oemPass, 7, key2, 0, length - 7); 548 } 549 550 // We create two DES keys using key1 and key2 to on the magic string 551 final SecretKey lowKey = new SecretKeySpec(addParity(key1), "DES"); 552 final SecretKey highKey = new SecretKeySpec(addParity(key2), "DES"); 553 final Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); 554 des.init(Cipher.ENCRYPT_MODE, lowKey); 555 final byte[] lowHash = des.doFinal(MAGIC_STR.getBytes()); 556 des.init(Cipher.ENCRYPT_MODE, highKey); 557 final byte[] highHash = des.doFinal(MAGIC_STR.getBytes()); 558 559 // We finally merge hashes and return them to the client 560 561 final byte[] lmHash = new byte[16]; 562 System.arraycopy(lowHash, 0, lmHash, 0, 8); 563 System.arraycopy(highHash, 0, lmHash, 8, 8); 564 return toLowerCase(bytesToHexNoSpace(lmHash)); 565 } 566 567 568 569 /** 570 * Creates a NTLM hash from a clear-text password. 571 * 572 * @param password 573 * Clear text password. 574 * @return Returns a NTLM hash. 575 * @throws NoSuchProviderException 576 * if the BouncyCastle provider does not load 577 * @throws NoSuchAlgorithmException 578 * if the MD4 algorithm is not found 579 * @throws UnsupportedEncodingException 580 * if the encoding <code>UnicodeLittleUnmarked</code> is not 581 * supported. 582 */ 583 private static String ntHash(final String password) 584 throws NoSuchProviderException, UnsupportedEncodingException, 585 NoSuchAlgorithmException 586 { 587 final byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked"); 588 final MessageDigest md4 = new MD4MessageDigest(); 589 return toLowerCase(bytesToHexNoSpace(md4.digest(unicodePassword))); 590 } 591 592 593 594 /** 595 * Set the parity bit for an integer. 596 * 597 * @param integer 598 * to add the parity bit for. 599 * @return integer with the parity bit set. 600 */ 601 private static int setOddParity(final int parity) 602 { 603 final boolean hasEvenBits = (parity >>> 7 ^ parity >>> 6 604 ^ parity >>> 5 ^ parity >>> 4 605 ^ parity >>> 3 ^ parity >>> 2 606 ^ ((parity >>> 1) & 0x01)) == 0; 607 if (hasEvenBits) 608 { 609 return parity | 0x01; 610 } 611 else 612 { 613 return parity & 0xFE; 614 } 615 } 616 617 618 619 /** Default constructor. */ 620 public SambaPasswordPlugin() 621 { 622 super(); 623 } 624 625 626 627 @Override 628 public ConfigChangeResult applyConfigurationChange( 629 final SambaPasswordPluginCfg newConfig) 630 { 631 632 // No validation required and no restart required. 633 config = newConfig; 634 635 return new ConfigChangeResult(); 636 } 637 638 639 640 @Override 641 public PluginResult.PostOperation doPostOperation( 642 final PostOperationExtendedOperation extendedOperation) 643 { 644 /* If the operation is not Password Modify Extended Operation then skip this operation. */ 645 if (!extendedOperation.getRequestOID().equals(PWMOD_EXTOP_OID)) 646 { 647 return PluginResult.PostOperation.continueOperationProcessing(); 648 } 649 650 /* If the operation has not been successful then ignore the operation. */ 651 if (extendedOperation.getResultCode() != ResultCode.SUCCESS) 652 { 653 return PluginResult.PostOperation.continueOperationProcessing(); 654 } 655 656 /* 657 * Verify if the operation has been initiated by what was defined as Samba 658 * administrative user. If so, we will skip this operation to avoid double 659 * synchronization of Samba attributes. 660 */ 661 final DN authDN = extendedOperation.getAuthorizationDN(); 662 final DN sambaAdminDN = config.getSambaAdministratorDN(); 663 if (sambaAdminDN != null 664 && !sambaAdminDN.isRootDN() 665 && authDN.equals(sambaAdminDN)) 666 { 667 if (logger.isTraceEnabled()) 668 { 669 logger.trace("This operation will be skipped because" 670 + " it was performed by Samba admin user: " + sambaAdminDN); 671 } 672 return PluginResult.PostOperation.continueOperationProcessing(); 673 } 674 675 // Get the name of the entry and clear passwords from the operation 676 // attachments. 677 final DN dn = (DN) extendedOperation 678 .getAttachment(PasswordModifyExtendedOperation.AUTHZ_DN_ATTACHMENT); 679 if (dn == null) 680 { 681 // The attachment is missing which should never happen. 682 if (logger.isTraceEnabled()) 683 { 684 logger.trace("SambaPasswordPlugin: missing DN attachment"); 685 } 686 return PluginResult.PostOperation.continueOperationProcessing(); 687 } 688 689 final String password = extendedOperation.getAttachment( 690 PasswordModifyExtendedOperation.CLEAR_PWD_ATTACHMENT).toString(); 691 if (password == null) 692 { 693 if (logger.isTraceEnabled()) 694 { 695 logger.trace("SambaPasswordPlugin: skipping syncing " 696 + "pre-encoded password"); 697 } 698 return PluginResult.PostOperation.continueOperationProcessing(); 699 } 700 701 @SuppressWarnings("unchecked") 702 final List<ByteString> encPasswords = (List<ByteString>) extendedOperation 703 .getAttachment(PasswordModifyExtendedOperation.ENCODED_PWD_ATTACHMENT); 704 705 try 706 { 707 // Before proceeding make sure this entry has samba object class. 708 final Entry entry = DirectoryServer.getEntry(dn); 709 if (!isSynchronizable(entry)) 710 { 711 if (logger.isTraceEnabled()) 712 { 713 logger.trace("The entry is not Samba object."); 714 } 715 return PluginResult.PostOperation.continueOperationProcessing(); 716 } 717 718 /* 719 * Make an internal connection to process the password modification. It 720 * will not trigger this plugin again with the pre-operation modify since 721 * the password passed would be encoded hence the pre operation part would 722 * skip it. 723 */ 724 final InternalClientConnection connection = InternalClientConnection 725 .getRootConnection(); 726 727 final List<Modification> modifications = getModifications(password); 728 729 // Use an assertion control to avoid race conditions since extended 730 // operation post-ops are done outside of the write lock. 731 List<Control> controls = null; 732 if (!encPasswords.isEmpty()) 733 { 734 final AttributeType pwdAttribute = (AttributeType) extendedOperation 735 .getAttachment( 736 PasswordModifyExtendedOperation.PWD_ATTRIBUTE_ATTACHMENT); 737 final LDAPFilter filter = RawFilter.createEqualityFilter( 738 pwdAttribute.getNameOrOID(), encPasswords.get(0)); 739 final Control assertionControl = new LDAPAssertionRequestControl(true, 740 filter); 741 controls = Collections.singletonList(assertionControl); 742 } 743 744 final ModifyOperation modifyOperation = connection.processModify(dn, 745 modifications, controls); 746 747 if (logger.isTraceEnabled()) 748 { 749 logger.trace("rc=%s", modifyOperation.getResultCode()); 750 } 751 } 752 catch (final DirectoryException e) 753 { 754 /* 755 * This exception occurs if there is a problem while retrieving the entry. 756 * This should never happen as we are processing the post-operation which 757 * succeeded so the entry has to exist if we have reached this point. 758 */ 759 logger.traceException(e); 760 } 761 762 return PluginResult.PostOperation.continueOperationProcessing(); 763 } 764 765 766 767 @Override 768 public PluginResult.PreOperation doPreOperation( 769 final PreOperationModifyOperation modifyOperation) 770 { 771 /* 772 * If the passwords are changed in clear text they will be available with 773 * the getNewPasswords() method. If they are encoded the method would return 774 * null. The list of passwords should not be modified. 775 */ 776 final List<ByteString> passwords = modifyOperation.getNewPasswords(); 777 778 /* 779 * If the password list is not empty, we can be sure the current operation 780 * is the one that applies to our case: - it's a modify operation on 781 * userPassword attribute - it's replaces or adds new userPassword attribute 782 * value. If it doesn't then we skip this modify operation. 783 */ 784 if (passwords == null) 785 { 786 return PluginResult.PreOperation.continueOperationProcessing(); 787 } 788 789 // Skip synchronization operations. 790 if (modifyOperation.isSynchronizationOperation()) 791 { 792 if (logger.isTraceEnabled()) 793 { 794 logger.trace("Synchronization operation. Skipping."); 795 } 796 return PluginResult.PreOperation.continueOperationProcessing(); 797 } 798 799 /* 800 * Verify if the operation has been initiated by the Samba administrative 801 * user. If so, we will skip this operation to avoid double synchronization 802 * of Samba attributes. 803 */ 804 final DN authDN = modifyOperation.getAuthorizationDN(); 805 final DN sambaAdminDN = config.getSambaAdministratorDN(); 806 if (sambaAdminDN != null 807 && !sambaAdminDN.isRootDN() 808 && authDN.equals(sambaAdminDN)) 809 { 810 if (logger.isTraceEnabled()) 811 { 812 logger.trace("This operation will be skipped because" 813 + " it was performed by Samba admin user: " + sambaAdminDN); 814 } 815 return PluginResult.PreOperation.continueOperationProcessing(); 816 } 817 818 /* 819 * Before proceeding with the modification, we have to make sure this entry 820 * is indeed a Samba object. 821 */ 822 if (!isSynchronizable(modifyOperation.getCurrentEntry())) 823 { 824 if (logger.isTraceEnabled()) 825 { 826 logger.trace("Skipping '%s' because it does not have Samba object class.", modifyOperation.getEntryDN()); 827 } 828 return PluginResult.PreOperation.continueOperationProcessing(); 829 } 830 831 /* 832 * Proceed with processing: add a new modification to the current modify 833 * operation, so they could be executed at the same time. 834 */ 835 processModification(modifyOperation, passwords); 836 837 // Continue plugin processing. 838 return PluginResult.PreOperation.continueOperationProcessing(); 839 } 840 841 842 843 @Override 844 public void initializePlugin(final Set<PluginType> pluginTypes, 845 final SambaPasswordPluginCfg configuration) throws ConfigException, 846 InitializationException 847 { 848 849 // Verify config parameters. 850 final LinkedList<LocalizableMessage> messages = new LinkedList<>(); 851 if (!isConfigurationAcceptable(configuration, messages)) 852 { 853 for (final LocalizableMessage m : messages) 854 { 855 logger.error(m); 856 } 857 throw new ConfigException(messages.poll()); 858 } 859 860 // Register the configuration change listener. 861 configuration.addSambaPasswordChangeListener(this); 862 863 // Save the configuration. 864 this.config = configuration; 865 } 866 867 868 869 /** 870 * Verifies if the plugin configuration is acceptable. 871 * 872 * @param configuration 873 * The plugin configuration. 874 * @param unacceptableReasons 875 * Reasons why the configuration is not acceptable. 876 * @return Returns <code>true</code> for the correct configuration and 877 * <code>false</code> for the incorrect one. 878 */ 879 public boolean isConfigurationAcceptable( 880 final SambaPasswordPluginCfg configuration, 881 final List<LocalizableMessage> unacceptableReasons) 882 { 883 return isConfigurationChangeAcceptable(configuration, unacceptableReasons); 884 } 885 886 887 888 @Override 889 public boolean isConfigurationChangeAcceptable( 890 final SambaPasswordPluginCfg newConfig, final List<LocalizableMessage> messages) 891 { 892 /* 893 * The plugin implements only postoperationmodify and postoperationextended 894 * plugin types. The rest should be rejected. 895 */ 896 897 final SortedSet<PluginCfgDefn.PluginType> pluginTypes = newConfig 898 .getPluginType(); 899 for (final PluginCfgDefn.PluginType t : pluginTypes) 900 { 901 switch (t) 902 { 903 case PREOPERATIONMODIFY: 904 case POSTOPERATIONEXTENDED: 905 break; 906 default: 907 messages.add(ERR_PLUGIN_SAMBA_SYNC_INVALID_PLUGIN_TYPE.get(t)); 908 return false; 909 } 910 } 911 912 return true; 913 } 914 915 916 917 /** 918 * Creates the modifications to modify Samba password attributes. It uses 919 * clear-text password and encodes it with the appropriate algorithm for it's 920 * respective type (NTLM or LanMan); then it wraps it in the modifications to 921 * be added to the modify operation. 922 * 923 * @param password 924 * New password which is to be encoded for Samba. 925 * @return Returns a list of modifications, or null if a problem occurs. 926 */ 927 private List<Modification> getModifications(final String password) 928 { 929 ArrayList<Modification> modifications = new ArrayList<>(); 930 try 931 { 932 if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_NT_PASSWORD)) 933 { 934 final Attribute attribute = Attributes.create( 935 SAMBA_NT_PASSWORD_ATTRIBUTE_NAME, ntHash(password)); 936 modifications.add(new Modification(ModificationType.REPLACE, attribute)); 937 } 938 939 if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_LM_PASSWORD)) 940 { 941 final Attribute attribute = Attributes.create( 942 SAMBA_LM_PASSWORD_ATTRIBUTE_NAME, lmHash(password)); 943 modifications.add(new Modification(ModificationType.REPLACE, attribute)); 944 } 945 final Attribute pwdLastSet = Attributes.create( 946 SAMBA_PWD_LAST_SET_NAME, 947 String.valueOf(timeStampProvider.getCurrentTime())); 948 modifications.add(new Modification(ModificationType.REPLACE, pwdLastSet)); 949 } 950 catch (final Exception e) 951 { 952 logger.info(ERR_PLUGIN_SAMBA_SYNC_ENCODING, e.getMessage(), e); 953 modifications = null; 954 } 955 956 return modifications; 957 } 958 959 960 961 /** 962 * Verify if the target entry contains pre-defined Samba object class. 963 * 964 * @param entry 965 * The entry being modified. 966 * @return Returns true if the entry has Samba object class, otherwise returns 967 * false. 968 */ 969 private boolean isSynchronizable(final Entry entry) 970 { 971 final Schema schema = DirectoryServer.getSchema(); 972 final ObjectClass sambaOc = schema.getObjectClass(SAMBA_SAM_ACCOUNT_OC_NAME); 973 return !sambaOc.isPlaceHolder() && entry.hasObjectClass(sambaOc); 974 } 975 976 977 978 /** 979 * Adds modifications for the configured Samba password attributes to the 980 * current modify operation. 981 * 982 * @param modifyOperation 983 * Current modify operation which will be modified to add Samba 984 * password attribute changes. 985 * @param passwords 986 * List of userPassword clear-text attribute values to be hashed for 987 * Samba 988 */ 989 private void processModification( 990 final PreOperationModifyOperation modifyOperation, 991 final List<ByteString> passwords) 992 { 993 // Get the last password (in case there is more then one). 994 final String password = passwords.get(passwords.size() - 1).toString(); 995 try 996 { 997 // Generate the necessary modifications. 998 for (final Modification modification : getModifications(password)) 999 { 1000 modifyOperation.addModification(modification); 1001 } 1002 } 1003 catch (final DirectoryException e) 1004 { 1005 logger.info(ERR_PLUGIN_SAMBA_SYNC_MODIFICATION_PROCESSING, e.getMessage(), e); 1006 } 1007 } 1008 1009 /** Timestamp provider interface. Intended primarily for testing purposes. */ 1010 static interface TimeStampProvider 1011 { 1012 /** 1013 * Generates a custom time stamp. 1014 * 1015 * @return A timestamp in UNIX format. 1016 */ 1017 long getCurrentTime(); 1018 } 1019 1020 /** 1021 * Use custom timestamp provider. Intended primarily for testing purposes. 1022 * 1023 * @param timeStampProvider Provider object that implements the 1024 * TimeStampProvider interface. 1025 */ 1026 void setTimeStampProvider(TimeStampProvider timeStampProvider) 1027 { 1028 this.timeStampProvider = (timeStampProvider == null) 1029 ? DEFAULT_TIMESTAMP_PROVIDER : timeStampProvider; 1030 } 1031}