Interact with ActiveDirectory through Powershell over SSH with Java

Alexander Bij

Active Directory is Microsoft's implementation of LDAP. LDAP is often used for user authentication and authorization. There are options to modify the LDAP directly from Java without the Powershell. With Spring-LDAP in combination with Spring-ODM you can read, write and query the AD (ActiveDirectory). We ‘developers’ like it, but operations don’t.

Using such an API is low-level and you have to make sure when manipulating the AD you set the right fields and attributes:

  • You have to know the required fields (unicodePwd, instead of userPassword)
  • You must understand the encoded fields like userAccountControl, password in bytes
  • You as developer are responsible for the interaction, and you can screw the AD.
  • You must fix problems when operations may change, update or upgrade AD.

Most important in our case is that operations are the owners of AD and responsible for it. They strongly suggested to use the Powershell when modifying user accounts. The Powershell is running on the Domain Controller where AD is located and can be used as an API. It only runs on Windows, but we want to host our Java software on Linux, so we decided to communicate over SSH.

1. You need SSH-server on the Domain Controller
The point of the SSH-server is to connect securely and when the client is connected don’t use the cmd.exe, but the powershell.exe. In all the products it is possible to change cmd -> powershell. We have tried different SSH-server applications. Here is a list we tried and with our experience:

  • Cygwin + OpenSSH, free do-it-yourself solution. Not liked by Windows operators.
  • FreeSSHd, free but did not work after installing.
  • Powershellinside, did work, but we experience some problems interacting through SSH. We reported the issue and was fixed within a week. In the mean time we tried other products.
  • Bitvise ssh-server, this became our choice it has a GUI and just works.

After setting up the tool, sit next to the operations guy until you can connect with PuTTy, login and see the powershell. Make user the module ActiveDirectory is available by typing in the powershell:
import-module ActiveDirectory
If not, the operation guy needs to make sure it is installed: http://blogs.msdn.com/b/rkramesh/archive/2012/01/17/how-to-add-active-directory-module-in-powershell-in-windows-7.aspx

2. Connect from Java over SSH:
A good Java API is Jsch. Beside thestandard version there is rewrite from vngx:
“It has been updated to Java 6 with all the latest language features and improved code clarity.” In this example I will use the rewritten version of Jsch, both are available in the mavenrepo.
Thrust the server by IP?
When connecting with PuTTy, you are asked if you thrust the server’s fingerprint:
The server's host key is not cached in the registry. You
have no guarantee that the server is the computer you
think it is.
The server's key fingerprint is:
ssh-rsa 1024 7b:e5:6f:a7:f4:f9:81:62:5c:e3:1f:bf:8b:57:6c:5a
If you trust this host, hit Yes to add the key to
PuTTY's cache and carry on connecting.
If you want to carry on connecting just once, without
adding the key to the cache, hit No.
If you do not trust this host, hit Cancel to abandon the
connection.

When connecting with Jsch you can do the same. You have to provide the knownHostFile which contains the fingerprints of servers you ‘know’. On linux you can find the file under ~/.ssh/known_hosts. On Windows PuTTy will store the fingerprints in the registry. I did use Cygwin and connect with ssh to the domain controller and in a subfolder of cygwin the known_hosts file appears.

In my code-example I check if the file is set otherwise don’t check the servers fingerprint when connecting.

Jsch channel type shell
We want to interact with the console, write a command and check the resultmessage. When using the shell channeltype it is treated as a shell-window with a width and height. The reads from the server contain control-characters for location, enters, colors etc. I didn’t find a way to make it work while setting setPty(false) on the session. With that setting only the first read will work without control characters, but after that no more input is read. To interact with the ActiveDirectory the corresponding module is must be loaded first. I have experience that it will take a while ~1,5 seconds. It needs to happen only once, which you don’t want for every command you execute. To make it perform well, I chose to hold 1 session and 1 channel open and only load the module the first time.

3. Create a user! and remove it.
All we need to do is send the 'New-ADUser'-command to the powershell and let AD do the work. We want to check if the command was successful or use the error message. With the command $? you can check the status of the last command. True -> Oke and False -> something happend... If the command was not successful I use the previous message from the server which contains the error. Then a new PowershellException is thrown with the previous message. In the PowershellException the the command and the root cause message are parsed to a readable format, without the control characters.

You can try to execute the command in the Powershell or in PuTTy first (with module ActiveDirectory loaded!):

New-ADUser -SamAccountName "abij" -Name "abij" -DisplayName "Alexander" -EmailAddress "Fake@Email.com" -AccountPassword (ConvertTo-SecureString -AsPlainText "Welkcome01" -Force) -ChangePasswordAtLogon $false -PasswordNeverExpires $true -Enabled $true -path "ou=Users"

Remove-ADUser -Identity "abij" -Confirm:$false

You can do a lot with the module ActiveDirectory, check all available options

