001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.protocols.jmx;
028
029import static org.opends.messages.ProtocolMessages.*;
030
031import java.util.ArrayList;
032
033import javax.management.remote.JMXAuthenticator;
034import javax.security.auth.Subject;
035
036import org.forgerock.i18n.LocalizableMessage;
037import org.forgerock.i18n.slf4j.LocalizedLogger;
038import org.forgerock.opendj.ldap.ByteString;
039import org.forgerock.opendj.ldap.ResultCode;
040import org.opends.messages.CoreMessages;
041import org.opends.server.api.plugin.PluginResult;
042import org.opends.server.core.BindOperationBasis;
043import org.opends.server.core.DirectoryServer;
044import org.opends.server.core.PluginConfigManager;
045import org.opends.server.protocols.ldap.LDAPResultCode;
046import org.opends.server.types.AuthenticationInfo;
047import org.opends.server.types.Control;
048import org.opends.server.types.DN;
049import org.opends.server.types.DisconnectReason;
050import org.opends.server.types.LDAPException;
051import org.opends.server.types.Privilege;
052
053/**
054 * A <code>RMIAuthenticator</code> manages authentication for the secure
055 * RMI connectors. It receives authentication requests from clients as a
056 * SASL/PLAIN challenge and relies on a SASL server plus the local LDAP
057 * authentication accept or reject the user being connected.
058 */
059public class RmiAuthenticator implements JMXAuthenticator
060{
061  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
062
063    /**
064     * Indicate if the we are in the finalized phase.
065     *
066     * @see JmxConnectionHandler
067     */
068    private boolean finalizedPhase;
069
070  /** The JMX Client connection to be used to perform the bind (auth) call. */
071  private JmxConnectionHandler jmxConnectionHandler;
072
073  /**
074   * Constructs a <code>RmiAuthenticator</code>.
075   *
076   * @param jmxConnectionHandler
077   *        The jmxConnectionHandler associated to this RmiAuthenticator
078   */
079  public RmiAuthenticator(JmxConnectionHandler jmxConnectionHandler)
080  {
081    this.jmxConnectionHandler = jmxConnectionHandler;
082  }
083
084  /**
085   * Set that we are in the finalized phase.
086   *
087   * @param finalizedPhase Set to true, it indicates that we are in
088   * the finalized phase that that we other connection should be accepted.
089   *
090   * @see JmxConnectionHandler
091   */
092  public synchronized void setFinalizedPhase(boolean finalizedPhase)
093  {
094    this.finalizedPhase = finalizedPhase;
095  }
096
097  /**
098   * Authenticates a RMI client. The credentials received are composed of
099   * a SASL/PLAIN authentication id and a password.
100   *
101   * @param credentials
102   *            the SASL/PLAIN credentials to validate
103   * @return a <code>Subject</code> holding the principal(s)
104   *         authenticated
105   */
106  @Override
107  public Subject authenticate(Object credentials)
108  {
109    // If we are in the finalized phase, we should not accept new connection
110    if (finalizedPhase
111        || credentials == null)
112    {
113      throw new SecurityException();
114    }
115    Object c[] = (Object[]) credentials;
116    String authcID = (String) c[0];
117    String password = (String) c[1];
118
119    // The authcID is used at forwarder level to identify the calling client
120    if (authcID == null)
121    {
122      logger.trace("User name is Null");
123      throw new SecurityException();
124    }
125    if (password == null)
126    {
127      logger.trace("User password is Null ");
128      throw new SecurityException();
129    }
130
131    logger.trace("UserName = %s", authcID);
132
133    // Try to see if we have an Ldap Authentication
134    // Which should be the case in the current implementation
135    JmxClientConnection jmxClientConnection;
136    try
137    {
138      jmxClientConnection = bind(authcID, password);
139    }
140    catch (Exception e)
141    {
142      logger.traceException(e);
143      SecurityException se = new SecurityException(e.getMessage());
144      throw se;
145    }
146
147    // If we've gotten here, then the authentication was successful.
148    // We'll take the connection so invoke the post-connect plugins.
149    PluginConfigManager pluginManager = DirectoryServer.getPluginConfigManager();
150    PluginResult.PostConnect pluginResult = pluginManager.invokePostConnectPlugins(jmxClientConnection);
151    if (!pluginResult.continueProcessing())
152    {
153      jmxClientConnection.disconnect(pluginResult.getDisconnectReason(),
154          pluginResult.sendDisconnectNotification(),
155          pluginResult.getErrorMessage());
156
157      if (logger.isTraceEnabled())
158      {
159        logger.trace("Disconnect result from post connect plugins: " +
160            "%s: %s ", pluginResult.getDisconnectReason(),
161            pluginResult.getErrorMessage());
162      }
163
164      throw new SecurityException();
165    }
166
167    // initialize a subject
168    Subject s = new Subject();
169
170    // Add the Principal. The current implementation doesn't use it
171    s.getPrincipals().add(new OpendsJmxPrincipal(authcID));
172
173    // add the connection client object
174    // this connection client is used at forwarder level to identify the calling client
175    s.getPrivateCredentials().add(new Credential(jmxClientConnection));
176
177    return s;
178  }
179
180  /**
181   * Process bind operation.
182   *
183   * @param authcID
184   *            The LDAP user.
185   * @param password
186   *            The Ldap password associated to the user.
187   */
188  private JmxClientConnection bind(String authcID, String password)
189  {
190    try
191    {
192      DN.valueOf(authcID);
193    }
194    catch (Exception e)
195    {
196      LDAPException ldapEx = new LDAPException(
197          LDAPResultCode.INVALID_CREDENTIALS,
198          CoreMessages.INFO_RESULT_INVALID_CREDENTIALS.get());
199      throw new SecurityException(ldapEx);
200    }
201
202    ArrayList<Control> requestControls = new ArrayList<>();
203    ByteString bindPW = password != null ? ByteString.valueOfUtf8(password) : null;
204
205    AuthenticationInfo authInfo = new AuthenticationInfo();
206    JmxClientConnection jmxClientConnection = new JmxClientConnection(
207        jmxConnectionHandler, authInfo);
208
209    BindOperationBasis bindOp = new BindOperationBasis(jmxClientConnection,
210        jmxClientConnection.nextOperationID(),
211        jmxClientConnection.nextMessageID(), requestControls,
212        jmxConnectionHandler.getRMIConnector().getProtocolVersion(),
213        ByteString.valueOfUtf8(authcID), bindPW);
214
215    bindOp.run();
216    if (bindOp.getResultCode() == ResultCode.SUCCESS)
217    {
218      logger.trace("User is authenticated");
219
220      authInfo = bindOp.getAuthenticationInfo();
221      jmxClientConnection.setAuthenticationInfo(authInfo);
222
223      // Check JMX_READ privilege.
224      if (! jmxClientConnection.hasPrivilege(Privilege.JMX_READ, null))
225      {
226        LocalizableMessage message = ERR_JMX_INSUFFICIENT_PRIVILEGES.get();
227
228        jmxClientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED,
229            false, message);
230
231        throw new SecurityException(message.toString());
232      }
233      return jmxClientConnection;
234    }
235    else
236    {
237      // Set the initcause.
238      LDAPException ldapEx = new LDAPException(
239          LDAPResultCode.INVALID_CREDENTIALS,
240          CoreMessages.INFO_RESULT_INVALID_CREDENTIALS.get());
241      SecurityException se = new SecurityException("return code: " + bindOp.getResultCode());
242      se.initCause(ldapEx);
243      throw se;
244    }
245  }
246}