org.apache.ranger
diff --git a/security-admin/src/main/java/org/apache/ranger/biz/SessionMgr.java b/security-admin/src/main/java/org/apache/ranger/biz/SessionMgr.java
index 107c702477..9205a610b8 100644
--- a/security-admin/src/main/java/org/apache/ranger/biz/SessionMgr.java
+++ b/security-admin/src/main/java/org/apache/ranger/biz/SessionMgr.java
@@ -511,9 +511,11 @@ private void getSSOSpnegoAuthCheckForAPI(String currentLoginId, HttpServletReque
UserSessionBase session = context != null ? context.getUserSession() : null;
boolean ssoEnabled = session != null ? session.isSSOEnabled() : PropertiesUtil.getBooleanProperty("ranger.sso.enabled", false);
XXPortalUser gjUser = daoManager.getXXPortalUser().findByLoginId(currentLoginId);
+ String authMethod = PropertiesUtil.getProperty("ranger.authentication.method", "NONE");
+ boolean samlEnabled = "SAML".equalsIgnoreCase(authMethod);
- if (gjUser == null && ((request.getAttribute("spnegoEnabled") != null && (boolean) request.getAttribute("spnegoEnabled")) || (ssoEnabled))) {
- logger.debug("User : {} doesn't exist in Ranger DB So creating user as it's SSO or Spnego authenticated", currentLoginId);
+ if (gjUser == null && ((request.getAttribute("spnegoEnabled") != null && (boolean) request.getAttribute("spnegoEnabled")) || (ssoEnabled) || (samlEnabled))) {
+ logger.debug("User : {} doesn't exist in Ranger DB So creating user as it's SSO or Spnego or saml authenticated", currentLoginId);
xUserMgr.createServiceConfigUser(currentLoginId);
}
diff --git a/security-admin/src/main/java/org/apache/ranger/common/RangerCommonEnums.java b/security-admin/src/main/java/org/apache/ranger/common/RangerCommonEnums.java
index 2aefb8aaca..9e03579e07 100644
--- a/security-admin/src/main/java/org/apache/ranger/common/RangerCommonEnums.java
+++ b/security-admin/src/main/java/org/apache/ranger/common/RangerCommonEnums.java
@@ -468,6 +468,7 @@ public class RangerCommonEnums {
public static final int USER_UNIX = 4;
public static final int USER_REPO = 5;
public static final int USER_FEDERATED = 6;
+ public static final int USER_SAML = 7;
public static final int GROUP_INTERNAL = 0;
public static final int GROUP_EXTERNAL = 1;
diff --git a/security-admin/src/main/java/org/apache/ranger/entity/XXAuthSession.java b/security-admin/src/main/java/org/apache/ranger/entity/XXAuthSession.java
index 00132e7f0e..abe2a3837e 100644
--- a/security-admin/src/main/java/org/apache/ranger/entity/XXAuthSession.java
+++ b/security-admin/src/main/java/org/apache/ranger/entity/XXAuthSession.java
@@ -116,10 +116,15 @@ public class XXAuthSession extends XXDBBase implements java.io.Serializable {
*/
public static final int AUTH_TYPE_TRUSTED_PROXY = 4;
+ /**
+ * AUTH_TYPE_SAML is an element of enum AuthType. Its value is "AUTH_TYPE_SAML".
+ */
+ public static final int AUTH_TYPE_SAML = 5;
+
/**
* Max value for enum AuthType_MAX
*/
- public static final int AuthType_MAX = 4;
+ public static final int AuthType_MAX = 5;
@Id
@SequenceGenerator(name = "X_AUTH_SESS_SEQ", sequenceName = "X_AUTH_SESS_SEQ", allocationSize = 1)
diff --git a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
index 8f8692afd5..082e7020f5 100644
--- a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
+++ b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
@@ -274,6 +274,7 @@ public VXPortalUser getUserProfile(@Context HttpServletRequest request) {
long inactivityTimeout = PropertiesUtil.getLongProperty("ranger.service.inactivity.timeout", 15 * 60);
configProperties.put("inactivityTimeout", Long.toString(inactivityTimeout));
+ configProperties.put("authenticationMethod", PropertiesUtil.getProperty("ranger.authentication.method", "NONE"));
VXPortalUser userProfile = userManager.getUserProfileByLoginId();
diff --git a/security-admin/src/main/java/org/apache/ranger/security/context/RangerApplicationContextInitializer.java b/security-admin/src/main/java/org/apache/ranger/security/context/RangerApplicationContextInitializer.java
new file mode 100644
index 0000000000..df344c3a0f
--- /dev/null
+++ b/security-admin/src/main/java/org/apache/ranger/security/context/RangerApplicationContextInitializer.java
@@ -0,0 +1,117 @@
+/*
+ * 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.ranger.security.context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import java.io.InputStream;
+
+/**
+ * Activates the "saml" Spring profile when ranger.authentication.method=SAML.
+ * This ensures OpenSAML beans are only instantiated when SAML is configured,
+ * allowing non-SAML deployments to run safely on JDK 8.
+ *
+ * IMPORTANT: We cannot use PropertiesUtil here because it is populated by a
+ * BeanFactoryPostProcessor that runs after ApplicationContextInitializer.
+ */
+public class RangerApplicationContextInitializer implements ApplicationContextInitializer {
+ private static final Logger LOG = LoggerFactory.getLogger(RangerApplicationContextInitializer.class);
+
+ private static final String SAML_AUTH_METHOD = "SAML";
+ private static final String SAML_PROFILE = "saml";
+ private static final String AUTH_METHOD_PROPERTY = "ranger.authentication.method";
+ private static final String CONFIG_RESOURCE = "ranger-admin-site.xml";
+
+ @Override
+ public void initialize(ConfigurableApplicationContext applicationContext) {
+ String authMethod = readAuthMethodFromConfigFile();
+
+ if (SAML_AUTH_METHOD.equalsIgnoreCase(authMethod)) {
+ // Critical safety check for JDK compatibility
+ try {
+ Class.forName("org.opensaml.saml.saml2.core.AuthnRequest", false, getClass().getClassLoader());
+ } catch (ClassNotFoundException | UnsupportedClassVersionError e) {
+ throw new IllegalStateException(
+ "SAML 2.0 authentication is enabled (ranger.authentication.method=SAML), " +
+ "but it requires Java 11 or higher. OpenSAML 4.x is not compatible with Java 8. " +
+ "Please upgrade Ranger Admin JVM to Java 11+ or change the authentication method.", e);
+ }
+ LOG.info("RangerApplicationContextInitializer: activating '{}' Spring profile (ranger.authentication.method={})", SAML_PROFILE, authMethod);
+ applicationContext.getEnvironment().addActiveProfile(SAML_PROFILE);
+ } else {
+ LOG.info("RangerApplicationContextInitializer: SAML profile not activated (ranger.authentication.method={})", authMethod);
+ }
+ }
+
+ /**
+ * Reads ranger.authentication.method directly from ranger-admin-site.xml on the
+ * classpath. This avoids the PropertiesUtil dependency, which is not yet
+ * initialised at ApplicationContextInitializer time.
+ *
+ * @return the configured authentication method, or {@code "NONE"} if the
+ * property is absent or the file cannot be read.
+ */
+ private String readAuthMethodFromConfigFile() {
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream(CONFIG_RESOURCE)) {
+ if (is == null) {
+ LOG.warn("RangerApplicationContextInitializer: {} not found on classpath; defaulting to NONE", CONFIG_RESOURCE);
+ return "NONE";
+ }
+
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ // Harden against XXE
+ dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ dbf.setExpandEntityReferences(false);
+ dbf.setNamespaceAware(false);
+
+ Document doc = dbf.newDocumentBuilder().parse(is);
+ NodeList properties = doc.getElementsByTagName("property");
+
+ for (int i = 0; i < properties.getLength(); i++) {
+ Element property = (Element) properties.item(i);
+ NodeList names = property.getElementsByTagName("name");
+
+ if (names.getLength() > 0 &&
+ AUTH_METHOD_PROPERTY.equals(names.item(0).getTextContent().trim())) {
+ NodeList values = property.getElementsByTagName("value");
+
+ if (values.getLength() > 0) {
+ String value = values.item(0).getTextContent().trim();
+ LOG.info("RangerApplicationContextInitializer: read {}={} from {}", AUTH_METHOD_PROPERTY, value, CONFIG_RESOURCE);
+ return value;
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("RangerApplicationContextInitializer: failed to read {} — defaulting to NONE", CONFIG_RESOURCE, e);
+ }
+ LOG.warn("RangerApplicationContextInitializer: {} not found in {}; defaulting to NONE", AUTH_METHOD_PROPERTY, CONFIG_RESOURCE);
+ return "NONE";
+ }
+}
diff --git a/security-admin/src/main/java/org/apache/ranger/security/handler/RangerAuthenticationProvider.java b/security-admin/src/main/java/org/apache/ranger/security/handler/RangerAuthenticationProvider.java
index 884a03d05c..9d2b06d68d 100644
--- a/security-admin/src/main/java/org/apache/ranger/security/handler/RangerAuthenticationProvider.java
+++ b/security-admin/src/main/java/org/apache/ranger/security/handler/RangerAuthenticationProvider.java
@@ -55,6 +55,8 @@
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
@@ -144,6 +146,12 @@ public Authentication authenticate(Authentication authentication) throws Authent
} else if ("PAM".equalsIgnoreCase(rangerAuthenticationMethod)) {
authentication = getPamAuthentication(authentication);
+ if (authentication != null && authentication.isAuthenticated()) {
+ return authentication;
+ }
+ } else if ("SAML".equalsIgnoreCase(rangerAuthenticationMethod)) {
+ authentication = getSAMLAuthentication(authentication);
+
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
@@ -215,7 +223,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
@Override
public boolean supports(Class> authentication) {
- return authentication.equals(UsernamePasswordAuthenticationToken.class);
+ return authentication.equals(UsernamePasswordAuthenticationToken.class) || Saml2Authentication.class.isAssignableFrom(authentication);
}
public Authentication getADAuthentication(Authentication authentication) {
@@ -366,6 +374,60 @@ public Authentication getUnixAuthentication(Authentication authentication) {
return authentication;
}
+ private Authentication getSAMLAuthentication(Authentication authentication) {
+ try {
+ if (!(authentication instanceof Saml2Authentication)) {
+ logger.debug("Not a SAML authentication, skipping");
+ return null;
+ }
+
+ String username = authentication.getName();
+ if (StringUtil.isEmpty(username)) {
+ throw new BadCredentialsException("No username in SAML authentication");
+ }
+
+ Object principal = authentication.getPrincipal();
+ if (!(principal instanceof Saml2AuthenticatedPrincipal)) {
+ logger.warn("Expected Saml2AuthenticatedPrincipal but got {}", principal.getClass().getName());
+ return null;
+ }
+
+ Saml2AuthenticatedPrincipal samlPrincipal = (Saml2AuthenticatedPrincipal) principal;
+
+ // Get username from attribute or NameID
+ String usernameAttr = PropertiesUtil.getProperty("ranger.saml.attribute.username", "NameID");
+ if (!"NameID".equalsIgnoreCase(usernameAttr)) {
+ String attrValue = samlPrincipal.getFirstAttribute(usernameAttr);
+ if (!StringUtil.isEmpty(attrValue)) {
+ username = attrValue;
+ }
+ }
+
+ String email = samlPrincipal.getFirstAttribute("email");
+ String groupAttrName = PropertiesUtil.getProperty("ranger.saml.attribute.role", "groups");
+ List groups = samlPrincipal.getAttribute(groupAttrName);
+ String rangerSamlDefaultRole = PropertiesUtil.getProperty("ranger.saml.default.role", "ROLE_USER");
+
+ logger.info("SAML authenticated user: {}, email: {}, groups: {}", username, email, groups);
+
+ List grantedAuths = getAuthorities(username);
+ if (grantedAuths.isEmpty()) {
+ logger.info("SAML user '{}' not found in Ranger DB. " + "Assigning session default role: {}", username, rangerSamlDefaultRole);
+ grantedAuths.add(new SimpleGrantedAuthority(rangerSamlDefaultRole));
+ }
+ final UserDetails userDetails = new User(username, "", grantedAuths);
+ UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, "", grantedAuths);
+ result.setDetails(authentication.getDetails());
+ logger.info("SAML authentication successful for user: {} with authorities: {}", username, grantedAuths);
+ return result;
+ } catch (BadCredentialsException e) {
+ throw e;
+ } catch (Exception e) {
+ logger.error("SAML authentication processing failed", e);
+ throw new BadCredentialsException("SAML processing error", e);
+ }
+ }
+
public String getRangerAuthenticationMethod() {
return rangerAuthenticationMethod;
}
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerDelegatingAuthenticationEntryPoint.java b/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerDelegatingAuthenticationEntryPoint.java
new file mode 100644
index 0000000000..48eb3ca809
--- /dev/null
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerDelegatingAuthenticationEntryPoint.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ranger.security.web.authentication;
+
+import org.apache.ranger.common.PropertiesUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class RangerDelegatingAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ private static final Logger logger = LoggerFactory.getLogger(RangerDelegatingAuthenticationEntryPoint.class);
+
+ private static final String AUTH_METHOD_SAML = "SAML";
+
+ private final LoginUrlAuthenticationEntryPoint samlEntryPoint;
+ private final AuthenticationEntryPoint defaultEntryPoint;
+
+ public RangerDelegatingAuthenticationEntryPoint(String samlLoginUrl, AuthenticationEntryPoint defaultEntryPoint) {
+ this.samlEntryPoint = new LoginUrlAuthenticationEntryPoint(samlLoginUrl);
+ this.defaultEntryPoint = defaultEntryPoint;
+ }
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException authException) throws IOException, ServletException {
+ String authMethod = PropertiesUtil.getProperty("ranger.authentication.method", "NONE");
+ logger.debug("RangerDelegatingAuthenticationEntryPoint.commence() authMethod={}", authMethod);
+ if (AUTH_METHOD_SAML.equalsIgnoreCase(authMethod)) {
+ samlEntryPoint.commence(request, response, authException);
+ } else {
+ defaultEntryPoint.commence(request, response, authException);
+ }
+ }
+}
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerDelegatingLogoutSuccessHandler.java b/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerDelegatingLogoutSuccessHandler.java
new file mode 100644
index 0000000000..b6e46ea2ce
--- /dev/null
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerDelegatingLogoutSuccessHandler.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ranger.security.web.authentication;
+
+import org.apache.ranger.common.PropertiesUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class RangerDelegatingLogoutSuccessHandler implements LogoutSuccessHandler {
+ private static final Logger logger = LoggerFactory.getLogger(RangerDelegatingLogoutSuccessHandler.class);
+ private static final String AUTH_METHOD_SAML = "SAML";
+
+ private final Saml2RelyingPartyInitiatedLogoutSuccessHandler samlLogoutSuccessHandler;
+ private final LogoutSuccessHandler defaultLogoutSuccessHandler;
+
+ public RangerDelegatingLogoutSuccessHandler(Saml2RelyingPartyInitiatedLogoutSuccessHandler samlLogoutSuccessHandler,
+ LogoutSuccessHandler defaultLogoutSuccessHandler) {
+ this.samlLogoutSuccessHandler = samlLogoutSuccessHandler;
+ this.defaultLogoutSuccessHandler = defaultLogoutSuccessHandler;
+ }
+
+ @Override
+ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
+ Authentication authentication) throws IOException, ServletException {
+ String authMethod = PropertiesUtil.getProperty("ranger.authentication.method", "NONE");
+ logger.debug("RangerDelegatingLogoutSuccessHandler.onLogoutSuccess() authMethod={}", authMethod);
+ if (AUTH_METHOD_SAML.equalsIgnoreCase(authMethod)) {
+ samlLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
+ } else {
+ defaultLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
+ }
+ }
+}
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java
index cf03c6cbdb..b244741bbf 100644
--- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java
@@ -174,7 +174,7 @@ private int getAuthType(Authentication auth, HttpServletRequest request) {
Object ssoEnabledObj = request.getAttribute("ssoEnabled");
boolean ssoEnabled = ssoEnabledObj != null ? Boolean.parseBoolean(String.valueOf(ssoEnabledObj)) : PropertiesUtil.getBooleanProperty("ranger.sso.enabled", false);
-
+ String authMethod = PropertiesUtil.getProperty("ranger.authentication.method", "NONE");
if (ssoEnabled) {
return XXAuthSession.AUTH_TYPE_SSO;
} else if (request.getAttribute("spnegoEnabled") != null && Boolean.parseBoolean(String.valueOf(request.getAttribute("spnegoEnabled")))) {
@@ -185,6 +185,8 @@ private int getAuthType(Authentication auth, HttpServletRequest request) {
} else {
return XXAuthSession.AUTH_TYPE_KERBEROS;
}
+ } else if ("SAML".equalsIgnoreCase(authMethod)) {
+ return XXAuthSession.AUTH_TYPE_SAML;
}
return XXAuthSession.AUTH_TYPE_PASSWORD;
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/saml/RangerSamlRegistrationFactory.java b/security-admin/src/main/java/org/apache/ranger/security/web/saml/RangerSamlRegistrationFactory.java
new file mode 100644
index 0000000000..8a1f56a816
--- /dev/null
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/saml/RangerSamlRegistrationFactory.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ranger.security.web.saml;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.security.converter.RsaKeyConverters;
+import org.springframework.security.saml2.core.Saml2X509Credential;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+
+public class RangerSamlRegistrationFactory {
+ private static final Logger LOG = LoggerFactory.getLogger(RangerSamlRegistrationFactory.class);
+
+ private RangerSamlRegistrationFactory() {
+ }
+
+ public static RelyingPartyRegistration buildWithSigningCredential(RelyingPartyRegistration.Builder builder, String privateKeyPath, String certPath) throws Exception {
+ if (privateKeyPath == null || privateKeyPath.trim().isEmpty() || certPath == null || certPath.trim().isEmpty()) {
+ LOG.info("SAML signing credentials not configured, skipping credential setup");
+ throw new IllegalStateException("SAML is enabled but signing credentials are not configured. " +
+ "Set ranger.saml.sp.key and ranger.saml.sp.cert in ranger-admin-site.xml " +
+ "before starting Ranger with SAML authentication.");
+ }
+ RSAPrivateKey privateKey = RsaKeyConverters.pkcs8().convert(new FileSystemResource(privateKeyPath).getInputStream());
+ X509Certificate certificate;
+ try (FileInputStream fis = new FileInputStream(certPath)) {
+ certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(fis);
+ }
+ Saml2X509Credential signingCredential = Saml2X509Credential.signing(privateKey, certificate);
+ return builder
+ .signingX509Credentials(c -> c.add(signingCredential))
+ .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
+ .singleLogoutServiceResponseLocation("{baseUrl}/logout/saml2/slo")
+ .singleLogoutServiceBinding(Saml2MessageBinding.POST)
+ .build();
+ }
+
+ public static RelyingPartyRegistration buildFromKeystore(RelyingPartyRegistration.Builder builder, String keystorePath,
+ String alias, String password) throws Exception {
+ if (keystorePath == null || keystorePath.trim().isEmpty()) {
+ throw new IllegalArgumentException("ranger.service.https.attrib.keystore.file must not be null or empty");
+ }
+ if (alias == null || alias.trim().isEmpty()) {
+ throw new IllegalArgumentException("ranger.service.https.attrib.keystore.keyalias must not be null or empty");
+ }
+ if (password == null || password.trim().isEmpty()) {
+ throw new IllegalArgumentException("ranger.service.https.attrib.keystore.pass must not be null or empty");
+ }
+ KeyStore ks = KeyStore.getInstance("JKS");
+ try (FileInputStream fis = new FileInputStream(keystorePath)) {
+ ks.load(fis, password.toCharArray());
+ }
+ PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
+ X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
+ Saml2X509Credential signingCredential = Saml2X509Credential.signing((RSAPrivateKey) privateKey, cert);
+ return builder
+ .signingX509Credentials(c -> c.add(signingCredential))
+ .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
+ .singleLogoutServiceResponseLocation("{baseUrl}/logout/saml2/slo")
+ .singleLogoutServiceBinding(Saml2MessageBinding.POST)
+ .build();
+ }
+}
diff --git a/security-admin/src/main/java/org/apache/ranger/util/RangerEnumUtil.java b/security-admin/src/main/java/org/apache/ranger/util/RangerEnumUtil.java
index 6f00e7a835..46f370d1cb 100644
--- a/security-admin/src/main/java/org/apache/ranger/util/RangerEnumUtil.java
+++ b/security-admin/src/main/java/org/apache/ranger/util/RangerEnumUtil.java
@@ -1957,6 +1957,14 @@ protected void init() {
vElement.setRbKey("xa.enum.AuthType.AUTH_TYPE_TRUSTED_PROXY");
vElement.setEnumName(vEnum.getEnumName());
+ vEnum.getElementList().add(vElement);
+ vElement = new VEnumElement();
+ vElement.setElementName("AUTH_TYPE_SAML");
+ vElement.setElementValue(5);
+ vElement.setElementLabel("SAML");
+ vElement.setRbKey("xa.enum.AuthType.AUTH_TYPE_SAML");
+ vElement.setEnumName(vEnum.getEnumName());
+
vEnum.getElementList().add(vElement);
///////////////////////////////////
diff --git a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
index d1fccc27d7..7b92c777da 100644
--- a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
+++ b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
@@ -453,4 +453,36 @@
xasecure.audit.jaas.Client.option.principal
+
+ ranger.saml.attribute.username
+ NameID
+
+
+ ranger.saml.attribute.role
+ groups
+
+
+ ranger.saml.default.role
+ ROLE_USER
+
+
+ ranger.saml.admin.group
+ ranger-admins
+
+
+
+ ranger.saml.entity.id
+ ranger-saml
+ SAML SP Entity ID must match the Client ID registered in the IdP (e.g. Keycloak)
+
+
+ ranger.saml.idp.metadata.location
+
+ IdP metadata file location (file: or http: URI)
+
+
+ ranger.saml.success.url
+ /index.html
+ URL to redirect to after successful SAML authentication
+
diff --git a/security-admin/src/main/resources/conf.dist/security-applicationContext.xml b/security-admin/src/main/resources/conf.dist/security-applicationContext.xml
index bbb94a3a43..dd4beb1f4d 100644
--- a/security-admin/src/main/resources/conf.dist/security-applicationContext.xml
+++ b/security-admin/src/main/resources/conf.dist/security-applicationContext.xml
@@ -16,12 +16,12 @@
limitations under the License.
-->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+ />
+
+
+
+
+
+
+
+
+
diff --git a/security-admin/src/main/webapp/WEB-INF/web.xml b/security-admin/src/main/webapp/WEB-INF/web.xml
index c2f1c985ed..1781ba7933 100644
--- a/security-admin/src/main/webapp/WEB-INF/web.xml
+++ b/security-admin/src/main/webapp/WEB-INF/web.xml
@@ -21,6 +21,10 @@
index.html
+
+ contextInitializerClasses
+ org.apache.ranger.security.context.RangerApplicationContextInitializer
+
contextConfigLocation
META-INF/applicationContext.xml
diff --git a/security-admin/src/main/webapp/login.jsp b/security-admin/src/main/webapp/login.jsp
index b25cb78ad0..05a864c0ab 100644
--- a/security-admin/src/main/webapp/login.jsp
+++ b/security-admin/src/main/webapp/login.jsp
@@ -14,6 +14,19 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<%@ page import="org.apache.ranger.common.PropertiesUtil" %>
+<%
+ String authMethod = PropertiesUtil.getProperty("ranger.authentication.method", "NONE");
+ // Only auto-redirect to IdP if we are NOT coming back from a logout.
+ // After a successful logout, samlPostLogoutRedirectHandler sends the user here
+ // with ?loggedOut=true so we show the login page instead of looping back to the IdP.
+ String loggedOut = request.getParameter("loggedOut");
+ if ("SAML".equalsIgnoreCase(authMethod) && !"true".equals(loggedOut)) {
+ String entityId = PropertiesUtil.getProperty("ranger.saml.entity.id", "ranger-saml");
+ response.sendRedirect(request.getContextPath() + "/saml2/authenticate/" + entityId);
+ return;
+ }
+%>
diff --git a/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js b/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js
index 0f9f66b589..5240556fa9 100644
--- a/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js
+++ b/security-admin/src/main/webapp/react-webapp/src/utils/XAUtils.js
@@ -1420,6 +1420,17 @@ export const updateTagActive = (isTagView) => {
export const handleLogout = async (checkKnoxSSOVal, navigate) => {
try {
+ // For SAML, we must do a full browser navigation to /logout so Spring Security
+ // can perform the SLO redirect chain (Ranger -> IdP -> back to Ranger).
+ // An AJAX call cannot follow the cross-domain SLO redirect, which causes the
+ // IdP session to remain active and results in an automatic re-login loop.
+ const userProfile = getUserProfile();
+ const authMethod = userProfile?.configProperties?.authenticationMethod;
+ if (authMethod?.toUpperCase() === "SAML") {
+ window.location.replace("logout");
+ return;
+ }
+
await fetchApi({
url: "logout",
baseURL: "",
diff --git a/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx b/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx
index a8e87f8ab3..1812d9ea93 100644
--- a/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx
+++ b/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx
@@ -220,6 +220,8 @@ export const SideBarBody = (props) => {
if (checkKnoxSSOresp?.status == "419") {
setUserProfile(null);
window.location.replace("login.jsp");
+ } else {
+ handleLogout(null);
}
console.error(`Error occurred while logout! ${error}`);
}
@@ -227,6 +229,17 @@ export const SideBarBody = (props) => {
const handleLogout = async (checkKnoxSSOVal) => {
try {
+ // For SAML, we must do a full browser navigation to /logout so Spring Security
+ // can perform the SLO redirect chain (Ranger -> IdP -> back to Ranger).
+ // An AJAX call cannot follow the cross-domain SLO redirect, which causes the
+ // IdP session to remain active and results in an automatic re-login loop.
+ const currentUserProfile = getUserProfile();
+ const authMethod = currentUserProfile?.configProperties?.authenticationMethod;
+ if (authMethod?.toUpperCase() === "SAML") {
+ window.location.replace("logout");
+ return;
+ }
+
await fetchApi({
url: "logout",
baseURL: "",
@@ -234,7 +247,7 @@ export const SideBarBody = (props) => {
"cache-control": "no-cache"
}
});
- if (checkKnoxSSOVal !== undefined || checkKnoxSSOVal !== null) {
+ if (checkKnoxSSOVal !== undefined && checkKnoxSSOVal !== null) {
if (checkKnoxSSOVal?.toString() == "false") {
window.location.replace("locallogin");
window.localStorage.clear();