4. The code
Check https://github.com/abij/ad-powershell-over-ssh for the full code example. Use maven to handle the dependencies and than you can use the PowershellServiceIntegrationTest to interact with your AD.

public class PowershellService {

    private String adUserPath;

    private static final String CREATE_USER =
            "New-ADUser -SamAccountName \"${samAccountName}\"" +
            " -Name \"${name}\"" +
            " -DisplayName \"${displayname}\"" +
            " -EmailAddress \"${email}\"" +
            " -AccountPassword (ConvertTo-SecureString -AsPlainText \"${passwordPlain}\" -Force)" +
            " -ChangePasswordAtLogon $false -PasswordNeverExpires $true -Enabled $true" +
            " -path \"${adUserPath}\"";

    private static final String REMOVE_USER =
            "Remove-ADUser -identity \"${samAccountName}\" -Confirm:$false";

    public void createUser(String displayname, String email, String username, String password) throws JSchException, IOException {
        Map<String, String> model = new LinkedHashMap<String, String>();
        model.put("samAccountName", username);
        model.put("name", username);
        model.put("password", password);
        model.put("displayname", displayname);
        model.put("email", email);
        model.put("passwordPlain", password);
        model.put("adUserPath", adUserPath);

        PowershellSession.getInstance().execute(commando(CREATE_USER, model));
    }

    public void removeUser(String username) throws JSchException, IOException {
        Map<String, String> model = new LinkedHashMap<String, String>();
        model.put("samAccountName", username);

        PowershellSession.getInstance().execute(commando(REMOVE_USER, model));
    }

    /**
     * Fill the given String with the model.
     * Template "Hi ${foo}", with model ("foo" -> "bar") will be result in: "Hi bar".
     *
     * @throws IllegalStateException when not all replace-fields are filled.
     * @see StringUtils#replaceEach(String, String[], String[])
     */
    private String commando(String template, Map<String, String> model) {
        List<String> searchList = new ArrayList<String>(model.size());
        for (String key : model.keySet()) {
            searchList.add("${" + key + "}");
        }
        String[] replaceList = model.values().toArray(new String[model.size()]);
        String command = StringUtils.replaceEach(template, searchList.toArray(new String[model.size()]), replaceList);

        //TODO Nicer: pattern matching ${.+}
        if (command.contains("${")) {
            throw new IllegalStateException("Command contains unfilled parameters: " + command);
        }
        return command;
    }

    public void setAdUserPath(String adUserPath) {
        this.adUserPath = adUserPath;
    }
}

And the corresponding session:

/**
 * PowerShell session as a Singleton. With synchronization around execute that allows
 * this impl to perform 1 command at the time over 1 channel. With Jsch you can
 * open multiple channels and execute commands in parallel.
 *
 * Nothing will stop you to modify this example to your needs.
 */
public class PowershellSession {

    private static final Logger LOG = LoggerFactory.getLogger(PowershellSession.class);

    // There is only 1 session, with getInstance() you can use it.
    private PowershellSession() {}
    private static final PowershellSession INSTANCE = new PowershellSession();
    public static PowershellSession getInstance() {
        return INSTANCE;
    }

    private static final int DEFAULT_BUFFER_SIZE = 1024; 
    private static final Charset UTF8 = Charset.forName("UTF-8");
    private static final int AD_MODULE_LOADINGTIME = 1500; // 1,5 sec wait for loading AD module.

    // required settings
    private String username;
    private byte[] password;
    private String host;

    // optional settings with defaults:
    private int port = 22;
    private String knownHostFile = null; // No knownHostFile -> don't check server fingerprint.
    private int socketTimeout = 5 * 1000;// 5s.
    private int readTimeout = 3 * 1000;  // 3s max time wait & read from server
    private int pollTimeout = 20;        // 20 ms before read again.

    private Session session;
    private ChannelShell shell;
    private InputStream fromServer;
    private OutputStream toServer;

    public void init() throws JSchException {
        JSch jSch = JSch.getInstance();

        SessionConfig config = new SessionConfig();
        config.setProperty(SSHConfigConstants.PREFFERED_AUTHS, "password");
        if (knownHostFile == null) {
            config.setProperty(SSHConfigConstants.STRICT_HOST_KEY_CHECKING, "no");
        }
        session = jSch.createSession(username, host, port, config);
    }

    public void disconnect() {
        if (shell != null && shell.isConnected()) {
            shell.disconnect();
            LOG.debug("Channel Shell is disconnected.");
        }
        if (session != null && session.isConnected()) {
            session.disconnect();
            LOG.debug("Session is disconnected.");
        }
    }

    public synchronized void execute(String command) throws JSchException, IOException {
        checkConnection();
        writeToServer(command);
        verifyCommandSucceded();
    }

  // ******* Helper methodes ******************************************************

