ATLAS-1804 : Allow PAM for authentication.

Signed-off-by: nixonrodrigues <nixon@apache.org>
diff --git a/webapp/pom.xml b/webapp/pom.xml
index 4cc0112..465d2a5 100755
--- a/webapp/pom.xml
+++ b/webapp/pom.xml
@@ -476,6 +476,18 @@
             <groupId>com.webcohesion.enunciate</groupId>
             <artifactId>enunciate-core-annotations</artifactId>
         </dependency>
+        <!-- PAM -->
+        <dependency>
+            <groupId>org.kohsuke</groupId>
+            <artifactId>libpam4j</artifactId>
+            <version>1.8</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.java.dev.jna</groupId>
+            <artifactId>jna</artifactId>
+            <version>4.1.0</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/webapp/src/main/java/org/apache/atlas/web/security/AtlasAuthenticationProvider.java b/webapp/src/main/java/org/apache/atlas/web/security/AtlasAuthenticationProvider.java
index 80d6604..6827aec 100644
--- a/webapp/src/main/java/org/apache/atlas/web/security/AtlasAuthenticationProvider.java
+++ b/webapp/src/main/java/org/apache/atlas/web/security/AtlasAuthenticationProvider.java
@@ -34,10 +34,12 @@
             .getLogger(AtlasAuthenticationProvider.class);
 
     private boolean fileAuthenticationMethodEnabled = true;
+    private boolean pamAuthenticationEnabled = false;
     private String ldapType = "NONE";
     public static final String FILE_AUTH_METHOD = "atlas.authentication.method.file";
     public static final String LDAP_AUTH_METHOD = "atlas.authentication.method.ldap";
     public static final String LDAP_TYPE = "atlas.authentication.method.ldap.type";
+    public static final String PAM_AUTH_METHOD = "atlas.authentication.method.pam";
 
 
 
@@ -49,13 +51,17 @@
 
     final AtlasADAuthenticationProvider adAuthenticationProvider;
 
+    final AtlasPamAuthenticationProvider pamAuthenticationProvider;
+
     @Inject
     public AtlasAuthenticationProvider(AtlasLdapAuthenticationProvider ldapAuthenticationProvider,
                                        AtlasFileAuthenticationProvider fileAuthenticationProvider,
-                                       AtlasADAuthenticationProvider adAuthenticationProvider) {
+                                       AtlasADAuthenticationProvider adAuthenticationProvider,
+                                       AtlasPamAuthenticationProvider pamAuthenticationProvider) {
         this.ldapAuthenticationProvider = ldapAuthenticationProvider;
         this.fileAuthenticationProvider = fileAuthenticationProvider;
         this.adAuthenticationProvider = adAuthenticationProvider;
+        this.pamAuthenticationProvider = pamAuthenticationProvider;
     }
 
     @PostConstruct
@@ -65,6 +71,8 @@
 
             this.fileAuthenticationMethodEnabled = configuration.getBoolean(FILE_AUTH_METHOD, true);
 
+            this.pamAuthenticationEnabled = configuration.getBoolean(PAM_AUTH_METHOD, false);
+
             boolean ldapAuthenticationEnabled = configuration.getBoolean(LDAP_AUTH_METHOD, false);
 
             if (ldapAuthenticationEnabled) {
@@ -102,13 +110,19 @@
                 } catch (Exception ex) {
                     LOG.error("Error while AD authentication", ex);
                 }
