/*
|
* The contents of this file are subject to the terms of the Common Development and
|
* Distribution License (the License). You may not use this file except in compliance with the
|
* License.
|
*
|
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
|
* specific language governing permission and limitations under the License.
|
*
|
* When distributing Covered Software, include this CDDL Header Notice in each file and include
|
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
|
* Header, with the fields enclosed by brackets [] replaced by your own identifying
|
* information: "Portions Copyright [year] [name of copyright owner]".
|
*
|
* Copyright 2011-2012 profiq s.r.o.
|
* Portions Copyright 2011-2016 ForgeRock AS.
|
*/
|
package org.opends.server.plugins;
|
|
import java.io.UnsupportedEncodingException;
|
import java.security.InvalidKeyException;
|
import java.security.MessageDigest;
|
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchProviderException;
|
import java.util.*;
|
|
import javax.crypto.*;
|
import javax.crypto.spec.SecretKeySpec;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.forgerock.opendj.ldap.ByteString;
|
import org.forgerock.opendj.ldap.DN;
|
import org.forgerock.opendj.ldap.ModificationType;
|
import org.opends.server.admin.server.ConfigurationChangeListener;
|
import org.opends.server.admin.std.meta.PluginCfgDefn;
|
import org.opends.server.admin.std.meta.SambaPasswordPluginCfgDefn.PwdSyncPolicy;
|
import org.opends.server.admin.std.server.SambaPasswordPluginCfg;
|
import org.opends.server.api.plugin.DirectoryServerPlugin;
|
import org.opends.server.api.plugin.PluginResult;
|
import org.opends.server.api.plugin.PluginType;
|
import org.forgerock.opendj.config.server.ConfigChangeResult;
|
import org.forgerock.opendj.config.server.ConfigException;
|
import org.opends.server.controls.LDAPAssertionRequestControl;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.core.ModifyOperation;
|
import org.opends.server.extensions.PasswordModifyExtendedOperation;
|
import org.opends.server.protocols.internal.InternalClientConnection;
|
import org.opends.server.protocols.ldap.LDAPFilter;
|
import org.forgerock.opendj.ldap.schema.AttributeType;
|
import org.opends.server.types.*;
|
import org.forgerock.opendj.ldap.ResultCode;
|
import org.opends.server.types.operation.PostOperationExtendedOperation;
|
import org.opends.server.types.operation.PreOperationModifyOperation;
|
|
import static org.opends.messages.PluginMessages.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
/**
|
* The Samba password synchronization plugin implementation class.
|
* <p>
|
* This plugin synchronizes the userPassword attribute with the Samba password
|
* attribute(s) for all entries containing the specified Samba object class.
|
* <p>
|
* It handles clear-text userPassword modify operations and password modify
|
* extended operations. It does not cover the case of using pre-encoded
|
* password.
|
*/
|
public final class SambaPasswordPlugin extends
|
DirectoryServerPlugin<SambaPasswordPluginCfg> implements
|
ConfigurationChangeListener<SambaPasswordPluginCfg>
|
{
|
|
/**
|
* The implementation of this algorithm has been derived from BouncyCastle.org
|
* whose license can be found at http://www.bouncycastle.org/licence.html:
|
* <p>
|
* Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle
|
* (http://www.bouncycastle.org)
|
* <p>
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
* copy of this software and associated documentation files (the "Software"),
|
* to deal in the Software without restriction, including without limitation
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
* and/or sell copies of the Software, and to permit persons to whom the
|
* Software is furnished to do so, subject to the following conditions:
|
* <p>
|
* The above copyright notice and this permission notice shall be included in
|
* all copies or substantial portions of the Software.
|
* <p>
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
* DEALINGS IN THE SOFTWARE.
|
*/
|
static final class MD4MessageDigest extends MessageDigest
|
{
|
/** Class is package private for testing. */
|
private final byte[] xBuf = new byte[4];
|
private int xBufOff;
|
private long byteCount;
|
|
private static final int DIGEST_LENGTH = 16;
|
/** IV's. */
|
private int H1, H2, H3, H4;
|
private final int[] X = new int[16];
|
private int xOff;
|
|
/** Round 1 left rotates. */
|
private static final int S11 = 3;
|
private static final int S12 = 7;
|
private static final int S13 = 11;
|
private static final int S14 = 19;
|
|
/** Round 2 left rotates. */
|
private static final int S21 = 3;
|
private static final int S22 = 5;
|
private static final int S23 = 9;
|
private static final int S24 = 13;
|
|
/** Round 3 left rotates. */
|
private static final int S31 = 3;
|
private static final int S32 = 9;
|
private static final int S33 = 11;
|
private static final int S34 = 15;
|
|
|
|
/**
|
* Creates a new MD4 message digest algorithm.
|
*/
|
MD4MessageDigest()
|
{
|
super("MD4");
|
engineReset();
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public byte[] engineDigest()
|
{
|
final byte[] digestBytes = new byte[DIGEST_LENGTH];
|
finish();
|
unpackWord(H1, digestBytes, 0);
|
unpackWord(H2, digestBytes, 4);
|
unpackWord(H3, digestBytes, 8);
|
unpackWord(H4, digestBytes, 12);
|
engineReset();
|
return digestBytes;
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void engineReset()
|
{
|
byteCount = 0;
|
xBufOff = 0;
|
for (int i = 0; i < xBuf.length; i++)
|
{
|
xBuf[i] = 0;
|
}
|
|
H1 = 0x67452301;
|
H2 = 0xefcdab89;
|
H3 = 0x98badcfe;
|
H4 = 0x10325476;
|
xOff = 0;
|
for (int i = 0; i != X.length; i++)
|
{
|
X[i] = 0;
|
}
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void engineUpdate(final byte input)
|
{
|
xBuf[xBufOff++] = input;
|
if (xBufOff == xBuf.length)
|
{
|
processWord(xBuf, 0);
|
xBufOff = 0;
|
}
|
byteCount++;
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void engineUpdate(final byte[] in, int inOff, int len)
|
{
|
//
|
// fill the current word
|
//
|
while (xBufOff != 0 && len > 0)
|
{
|
update(in[inOff]);
|
|
inOff++;
|
len--;
|
}
|
|
//
|
// process whole words.
|
//
|
while (len > xBuf.length)
|
{
|
processWord(in, inOff);
|
|
inOff += xBuf.length;
|
len -= xBuf.length;
|
byteCount += xBuf.length;
|
}
|
|
//
|
// load in the remainder.
|
//
|
while (len > 0)
|
{
|
update(in[inOff]);
|
|
inOff++;
|
len--;
|
}
|
}
|
|
|
|
/**
|
* F, G, H and I are the basic MD4 functions.
|
*/
|
private int F(final int u, final int v, final int w)
|
{
|
return (u & v) | (~u & w);
|
}
|
|
|
|
private void finish()
|
{
|
final long bitLength = byteCount << 3;
|
|
//
|
// add the pad bytes.
|
//
|
engineUpdate((byte) 128);
|
while (xBufOff != 0)
|
{
|
engineUpdate((byte) 0);
|
}
|
processLength(bitLength);
|
processBlock();
|
}
|
|
|
|
private int G(final int u, final int v, final int w)
|
{
|
return (u & v) | (u & w) | (v & w);
|
}
|
|
|
|
private int H(final int u, final int v, final int w)
|
{
|
return u ^ v ^ w;
|
}
|
|
|
|
private void processBlock()
|
{
|
int a = H1;
|
int b = H2;
|
int c = H3;
|
int d = H4;
|
|
//
|
// Round 1 - F cycle, 16 times.
|
//
|
a = rotateLeft(a + F(b, c, d) + X[0], S11);
|
d = rotateLeft(d + F(a, b, c) + X[1], S12);
|
c = rotateLeft(c + F(d, a, b) + X[2], S13);
|
b = rotateLeft(b + F(c, d, a) + X[3], S14);
|
a = rotateLeft(a + F(b, c, d) + X[4], S11);
|
d = rotateLeft(d + F(a, b, c) + X[5], S12);
|
c = rotateLeft(c + F(d, a, b) + X[6], S13);
|
b = rotateLeft(b + F(c, d, a) + X[7], S14);
|
a = rotateLeft(a + F(b, c, d) + X[8], S11);
|
d = rotateLeft(d + F(a, b, c) + X[9], S12);
|
c = rotateLeft(c + F(d, a, b) + X[10], S13);
|
b = rotateLeft(b + F(c, d, a) + X[11], S14);
|
a = rotateLeft(a + F(b, c, d) + X[12], S11);
|
d = rotateLeft(d + F(a, b, c) + X[13], S12);
|
c = rotateLeft(c + F(d, a, b) + X[14], S13);
|
b = rotateLeft(b + F(c, d, a) + X[15], S14);
|
|
//
|
// Round 2 - G cycle, 16 times.
|
//
|
a = rotateLeft(a + G(b, c, d) + X[0] + 0x5a827999, S21);
|
d = rotateLeft(d + G(a, b, c) + X[4] + 0x5a827999, S22);
|
c = rotateLeft(c + G(d, a, b) + X[8] + 0x5a827999, S23);
|
b = rotateLeft(b + G(c, d, a) + X[12] + 0x5a827999, S24);
|
a = rotateLeft(a + G(b, c, d) + X[1] + 0x5a827999, S21);
|
d = rotateLeft(d + G(a, b, c) + X[5] + 0x5a827999, S22);
|
c = rotateLeft(c + G(d, a, b) + X[9] + 0x5a827999, S23);
|
b = rotateLeft(b + G(c, d, a) + X[13] + 0x5a827999, S24);
|
a = rotateLeft(a + G(b, c, d) + X[2] + 0x5a827999, S21);
|
d = rotateLeft(d + G(a, b, c) + X[6] + 0x5a827999, S22);
|
c = rotateLeft(c + G(d, a, b) + X[10] + 0x5a827999, S23);
|
b = rotateLeft(b + G(c, d, a) + X[14] + 0x5a827999, S24);
|
a = rotateLeft(a + G(b, c, d) + X[3] + 0x5a827999, S21);
|
d = rotateLeft(d + G(a, b, c) + X[7] + 0x5a827999, S22);
|
c = rotateLeft(c + G(d, a, b) + X[11] + 0x5a827999, S23);
|
b = rotateLeft(b + G(c, d, a) + X[15] + 0x5a827999, S24);
|
|
//
|
// Round 3 - H cycle, 16 times.
|
//
|
a = rotateLeft(a + H(b, c, d) + X[0] + 0x6ed9eba1, S31);
|
d = rotateLeft(d + H(a, b, c) + X[8] + 0x6ed9eba1, S32);
|
c = rotateLeft(c + H(d, a, b) + X[4] + 0x6ed9eba1, S33);
|
b = rotateLeft(b + H(c, d, a) + X[12] + 0x6ed9eba1, S34);
|
a = rotateLeft(a + H(b, c, d) + X[2] + 0x6ed9eba1, S31);
|
d = rotateLeft(d + H(a, b, c) + X[10] + 0x6ed9eba1, S32);
|
c = rotateLeft(c + H(d, a, b) + X[6] + 0x6ed9eba1, S33);
|
b = rotateLeft(b + H(c, d, a) + X[14] + 0x6ed9eba1, S34);
|
a = rotateLeft(a + H(b, c, d) + X[1] + 0x6ed9eba1, S31);
|
d = rotateLeft(d + H(a, b, c) + X[9] + 0x6ed9eba1, S32);
|
c = rotateLeft(c + H(d, a, b) + X[5] + 0x6ed9eba1, S33);
|
b = rotateLeft(b + H(c, d, a) + X[13] + 0x6ed9eba1, S34);
|
a = rotateLeft(a + H(b, c, d) + X[3] + 0x6ed9eba1, S31);
|
d = rotateLeft(d + H(a, b, c) + X[11] + 0x6ed9eba1, S32);
|
c = rotateLeft(c + H(d, a, b) + X[7] + 0x6ed9eba1, S33);
|
b = rotateLeft(b + H(c, d, a) + X[15] + 0x6ed9eba1, S34);
|
|
H1 += a;
|
H2 += b;
|
H3 += c;
|
H4 += d;
|
|
//
|
// reset the offset and clean out the word buffer.
|
//
|
xOff = 0;
|
for (int i = 0; i != X.length; i++)
|
{
|
X[i] = 0;
|
}
|
}
|
|
|
|
private void processLength(final long bitLength)
|
{
|
if (xOff > 14)
|
{
|
processBlock();
|
}
|
|
X[14] = (int) (bitLength & 0xffffffff);
|
X[15] = (int) (bitLength >>> 32);
|
}
|
|
|
|
private void processWord(final byte[] in, final int inOff)
|
{
|
X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8)
|
| ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24);
|
|
if (xOff == 16)
|
{
|
processBlock();
|
}
|
}
|
|
|
|
/*
|
* rotate int x left n bits.
|
*/
|
private int rotateLeft(final int x, final int n)
|
{
|
return (x << n) | (x >>> 32 - n);
|
}
|
|
|
|
private void unpackWord(final int word, final byte[] out, final int outOff)
|
{
|
out[outOff] = (byte) word;
|
out[outOff + 1] = (byte) (word >>> 8);
|
out[outOff + 2] = (byte) (word >>> 16);
|
out[outOff + 3] = (byte) (word >>> 24);
|
}
|
}
|
|
|
|
/**
|
* Plugin configuration object.
|
*/
|
private SambaPasswordPluginCfg config;
|
|
/** The name of the Samba LanMan password attribute. */
|
private static final String SAMBA_LM_PASSWORD_ATTRIBUTE_NAME =
|
"sambaLMPassword";
|
|
/** The name of the Samba NT password attribute. */
|
private static final String SAMBA_NT_PASSWORD_ATTRIBUTE_NAME =
|
"sambaNTPassword";
|
|
/** The name of the Samba account object class. */
|
private static final String SAMBA_SAM_ACCOUNT_OC_NAME = "sambaSAMAccount";
|
|
/** The name of the Samba last password change attribute. */
|
private static final String SAMBA_PWD_LAST_SET_NAME = "sambaPwdLastSet";
|
|
/** Debug tracer object to log debugging information. */
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
/** Password Modify Extended Operation OID. */
|
private static final String PWMOD_EXTOP_OID = "1.3.6.1.4.1.4203.1.11.1";
|
|
/** Magic string to be used as salt. */
|
private static final String MAGIC_STR = "KGS!@#$%";
|
|
/** Default timestamp provider implementation. */
|
private static final TimeStampProvider DEFAULT_TIMESTAMP_PROVIDER =
|
new TimeStampProvider()
|
{
|
@Override
|
public long getCurrentTime()
|
{
|
return System.currentTimeMillis() / 1000L;
|
}
|
};
|
|
/** Use the default implementation of the timestamp provider... by default. */
|
private TimeStampProvider timeStampProvider = DEFAULT_TIMESTAMP_PROVIDER;
|
|
|
/**
|
* Add the parity to the 56-bit key converting it to 64-bit key.
|
*
|
* @param key56
|
* 56-bit key.
|
* @return 64-bit key.
|
*/
|
private static byte[] addParity(final byte[] key56)
|
{
|
final byte[] key64 = new byte[8];
|
final int[] key7 = new int[7];
|
final int[] key8 = new int[8];
|
|
for (int i = 0; i < 7; i++)
|
{
|
key7[i] = key56[i] & 0xFF;
|
}
|
|
key8[0] = key7[0];
|
key8[1] = ((key7[0] << 7) & 0xFF) | (key7[1] >> 1);
|
key8[2] = ((key7[1] << 6) & 0xFF) | (key7[2] >> 2);
|
key8[3] = ((key7[2] << 5) & 0xFF) | (key7[3] >> 3);
|
key8[4] = ((key7[3] << 4) & 0xFF) | (key7[4] >> 4);
|
key8[5] = ((key7[4] << 3) & 0xFF) | (key7[5] >> 5);
|
key8[6] = ((key7[5] << 2) & 0xFF) | (key7[6] >> 6);
|
key8[7] = key7[6] << 1;
|
|
for (int i = 0; i < 8; i++)
|
{
|
key64[i] = (byte) setOddParity(key8[i]);
|
}
|
|
return key64;
|
|
}
|
|
|
|
/**
|
* Create a LanMan hash from a clear-text password.
|
*
|
* @param password
|
* Clear-text password.
|
* @return Hex string version of the hash based on the clear-text password.
|
* @throws UnsupportedEncodingException
|
* if the <code>US-ASCII</code> coding is not available.
|
* @throws NoSuchAlgorithmException
|
* if the algorithm does not exist for the used provider.
|
* @throws InvalidKeyException
|
* if the key is inappropriate to initialize the cipher.
|
* @throws NoSuchPaddingException
|
* if the padding scheme is not available.
|
* @throws IllegalBlockSizeException
|
* if this encryption algorithm is unable to process the input data
|
* provided
|
* @throws BadPaddingException
|
* if this cipher is in decryption mode, and (un)padding has been
|
* requested, but the decrypted data is not bounded by the
|
* appropriate padding bytes
|
*/
|
private static String lmHash(final String password)
|
throws UnsupportedEncodingException, NoSuchAlgorithmException,
|
InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException,
|
BadPaddingException
|
{
|
// Password has to be OEM encoded and in upper case
|
final byte[] oemPass = password.toUpperCase().getBytes("US-ASCII");
|
|
// It shouldn't be longer then 14 bytes
|
int length = 14;
|
if (oemPass.length < length)
|
{
|
length = oemPass.length;
|
}
|
|
// The password should be divided into two 7-byte keys
|
final byte[] key1 = new byte[7];
|
final byte[] key2 = new byte[7];
|
if (length <= 7)
|
{
|
System.arraycopy(oemPass, 0, key1, 0, length);
|
}
|
else
|
{
|
System.arraycopy(oemPass, 0, key1, 0, 7);
|
System.arraycopy(oemPass, 7, key2, 0, length - 7);
|
}
|
|
// We create two DES keys using key1 and key2 to on the magic string
|
final SecretKey lowKey = new SecretKeySpec(addParity(key1), "DES");
|
final SecretKey highKey = new SecretKeySpec(addParity(key2), "DES");
|
final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
|
des.init(Cipher.ENCRYPT_MODE, lowKey);
|
final byte[] lowHash = des.doFinal(MAGIC_STR.getBytes());
|
des.init(Cipher.ENCRYPT_MODE, highKey);
|
final byte[] highHash = des.doFinal(MAGIC_STR.getBytes());
|
|
// We finally merge hashes and return them to the client
|
|
final byte[] lmHash = new byte[16];
|
System.arraycopy(lowHash, 0, lmHash, 0, 8);
|
System.arraycopy(highHash, 0, lmHash, 8, 8);
|
return toLowerCase(bytesToHexNoSpace(lmHash));
|
}
|
|
|
|
/**
|
* Creates a NTLM hash from a clear-text password.
|
*
|
* @param password
|
* Clear text password.
|
* @return Returns a NTLM hash.
|
* @throws NoSuchProviderException
|
* if the BouncyCastle provider does not load
|
* @throws NoSuchAlgorithmException
|
* if the MD4 algorithm is not found
|
* @throws UnsupportedEncodingException
|
* if the encoding <code>UnicodeLittleUnmarked</code> is not
|
* supported.
|
*/
|
private static String ntHash(final String password)
|
throws NoSuchProviderException, UnsupportedEncodingException,
|
NoSuchAlgorithmException
|
{
|
final byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked");
|
final MessageDigest md4 = new MD4MessageDigest();
|
return toLowerCase(bytesToHexNoSpace(md4.digest(unicodePassword)));
|
}
|
|
|
|
/**
|
* Set the parity bit for an integer.
|
*
|
* @param integer
|
* to add the parity bit for.
|
* @return integer with the parity bit set.
|
*/
|
private static int setOddParity(final int parity)
|
{
|
final boolean hasEvenBits = (parity >>> 7 ^ parity >>> 6
|
^ parity >>> 5 ^ parity >>> 4
|
^ parity >>> 3 ^ parity >>> 2
|
^ ((parity >>> 1) & 0x01)) == 0;
|
if (hasEvenBits)
|
{
|
return parity | 0x01;
|
}
|
else
|
{
|
return parity & 0xFE;
|
}
|
}
|
|
|
|
/**
|
* Default constructor.
|
*/
|
public SambaPasswordPlugin()
|
{
|
super();
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public ConfigChangeResult applyConfigurationChange(
|
final SambaPasswordPluginCfg newConfig)
|
{
|
|
// No validation required and no restart required.
|
config = newConfig;
|
|
return new ConfigChangeResult();
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public PluginResult.PostOperation doPostOperation(
|
final PostOperationExtendedOperation extendedOperation)
|
{
|
/*
|
* If the operation is not Password Modify Extended Operation then skip this
|
* operation.
|
*/
|
if (!extendedOperation.getRequestOID().equals(PWMOD_EXTOP_OID))
|
{
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
/*
|
* If the operation has not been successful then ignore the operation.
|
*/
|
if (extendedOperation.getResultCode() != ResultCode.SUCCESS)
|
{
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
/*
|
* Verify if the operation has been initiated by what was defined as Samba
|
* administrative user. If so, we will skip this operation to avoid double
|
* synchronization of Samba attributes.
|
*/
|
final DN authDN = extendedOperation.getAuthorizationDN();
|
final DN sambaAdminDN = config.getSambaAdministratorDN();
|
if (sambaAdminDN != null
|
&& !sambaAdminDN.isRootDN()
|
&& authDN.equals(sambaAdminDN))
|
{
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("This operation will be skipped because"
|
+ " it was performed by Samba admin user: " + sambaAdminDN);
|
}
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
// Get the name of the entry and clear passwords from the operation
|
// attachments.
|
final DN dn = (DN) extendedOperation
|
.getAttachment(PasswordModifyExtendedOperation.AUTHZ_DN_ATTACHMENT);
|
if (dn == null)
|
{
|
// The attachment is missing which should never happen.
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("SambaPasswordPlugin: missing DN attachment");
|
}
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
final String password = extendedOperation.getAttachment(
|
PasswordModifyExtendedOperation.CLEAR_PWD_ATTACHMENT).toString();
|
if (password == null)
|
{
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("SambaPasswordPlugin: skipping syncing "
|
+ "pre-encoded password");
|
}
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
@SuppressWarnings("unchecked")
|
final List<ByteString> encPasswords = (List<ByteString>) extendedOperation
|
.getAttachment(PasswordModifyExtendedOperation.ENCODED_PWD_ATTACHMENT);
|
|
try
|
{
|
// Before proceeding make sure this entry has samba object class.
|
final Entry entry = DirectoryServer.getEntry(dn);
|
if (!isSynchronizable(entry))
|
{
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("The entry is not Samba object.");
|
}
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
/*
|
* Make an internal connection to process the password modification. It
|
* will not trigger this plugin again with the pre-operation modify since
|
* the password passed would be encoded hence the pre operation part would
|
* skip it.
|
*/
|
final InternalClientConnection connection = InternalClientConnection
|
.getRootConnection();
|
|
final List<Modification> modifications = getModifications(password);
|
|
// Use an assertion control to avoid race conditions since extended
|
// operation post-ops are done outside of the write lock.
|
List<Control> controls = null;
|
if (!encPasswords.isEmpty())
|
{
|
final AttributeType pwdAttribute = (AttributeType) extendedOperation
|
.getAttachment(
|
PasswordModifyExtendedOperation.PWD_ATTRIBUTE_ATTACHMENT);
|
final LDAPFilter filter = RawFilter.createEqualityFilter(
|
pwdAttribute.getNameOrOID(), encPasswords.get(0));
|
final Control assertionControl = new LDAPAssertionRequestControl(true,
|
filter);
|
controls = Collections.singletonList(assertionControl);
|
}
|
|
final ModifyOperation modifyOperation = connection.processModify(dn,
|
modifications, controls);
|
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("rc=%s", modifyOperation.getResultCode());
|
}
|
}
|
catch (final DirectoryException e)
|
{
|
/*
|
* This exception occurs if there is a problem while retrieving the entry.
|
* This should never happen as we are processing the post-operation which
|
* succeeded so the entry has to exist if we have reached this point.
|
*/
|
logger.traceException(e);
|
}
|
|
return PluginResult.PostOperation.continueOperationProcessing();
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public PluginResult.PreOperation doPreOperation(
|
final PreOperationModifyOperation modifyOperation)
|
{
|
/*
|
* If the passwords are changed in clear text they will be available with
|
* the getNewPasswords() method. If they are encoded the method would return
|
* null. The list of passwords should not be modified.
|
*/
|
final List<ByteString> passwords = modifyOperation.getNewPasswords();
|
|
/*
|
* If the password list is not empty, we can be sure the current operation
|
* is the one that applies to our case: - it's a modify operation on
|
* userPassword attribute - it's replaces or adds new userPassword attribute
|
* value. If it doesn't then we skip this modify operation.
|
*/
|
if (passwords == null)
|
{
|
return PluginResult.PreOperation.continueOperationProcessing();
|
}
|
|
// Skip synchronization operations.
|
if (modifyOperation.isSynchronizationOperation())
|
{
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("Synchronization operation. Skipping.");
|
}
|
return PluginResult.PreOperation.continueOperationProcessing();
|
}
|
|
/*
|
* Verify if the operation has been initiated by the Samba administrative
|
* user. If so, we will skip this operation to avoid double synchronization
|
* of Samba attributes.
|
*/
|
final DN authDN = modifyOperation.getAuthorizationDN();
|
final DN sambaAdminDN = config.getSambaAdministratorDN();
|
if (sambaAdminDN != null
|
&& !sambaAdminDN.isRootDN()
|
&& authDN.equals(sambaAdminDN))
|
{
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("This operation will be skipped because"
|
+ " it was performed by Samba admin user: " + sambaAdminDN);
|
}
|
return PluginResult.PreOperation.continueOperationProcessing();
|
}
|
|
/*
|
* Before proceeding with the modification, we have to make sure this entry
|
* is indeed a Samba object.
|
*/
|
if (!isSynchronizable(modifyOperation.getCurrentEntry()))
|
{
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("Skipping '%s' because it does not have Samba object class.", modifyOperation.getEntryDN());
|
}
|
return PluginResult.PreOperation.continueOperationProcessing();
|
}
|
|
/*
|
* Proceed with processing: add a new modification to the current modify
|
* operation, so they could be executed at the same time.
|
*/
|
processModification(modifyOperation, passwords);
|
|
// Continue plugin processing.
|
return PluginResult.PreOperation.continueOperationProcessing();
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public void initializePlugin(final Set<PluginType> pluginTypes,
|
final SambaPasswordPluginCfg configuration) throws ConfigException,
|
InitializationException
|
{
|
|
// Verify config parameters.
|
final LinkedList<LocalizableMessage> messages = new LinkedList<>();
|
if (!isConfigurationAcceptable(configuration, messages))
|
{
|
for (final LocalizableMessage m : messages)
|
{
|
logger.error(m);
|
}
|
throw new ConfigException(messages.poll());
|
}
|
|
// Register the configuration change listener.
|
configuration.addSambaPasswordChangeListener(this);
|
|
// Save the configuration.
|
this.config = configuration;
|
}
|
|
|
|
/**
|
* Verifies if the plugin configuration is acceptable.
|
*
|
* @param configuration
|
* The plugin configuration.
|
* @param unacceptableReasons
|
* Reasons why the configuration is not acceptable.
|
* @return Returns <code>true</code> for the correct configuration and
|
* <code>false</code> for the incorrect one.
|
*/
|
public boolean isConfigurationAcceptable(
|
final SambaPasswordPluginCfg configuration,
|
final List<LocalizableMessage> unacceptableReasons)
|
{
|
return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
|
}
|
|
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean isConfigurationChangeAcceptable(
|
final SambaPasswordPluginCfg newConfig, final List<LocalizableMessage> messages)
|
{
|
/*
|
* The plugin implements only postoperationmodify and postoperationextended
|
* plugin types. The rest should be rejected.
|
*/
|
|
final SortedSet<PluginCfgDefn.PluginType> pluginTypes = newConfig
|
.getPluginType();
|
for (final PluginCfgDefn.PluginType t : pluginTypes)
|
{
|
switch (t)
|
{
|
case PREOPERATIONMODIFY:
|
case POSTOPERATIONEXTENDED:
|
break;
|
default:
|
messages.add(ERR_PLUGIN_SAMBA_SYNC_INVALID_PLUGIN_TYPE.get(t));
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
|
|
/**
|
* Creates the modifications to modify Samba password attributes. It uses
|
* clear-text password and encodes it with the appropriate algorithm for it's
|
* respective type (NTLM or LanMan); then it wraps it in the modifications to
|
* be added to the modify operation.
|
*
|
* @param password
|
* New password which is to be encoded for Samba.
|
* @return Returns a list of modifications, or null if a problem occurs.
|
*/
|
private List<Modification> getModifications(final String password)
|
{
|
ArrayList<Modification> modifications = new ArrayList<>();
|
try
|
{
|
if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_NT_PASSWORD))
|
{
|
final Attribute attribute = Attributes.create(
|
SAMBA_NT_PASSWORD_ATTRIBUTE_NAME, ntHash(password));
|
modifications.add(new Modification(ModificationType.REPLACE, attribute));
|
}
|
|
if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_LM_PASSWORD))
|
{
|
final Attribute attribute = Attributes.create(
|
SAMBA_LM_PASSWORD_ATTRIBUTE_NAME, lmHash(password));
|
modifications.add(new Modification(ModificationType.REPLACE, attribute));
|
}
|
final Attribute pwdLastSet = Attributes.create(
|
SAMBA_PWD_LAST_SET_NAME,
|
String.valueOf(timeStampProvider.getCurrentTime()));
|
modifications.add(new Modification(ModificationType.REPLACE, pwdLastSet));
|
}
|
catch (final Exception e)
|
{
|
logger.info(ERR_PLUGIN_SAMBA_SYNC_ENCODING, e.getMessage(), e);
|
modifications = null;
|
}
|
|
return modifications;
|
}
|
|
|
|
/**
|
* Verify if the target entry contains pre-defined Samba object class.
|
*
|
* @param entry
|
* The entry being modified.
|
* @return Returns true if the entry has Samba object class, otherwise returns
|
* false.
|
*/
|
private boolean isSynchronizable(final Entry entry)
|
{
|
final Schema schema = DirectoryServer.getSchema();
|
final ObjectClass sambaOc = schema
|
.getObjectClass(toLowerCase(SAMBA_SAM_ACCOUNT_OC_NAME));
|
return sambaOc != null && entry.hasObjectClass(sambaOc);
|
}
|
|
|
|
/**
|
* Adds modifications for the configured Samba password attributes to the
|
* current modify operation.
|
*
|
* @param modifyOperation
|
* Current modify operation which will be modified to add Samba
|
* password attribute changes.
|
* @param passwords
|
* List of userPassword clear-text attribute values to be hashed for
|
* Samba
|
*/
|
private void processModification(
|
final PreOperationModifyOperation modifyOperation,
|
final List<ByteString> passwords)
|
{
|
// Get the last password (in case there is more then one).
|
final String password = passwords.get(passwords.size() - 1).toString();
|
try
|
{
|
// Generate the necessary modifications.
|
for (final Modification modification : getModifications(password))
|
{
|
modifyOperation.addModification(modification);
|
}
|
}
|
catch (final DirectoryException e)
|
{
|
logger.info(ERR_PLUGIN_SAMBA_SYNC_MODIFICATION_PROCESSING, e.getMessage(), e);
|
}
|
}
|
|
/**
|
* Timestamp provider interface. Intended primarily for testing purposes.
|
*/
|
static interface TimeStampProvider
|
{
|
/**
|
* Generates a custom time stamp.
|
*
|
* @return A timestamp in UNIX format.
|
*/
|
long getCurrentTime();
|
}
|
|
/**
|
* Use custom timestamp provider. Intended primarily for testing purposes.
|
*
|
* @param timeStampProvider Provider object that implements the
|
* TimeStampProvider interface.
|
*/
|
void setTimeStampProvider(TimeStampProvider timeStampProvider)
|
{
|
this.timeStampProvider = (timeStampProvider == null)
|
? DEFAULT_TIMESTAMP_PROVIDER : timeStampProvider;
|
}
|
}
|