    private void checkConnection() throws JSchException, IOException {
        if (!session.isConnected()) {
            session.connect(socketTimeout, password);
            LOG.debug("Session connected to host: {}", session.getHost());
        } else {
            LOG.debug("Session is still connected.");
        }
        if (shell == null || !shell.isConnected()) {
            shell = session.openChannel(ChannelType.SHELL);
            shell.connect(socketTimeout);
            LOG.debug("ChannelShell is connected.");

            fromServer = shell.getInputStream();
            toServer = shell.getOutputStream();

            readFromServer(); // Read initial data: Windows PowerShell ... All rights reserved.
            loadModuleActiveDirectory();
        } else {
            LOG.debug("Channel (shell) still open.");
        }
    }

    private void loadModuleActiveDirectory() throws IOException {
        LOG.debug("import-module ActiveDirectory...");
        writeToServer("import-module ActiveDirectory");
        sleep(AD_MODULE_LOADINGTIME, "Failed to sleep after loading module ActiveDirectory.");
        verifyCommandSucceded();
    }

    private void writeToServer(String command) throws IOException {
        String commandWithEnter = command;
        if (!command.endsWith("\r\n")) {
            commandWithEnter += "\r\n";
        }
        toServer.write((commandWithEnter).getBytes(UTF8));
        toServer.flush();
    }

    private String readFromServer() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

        String linePrompt = "\\" + username + ">"; // indicates console has new-line, stop reading.
        long timeout = System.currentTimeMillis() + readTimeout;

        while (System.currentTimeMillis() < timeout
               && !Util.byte2str(bos.toByteArray()).contains(linePrompt)) {
            while (fromServer.available() > 0) {
                int count = fromServer.read(buffer, 0, DEFAULT_BUFFER_SIZE);
                if (count >= 0) {
                    bos.write(buffer, 0, count);
                } else {
                    break;
                }
            }
            if (shell.isClosed()) {
                break;
            }
            // Don't spin like crazy though the while loop
            sleep(pollTimeout, "Failed to sleep between reads with pollTimeout: " + pollTimeout);
        }
        String result = bos.toString("UTF-8");
        LOG.debug("read from server:\n{}", result);
        return result;
    }

    /**
     * @throws PowershellException If the message was not executed successfully, with details info.
     */
    private void verifyCommandSucceded() throws IOException {
        String message = readFromServer();
        writeToServer("$?"); // Aks powershell status of last command?

        if (!readFromServer().contains("True")) {
            throw new PowershellException(message);
        }
    }

    private void sleep(long timeout, String msg) {
        try {
            Thread.sleep(timeout);
        } catch (Exception ee) {
            LOG.debug(msg);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        disconnect(); // Always disconnect!
    }

  // ******* Setters **********************************************************************
    public void setUsername(String username) {this.username = username;}
    public void setPassword(String password) {this.password = password.getBytes(UTF8);}
    public void setHost(String host) {this.host = host;}
    public void setPort(int port) {this.port = port;}
    public void setKnownHostFile(String knownHostFile) {this.knownHostFile = knownHostFile;}
    public void setSocketTimeout(int socketTimeout) {this.socketTimeout = socketTimeout;}
    public void setSession(Session session) {this.session = session;}
    public void setReadTimeout(int readTimeout) {this.readTimeout = readTimeout;}
    public void setPollTimeout(int pollTimeout) {this.pollTimeout = pollTimeout;}
}

Comments (3)

  1. Vincent Partington - Reply

    November 14, 2012 at 12:30 pm

    Hi Alexander,

    Interesting stuff! :-)

    To connect to Windows machine, you might want to have a look at Overthere Overthere, the open source remoting framework we've built at XebiaLabs to work with remote Unix and Windows machines. It supports OpenSSH, WinSSHD, WinRM and Telnet in a transparent manner. WinRM is especially useful as it is usually available everywhere PowerShell is available.

    Also, we've added capability to invoke PowerShell scripts to version 3.7 of Deployit and are now building more and more content on top of that. Deployit 3.8 includes out-of-the-box content for IIS and basic Windows administration and we are adding support for BizTalk now. If you want to know more, drop me an email on vpartington@xebialabs.com.

    Regards, Vincent.

  2. Deepak - Reply

    September 28, 2013 at 1:14 pm

    I am getting connection refused. I am testing my own machine. winrm is running. jsch connects to 22 port. I tried changing to 80 and few other but no luck.
    One question, jsch connects over ssh so is that mean my windows machine should run a ssh server first?

  3. Alexander Bij - Reply

    September 30, 2013 at 2:57 pm

    Yes, If you want to run and try it on you local machine you must start a SSH-server.
    Here (http://www.powershellserver.com) you can download a server with 1 connection for free.

    WinRM is a tool to do remote management with SOAP calls. This implementation with Jsch does not use WinRM. The commando's it sends are only for the Powershell.

Add a Comment