content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.cassandra.auth;
  
  import java.io.IOException;
  import java.util.HashMap;
  import java.util.Map;
  import javax.security.auth.Subject;
  import javax.security.auth.callback.*;
  import javax.security.auth.login.FailedLoginException;
  import javax.security.auth.login.LoginException;
  import javax.security.auth.spi.LoginModule;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import org.apache.cassandra.config.DatabaseDescriptor;
  import org.apache.cassandra.exceptions.AuthenticationException;
  import org.apache.cassandra.service.StorageService;
  
  /**
   * LoginModule which authenticates a user towards the Cassandra database using
   * the internal authentication mechanism.
   */
  public class CassandraLoginModule implements LoginModule
  {
      private static final Logger logger = LoggerFactory.getLogger(CassandraLoginModule.class);
  
      // initial state
      private Subject subject;
      private CallbackHandler callbackHandler;
  
      // the authentication status
      private boolean succeeded = false;
      private boolean commitSucceeded = false;
  
      // username and password
      private String username;
      private char[] password;
  
      private CassandraPrincipal principal;
  
      /**
       * Initialize this {@code}LoginModule{@code}.
       *
       * @param subject the {@code}Subject{@code} to be authenticated. <p>
       * @param callbackHandler a {@code}CallbackHandler{@code} for communicating
       *        with the end user (prompting for user names and passwords, for example)
       * @param sharedState shared {@code}LoginModule{@code} state. This param is unused.
       * @param options options specified in the login {@code}Configuration{@code} for this particular
       *        {@code}LoginModule{@code}. This param is unused
       */
      @Override
      public void initialize(Subject subject,
                             CallbackHandler callbackHandler,
                             Map<java.lang.String, ?> sharedState,
                             Map<java.lang.String, ?> options)
      {
          this.subject = subject;
          this.callbackHandler = callbackHandler;
      }
  
      /**
       * Authenticate the user, obtaining credentials from the CallbackHandler
       * supplied in {@code}initialize{@code}. As long as the configured
       * {@code}IAuthenticator{@code} supports the optional
       * {@code}legacyAuthenticate{@code} method, it can be used here.
       *
       * @return true in all cases since this {@code}LoginModule{@code}
       *         should not be ignored.
       * @exception FailedLoginException if the authentication fails.
       * @exception LoginException if this {@code}LoginModule{@code} is unable to
       * perform the authentication.
       */
      @Override
      public boolean login() throws LoginException
      {
          // prompt for a user name and password
          if (callbackHandler == null)
          {
              logger.info("No CallbackHandler available for authentication");
              throw new LoginException("Authentication failed");
          }
  
          NameCallback nc = new NameCallback("username: ");
          PasswordCallback pc = new PasswordCallback("password: ", false);
          try
          {
              callbackHandler.handle(new Callback[]{nc, pc});
              username = nc.getName();
              char[] tmpPassword = pc.getPassword();
              if (tmpPassword == null)
                  tmpPassword = new char[0];
              password = new char[tmpPassword.length];
              System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
              pc.clearPassword();
          }
          catch (IOException | UnsupportedCallbackException e)
          {
              logger.info("Unexpected exception processing authentication callbacks", e);
              throw new LoginException("Authentication failed");
          }
  
          // verify the credentials
          try
          {
              authenticate();
          }
          catch (AuthenticationException e)
          {
              // authentication failed -- clean up
              succeeded = false;
              cleanUpInternalState();
              throw new FailedLoginException(e.getMessage());
          }
  
          succeeded = true;
          return true;
      }
  
      private void authenticate()
      {
          if (!StorageService.instance.isAuthSetupComplete())
              throw new AuthenticationException("Cannot login as server authentication setup is not yet completed");
  
          IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
          Map<String, String> credentials = new HashMap<>();
          credentials.put(PasswordAuthenticator.USERNAME_KEY, username);
          credentials.put(PasswordAuthenticator.PASSWORD_KEY, String.valueOf(password));
          AuthenticatedUser user = authenticator.legacyAuthenticate(credentials);
          // Only actual users should be allowed to authenticate for JMX
          if (user.isAnonymous() || user.isSystem())
              throw new AuthenticationException(String.format("Invalid user %s", user.getName()));
  
          // The LOGIN privilege is required to authenticate - c.f. ClientState::login
          if (!DatabaseDescriptor.getRoleManager().canLogin(user.getPrimaryRole()))
              throw new AuthenticationException(user.getName() + " is not permitted to log in");
      }
  
      /**
       * This method is called if the LoginContext's overall authentication succeeded
       * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
       * succeeded).
       *
       * If this LoginModule's own authentication attempt succeeded (checked by
       * retrieving the private state saved by the {@code}login{@code} method),
       * then this method associates a {@code}CassandraPrincipal{@code}
       * with the {@code}Subject{@code}.
       * If this LoginModule's own authentication attempted failed, then this
       * method removes any state that was originally saved.
       *
       * @return true if this LoginModule's own login and commit attempts succeeded, false otherwise.
       * @exception LoginException if the commit fails.
       */
      @Override
      public boolean commit() throws LoginException
      {
          if (!succeeded)
          {
              return false;
          }
          else
          {
              // add a Principal (authenticated identity)
              // to the Subject
              principal = new CassandraPrincipal(username);
              if (!subject.getPrincipals().contains(principal))
                  subject.getPrincipals().add(principal);
  
              cleanUpInternalState();
              commitSucceeded = true;
              return true;
          }
      }
  
      /**
       * This method is called if the LoginContext's  overall authentication failed.
       * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
       * did not succeed).
       *
       * If this LoginModule's own authentication attempt succeeded (checked by
       * retrieving the private state saved by the {@code}login{@code} and
       * {@code}commit{@code} methods), then this method cleans up any state that
       * was originally saved.
       *
       * @return false if this LoginModule's own login and/or commit attempts failed, true otherwise.
       * @throws LoginException if the abort fails.
       */
      @Override
      public boolean abort() throws LoginException
      {
          if (!succeeded)
          {
              return false;
          }
          else if (!commitSucceeded)
          {
              // login succeeded but overall authentication failed
              succeeded = false;
              cleanUpInternalState();
              principal = null;
          }
          else
          {
              // overall authentication succeeded and commit succeeded,
              // but someone else's commit failed
              logout();
          }
          return true;
      }
  
      /**
       * Logout the user.
       *
       * This method removes the principal that was added by the
       * {@code}commit{@code} method.
       *
       * @return true in all cases since this {@code}LoginModule{@code}
       *         should not be ignored.
       * @throws LoginException if the logout fails.
       */
      @Override
      public boolean logout() throws LoginException
      {
          subject.getPrincipals().remove(principal);
          succeeded = false;
          cleanUpInternalState();
          principal = null;
          return true;
      }
  
      private void cleanUpInternalState()
      {
          username = null;
          if (password != null)
          {
              for (int i = 0; i < password.length; i++)
                  password[i] = ' ';
              password = null;
          }
      }
  }
  