Skip to content

Commit 0ea5ec5

Browse files
kumaabmneethiraj
andauthored
RANGER-5496: Decouple JWT lifecycle from Ranger plugin via pluggable JWT provider (#857)
Co-authored-by: Madhan Neethiraj <madhan@apache.org>
1 parent 6422a6e commit 0ea5ec5

6 files changed

Lines changed: 193 additions & 59 deletions

File tree

agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminRESTClient.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.ranger.audit.provider.MiscUtil;
2929
import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
3030
import org.apache.ranger.authorization.utils.StringUtil;
31+
import org.apache.ranger.plugin.authn.JwtProvider;
3132
import org.apache.ranger.plugin.model.RangerRole;
3233
import org.apache.ranger.plugin.util.GrantRevokeRequest;
3334
import org.apache.ranger.plugin.util.GrantRevokeRoleRequest;
@@ -1024,7 +1025,13 @@ public ServiceGdsInfo getGdsInfoIfUpdated(long lastKnownVersion, long lastActiva
10241025

10251026
@Override
10261027
public boolean isAuthenticationEnabled() {
1027-
return restClient.isAuthFilterPresent() || super.isAuthenticationEnabled();
1028+
return (restClient != null && restClient.isAuthFilterPresent()) || super.isAuthenticationEnabled();
1029+
}
1030+
1031+
public void setJwtProvider(JwtProvider jwtProvider) {
1032+
if (restClient != null) {
1033+
restClient.setJwtProvider(jwtProvider);
1034+
}
10281035
}
10291036

10301037
private void init(String url, String sslConfigFileName, int restClientConnTimeOutMs, int restClientReadTimeOutMs, int restClientMaxRetryAttempts, int restClientRetryIntervalMs, Configuration config) {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.ranger.plugin.authn;
21+
22+
import org.apache.commons.lang3.StringUtils;
23+
import org.apache.hadoop.conf.Configuration;
24+
import org.apache.ranger.authorization.hadoop.utils.RangerCredentialProvider;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import java.io.BufferedReader;
29+
import java.io.File;
30+
import java.io.FileReader;
31+
import java.io.IOException;
32+
33+
public class DefaultJwtProvider implements JwtProvider {
34+
private static final Logger LOG = LoggerFactory.getLogger(DefaultJwtProvider.class);
35+
36+
public static final String JWT_SOURCE = ".jwt.source";
37+
public static final String JWT_ENV = ".jwt.env";
38+
public static final String JWT_FILE = ".jwt.file";
39+
public static final String JWT_CRED_FILE = ".jwt.cred.file";
40+
public static final String JWT_CRED_ALIAS = ".jwt.cred.alias";
41+
42+
private final String jwtEnvVar;
43+
private final String jwtFilePath;
44+
private final String jwtCredFilePath;
45+
private final String jwtCredAlias;
46+
private long jwtFileLastModified;
47+
private long jwtCredFileLastCheckedAt;
48+
49+
private volatile String jwt;
50+
51+
public DefaultJwtProvider(String propertyPrefix, Configuration config) {
52+
String jwtSrc = config.get(propertyPrefix + JWT_SOURCE);
53+
54+
if (jwtSrc == null) {
55+
jwtSrc = "";
56+
}
57+
58+
switch (jwtSrc) {
59+
case "env":
60+
this.jwtEnvVar = config.get(propertyPrefix + JWT_ENV);
61+
this.jwtFilePath = null;
62+
this.jwtCredFilePath = null;
63+
this.jwtCredAlias = null;
64+
break;
65+
66+
case "file":
67+
this.jwtEnvVar = null;
68+
this.jwtFilePath = config.get(propertyPrefix + JWT_FILE);
69+
this.jwtCredFilePath = null;
70+
this.jwtCredAlias = null;
71+
break;
72+
73+
case "cred":
74+
this.jwtEnvVar = null;
75+
this.jwtFilePath = null;
76+
this.jwtCredFilePath = config.get(propertyPrefix + JWT_CRED_FILE);
77+
this.jwtCredAlias = config.get(propertyPrefix + JWT_CRED_ALIAS);
78+
break;
79+
80+
default:
81+
this.jwtEnvVar = null;
82+
this.jwtFilePath = null;
83+
this.jwtCredFilePath = null;
84+
this.jwtCredAlias = null;
85+
break;
86+
}
87+
}
88+
89+
@Override
90+
public String getJwt() {
91+
if (StringUtils.isNotEmpty(jwtEnvVar)) {
92+
jwt = System.getenv(jwtEnvVar);
93+
} else if (StringUtils.isNotEmpty(jwtFilePath)) {
94+
File jwtFile = new File(jwtFilePath);
95+
96+
if (jwtFile.lastModified() != jwtFileLastModified && jwtFile.canRead()) {
97+
try (BufferedReader reader = new BufferedReader(new FileReader(jwtFile))) {
98+
String line;
99+
100+
while ((line = reader.readLine()) != null) {
101+
if (StringUtils.isNotBlank(line) && !line.startsWith("#")) {
102+
break;
103+
}
104+
}
105+
106+
jwt = line;
107+
jwtFileLastModified = jwtFile.lastModified();
108+
} catch (IOException e) {
109+
LOG.error("Failed to read JWT from file: {}", jwtFilePath, e);
110+
}
111+
}
112+
} else if (StringUtils.isNotEmpty(jwtCredFilePath) && StringUtils.isNotEmpty(jwtCredAlias)) {
113+
long currentTime = System.currentTimeMillis();
114+
115+
if ((currentTime - jwtCredFileLastCheckedAt) > 60_000) { // last check should be more than 1 minute ago
116+
jwt = RangerCredentialProvider.getInstance().getCredentialString(jwtCredFilePath, jwtCredAlias);
117+
jwtCredFileLastCheckedAt = currentTime;
118+
}
119+
}
120+
121+
return jwt;
122+
}
123+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.ranger.plugin.authn;
21+
22+
public interface JwtProvider {
23+
String getJwt();
24+
}

agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerPluginContext.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.apache.ranger.admin.client.RangerAdminClient;
2424
import org.apache.ranger.admin.client.RangerAdminRESTClient;
2525
import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
26+
import org.apache.ranger.plugin.authn.DefaultJwtProvider;
27+
import org.apache.ranger.plugin.authn.JwtProvider;
2628
import org.apache.ranger.plugin.model.RangerPolicy;
2729
import org.apache.ranger.plugin.resourcematcher.RangerResourceMatcher;
2830
import org.apache.ranger.plugin.service.RangerAuthContext;
@@ -40,12 +42,14 @@ public class RangerPluginContext {
4042
private final RangerPluginConfig config;
4143
private final Map<String, Map<RangerPolicy.RangerPolicyResource, RangerResourceMatcher>> resourceMatchers = new HashMap<>();
4244
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); // fair lock
45+
private JwtProvider jwtProvider;
4346
private RangerAuthContext authContext;
4447
private RangerAuthContextListener authContextListener;
4548
private RangerAdminClient adminClient;
4649

4750
public RangerPluginContext(RangerPluginConfig config) {
4851
this.config = config;
52+
this.jwtProvider = new DefaultJwtProvider(config.getPropertyPrefix() + ".policy.rest.client", config);
4953
}
5054

5155
public RangerPluginConfig getConfig() {
@@ -151,6 +155,9 @@ public RangerAdminClient createAdminClient(RangerPluginConfig pluginConfig) {
151155

152156
if (ret == null) {
153157
ret = new RangerAdminRESTClient();
158+
if (jwtProvider != null) {
159+
((RangerAdminRESTClient) ret).setJwtProvider(jwtProvider);
160+
}
154161
}
155162

156163
ret.init(pluginConfig.getServiceName(), pluginConfig.getAppId(), pluginConfig.getPropertyPrefix(), pluginConfig);
@@ -163,6 +170,19 @@ public RangerAdminClient createAdminClient(RangerPluginConfig pluginConfig) {
163170
return ret;
164171
}
165172

173+
public void registerJWTProvider(JwtProvider jwtProvider) {
174+
this.jwtProvider = jwtProvider;
175+
176+
RangerAdminRESTClient restClient = (adminClient instanceof RangerAdminRESTClient) ? (RangerAdminRESTClient) adminClient : null;
177+
if (restClient != null) {
178+
restClient.setJwtProvider(jwtProvider);
179+
}
180+
}
181+
182+
public JwtProvider getJwtProvider() {
183+
return jwtProvider;
184+
}
185+
166186
void cleanResourceMatchers() {
167187
LOG.debug("==> cleanResourceMatchers()");
168188

agents-common/src/main/java/org/apache/ranger/plugin/service/RangerBasePlugin.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.ranger.authorization.hadoop.config.RangerAuditConfig;
3333
import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
3434
import org.apache.ranger.authorization.utils.StringUtil;
35+
import org.apache.ranger.plugin.authn.JwtProvider;
3536
import org.apache.ranger.plugin.contextenricher.RangerAdminGdsInfoRetriever;
3637
import org.apache.ranger.plugin.contextenricher.RangerAdminUserStoreRetriever;
3738
import org.apache.ranger.plugin.contextenricher.RangerContextEnricher;
@@ -302,6 +303,10 @@ public static RangerResourceACLs getMergedResourceACLs(RangerResourceACLs baseAC
302303
return baseACLs;
303304
}
304305

306+
public void registerJwtProvider(JwtProvider jwtProvider) {
307+
pluginContext.registerJWTProvider(jwtProvider);
308+
}
309+
305310
public String getServiceType() {
306311
return pluginConfig.getServiceType();
307312
}

agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java

Lines changed: 13 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.ranger.authorization.hadoop.utils.RangerCredentialProvider;
3838
import org.apache.ranger.authorization.utils.JsonUtils;
3939
import org.apache.ranger.authorization.utils.StringUtil;
40+
import org.apache.ranger.plugin.authn.JwtProvider;
4041
import org.slf4j.Logger;
4142
import org.slf4j.LoggerFactory;
4243

@@ -49,11 +50,9 @@
4950
import javax.ws.rs.core.Cookie;
5051
import javax.ws.rs.core.Response;
5152

52-
import java.io.BufferedReader;
5353
import java.io.File;
5454
import java.io.FileInputStream;
5555
import java.io.FileNotFoundException;
56-
import java.io.FileReader;
5756
import java.io.IOException;
5857
import java.io.InputStream;
5958
import java.security.KeyManagementException;
@@ -109,6 +108,7 @@ public class RangerRESTClient {
109108
private int lastKnownActiveUrlIndex;
110109
private volatile Client client;
111110
private volatile Client cookieAuthClient;
111+
private JwtProvider jwtProvider;
112112
private ClientFilter jwtAuthFilter;
113113
private ClientFilter basicAuthFilter;
114114

@@ -190,6 +190,10 @@ public void setBasicAuthInfo(String username, String password) {
190190
setBasicAuthFilter(username, password);
191191
}
192192

193+
public void setJwtProvider(JwtProvider jwtProvider) {
194+
this.jwtProvider = jwtProvider;
195+
}
196+
193197
public WebResource getResource(String relativeUrl) {
194198
return getClient().resource(getUrl() + relativeUrl);
195199
}
@@ -773,14 +777,16 @@ private Client buildClient() {
773777
return client;
774778
}
775779

776-
private void setJWTFilter(String jwtAsString) {
777-
if (StringUtils.isNotBlank(jwtAsString)) {
780+
private void setJWTFilter() {
781+
JwtProvider jwtProvider = this.jwtProvider;
782+
if (jwtProvider != null) {
778783
LOG.info("Registering JWT auth header in REST client");
779-
780784
jwtAuthFilter = new ClientFilter() {
781785
@Override
782786
public ClientResponse handle(ClientRequest clientRequest) throws ClientHandlerException {
783-
clientRequest.getHeaders().add("Authorization", JWT_HEADER_PREFIX + jwtAsString);
787+
String jwt = jwtProvider.getJwt();
788+
789+
clientRequest.getHeaders().putSingle("Authorization", JWT_HEADER_PREFIX + jwt);
784790

785791
return getNext().handle(clientRequest);
786792
}
@@ -827,67 +833,16 @@ private void init(Configuration config) {
827833
}
828834
}
829835

830-
String jwtAsString = fetchJWT(propertyPrefix, config);
831836
String username = config.get(propertyPrefix + ".policy.rest.client.username");
832837
String password = config.get(propertyPrefix + ".policy.rest.client.password");
833838

834-
setJWTFilter(jwtAsString);
839+
setJWTFilter();
835840

836841
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
837842
setBasicAuthFilter(username, password);
838843
}
839844
}
840845

841-
private String fetchJWT(String propertyPrefix, Configuration config) {
842-
final String jwtSrc = config.get(propertyPrefix + ".policy.rest.client.jwt.source");
843-
844-
if (StringUtils.isNotEmpty(jwtSrc)) {
845-
switch (jwtSrc) {
846-
case "env":
847-
String jwtEnvVar = config.get(propertyPrefix + ".policy.rest.client.jwt.env");
848-
if (StringUtils.isNotEmpty(jwtEnvVar)) {
849-
String jwt = System.getenv(jwtEnvVar);
850-
if (StringUtils.isNotBlank(jwt)) {
851-
return jwt;
852-
}
853-
}
854-
break;
855-
case "file":
856-
String jwtFilePath = config.get(propertyPrefix + ".policy.rest.client.jwt.file");
857-
if (StringUtils.isNotEmpty(jwtFilePath)) {
858-
File jwtFile = new File(jwtFilePath);
859-
if (jwtFile.exists()) {
860-
try (BufferedReader reader = new BufferedReader(new FileReader(jwtFile))) {
861-
String line = null;
862-
while ((line = reader.readLine()) != null) {
863-
if (StringUtils.isNotBlank(line) && !line.startsWith("#")) {
864-
return line;
865-
}
866-
}
867-
} catch (IOException e) {
868-
LOG.error("Failed to read JWT from file: {}", jwtFilePath, e);
869-
}
870-
}
871-
}
872-
break;
873-
case "cred":
874-
String credFilePath = config.get(propertyPrefix + ".policy.rest.client.jwt.cred.file");
875-
String credAlias = config.get(propertyPrefix + ".policy.rest.client.jwt.cred.alias");
876-
if (StringUtils.isNotEmpty(credFilePath) && StringUtils.isNotEmpty(credAlias)) {
877-
String jwt = RangerCredentialProvider.getInstance().getCredentialString(credFilePath, credAlias);
878-
if (StringUtils.isNotBlank(jwt)) {
879-
return jwt;
880-
}
881-
}
882-
break;
883-
}
884-
} else {
885-
LOG.info("JWT source not configured, proceeding without JWT");
886-
}
887-
888-
return null;
889-
}
890-
891846
private boolean isSslEnabled(String url) {
892847
return !StringUtils.isEmpty(url) && url.toLowerCase().startsWith("https");
893848
}

0 commit comments

Comments
 (0)