+            } else if (pamAuthenticationEnabled) {
+                try {
+                    authentication = pamAuthenticationProvider.authenticate(authentication);
+                } catch (Exception ex) {
+                    LOG.error("Error while PAM authentication", ex);
+                }
             }
         }
 
         if (authentication != null) {
             if (authentication.isAuthenticated()) {
                 return authentication;
-            } else if (fileAuthenticationMethodEnabled) {  // If the LDAP/AD authentication fails try the local filebased login method
+            } else if (fileAuthenticationMethodEnabled) {  // If the LDAP/AD/PAM authentication fails try the local filebased login method
                 authentication = fileAuthenticationProvider.authenticate(authentication);
 
                 if (authentication != null && authentication.isAuthenticated()) {
diff --git a/webapp/src/main/java/org/apache/atlas/web/security/AtlasPamAuthenticationProvider.java b/webapp/src/main/java/org/apache/atlas/web/security/AtlasPamAuthenticationProvider.java
new file mode 100644
index 0000000..9a5a183
--- /dev/null
+++ b/webapp/src/main/java/org/apache/atlas/web/security/AtlasPamAuthenticationProvider.java
@@ -0,0 +1,155 @@
+/*
+ * 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.apache.atlas.ApplicationProperties;
+import org.apache.atlas.web.model.User;
+import org.apache.commons.configuration.ConfigurationConverter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider;
+import org.springframework.security.authentication.jaas.memory.InMemoryConfiguration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+@Component
+public class AtlasPamAuthenticationProvider extends AtlasAbstractAuthenticationProvider {
+
+    private static Logger LOG = LoggerFactory.getLogger(AtlasPamAuthenticationProvider.class);
+    private boolean isDebugEnabled = LOG.isDebugEnabled();
+    private static String loginModuleName = "org.apache.atlas.web.security.PamLoginModule";
+    private static AppConfigurationEntry.LoginModuleControlFlag controlFlag =
+            AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;
+    private Map<String, String> options = new HashMap<String, String>();
+    private boolean groupsFromUGI;
+    private DefaultJaasAuthenticationProvider jaasAuthenticationProvider =
+            new DefaultJaasAuthenticationProvider();
+
+    @PostConstruct
+    public void setup() {
+        setPamProperties();
+        init();
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        Authentication auth = getPamAuthentication(authentication);
+        if (auth != null && auth.isAuthenticated()) {
+            return auth;
+        } else {
+            throw new AtlasAuthenticationException("PAM Authentication Failed");
+        }
+    }
+
+    private Authentication getPamAuthentication(Authentication authentication) {
+        if (isDebugEnabled) {
+            LOG.debug("==> AtlasPamAuthenticationProvider getPamAuthentication");
+        }
+        try {
+            String userName = authentication.getName();
+            String userPassword = "";
+            if (authentication.getCredentials() != null) {
+                userPassword = authentication.getCredentials().toString();
+            }
+
+            // getting user authenticated
+            if (userName != null && userPassword != null
+                    && !userName.trim().isEmpty()
+                    && !userPassword.trim().isEmpty()) {
+                final List<GrantedAuthority> grantedAuths = getAuthorities(userName);
+
+                final UserDetails principal = new User(userName, userPassword,
+                        grantedAuths);
+
+                final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(
+                        principal, userPassword, grantedAuths);
+
+                authentication = jaasAuthenticationProvider
+                        .authenticate(finalAuthentication);
+
+                if(groupsFromUGI) {
+                    authentication = getAuthenticationWithGrantedAuthorityFromUGI(authentication);
+                } else {
+                    authentication = getAuthenticationWithGrantedAuthority(authentication);
+                }
+                return authentication;
+            } else {
+                return authentication;
+            }
+
+        } catch (Exception e) {
+            LOG.debug("Pam Authentication Failed:", e);
+        }
+        if (isDebugEnabled) {
+            LOG.debug("<== AtlasPamAuthenticationProvider getPamAuthentication");
+        }
+        return authentication;
+    }
+
+    private void setPamProperties() {
+        try {
+            this.groupsFromUGI = ApplicationProperties.get().getBoolean("atlas.authentication.method.pam.ugi-groups", true);
+            Properties properties = ConfigurationConverter.getProperties(ApplicationProperties.get()
+                    .subset("atlas.authentication.method.pam"));
+            for (String key : properties.stringPropertyNames()) {
+                String value = properties.getProperty(key);
+                options.put(key, value);
+            }
+            if (!options.containsKey("service")) {
+                options.put("service", "atlas-login");
+            }
+        } catch (Exception e) {
+            LOG.error("Exception while setLdapProperties", e);
+        }
+    }
+
+    private void init() {
+        try {
+            AppConfigurationEntry appConfigurationEntry = new AppConfigurationEntry(
+                    loginModuleName, controlFlag, options);
+            AppConfigurationEntry[] appConfigurationEntries = new AppConfigurationEntry[]{appConfigurationEntry};
+            Map<String, AppConfigurationEntry[]> appConfigurationEntriesOptions =
+                    new HashMap<String, AppConfigurationEntry[]>();
+            appConfigurationEntriesOptions.put("SPRINGSECURITY",
+                    appConfigurationEntries);
+            Configuration configuration = new InMemoryConfiguration(
+                    appConfigurationEntriesOptions);
+            jaasAuthenticationProvider.setConfiguration(configuration);
+            UserAuthorityGranter authorityGranter = new UserAuthorityGranter();
+            UserAuthorityGranter[] authorityGranters = new UserAuthorityGranter[]{authorityGranter};
+            jaasAuthenticationProvider.setAuthorityGranters(authorityGranters);
+            jaasAuthenticationProvider.afterPropertiesSet();
+        } catch (Exception e) {
+            LOG.error("Failed to init PAM Authentication", e);
+        }
+    }
+}
diff --git a/webapp/src/main/java/org/apache/atlas/web/security/PamLoginModule.java b/webapp/src/main/java/org/apache/atlas/web/security/PamLoginModule.java
new file mode 100644
index 0000000..802f6f1
--- /dev/null
+++ b/webapp/src/main/java/org/apache/atlas/web/security/PamLoginModule.java
@@ -0,0 +1,221 @@
+/*
+ * 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 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 java.io.IOException;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class PamLoginModule extends Object implements LoginModule {
+    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();
+    }
+
+    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);
+        }
+        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;
+
+            return true;
+        }
+        catch (PAMException ex)
+        {
+            LoginException le = new FailedLoginException("Invalid username or password");
+            le.initCause(ex);
+            throw le;
+        }
+    }
+
+    @Override
+    public boolean commit() throws LoginException
+    {
+        if (authSucceeded == false)
+        {
+            return false;
+        }
+
+        if (subject.isReadOnly())
+        {
+            cleanup();
+            throw new LoginException("Subject is read-only");
+        }
+
+        Set<Principal> principals = subject.getPrincipals();
+        if (principals.contains(principal) == false)
+        {
+            principals.add(principal);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean abort() throws LoginException
+    {
+        if (authSucceeded == false)
+        {
+            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 cleanup()
+    {
+        authSucceeded = false;
+        username = null;
+        password = null;
+        principal = null;
+        pam.dispose();
+    }
+}
diff --git a/webapp/src/main/java/org/apache/atlas/web/security/PamPrincipal.java b/webapp/src/main/java/org/apache/atlas/web/security/PamPrincipal.java
new file mode 100644
index 0000000..e7c80af
--- /dev/null
+++ b/webapp/src/main/java/org/apache/atlas/web/security/PamPrincipal.java
@@ -0,0 +1,84 @@
+/*
+ * 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.UnixUser;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Set;
+
+public class PamPrincipal extends Object implements Principal {
+    private String userName;
+    private String gecos;
+    private String homeDir;
+    private String shell;
+    private int uid;
+    private int gid;
+    private Set<String> groups;
+
+    public PamPrincipal(UnixUser user)
+    {
+        super();
+        userName = user.getUserName();
+        gecos = user.getGecos();
+        homeDir = user.getDir();
+        shell = user.getShell();
+        uid = user.getUID();
+        gid = user.getGID();
+        groups = Collections.unmodifiableSet(user.getGroups());
+    }
+
+    @Override
+    public String getName()
+    {
+        return userName;
+    }
+
+    public String getGecos()
+    {
+        return gecos;
+    }
+
+    public String getHomeDir()
+    {
+        return homeDir;
+    }
+
+    public String getShell()
+    {
+        return shell;
+    }
+
+    public int getUid()
+    {
+        return uid;
+    }
+
+    public int getGid()
+    {
+        return gid;
+    }
+
+    public Set<String> getGroups()
+    {
+        return groups;
+    }
+}
diff --git a/webapp/src/main/java/org/apache/atlas/web/security/UserAuthorityGranter.java b/webapp/src/main/java/org/apache/atlas/web/security/UserAuthorityGranter.java
new file mode 100644
index 0000000..430dbd9
--- /dev/null
+++ b/webapp/src/main/java/org/apache/atlas/web/security/UserAuthorityGranter.java
@@ -0,0 +1,35 @@
+/*
+ * 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.springframework.security.authentication.jaas.AuthorityGranter;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Set;
+
+public class UserAuthorityGranter implements AuthorityGranter {
+
+    @Override
+    public Set<String> grant(Principal principal) {
+        Collections.singleton("DATA_SCIENTIST");
+        return null;
+    }
+}