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}