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.atlas.web.security;
  
  import org.jvnet.libpam.PAM;
  import org.jvnet.libpam.PAMException;
  import org.jvnet.libpam.UnixUser;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import javax.security.auth.Subject;
  import javax.security.auth.callback.Callback;
  import javax.security.auth.callback.CallbackHandler;
  import javax.security.auth.callback.NameCallback;
  import javax.security.auth.callback.PasswordCallback;
  import javax.security.auth.callback.UnsupportedCallbackException;
  import javax.security.auth.login.FailedLoginException;
  import javax.security.auth.login.LoginException;
  import javax.security.auth.spi.LoginModule;
  
  import java.io.IOException;
  import java.security.Principal;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Set;
  
  public class PamLoginModule implements LoginModule {
      private static final Logger LOG = LoggerFactory.getLogger(PamLoginModule.class);
  
      public static final String SERVICE_KEY = "service";
  
      private PAM             pam;
      private Subject         subject;
      private CallbackHandler callbackHandler;
      private Map<String, ?>  options;
      private String          username;
      private String          password;
      private boolean         authSucceeded;
      private PamPrincipal    principal;
  
      public PamLoginModule() {
          super();
  
          authSucceeded = false;
      }
  
      @Override
      public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
          this.subject         = subject;
          this.callbackHandler = callbackHandler;
          this.options         = new HashMap<>(options);
      }
  
      @Override
      public boolean login() throws LoginException {
          initializePam();
          obtainUserAndPassword();
  
          return performLogin();
      }
  
      @Override
      public boolean commit() throws LoginException {
          if (!authSucceeded) {
              return false;
          }
  
          if (subject.isReadOnly()) {
              cleanup();
  
              throw new LoginException("Subject is read-only");
          }
  
          Set<Principal> principals = subject.getPrincipals();
  
          if (!principals.contains(principal)) {
              principals.add(principal);
          }
  
          return true;
      }
  
      @Override
      public boolean abort() {
          if (!authSucceeded) {
              return false;
          }
  
          cleanup();
  
          return true;
      }
  
      @Override
      public boolean logout() throws LoginException {
          if (subject.isReadOnly()) {
              cleanup();
  
              throw new LoginException("Subject is read-only");
          }
  
          subject.getPrincipals().remove(principal);
  
          cleanup();
  
          return true;
      }
  
      private void initializePam() throws LoginException {
          String service = (String) options.get(SERVICE_KEY);
  
          if (service == null) {
              throw new LoginException("Error: PAM service was not defined");
          }
  
          createPam(service);
      }
  
      private void createPam(String service) throws LoginException {
          try {
              pam = new PAM(service);
          } catch (PAMException ex) {
              LoginException le = new LoginException("Error initializing PAM");
  
              le.initCause(ex);
  
              throw le;
          }
      }
  
      private void obtainUserAndPassword() throws LoginException {
          if (callbackHandler == null) {
              throw new LoginException("Error: no CallbackHandler available  to gather authentication information from the user");
          }
  
          try {
              NameCallback     nameCallback     = new NameCallback("username");
              PasswordCallback passwordCallback = new PasswordCallback("password", false);
  
              invokeCallbackHandler(nameCallback, passwordCallback);
  
              initUserName(nameCallback);
              initPassword(passwordCallback);
  
              LOG.debug("Searching for user {}", nameCallback.getName());
          } catch (IOException | UnsupportedCallbackException ex) {
              LoginException le = new LoginException("Error in callbacks");
  
              le.initCause(ex);
  
              throw le;
          }
      }
  
      private void invokeCallbackHandler(NameCallback nameCallback, PasswordCallback passwordCallback) throws IOException, UnsupportedCallbackException {
          Callback[] callbacks = new Callback[2];
  
          callbacks[0] = nameCallback;
          callbacks[1] = passwordCallback;
  
          callbackHandler.handle(callbacks);
      }
  
      private void initUserName(NameCallback nameCallback) {
          username = nameCallback.getName();
      }
  
      private void initPassword(PasswordCallback passwordCallback) {
          char[] password = passwordCallback.getPassword();
  
          if (password != null) {
              this.password = new String(password);
          }
  
          passwordCallback.clearPassword();
      }
  
      private boolean performLogin() throws LoginException {
          try {
              UnixUser user = pam.authenticate(username, password);
  
              principal     = new PamPrincipal(user);
              authSucceeded = true;
  
              LOG.debug("user {}", username);
  
              return true;
          } catch (PAMException ex) {
              LoginException le = new FailedLoginException("Invalid username or password");
  
              le.initCause(ex);
  
              throw le;
          }
      }
  
      private void cleanup() {
          authSucceeded = false;
          username      = null;
          password      = null;
          principal     = null;
  
          pam.dispose();
      }
  }
  