diff --git a/build.gradle b/build.gradle index e557cd12..ae48de71 100644 --- a/build.gradle +++ b/build.gradle @@ -130,6 +130,7 @@ configurations { dependencies { outboundDep group: 'org.pac4j', name: 'spark-pac4j', version: '1.2.3' outboundDep group: 'org.pac4j', name: 'pac4j-ldap', version: '1.9.4' + outboundDep group: 'org.pac4j', name: 'pac4j-oauth', version: '1.9.4' outboundDep group: 'org.pac4j', name: 'pac4j-jwt', version: '1.9.4' outboundDep group: 'com.sparkjava', name: 'spark-core', version: '2.5' outboundDep group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0' diff --git a/src/main/java/com/paypal/namenode/NNAnalyticsRestAPI.java b/src/main/java/com/paypal/namenode/NNAnalyticsRestAPI.java index e7e3b24e..f5f36484 100644 --- a/src/main/java/com/paypal/namenode/NNAnalyticsRestAPI.java +++ b/src/main/java/com/paypal/namenode/NNAnalyticsRestAPI.java @@ -100,6 +100,8 @@ import org.ldaptive.pool.SearchValidator; import org.ldaptive.ssl.KeyStoreCredentialConfig; import org.ldaptive.ssl.SslConfig; +import org.pac4j.core.client.Clients; +import org.pac4j.core.config.Config; import org.pac4j.core.exception.BadCredentialsException; import org.pac4j.core.profile.CommonProfile; import org.pac4j.jwt.config.encryption.EncryptionConfiguration; @@ -109,6 +111,7 @@ import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator; import org.pac4j.jwt.profile.JwtGenerator; import org.pac4j.ldap.credentials.authenticator.LdapAuthenticator; +import org.pac4j.oauth.client.GenericOAuth20Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Spark; @@ -202,9 +205,16 @@ public void init( } boolean ldapEnabled = conf.getLdapEnabled(); + boolean oauthEnabled = conf.getOAuthEnabled(); + if (ldapEnabled && oauthEnabled) { + throw new IllegalStateException( + "Illegal authentication configuration. Please only enable one authentication method."); + } + Spark.port(conf.getPort()); + if (ldapEnabled) { - LOG.info("Enabled web security."); + LOG.info("Enabling web security: LDAP"); // jwt: SignatureConfiguration sigConf = new SecretSignatureConfiguration(conf.getJwtSignatureSecret()); @@ -252,15 +262,43 @@ public void init( LdapAuthenticator ldapAuth = new LdapAuthenticator(); ldapAuth.setLdapAuthenticator(ldaptiveAuthenticator); - secContext.init(conf, jwtAuthenticator, jwtGenerator, ldapAuth); + secContext.initLdap(conf, jwtAuthenticator, jwtGenerator, ldapAuth); + } else if (oauthEnabled) { + LOG.info("Enabling web security: OAUTH"); + // jwt: + SignatureConfiguration sigConf = + new SecretSignatureConfiguration(conf.getJwtSignatureSecret()); + EncryptionConfiguration encConf = + new SecretEncryptionConfiguration( + conf.getJwtEncryptionSecret(), JWEAlgorithm.DIR, EncryptionMethod.A128GCM); + JwtGenerator jwtGenerator = new JwtGenerator<>(sigConf, encConf); + JwtAuthenticator jwtAuthenticator = new JwtAuthenticator(sigConf, encConf); + + // oauth: + GenericOAuth20Client oauth = new GenericOAuth20Client(); + oauth.setName("oauth"); + oauth.setKey(conf.getOAuthKey()); + oauth.setSecret(conf.getOAuthSecret()); + oauth.setAuthUrl(conf.getOAuthAuthUrl()); + oauth.setProfileUrl(conf.getOAuthProfileUrl()); + oauth.setTokenUrl(conf.getOAuthTokenUrl()); + oauth.setScope(conf.getOAuthScope()); + + final Clients clients = new Clients("http://localhost:8080/callback", oauth); + final Config config = new Config(clients); + + secContext.initOAuth(conf, jwtAuthenticator, jwtGenerator, config); } else { - secContext.init(conf, null, null, null); - LOG.info("Disabled web security."); + LOG.info("Disabling web security."); + secContext.initNoAuth(conf); } /* This is the call to load everything under ./resources/public as HTML resources. */ Spark.staticFileLocation("/public"); + /* CALLBACK is used by indirect authentication clients as part of pac4j */ + get("/callback", secContext.getCallback()); + /* ENDPOINTS endpoint is meant to showcase all available REST API endpoints in JSON list form. */ get( "/endpoints", diff --git a/src/main/java/com/paypal/security/SecurityConfiguration.java b/src/main/java/com/paypal/security/SecurityConfiguration.java index fbe17779..74f533bb 100644 --- a/src/main/java/com/paypal/security/SecurityConfiguration.java +++ b/src/main/java/com/paypal/security/SecurityConfiguration.java @@ -40,6 +40,7 @@ public class SecurityConfiguration { private static final String NNA_PORT_DEFAULT = "8080"; private static final String NNA_HISTORICAL_DEFAULT = "false"; private static final String LDAP_ENABLED_DEFAULT = "false"; + private static final String OAUTH_ENABLED_DEFAULT = "false"; private static final String AUTHORIZATION_ENABLED_DEFAULT = "false"; private static final String LDAP_USE_STARTTLS_DEFAULT = "false"; private static final String LDAP_CONNECT_TIMEOUT_DEFAULT = "1000"; @@ -123,6 +124,34 @@ public int getLdapConnectionPoolMaxSize() { return Integer.parseInt(properties.getProperty("ldap.connection.pool.max.size")); } + public boolean getOAuthEnabled() { + return Boolean.parseBoolean(properties.getProperty("oauth.enable", OAUTH_ENABLED_DEFAULT)); + } + + public String getOAuthKey() { + return properties.getProperty("oauth.key"); + } + + public String getOAuthSecret() { + return properties.getProperty("oauth.secret"); + } + + public String getOAuthAuthUrl() { + return properties.getProperty("oauth.auth.url"); + } + + public String getOAuthProfileUrl() { + return properties.getProperty("oauth.profile.url"); + } + + public String getOAuthTokenUrl() { + return properties.getProperty("oauth.token.url"); + } + + public String getOAuthScope() { + return properties.getProperty("oauth.scope"); + } + public int getSuggestionsReloadSleepMs() { return Integer.parseInt( properties.getProperty( diff --git a/src/main/java/com/paypal/security/SecurityContext.java b/src/main/java/com/paypal/security/SecurityContext.java index 9d7a4419..03fba2cb 100644 --- a/src/main/java/com/paypal/security/SecurityContext.java +++ b/src/main/java/com/paypal/security/SecurityContext.java @@ -26,6 +26,7 @@ import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authorize.AuthorizationException; import org.ldaptive.auth.FormatDnResolver; +import org.pac4j.core.config.Config; import org.pac4j.core.credentials.UsernamePasswordCredentials; import org.pac4j.core.exception.HttpAction; import org.pac4j.core.profile.CommonProfile; @@ -33,11 +34,14 @@ import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator; import org.pac4j.jwt.profile.JwtGenerator; import org.pac4j.ldap.credentials.authenticator.LdapAuthenticator; +import org.pac4j.sparkjava.CallbackRoute; +import org.pac4j.sparkjava.SecurityFilter; import org.pac4j.sparkjava.SparkWebContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Request; import spark.Response; +import spark.Route; public class SecurityContext { @@ -47,6 +51,7 @@ public class SecurityContext { private JwtAuthenticator jwtAuthenticator; private JwtGenerator jwtGenerator; private LdapAuthenticator ldapAuthenticator; + private Config oauthConfig; private UserSet adminUsers; private UserSet writeUsers; @@ -68,7 +73,19 @@ private enum ACCESS_LEVEL { public SecurityContext() {} - public void init( + public void initNoAuth(SecurityConfiguration secConf) { + this.securityConfiguration = secConf; + + this.adminUsers = new UserSet(secConf.getAdminUsers()); + this.writeUsers = new UserSet(secConf.getWriteUsers()); + this.readOnlyUsers = new UserSet(secConf.getReadOnlyUsers()); + this.cacheReaderUsers = new UserSet(secConf.getCacheReaderUsers()); + this.localOnlyUsers = new UserPasswordSet(secConf.getLocalOnlyUsers()); + + this.init = true; + } + + public void initLdap( SecurityConfiguration secConf, JwtAuthenticator jwtAuth, JwtGenerator jwtGen, @@ -87,6 +104,25 @@ public void init( this.init = true; } + public void initOAuth( + SecurityConfiguration secConf, + JwtAuthenticator jwtAuth, + JwtGenerator jwtGen, + Config oauthConfig) { + this.securityConfiguration = secConf; + this.jwtAuthenticator = jwtAuth; + this.jwtGenerator = jwtGen; + this.oauthConfig = oauthConfig; + + this.adminUsers = new UserSet(secConf.getAdminUsers()); + this.writeUsers = new UserSet(secConf.getWriteUsers()); + this.readOnlyUsers = new UserSet(secConf.getReadOnlyUsers()); + this.cacheReaderUsers = new UserSet(secConf.getCacheReaderUsers()); + this.localOnlyUsers = new UserPasswordSet(secConf.getLocalOnlyUsers()); + + this.init = true; + } + public synchronized void refresh(SecurityConfiguration secConf) { this.adminUsers = new UserSet(secConf.getAdminUsers()); this.writeUsers = new UserSet(secConf.getWriteUsers()); @@ -95,10 +131,18 @@ public synchronized void refresh(SecurityConfiguration secConf) { this.localOnlyUsers = new UserPasswordSet(secConf.getLocalOnlyUsers()); } + public Route getCallback() { + if (securityConfiguration.getOAuthEnabled() && oauthConfig != null && init) { + return new CallbackRoute(oauthConfig); + } + return null; + } + public void handleAuthentication(Request req, Response res) throws AuthenticationException, HttpAction { - boolean authenticationEnabled = securityConfiguration.getLdapEnabled(); - if (!authenticationEnabled) { + boolean ldapEnabled = securityConfiguration.getLdapEnabled(); + boolean oauthEnabled = securityConfiguration.getOAuthEnabled(); + if (!ldapEnabled && !oauthEnabled) { String reqUsername = req.queryParams("proxy"); if (reqUsername != null && !reqUsername.isEmpty()) { currentUser.set(reqUsername); @@ -158,20 +202,32 @@ public void handleAuthentication(Request req, Response res) } } - for (String ldapBaseDn : ldapBaseDns) { - String ldapDnRegexd = ldapBaseDn.replaceAll("%u", user); - ldapAuthenticator.getLdapAuthenticator().setDnResolver(new FormatDnResolver(ldapDnRegexd)); - credentials = new UsernamePasswordCredentials(user, password, req.ip()); + if (ldapEnabled) { + for (String ldapBaseDn : ldapBaseDns) { + String ldapDnRegexd = ldapBaseDn.replaceAll("%u", user); + ldapAuthenticator + .getLdapAuthenticator() + .setDnResolver(new FormatDnResolver(ldapDnRegexd)); + credentials = new UsernamePasswordCredentials(user, password, req.ip()); + + try { + ldapAuthenticator.validate(credentials, new SparkWebContext(req, res)); + } catch (RuntimeException e) { + authFailedEx = e; + continue; + } + + authFailedEx = null; + break; + } + } + if (oauthEnabled) { try { - ldapAuthenticator.validate(credentials, new SparkWebContext(req, res)); + new SecurityFilter(oauthConfig, "oauth").handle(req, res); } catch (RuntimeException e) { authFailedEx = e; - continue; } - - authFailedEx = null; - break; } if (authFailedEx != null) { diff --git a/src/main/java/org/apache/hadoop/hdfs/server/namenode/NNAConstants.java b/src/main/java/org/apache/hadoop/hdfs/server/namenode/NNAConstants.java index 655532ce..f4c84359 100644 --- a/src/main/java/org/apache/hadoop/hdfs/server/namenode/NNAConstants.java +++ b/src/main/java/org/apache/hadoop/hdfs/server/namenode/NNAConstants.java @@ -225,6 +225,7 @@ enum OPERATION { } enum ENDPOINT { + callback, endpoints, credentials, loadingStatus, @@ -270,6 +271,7 @@ enum ENDPOINT { EnumSet UNSECURED_ENDPOINTS = EnumSet.of( + ENDPOINT.callback, ENDPOINT.endpoints, ENDPOINT.credentials, ENDPOINT.sets,