diff --git a/build.gradle b/build.gradle index 217cf599f50..ebc3537d6ea 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,8 @@ ext { project(':h2o-bindings'), project(':h2o-avro-parser'), project(':h2o-orc-parser'), - project(':h2o-parquet-parser') + project(':h2o-parquet-parser'), + project(':h2o-jaas-pam') ] javaProjects = [ @@ -82,7 +83,8 @@ ext { project(':h2o-bindings'), project(':h2o-avro-parser'), project(':h2o-orc-parser'), - project(':h2o-parquet-parser') + project(':h2o-parquet-parser'), + project(':h2o-jaas-pam') ] scalaProjects = [ diff --git a/h2o-core/build.gradle b/h2o-core/build.gradle index 7d971b09550..9431cbbea54 100644 --- a/h2o-core/build.gradle +++ b/h2o-core/build.gradle @@ -18,6 +18,7 @@ dependencies { compile "org.eclipse.jetty.aggregate:jetty-servlet:8.1.17.v20150415" compile "org.eclipse.jetty:jetty-plus:8.1.17.v20150415" compile ("com.github.rwl:jtransforms:2.4.0") { exclude module: "junit" } + compile project(":h2o-jaas-pam") compile("log4j:log4j:1.2.15") { exclude module: "activation" diff --git a/h2o-core/src/main/java/water/H2O.java b/h2o-core/src/main/java/water/H2O.java index ade7db90494..619835df589 100644 --- a/h2o-core/src/main/java/water/H2O.java +++ b/h2o-core/src/main/java/water/H2O.java @@ -135,6 +135,9 @@ public static void printHelp() { " -kerberos_login\n" + " Use Kerberos LoginService\n" + "\n" + + " -pam_login\n" + + " Use PAM LoginService\n" + + "\n" + " -login_conf \n" + " LoginService configuration file\n" + "\n" + @@ -279,6 +282,9 @@ public static void printHelp() { /** -kerberos_login enables KerberosLoginService */ public boolean kerberos_login = false; + /** -pam_login enables PAMLoginService */ + public boolean pam_login = false; + /** -login_conf is login configuration service file on local filesystem */ public String login_conf = null; @@ -519,6 +525,9 @@ else if (s.matches("ldap_login")) { else if (s.matches("kerberos_login")) { ARGS.kerberos_login = true; } + else if (s.matches("pam_login")) { + ARGS.pam_login = true; + } else if (s.matches("login_conf")) { i = s.incrementAndCheck(i, args); ARGS.login_conf = args[i]; @@ -553,11 +562,12 @@ private static void validateArguments() { if (ARGS.hash_login) login_arg_count++; if (ARGS.ldap_login) login_arg_count++; if (ARGS.kerberos_login) login_arg_count++; + if (ARGS.pam_login) login_arg_count++; if (login_arg_count > 1) { - parseFailed("Can only specify one of -hash_login, -ldap_login, and -kerberos_login"); + parseFailed("Can only specify one of -hash_login, -ldap_login, -kerberos_login and -pam_login"); } - if (ARGS.hash_login || ARGS.ldap_login || ARGS.kerberos_login) { + if (ARGS.hash_login || ARGS.ldap_login || ARGS.kerberos_login || ARGS.pam_login) { if (H2O.ARGS.login_conf == null) { parseFailed("Must specify -login_conf argument"); } diff --git a/h2o-core/src/main/java/water/JettyHTTPD.java b/h2o-core/src/main/java/water/JettyHTTPD.java index 4cf378449a5..1847c5078a8 100644 --- a/h2o-core/src/main/java/water/JettyHTTPD.java +++ b/h2o-core/src/main/java/water/JettyHTTPD.java @@ -175,7 +175,7 @@ public void acceptRequests() { protected void createServer(Connector connector) throws Exception { _server.setConnectors(new Connector[]{connector}); - if (H2O.ARGS.hash_login || H2O.ARGS.ldap_login || H2O.ARGS.kerberos_login) { + if (H2O.ARGS.hash_login || H2O.ARGS.ldap_login || H2O.ARGS.kerberos_login || H2O.ARGS.pam_login) { // REFER TO http://www.eclipse.org/jetty/documentation/9.1.4.v20140401/embedded-examples.html#embedded-secured-hello-handler if (H2O.ARGS.login_conf == null) { Log.err("Must specify -login_conf argument"); @@ -197,6 +197,11 @@ else if (H2O.ARGS.kerberos_login) { System.setProperty("java.security.auth.login.config",H2O.ARGS.login_conf); loginService = new JAASLoginService("krb5loginmodule"); } + else if (H2O.ARGS.pam_login) { + Log.info("Configuring JAASLoginService (with PAM)"); + System.setProperty("java.security.auth.login.config",H2O.ARGS.login_conf); + loginService = new JAASLoginService("pamloginmodule"); + } else { throw H2O.fail(); } @@ -364,7 +369,7 @@ public void handle(String target, public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (!H2O.ARGS.ldap_login && !H2O.ARGS.kerberos_login) return; + if (!H2O.ARGS.ldap_login && !H2O.ARGS.kerberos_login && !H2O.ARGS.pam_login) return; String loginName = request.getUserPrincipal().getName(); if (!loginName.equals(H2O.ARGS.user_name)) { diff --git a/h2o-hadoop/h2o-mapreduce-generic/src/main/java/water/hadoop/h2odriver.java b/h2o-hadoop/h2o-mapreduce-generic/src/main/java/water/hadoop/h2odriver.java index 38d57c077aa..3032181b6f6 100644 --- a/h2o-hadoop/h2o-mapreduce-generic/src/main/java/water/hadoop/h2odriver.java +++ b/h2o-hadoop/h2o-mapreduce-generic/src/main/java/water/hadoop/h2odriver.java @@ -92,6 +92,7 @@ static boolean hashLogin = false; static boolean ldapLogin = false; static boolean kerberosLogin = false; + static boolean pamLogin = false; static String loginConfFileName = null; static String userName = System.getProperty("user.name"); @@ -547,8 +548,8 @@ static void usage() { " Extra memory for internal JVM use outside of Java heap.\n" + " mapreduce.map.memory.mb = mapperXmx * (1 + extramempercent/100)\n" + " o -libjars with an h2o.jar is required.\n" + - " o -driverif and -driverport/-driverportrange let the user optionally" + - " specify the network interface and port/port range (on the driver host)" + + " o -driverif and -driverport/-driverportrange let the user optionally\n" + + " specify the network interface and port/port range (on the driver host)\n" + " for callback messages from the mapper to the driver.\n" + " o -network allows the user to specify a list of networks that the\n" + " H2O nodes can bind to. Use this if you have multiple network\n" + @@ -860,6 +861,9 @@ else if (s.equals("-ldap_login")) { else if (s.equals("-kerberos_login")) { kerberosLogin = true; } + else if (s.equals("-pam_login")) { + pamLogin = true; + } else if (s.equals("-login_conf")) { i++; if (i >= args.length) { usage(); } loginConfFileName = args[i]; @@ -1330,6 +1334,9 @@ private int run2(String[] args) throws Exception { if (kerberosLogin) { addMapperArg(conf, "-kerberos_login"); } + if (pamLogin) { + addMapperArg(conf, "-pam_login"); + } addMapperArg(conf, "-user_name", userName); for (String s : extraArguments) { @@ -1352,6 +1359,15 @@ private int run2(String[] args) throws Exception { "};" ); addMapperConf(conf, "-login_conf", "login.conf", krbConfData); + } else if (pamLogin) { + // Use default PAM configuration file + final byte[] pamConfData = StringUtils.bytesOf( + "pamloginmodule {\n" + + " de.codedo.jaas.PamLoginModule required\n" + + " service = h2o;\n" + + "};" + ); + addMapperConf(conf, "-login_conf", "login.conf", pamConfData); } // SSL diff --git a/h2o-jaas-pam/LICENSE.txt b/h2o-jaas-pam/LICENSE.txt new file mode 100644 index 00000000000..c4414b5822d --- /dev/null +++ b/h2o-jaas-pam/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Dirk Olmes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/h2o-jaas-pam/build.gradle b/h2o-jaas-pam/build.gradle new file mode 100644 index 00000000000..5936b8e7d40 --- /dev/null +++ b/h2o-jaas-pam/build.gradle @@ -0,0 +1,9 @@ +// +// JAAS PAM Module +// +// This module is a copy of a project https://github.com/dirk-olmes/jaas-pam/ (MIT License) +description = "JAAS PAM Module" + +dependencies { + compile "org.kohsuke:libpam4j:1.8" +} \ No newline at end of file diff --git a/h2o-jaas-pam/src/main/java/de/codedo/jaas/PamLoginModule.java b/h2o-jaas-pam/src/main/java/de/codedo/jaas/PamLoginModule.java new file mode 100644 index 00000000000..a94aae535c9 --- /dev/null +++ b/h2o-jaas-pam/src/main/java/de/codedo/jaas/PamLoginModule.java @@ -0,0 +1,207 @@ +package de.codedo.jaas; + +import java.io.IOException; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +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 org.jvnet.libpam.PAM; +import org.jvnet.libpam.PAMException; +import org.jvnet.libpam.UnixUser; + +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 _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 sharedState, Map options) + { + _subject = subject; + _callbackHandler = callbackHandler; + _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(); + _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 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/h2o-jaas-pam/src/main/java/de/codedo/jaas/PamPrincipal.java b/h2o-jaas-pam/src/main/java/de/codedo/jaas/PamPrincipal.java new file mode 100644 index 00000000000..fbfcc287f65 --- /dev/null +++ b/h2o-jaas-pam/src/main/java/de/codedo/jaas/PamPrincipal.java @@ -0,0 +1,66 @@ +package de.codedo.jaas; + +import java.security.Principal; +import java.util.Collections; +import java.util.Set; + +import org.jvnet.libpam.UnixUser; + +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 _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 getGroups() + { + return _groups; + } +} diff --git a/h2o-jaas-pam/src/main/java/de/codedo/jaas/UsernamePasswordCallbackHandler.java b/h2o-jaas-pam/src/main/java/de/codedo/jaas/UsernamePasswordCallbackHandler.java new file mode 100644 index 00000000000..3bb6044dcba --- /dev/null +++ b/h2o-jaas-pam/src/main/java/de/codedo/jaas/UsernamePasswordCallbackHandler.java @@ -0,0 +1,53 @@ +package de.codedo.jaas; + +import java.io.IOException; + +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; + +public class UsernamePasswordCallbackHandler extends Object implements CallbackHandler +{ + private String _user; + private String _password; + + public UsernamePasswordCallbackHandler(String user, String password) + { + super(); + _user = user; + _password = password; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException + { + for (Callback callback : callbacks) + { + if (callback instanceof NameCallback) + { + handleName((NameCallback)callback); + } + else if (callback instanceof PasswordCallback) + { + handlePassword((PasswordCallback)callback); + } + else + { + throw new UnsupportedCallbackException(callback); + } + } + } + + private void handleName(NameCallback callback) + { + callback.setName(_user); + } + + private void handlePassword(PasswordCallback callback) + { + char[] passwordChars = _password.toCharArray(); + callback.setPassword(passwordChars); + } +} diff --git a/settings.gradle b/settings.gradle index fbdf2f4cb34..744b3d29f02 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include 'h2o-test-accuracy' include 'h2o-avro-parser' include 'h2o-orc-parser' include 'h2o-parquet-parser' +include 'h2o-jaas-pam' // GRPC support if ("true".equals(System.getenv("H2O_BUILD_GRPC"))) {