SambaPasswordPlugin.java revision f983fc4bc7a4dc0e9d175e77cfaf8a2127aaeb2d
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2011-2012 profiq s.r.o.
* Portions Copyright 2011-2015 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.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.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;
private int H1, H2, H3, H4; // IV's
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<LocalizableMessage>();
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<Modification>();
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;
}
}