Skip to content

Commit 13dc2c1

Browse files
RANGER-5539: Auth check for doAsUser parameter (apache#915)
1 parent 1134f61 commit 13dc2c1

5 files changed

Lines changed: 254 additions & 13 deletions

File tree

ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerDefaultJwtAuthHandler.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
2727
import org.apache.commons.lang3.StringUtils;
2828
import org.apache.ranger.authz.handler.RangerAuth;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
2931

3032
import javax.servlet.ServletRequest;
3133
import javax.servlet.http.Cookie;
@@ -35,6 +37,7 @@
3537
* Default implementation of Ranger JWT authentication
3638
*/
3739
public class RangerDefaultJwtAuthHandler extends RangerJwtAuthHandler {
40+
private static final Logger LOG = LoggerFactory.getLogger(RangerDefaultJwtAuthHandler.class);
3841
protected static final String AUTHORIZATION_HEADER = "Authorization";
3942
protected static final String DO_AS_PARAMETER = "doAs";
4043

@@ -81,12 +84,42 @@ public RangerAuth authenticate(HttpServletRequest httpServletRequest) {
8184
String jwtAuthHeaderStr = getJwtAuthHeader(httpServletRequest);
8285
String jwtCookieStr = StringUtils.isBlank(jwtAuthHeaderStr) ? getJwtCookie(httpServletRequest) : null;
8386
String doAsUser = httpServletRequest.getParameter(DO_AS_PARAMETER);
84-
String username = authenticate(jwtAuthHeaderStr, jwtCookieStr, doAsUser);
87+
// authenticate against the JWT first to get the real (token-verified) user
88+
String realUser = authenticate(jwtAuthHeaderStr, jwtCookieStr);
8589

86-
if (username != null) {
87-
rangerAuth = new RangerAuth(username, RangerAuth.AuthType.JWT_JWKS);
88-
}
90+
if (realUser != null) {
91+
String effectiveUser = realUser;
92+
93+
if (StringUtils.isNotBlank(doAsUser)) {
94+
LOG.debug("RangerDefaultJwtAuthHandler.authenticate(): doAs=[{}] requested. isProxyEnabled=[{}]", doAsUser, isProxyEnabled());
95+
96+
if (!isProxyEnabled()) {
97+
LOG.warn("doAs [{}] requested but trusted proxy is not enabled. Ignoring doAs, proceeding with real user [{}].",
98+
doAsUser, effectiveUser);
99+
} else {
100+
LOG.debug("RangerDefaultJwtAuthHandler.authenticate(): Calling authorizeProxyUser: realUser=[{}], doAs=[{}], remoteAddr=[{}]",
101+
realUser, doAsUser, httpServletRequest.getRemoteAddr());
102+
// Check: is realUser authorized to impersonate doAsUser
103+
if (!authorizeProxyUser(realUser, doAsUser, httpServletRequest.getRemoteAddr())) {
104+
LOG.warn("RangerDefaultJwtAuthHandler.authenticate(): doAs=[{}] not authorized for realUser=[{}]. Rejecting.", doAsUser, realUser);
105+
return null;
106+
}
107+
//Checks passed → switch to doAs user
108+
effectiveUser = doAsUser.trim();
109+
LOG.info("JWT doAs authorized: effectiveUser=[{}], realUser=[{}]", effectiveUser, realUser);
110+
}
111+
}
89112

113+
rangerAuth = new RangerAuth(effectiveUser, RangerAuth.AuthType.JWT_JWKS);
114+
}
90115
return rangerAuth;
91116
}
117+
118+
protected boolean isProxyEnabled() {
119+
return false;
120+
}
121+
122+
protected boolean authorizeProxyUser(String realUser, String doAsUser, String remoteAddr) {
123+
return false;
124+
}
92125
}

ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public void initialize(final Properties config) throws Exception {
114114

115115
public abstract ConfigurableJWTProcessor<SecurityContext> getJwtProcessor(JWSKeySelector<SecurityContext> keySelector);
116116

117-
protected String authenticate(final String jwtAuthHeader, final String jwtCookie, final String doAsUser) {
117+
protected String authenticate(final String jwtAuthHeader, final String jwtCookie) {
118118
if (LOG.isDebugEnabled()) {
119119
LOG.debug("===>>> RangerJwtAuthHandler.authenticate()");
120120
}
@@ -127,17 +127,10 @@ protected String authenticate(final String jwtAuthHeader, final String jwtCookie
127127
final SignedJWT jwtToken = SignedJWT.parse(serializedJWT);
128128
boolean valid = validateToken(jwtToken);
129129
if (valid) {
130-
String userName;
131-
132-
if (StringUtils.isNotBlank(doAsUser)) {
133-
userName = doAsUser.trim();
134-
} else {
135-
userName = jwtToken.getJWTClaimsSet().getSubject();
136-
}
130+
String userName = jwtToken.getJWTClaimsSet().getSubject();
137131

138132
if (LOG.isDebugEnabled()) {
139133
LOG.debug("RangerJwtAuthHandler.authenticate(): Issuing AuthenticationToken for user: [{}]", userName);
140-
LOG.debug("RangerJwtAuthHandler.authenticate(): Authentication successful for user [{}] and doAs user is [{}]", jwtToken.getJWTClaimsSet().getSubject(), doAsUser);
141134
}
142135
return userName;
143136
} else {

security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
*/
1919
package org.apache.ranger.security.web.filter;
2020

21+
import org.apache.hadoop.conf.Configuration;
22+
import org.apache.hadoop.security.UserGroupInformation;
23+
import org.apache.hadoop.security.authorize.AuthorizationException;
24+
import org.apache.hadoop.security.authorize.ProxyUsers;
2125
import org.apache.ranger.authz.handler.RangerAuth;
2226
import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
2327
import org.apache.ranger.authz.handler.jwt.RangerJwtAuthHandler;
@@ -76,6 +80,9 @@ public void initialize() {
7680
config.setProperty(RangerJwtAuthHandler.KEY_JWT_ISS, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_ISSUER, ""));
7781

7882
super.initialize(config);
83+
84+
Configuration conf = getProxyuserConfiguration();
85+
ProxyUsers.refreshSuperUserGroupsConfiguration(conf, "ranger.proxyuser.");
7986
} catch (Exception e) {
8087
LOG.error("Failed to initialize Ranger Admin JWT Auth Filter.", e);
8188
}
@@ -118,6 +125,36 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
118125
}
119126
}
120127

128+
@Override
129+
protected boolean isProxyEnabled() {
130+
return PropertiesUtil.getBooleanProperty("ranger.authentication.allow.trustedproxy", false);
131+
}
132+
133+
@Override
134+
protected boolean authorizeProxyUser(String realUser, String doAsUser, String remoteAddr) {
135+
try {
136+
UserGroupInformation ugi = UserGroupInformation.createRemoteUser(realUser);
137+
ugi = UserGroupInformation.createProxyUser(doAsUser, ugi);
138+
ProxyUsers.authorize(ugi, remoteAddr);
139+
LOG.debug("RangerJwtAuthFilter.authorizeProxyUser(): ProxyUsers.authorize SUCCEEDED for realUser=[{}], doAs=[{}]",
140+
realUser, doAsUser);
141+
return true;
142+
} catch (AuthorizationException ex) {
143+
LOG.warn("JWT ProxyUsers.authorize failed for doAs=[{}], realUser=[{}]: {}", doAsUser, realUser, ex.getMessage());
144+
return false;
145+
}
146+
}
147+
148+
private Configuration getProxyuserConfiguration() {
149+
Configuration conf = new Configuration(false);
150+
PropertiesUtil.getPropertiesMap().forEach((k, v) -> {
151+
if (k.startsWith("ranger.proxyuser.")) {
152+
conf.set(k, v);
153+
}
154+
});
155+
return conf;
156+
}
157+
121158
@Override
122159
public void destroy() {
123160
// Empty method

security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthFilter.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
*/
1919
package org.apache.ranger.security.web.filter;
2020

21+
import org.apache.hadoop.conf.Configuration;
2122
import org.apache.ranger.authz.handler.RangerAuth;
23+
import org.apache.ranger.common.PropertiesUtil;
2224
import org.junit.jupiter.api.AfterEach;
2325
import org.junit.jupiter.api.MethodOrderer;
2426
import org.junit.jupiter.api.Test;
@@ -38,6 +40,7 @@
3840
import javax.servlet.http.HttpServletRequest;
3941

4042
import java.io.IOException;
43+
import java.lang.reflect.Method;
4144
import java.util.Collection;
4245

4346
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -120,4 +123,54 @@ public void testDoFilter_leavesAuthenticationNullWhenAuthenticateReturnsNull()
120123

121124
assertNull(SecurityContextHolder.getContext().getAuthentication());
122125
}
126+
127+
@Test
128+
void testIsProxyEnabled_defaultFalse() {
129+
PropertiesUtil.getPropertiesMap().remove("ranger.authentication.allow.trustedproxy");
130+
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
131+
assertFalse(filter.isProxyEnabled());
132+
}
133+
134+
@Test
135+
void testIsProxyEnabled_trueWhenConfigured() {
136+
PropertiesUtil.getPropertiesMap().put("ranger.authentication.allow.trustedproxy", "true");
137+
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
138+
assertTrue(filter.isProxyEnabled());
139+
PropertiesUtil.getPropertiesMap().remove("ranger.authentication.allow.trustedproxy");
140+
}
141+
142+
@Test
143+
void testAuthorizeProxyUser_returnsFalseWhenNoProxyConfigLoaded() {
144+
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
145+
// no proxyuser config loaded into ProxyUsers -> should fail safely
146+
assertFalse(filter.authorizeProxyUser("knoxui", "admin", "10.0.0.1"));
147+
}
148+
149+
@Test
150+
void testGetProxyuserConfiguration_copiesOnlyProxyuserKeys() throws Exception {
151+
// Arrange: put both proxyuser keys and non-proxyuser keys
152+
PropertiesUtil.getPropertiesMap().put("ranger.proxyuser.knoxui.hosts", "*");
153+
PropertiesUtil.getPropertiesMap().put("ranger.proxyuser.knoxui.groups", "*");
154+
PropertiesUtil.getPropertiesMap().put("ranger.some.other.key", "shouldNotBeCopied");
155+
156+
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
157+
158+
// Call private getProxyuserConfiguration() via reflection
159+
Method m = RangerJwtAuthFilter.class.getDeclaredMethod("getProxyuserConfiguration");
160+
m.setAccessible(true);
161+
162+
Configuration conf = (Configuration) m.invoke(filter);
163+
164+
// Assert: proxyuser keys copied
165+
assertEquals("*", conf.get("ranger.proxyuser.knoxui.hosts"));
166+
assertEquals("*", conf.get("ranger.proxyuser.knoxui.groups"));
167+
168+
// Assert: non-proxyuser keys NOT copied
169+
assertNull(conf.get("ranger.some.other.key"));
170+
171+
// Cleanup
172+
PropertiesUtil.getPropertiesMap().remove("ranger.proxyuser.knoxui.hosts");
173+
PropertiesUtil.getPropertiesMap().remove("ranger.proxyuser.knoxui.groups");
174+
PropertiesUtil.getPropertiesMap().remove("ranger.some.other.key");
175+
}
123176
}

security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthWrapper.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,21 @@
2929
import org.junit.jupiter.api.extension.ExtendWith;
3030
import org.mockito.Mockito;
3131
import org.mockito.junit.jupiter.MockitoExtension;
32+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
33+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3234
import org.springframework.security.core.context.SecurityContextHolder;
3335

3436
import javax.servlet.FilterChain;
3537
import javax.servlet.ServletException;
3638
import javax.servlet.ServletRequest;
3739
import javax.servlet.ServletResponse;
40+
import javax.servlet.http.Cookie;
3841
import javax.servlet.http.HttpServletRequest;
3942
import javax.servlet.http.HttpServletResponse;
4043

4144
import java.io.IOException;
45+
import java.lang.reflect.Field;
46+
import java.util.Collections;
4247

4348
import static org.mockito.ArgumentMatchers.any;
4449
import static org.mockito.Mockito.atLeastOnce;
@@ -124,4 +129,124 @@ public void testDoFilter_skipsJwtWhenSsoEnabled() throws IOException, ServletExc
124129
verify(jwtFilter, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class), any(FilterChain.class));
125130
verify(chain, times(1)).doFilter(req, res);
126131
}
132+
133+
@Test
134+
void testDoFilter_invokesJwtFilter_whenBearerHeaderPresent() throws Exception {
135+
RangerContextHolder.resetSecurityContext();
136+
SecurityContextHolder.clearContext();
137+
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");
138+
139+
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
140+
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
141+
FilterChain chain = Mockito.mock(FilterChain.class);
142+
143+
Mockito.when(req.getHeader("Authorization")).thenReturn("Bearer token");
144+
145+
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
146+
147+
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
148+
setField(wrapper, "rangerJwtAuthFilter", jwt);
149+
150+
wrapper.doFilter(req, res, chain);
151+
152+
verify(jwt, times(1)).doFilter(req, res, chain);
153+
verify(chain, times(1)).doFilter(req, res);
154+
}
155+
156+
@Test
157+
void testDoFilter_skipsJwt_whenAlreadyAuthenticated_evenIfBearerHeaderPresent() throws Exception {
158+
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");
159+
160+
// mark request authenticated
161+
SecurityContextHolder.getContext().setAuthentication(
162+
new UsernamePasswordAuthenticationToken(
163+
"kafka",
164+
"",
165+
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))));
166+
167+
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
168+
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
169+
FilterChain chain = Mockito.mock(FilterChain.class);
170+
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
171+
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
172+
setField(wrapper, "rangerJwtAuthFilter", jwt);
173+
174+
wrapper.doFilter(req, res, chain);
175+
176+
verify(jwt, never()).doFilter(req, res, chain);
177+
verify(chain, times(1)).doFilter(req, res);
178+
}
179+
180+
@Test
181+
void testDoFilter_skipsJwtFilter_whenNoBearerAndNoCookie() throws Exception {
182+
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");
183+
184+
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
185+
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
186+
FilterChain chain = Mockito.mock(FilterChain.class);
187+
188+
// no bearer header, no cookies
189+
Mockito.when(req.getHeader("Authorization")).thenReturn(null);
190+
Mockito.when(req.getCookies()).thenReturn(null);
191+
192+
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
193+
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
194+
setField(wrapper, "rangerJwtAuthFilter", jwt);
195+
196+
wrapper.doFilter(req, res, chain);
197+
198+
verify(jwt, never()).doFilter(req, res, chain);
199+
verify(chain, times(1)).doFilter(req, res);
200+
}
201+
202+
@Test
203+
void testDoFilter_invokesJwtFilter_whenJwtCookiePresent() throws Exception {
204+
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");
205+
206+
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
207+
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
208+
FilterChain chain = Mockito.mock(FilterChain.class);
209+
210+
Mockito.when(req.getHeader("Authorization")).thenReturn(null);
211+
Mockito.when(req.getCookies()).thenReturn(new Cookie[] {new Cookie("hadoop-jwt", "abc")});
212+
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
213+
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
214+
setField(wrapper, "rangerJwtAuthFilter", jwt);
215+
216+
wrapper.doFilter(req, res, chain);
217+
218+
verify(jwt, times(1)).doFilter(req, res, chain);
219+
verify(chain, times(1)).doFilter(req, res);
220+
}
221+
222+
@Test
223+
void testDoFilter_redirectsToLogin_whenJwtAttemptedButUnauthenticated_andBrowserAgent() throws Exception {
224+
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");
225+
System.setProperty("ranger.default.browser-useragents", "Mozilla");
226+
227+
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
228+
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
229+
FilterChain chain = Mockito.mock(FilterChain.class);
230+
231+
Mockito.when(req.getHeader("Authorization")).thenReturn("Bearer token");
232+
Mockito.when(req.getHeader("User-Agent")).thenReturn("Mozilla/5.0");
233+
234+
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
235+
Mockito.doNothing().when(jwt).doFilter(req, res, chain);
236+
237+
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
238+
wrapper.initialize(); // loads browser agents from properties/system property
239+
setField(wrapper, "rangerJwtAuthFilter", jwt);
240+
241+
wrapper.doFilter(req, res, chain);
242+
243+
verify(res, times(1)).sendRedirect("/login.jsp");
244+
verify(chain, times(1)).doFilter(req, res);
245+
}
246+
247+
private static void setField(Object target, String fieldName, Object value) throws Exception {
248+
Field f = target.getClass().getDeclaredField(fieldName);
249+
f.setAccessible(true);
250+
f.set(target, value);
251+
}
127252
}

0 commit comments

Comments
 (0)