diff --git a/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier b/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier new file mode 100644 index 0000000000..81b257380d --- /dev/null +++ b/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier @@ -0,0 +1,16 @@ +# 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. + +org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier diff --git a/META-INF/services/org.apache.hadoop.security.token.TokenRenewer b/META-INF/services/org.apache.hadoop.security.token.TokenRenewer new file mode 100644 index 0000000000..e976a9e7c5 --- /dev/null +++ b/META-INF/services/org.apache.hadoop.security.token.TokenRenewer @@ -0,0 +1,16 @@ +# 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. + +org.apache.ranger.plugin.util.RangerTokenRenewer diff --git a/META-INF/services/org.apache.spark.security.HadoopDelegationTokenProvider b/META-INF/services/org.apache.spark.security.HadoopDelegationTokenProvider new file mode 100644 index 0000000000..6734fc14fe --- /dev/null +++ b/META-INF/services/org.apache.spark.security.HadoopDelegationTokenProvider @@ -0,0 +1,15 @@ +# +# 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. + +org.apache.kyuubi.plugin.spark.authz.ranger.RangerDelegationTokenProvider diff --git a/agents-audit/pom.xml b/agents-audit/pom.xml index 046abf3f51..ba6d8b2581 100644 --- a/agents-audit/pom.xml +++ b/agents-audit/pom.xml @@ -274,6 +274,17 @@ ranger-plugins-cred ${project.version} + + org.apache.solr + solr-hadoop-auth-framework + ${solr.version} + + + org.apache.hadoop + * + + + org.apache.solr solr-solrj diff --git a/agents-audit/src/main/java/org/apache/ranger/audit/destination/SolrAuditDestination.java b/agents-audit/src/main/java/org/apache/ranger/audit/destination/SolrAuditDestination.java index 4e7cbe8e3f..1225c94f59 100644 --- a/agents-audit/src/main/java/org/apache/ranger/audit/destination/SolrAuditDestination.java +++ b/agents-audit/src/main/java/org/apache/ranger/audit/destination/SolrAuditDestination.java @@ -27,15 +27,19 @@ import org.apache.ranger.audit.utils.KerberosAction; import org.apache.ranger.audit.utils.KerberosUser; import org.apache.ranger.audit.utils.KerberosJAASConfigUser; +import org.apache.hadoop.security.Credentials; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.impl.HttpClientUtil; +import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder; import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder; import org.apache.solr.client.solrj.impl.LBHttpSolrClient; import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.hadoop.SolrDelegationTokenUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -160,26 +164,57 @@ synchronized void connect() { LOG.info("Solr zkHosts=" + zkHosts + ", solrURLs=" + urls + ", collectionName=" + collectionName); + // Check for a Solr delegation token in UGI credentials + String delegationToken = null; + try { + Credentials creds = UserGroupInformation.getLoginUser().getCredentials(); + delegationToken = SolrDelegationTokenUtil.findTokenInCredentials(creds); + if (delegationToken != null) { + LOG.info("Found Solr delegation token in UGI credentials, will use it for authentication"); + } + } catch (Exception e) { + LOG.debug("Failed to look up Solr delegation token, falling back to Kerberos", e); + } + if (zkHosts != null && !zkHosts.isEmpty()) { LOG.info("Connecting to solr cloud using zkHosts=" + zkHosts); try { - // Instantiate - Krb5HttpClientBuilder krbBuild = new Krb5HttpClientBuilder(); - SolrHttpClientBuilder kb = krbBuild.getBuilder(); - HttpClientUtil.setHttpClientBuilder(kb); - final List zkhosts = new ArrayList(Arrays.asList(zkHosts.split(","))); - final CloudSolrClient solrCloudClient = MiscUtil.executePrivilegedAction(new PrivilegedExceptionAction() { - @Override - public CloudSolrClient run() throws Exception { - CloudSolrClient solrCloudClient = new CloudSolrClient.Builder(zkhosts, Optional.empty()).build(); - return solrCloudClient; - }; - }); - - solrCloudClient.setDefaultCollection(collectionName); - me = solrClient = solrCloudClient; + + if (delegationToken != null) { + final String dt = delegationToken; + final CloudSolrClient solrCloudClient = MiscUtil.executePrivilegedAction(new PrivilegedExceptionAction() { + @Override + public CloudSolrClient run() throws Exception { + return new CloudSolrClient.Builder(zkhosts, Optional.empty()) + .withLBHttpSolrClientBuilder(new LBHttpSolrClient.Builder() + .withConnectionTimeout(1000) + .withHttpSolrClientBuilder( + new HttpSolrClient.Builder() + .withKerberosDelegationToken(dt))) + .build(); + } + }); + solrCloudClient.setDefaultCollection(collectionName); + me = solrClient = solrCloudClient; + } else { + // Instantiate + Krb5HttpClientBuilder krbBuild = new Krb5HttpClientBuilder(); + SolrHttpClientBuilder kb = krbBuild.getBuilder(); + HttpClientUtil.setHttpClientBuilder(kb); + + final CloudSolrClient solrCloudClient = MiscUtil.executePrivilegedAction(new PrivilegedExceptionAction() { + @Override + public CloudSolrClient run() throws Exception { + CloudSolrClient solrCloudClient = new CloudSolrClient.Builder(zkhosts, Optional.empty()).build(); + return solrCloudClient; + }; + }); + + solrCloudClient.setDefaultCollection(collectionName); + me = solrClient = solrCloudClient; + } } catch (Throwable t) { LOG.error("Can't connect to Solr server. ZooKeepers=" + zkHosts, t); @@ -190,25 +225,46 @@ public CloudSolrClient run() throws Exception { } else if (solrURLs != null && !solrURLs.isEmpty()) { try { LOG.info("Connecting to Solr using URLs=" + solrURLs); - Krb5HttpClientBuilder krbBuild = new Krb5HttpClientBuilder(); - SolrHttpClientBuilder kb = krbBuild.getBuilder(); - HttpClientUtil.setHttpClientBuilder(kb); final List solrUrls = solrURLs; - final LBHttpSolrClient lbSolrClient = MiscUtil.executePrivilegedAction(new PrivilegedExceptionAction() { - @Override - public LBHttpSolrClient run() throws Exception { - LBHttpSolrClient.Builder builder = new LBHttpSolrClient.Builder(); - builder.withBaseSolrUrl(solrUrls.get(0)); - builder.withConnectionTimeout(1000); - LBHttpSolrClient lbSolrClient = builder.build(); - return lbSolrClient; - }; - }); - - for (int i = 1; i < solrURLs.size(); i++) { - lbSolrClient.addSolrServer(solrURLs.get(i)); + + if (delegationToken != null) { + final String dt = delegationToken; + final LBHttpSolrClient lbSolrClient = MiscUtil.executePrivilegedAction(new PrivilegedExceptionAction() { + @Override + public LBHttpSolrClient run() throws Exception { + return new LBHttpSolrClient.Builder() + .withBaseSolrUrl(solrUrls.get(0)) + .withConnectionTimeout(1000) + .withHttpSolrClientBuilder( + new HttpSolrClient.Builder() + .withKerberosDelegationToken(dt)) + .build(); + } + }); + for (int i = 1; i < solrURLs.size(); i++) { + lbSolrClient.addSolrServer(solrURLs.get(i)); + } + me = solrClient = lbSolrClient; + } else { + Krb5HttpClientBuilder krbBuild = new Krb5HttpClientBuilder(); + SolrHttpClientBuilder kb = krbBuild.getBuilder(); + HttpClientUtil.setHttpClientBuilder(kb); + final LBHttpSolrClient lbSolrClient = MiscUtil.executePrivilegedAction(new PrivilegedExceptionAction() { + @Override + public LBHttpSolrClient run() throws Exception { + LBHttpSolrClient.Builder builder = new LBHttpSolrClient.Builder(); + builder.withBaseSolrUrl(solrUrls.get(0)); + builder.withConnectionTimeout(1000); + LBHttpSolrClient lbSolrClient = builder.build(); + return lbSolrClient; + }; + }); + + for (int i = 1; i < solrURLs.size(); i++) { + lbSolrClient.addSolrServer(solrURLs.get(i)); + } + me = solrClient = lbSolrClient; } - me = solrClient = lbSolrClient; } catch (Throwable t) { LOG.error("Can't connect to Solr server. URL=" + solrURLs, t); diff --git a/agents-common/src/main/java/org/apache/ranger/admin/client/AbstractRangerAdminClient.java b/agents-common/src/main/java/org/apache/ranger/admin/client/AbstractRangerAdminClient.java index fbac4ea9f3..e440e675c0 100644 --- a/agents-common/src/main/java/org/apache/ranger/admin/client/AbstractRangerAdminClient.java +++ b/agents-common/src/main/java/org/apache/ranger/admin/client/AbstractRangerAdminClient.java @@ -23,12 +23,14 @@ import com.google.gson.GsonBuilder; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.token.Token; import org.apache.ranger.plugin.model.RangerRole; import org.apache.ranger.plugin.model.ResourceMappingDiffs; import org.apache.ranger.plugin.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; import java.util.List; public abstract class AbstractRangerAdminClient implements RangerAdminClient { @@ -127,6 +129,20 @@ public ResourceMappingDiffs getResourceMappingDiffs(String sourceService, String return null; } + @Override + public Token getDelegationToken(String renewer) throws Exception { + return null; + } + + @Override + public long renewDelegationToken(Token token) throws Exception { + return 0; + } + + @Override + public void cancelDelegationToken(Token token) throws Exception { + } + public boolean isKerberosEnabled(UserGroupInformation user) { final boolean ret; @@ -138,4 +154,22 @@ public boolean isKerberosEnabled(UserGroupInformation user) { return ret; } + + @SuppressWarnings("unchecked") + public static Token getDelegationTokenFromUGI() { + try { + UserGroupInformation ugi = UserGroupInformation.getLoginUser(); + if (ugi != null) { + Collection> tokens = ugi.getCredentials().getAllTokens(); + for (Token token : tokens) { + if (RangerDelegationTokenIdentifier.RANGER_DELEGATION_KIND.equals(token.getKind())) { + return (Token) token; + } + } + } + } catch (Exception e) { + LOG.debug("Failed to get delegation token from UGI", e); + } + return null; + } } diff --git a/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminClient.java b/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminClient.java index d16608460f..0dc5b7bbf7 100644 --- a/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminClient.java +++ b/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminClient.java @@ -21,10 +21,12 @@ import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.token.Token; import org.apache.ranger.plugin.model.RangerRole; import org.apache.ranger.plugin.model.ResourceMappingDiffs; import org.apache.ranger.plugin.util.GrantRevokeRequest; import org.apache.ranger.plugin.util.GrantRevokeRoleRequest; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; import org.apache.ranger.plugin.util.RangerRoles; import org.apache.ranger.plugin.util.ServicePolicies; import org.apache.ranger.plugin.util.ServiceTags; @@ -66,4 +68,10 @@ public interface RangerAdminClient { RangerUserStore getUserStoreIfUpdated(long lastKnownUserStoreVersion, long lastActivationTimeInMillis) throws Exception; ResourceMappingDiffs getResourceMappingDiffs(String sourceService, String targetService, Long diffId) throws Exception; + + Token getDelegationToken(String renewer) throws Exception; + + long renewDelegationToken(Token token) throws Exception; + + void cancelDelegationToken(Token token) throws Exception; } diff --git a/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminRESTClient.java b/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminRESTClient.java index f2e86aa406..bae3d295a9 100644 --- a/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminRESTClient.java +++ b/agents-common/src/main/java/org/apache/ranger/admin/client/RangerAdminRESTClient.java @@ -32,10 +32,13 @@ import javax.ws.rs.core.Cookie; import javax.ws.rs.core.NewCookie; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.token.Token; import org.apache.http.HttpStatus; import org.apache.ranger.admin.client.datatype.RESTResponse; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; import org.apache.ranger.audit.provider.MiscUtil; import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; import org.apache.ranger.authorization.utils.StringUtil; @@ -141,29 +144,9 @@ public ServicePolicies getServicePoliciesIfUpdated(final long lastKnownVersion, queryParams.put(RangerRESTUtils.REST_PARAM_SUPPORTS_POLICY_DELTAS, Boolean.toString(supportsPolicyDeltas)); queryParams.put(RangerRESTUtils.REST_PARAM_CAPABILITIES, pluginCapabilities); - if (isSecureMode) { - if (LOG.isDebugEnabled()) { - LOG.debug("Checking Service policy if updated as user : " + user); - } - - response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { - try { - String relativeURL = RangerRESTUtils.REST_URL_POLICY_GET_FOR_SECURE_SERVICE_IF_UPDATED + serviceNameUrlParam; - - return restClient.get(relativeURL, queryParams, sessionId); - } catch (Exception e) { - LOG.error("Failed to get response, Error is : "+e.getMessage()); - } - - return null; - }); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Checking Service policy if updated with old api call"); - } - String relativeURL = RangerRESTUtils.REST_URL_POLICY_GET_FOR_SERVICE_IF_UPDATED + serviceNameUrlParam; - response = restClient.get(relativeURL, queryParams, sessionId); - } + String secureURL = RangerRESTUtils.REST_URL_POLICY_GET_FOR_SECURE_SERVICE_IF_UPDATED + serviceNameUrlParam; + String nonSecureURL = RangerRESTUtils.REST_URL_POLICY_GET_FOR_SERVICE_IF_UPDATED + serviceNameUrlParam; + response = getWithAuth(secureURL, nonSecureURL, queryParams, user, isSecureMode, sessionId); checkAndResetSessionCookie(response); @@ -226,28 +209,9 @@ public RangerRoles getRolesIfUpdated(final long lastKnownRoleVersion, final long queryParams.put(RangerRESTUtils.REST_PARAM_CLUSTER_NAME, clusterName); queryParams.put(RangerRESTUtils.REST_PARAM_CAPABILITIES, pluginCapabilities); - if (isSecureMode) { - if (LOG.isDebugEnabled()) { - LOG.debug("Checking Roles updated as user : " + user); - } - response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { - try { - String relativeURL = RangerRESTUtils.REST_URL_SERVICE_SERCURE_GET_USER_GROUP_ROLES + serviceNameUrlParam; - - return restClient.get(relativeURL, queryParams, sessionId); - } catch (Exception e) { - LOG.error("Failed to get response, Error is : "+e.getMessage()); - } - - return null; - }); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Checking Roles updated as user : " + user); - } - String relativeURL = RangerRESTUtils.REST_URL_SERVICE_GET_USER_GROUP_ROLES + serviceNameUrlParam; - response = restClient.get(relativeURL, queryParams, sessionId); - } + String secureURL = RangerRESTUtils.REST_URL_SERVICE_SERCURE_GET_USER_GROUP_ROLES + serviceNameUrlParam; + String nonSecureURL = RangerRESTUtils.REST_URL_SERVICE_GET_USER_GROUP_ROLES + serviceNameUrlParam; + response = getWithAuth(secureURL, nonSecureURL, queryParams, user, isSecureMode, sessionId); checkAndResetSessionCookie(response); @@ -820,25 +784,9 @@ public ServiceTags getServiceTagsIfUpdated(final long lastKnownVersion, final lo queryParams.put(RangerRESTUtils.REST_PARAM_SUPPORTS_TAG_DELTAS, Boolean.toString(supportsTagDeltas)); queryParams.put(RangerRESTUtils.REST_PARAM_CAPABILITIES, pluginCapabilities); - if (isSecureMode) { - if (LOG.isDebugEnabled()) { - LOG.debug("getServiceTagsIfUpdated as user " + user); - } - response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { - try { - String relativeURL = RangerRESTUtils.REST_URL_GET_SECURE_SERVICE_TAGS_IF_UPDATED + serviceNameUrlParam; - - return restClient.get(relativeURL, queryParams, sessionId); - } catch (Exception e) { - LOG.error("Failed to get response, Error is : "+e.getMessage()); - } - - return null; - }); - } else { - String relativeURL = RangerRESTUtils.REST_URL_GET_SERVICE_TAGS_IF_UPDATED + serviceNameUrlParam; - response = restClient.get(relativeURL, queryParams, sessionId); - } + String secureURL = RangerRESTUtils.REST_URL_GET_SECURE_SERVICE_TAGS_IF_UPDATED + serviceNameUrlParam; + String nonSecureURL = RangerRESTUtils.REST_URL_GET_SERVICE_TAGS_IF_UPDATED + serviceNameUrlParam; + response = getWithAuth(secureURL, nonSecureURL, queryParams, user, isSecureMode, sessionId); checkAndResetSessionCookie(response); @@ -951,28 +899,9 @@ public RangerUserStore getUserStoreIfUpdated(long lastKnownUserStoreVersion, lon queryParams.put(RangerRESTUtils.REST_PARAM_CLUSTER_NAME, clusterName); queryParams.put(RangerRESTUtils.REST_PARAM_CAPABILITIES, pluginCapabilities); - if (isSecureMode) { - if (LOG.isDebugEnabled()) { - LOG.debug("Checking UserStore updated as user : " + user); - } - response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { - try { - String relativeURL = RangerRESTUtils.REST_URL_SERVICE_SERCURE_GET_USERSTORE + serviceNameUrlParam; - - return restClient.get(relativeURL, queryParams, sessionId); - } catch (Exception e) { - LOG.error("Failed to get response, Error is : "+e.getMessage()); - } - - return null; - }); - } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Checking UserStore updated as user : " + user); - } - String relativeURL = RangerRESTUtils.REST_URL_SERVICE_GET_USERSTORE + serviceNameUrlParam; - response = restClient.get(relativeURL, queryParams, sessionId); - } + String secureURL = RangerRESTUtils.REST_URL_SERVICE_SERCURE_GET_USERSTORE + serviceNameUrlParam; + String nonSecureURL = RangerRESTUtils.REST_URL_SERVICE_GET_USERSTORE + serviceNameUrlParam; + response = getWithAuth(secureURL, nonSecureURL, queryParams, user, isSecureMode, sessionId); checkAndResetSessionCookie(response); @@ -1021,8 +950,9 @@ public ResourceMappingDiffs getResourceMappingDiffs(String sourceService, String LOG.debug("==> RangerAdminRESTClient.getResourceMappingDiffs({}, {}, {})", sourceService, targetService, diffId); } - UserGroupInformation user = MiscUtil.getUGILoginUser(); - Cookie sessionId = this.sessionId; + final UserGroupInformation user = MiscUtil.getUGILoginUser(); + final boolean isSecureMode = isKerberosEnabled(user); + final Cookie sessionId = this.sessionId; Map queryParams = new HashMap<>(); if (diffId != null) { @@ -1030,40 +960,199 @@ public ResourceMappingDiffs getResourceMappingDiffs(String sourceService, String } String relativeURL = String.format("/service/resource-mappings/%s/%s/diffs/new", sourceService, targetService); - final ClientResponse response; - if (isKerberosEnabled(user)) { + final ClientResponse response = getWithAuth(relativeURL, relativeURL, queryParams, user, isSecureMode, sessionId); + + checkAndResetSessionCookie(response); + + ResourceMappingDiffs diffs; + if (response != null && response.getStatus() == HttpServletResponse.SC_OK) { + diffs = JsonUtilsV2.readResponse(response, ResourceMappingDiffs.class); + } else { + RESTResponse resp = RESTResponse.fromClientResponse(response); + LOG.error("Error getting resource mappings. Response={}", resp); + throw new Exception(resp.getMessage()); + } + + if(LOG.isDebugEnabled()) { + LOG.debug("<== RangerAdminRESTClient.getResourceMappingDiffs({}, {}, {})", sourceService, targetService, diffId); + } + + return diffs; + } + + @Override + public Token getDelegationToken(final String renewer) throws Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("==> RangerAdminRESTClient.getDelegationToken(renewer=" + renewer + ")"); + } + + final UserGroupInformation user = MiscUtil.getUGILoginUser(); + final boolean isSecureMode = isKerberosEnabled(user); + + if (!isSecureMode) { + throw new UnsupportedOperationException("Delegation tokens require Kerberos authentication"); + } + + final Cookie sessionId = this.sessionId; + + Map queryParams = new HashMap(); + if (renewer != null) { + queryParams.put(RangerRESTUtils.REST_PARAM_RENEWER, renewer); + } + + final ClientResponse response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { + try { + return restClient.get(RangerRESTUtils.REST_URL_DELEGATION_TOKEN, queryParams, sessionId); + } catch (Exception e) { + LOG.error("Failed to get delegation token", e); + } + return null; + }); + + checkAndResetSessionCookie(response); + + if (response != null && response.getStatus() == HttpServletResponse.SC_OK) { + @SuppressWarnings("unchecked") + Map result = JsonUtilsV2.readResponse(response, Map.class); + String tokenEncoded = result.get("urlString"); + Token token = new Token<>(); + token.decodeFromUrlString(tokenEncoded); + token.setService(new Text(restClient.getUrl())); + if (LOG.isDebugEnabled()) { - LOG.debug("getResourceMappingDiffs as user {}", user); + LOG.debug("<== RangerAdminRESTClient.getDelegationToken(): token obtained, service={}", restClient.getUrl()); } + return token; + } else { + RESTResponse resp = RESTResponse.fromClientResponse(response); + throw new Exception("Failed to get delegation token: " + (resp != null ? resp.getMessage() : "null response")); + } + } + + @Override + public long renewDelegationToken(final Token token) throws Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("==> RangerAdminRESTClient.renewDelegationToken()"); + } + + final UserGroupInformation user = MiscUtil.getUGILoginUser(); + final boolean isSecureMode = isKerberosEnabled(user); + final Cookie sessionId = this.sessionId; + + final Map requestBody = new HashMap<>(); + requestBody.put("token", token.encodeToUrlString()); + + final ClientResponse response; + if (isSecureMode) { response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { try { - return restClient.get(relativeURL, queryParams, sessionId); + return restClient.put(RangerRESTUtils.REST_URL_DELEGATION_TOKEN_RENEW, (Object) requestBody, sessionId); } catch (Exception e) { - LOG.error("Failed to get response", e); + LOG.error("Failed to renew delegation token", e); } - return null; }); } else { - response = restClient.get(relativeURL, queryParams, sessionId); + response = restClient.put(RangerRESTUtils.REST_URL_DELEGATION_TOKEN_RENEW, (Object) requestBody, sessionId); } checkAndResetSessionCookie(response); - ResourceMappingDiffs diffs; if (response != null && response.getStatus() == HttpServletResponse.SC_OK) { - diffs = JsonUtilsV2.readResponse(response, ResourceMappingDiffs.class); + @SuppressWarnings("unchecked") + Map result = JsonUtilsV2.readResponse(response, Map.class); + Number expiryTime = (Number) result.get("expirationTime"); + + if (LOG.isDebugEnabled()) { + LOG.debug("<== RangerAdminRESTClient.renewDelegationToken(): newExpiry={}", expiryTime); + } + return expiryTime.longValue(); } else { RESTResponse resp = RESTResponse.fromClientResponse(response); - LOG.error("Error getting resource mappings. Response={}", resp); - throw new Exception(resp.getMessage()); + throw new Exception("Failed to renew delegation token: " + (resp != null ? resp.getMessage() : "null response")); } + } - if(LOG.isDebugEnabled()) { - LOG.debug("<== RangerAdminRESTClient.getResourceMappingDiffs({}, {}, {})", sourceService, targetService, diffId); + @Override + public void cancelDelegationToken(final Token token) throws Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("==> RangerAdminRESTClient.cancelDelegationToken()"); } - return diffs; + final UserGroupInformation user = MiscUtil.getUGILoginUser(); + final boolean isSecureMode = isKerberosEnabled(user); + final Cookie sessionId = this.sessionId; + + final Map requestBody = new HashMap<>(); + requestBody.put("token", token.encodeToUrlString()); + + final ClientResponse response; + if (isSecureMode) { + response = MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { + try { + return restClient.put(RangerRESTUtils.REST_URL_DELEGATION_TOKEN_CANCEL, (Object) requestBody, sessionId); + } catch (Exception e) { + LOG.error("Failed to cancel delegation token", e); + } + return null; + }); + } else { + response = restClient.put(RangerRESTUtils.REST_URL_DELEGATION_TOKEN_CANCEL, (Object) requestBody, sessionId); + } + + checkAndResetSessionCookie(response); + + if (response == null || (response.getStatus() != HttpServletResponse.SC_OK && response.getStatus() != HttpServletResponse.SC_NO_CONTENT)) { + RESTResponse resp = RESTResponse.fromClientResponse(response); + throw new Exception("Failed to cancel delegation token: " + (resp != null ? resp.getMessage() : "null response")); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("<== RangerAdminRESTClient.cancelDelegationToken()"); + } + } + + private ClientResponse getWithAuth(String secureRelativeURL, String nonSecureRelativeURL, + Map queryParams, + UserGroupInformation user, boolean isSecureMode, + Cookie sessionId) throws Exception { + Token delegationToken = getDelegationTokenFromUGI(); + + if (delegationToken != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Using delegation token for Ranger auth"); + } + Map headers = new HashMap<>(); + headers.put(RangerRESTUtils.HEADER_DELEGATION_TOKEN, delegationToken.encodeToUrlString()); + ClientResponse response = restClient.get(secureRelativeURL, queryParams, sessionId, headers); + + if (response != null && response.getStatus() == HttpServletResponse.SC_UNAUTHORIZED && isSecureMode) { + LOG.warn("Delegation token auth failed (HTTP 401). Falling back to Kerberos/SPNEGO"); + + response = getWithKerberos(secureRelativeURL, queryParams, sessionId); + } + + return response; + } else if (isSecureMode) { + if (LOG.isDebugEnabled()) { + LOG.debug("Using Kerberos auth as user: " + user); + } + return getWithKerberos(secureRelativeURL, queryParams, sessionId); + } else { + return restClient.get(nonSecureRelativeURL, queryParams, sessionId); + } + } + + private ClientResponse getWithKerberos(String relativeURL, Map queryParams, + Cookie sessionId) throws Exception { + return MiscUtil.executePrivilegedAction((PrivilegedExceptionAction) () -> { + try { + return restClient.get(relativeURL, queryParams, sessionId); + } catch (Exception e) { + LOG.error("Kerberos/SPNEGO auth failed: " + e.getMessage()); + } + return null; + }); } private void checkAndResetSessionCookie(ClientResponse response) { diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerDelegationTokenIdentifier.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerDelegationTokenIdentifier.java new file mode 100644 index 0000000000..bcd23b5806 --- /dev/null +++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerDelegationTokenIdentifier.java @@ -0,0 +1,39 @@ +/* + * 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.plugin.util; + +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier; + +public class RangerDelegationTokenIdentifier extends AbstractDelegationTokenIdentifier { + public static final Text RANGER_DELEGATION_KIND = new Text("RANGER_DELEGATION_TOKEN"); + + public RangerDelegationTokenIdentifier() { + } + + public RangerDelegationTokenIdentifier(Text owner, Text renewer, Text realUser) { + super(owner, renewer, realUser); + } + + @Override + public Text getKind() { + return RANGER_DELEGATION_KIND; + } +} diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java index e5461c2e68..4df25a8122 100644 --- a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java +++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java @@ -531,6 +531,10 @@ public ClientResponse get(String relativeUrl, Map params) throws } public ClientResponse get(String relativeUrl, Map params, Cookie sessionId) throws Exception{ + return get(relativeUrl, params, sessionId, null); + } + + public ClientResponse get(String relativeUrl, Map params, Cookie sessionId, Map headers) throws Exception{ ClientResponse finalResponse = null; int startIndex = this.lastKnownActiveUrlIndex; int retryAttempt = 0; @@ -541,6 +545,12 @@ public ClientResponse get(String relativeUrl, Map params, Cookie try { WebResource.Builder br = createWebResource(currentIndex, relativeUrl, params, sessionId); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + br = br.header(entry.getKey(), entry.getValue()); + } + } + finalResponse = br.accept(RangerRESTUtils.REST_EXPECTED_MIME_TYPE).type(RangerRESTUtils.REST_MIME_TYPE_JSON).get(ClientResponse.class); if (finalResponse != null) { diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTUtils.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTUtils.java index 4018193b2d..6c00555b9a 100644 --- a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTUtils.java +++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTUtils.java @@ -89,6 +89,12 @@ public class RangerRESTUtils { public static final String REST_PARAM_DIFF_ID = "diffId"; + public static final String REST_URL_DELEGATION_TOKEN = "/service/delegation-token"; + public static final String REST_URL_DELEGATION_TOKEN_RENEW = "/service/delegation-token/renew"; + public static final String REST_URL_DELEGATION_TOKEN_CANCEL = "/service/delegation-token/cancel"; + public static final String REST_PARAM_RENEWER = "renewer"; + public static final String HEADER_DELEGATION_TOKEN = "X-Delegation-Token-Encoded"; + public static String hostname; static { diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerTokenRenewer.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerTokenRenewer.java new file mode 100644 index 0000000000..60003420d8 --- /dev/null +++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerTokenRenewer.java @@ -0,0 +1,108 @@ +/* + * 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.plugin.util; + +import java.io.IOException; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenRenewer; +import org.apache.ranger.admin.client.RangerAdminRESTClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RangerTokenRenewer extends TokenRenewer { + private static final Logger LOG = LoggerFactory.getLogger(RangerTokenRenewer.class); + + private static final String CONFIG_PREFIX = "ranger.plugin.delegation-token"; + + private volatile RangerAdminRESTClient client; + + @Override + public boolean handleKind(Text kind) { + return RangerDelegationTokenIdentifier.RANGER_DELEGATION_KIND.equals(kind); + } + + @Override + public boolean isManaged(Token token) throws IOException { + return handleKind(token.getKind()); + } + + @Override + @SuppressWarnings("unchecked") + public long renew(Token token, Configuration conf) throws IOException, InterruptedException { + LOG.info("Renewing Ranger delegation token"); + Token rangerToken = (Token) token; + try { + RangerAdminRESTClient adminClient = getOrCreateClient(conf, token); + long newExpiry = adminClient.renewDelegationToken(rangerToken); + LOG.info("Ranger delegation token renewed, new expiry={}", newExpiry); + return newExpiry; + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed to renew Ranger delegation token", e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void cancel(Token token, Configuration conf) throws IOException, InterruptedException { + LOG.info("Cancelling Ranger delegation token"); + Token rangerToken = (Token) token; + try { + RangerAdminRESTClient adminClient = getOrCreateClient(conf, token); + adminClient.cancelDelegationToken(rangerToken); + LOG.info("Ranger delegation token cancelled"); + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed to cancel Ranger delegation token", e); + } + } + + private RangerAdminRESTClient getOrCreateClient(Configuration conf, Token token) throws IOException { + RangerAdminRESTClient ret = client; + if (ret != null) { + return ret; + } + + synchronized (this) { + if (client == null) { + String rangerAdminUrl = token.getService().toString(); + if (rangerAdminUrl == null || rangerAdminUrl.isEmpty()) { + throw new IOException("Token does not have Ranger admin URL in service field"); + } + + conf.set(CONFIG_PREFIX + ".policy.rest.url", rangerAdminUrl); + + try { + RangerAdminRESTClient newClient = new RangerAdminRESTClient(); + newClient.init("ranger", "tokenRenewer", CONFIG_PREFIX, conf); + client = newClient; + } catch (Exception e) { + throw new IOException("Failed to create RangerAdminRESTClient for " + rangerAdminUrl, e); + } + } + return client; + } + } +} diff --git a/agents-common/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier b/agents-common/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier new file mode 100644 index 0000000000..2b699b087a --- /dev/null +++ b/agents-common/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier @@ -0,0 +1,17 @@ +# 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. + +org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier diff --git a/agents-common/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer b/agents-common/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer new file mode 100644 index 0000000000..deb853d8c6 --- /dev/null +++ b/agents-common/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer @@ -0,0 +1,17 @@ +# 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. + +org.apache.ranger.plugin.util.RangerTokenRenewer diff --git a/distro/src/main/assembly/admin-web.xml b/distro/src/main/assembly/admin-web.xml index 944624e427..8f8fef34e1 100644 --- a/distro/src/main/assembly/admin-web.xml +++ b/distro/src/main/assembly/admin-web.xml @@ -269,6 +269,7 @@ commons-lang:commons-lang commons-io:commons-io org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} org.apache.httpcomponents:httpclient:jar:${httpcomponents.httpclient.version} org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit diff --git a/distro/src/main/assembly/hbase-agent.xml b/distro/src/main/assembly/hbase-agent.xml index 02e35e3477..af8a542203 100644 --- a/distro/src/main/assembly/hbase-agent.xml +++ b/distro/src/main/assembly/hbase-agent.xml @@ -63,6 +63,7 @@ org.apache.httpcomponents:httpmime:jar:${httpcomponents.httpmime.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/hdfs-agent.xml b/distro/src/main/assembly/hdfs-agent.xml index 2a3425a5b5..8d66e71e53 100644 --- a/distro/src/main/assembly/hdfs-agent.xml +++ b/distro/src/main/assembly/hdfs-agent.xml @@ -89,6 +89,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/hive-agent.xml b/distro/src/main/assembly/hive-agent.xml index c26754bbbe..c6decc3db0 100644 --- a/distro/src/main/assembly/hive-agent.xml +++ b/distro/src/main/assembly/hive-agent.xml @@ -59,6 +59,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/kms.xml b/distro/src/main/assembly/kms.xml index e2063a457c..c1f7689b20 100755 --- a/distro/src/main/assembly/kms.xml +++ b/distro/src/main/assembly/kms.xml @@ -214,6 +214,7 @@ org.apache.hadoop:hadoop-common:jar:${hadoop.version} org.apache.hadoop:hadoop-auth:jar:${hadoop.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} org.apache.ranger:ranger-plugins-common com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} @@ -308,6 +309,7 @@ org.noggit:noggit:jar:${noggit.version} org.apache.zookeeper:zookeeper:jar:${zookeeper.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/knox-agent.xml b/distro/src/main/assembly/knox-agent.xml index ab21063de8..ef5b788e7f 100644 --- a/distro/src/main/assembly/knox-agent.xml +++ b/distro/src/main/assembly/knox-agent.xml @@ -68,6 +68,7 @@ com.fasterxml.jackson.core:jackson-core:jar:${fasterxml.jackson.version} com.fasterxml.jackson.core:jackson-databind:jar:${fasterxml.jackson.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/plugin-atlas.xml b/distro/src/main/assembly/plugin-atlas.xml index 9e45d6ba28..5fbd866fd5 100644 --- a/distro/src/main/assembly/plugin-atlas.xml +++ b/distro/src/main/assembly/plugin-atlas.xml @@ -64,6 +64,7 @@ org.apache.httpcomponents:httpclient:jar:${httpcomponents.httpclient.version} org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/plugin-elasticsearch.xml b/distro/src/main/assembly/plugin-elasticsearch.xml index aae97eed3c..b8a6763f5c 100644 --- a/distro/src/main/assembly/plugin-elasticsearch.xml +++ b/distro/src/main/assembly/plugin-elasticsearch.xml @@ -79,6 +79,7 @@ com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:jar:${fasterxml.jackson.version} com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:jar:${fasterxml.jackson.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} commons-codec:commons-codec com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} diff --git a/distro/src/main/assembly/plugin-kafka.xml b/distro/src/main/assembly/plugin-kafka.xml index ab509aee01..a6075f128a 100644 --- a/distro/src/main/assembly/plugin-kafka.xml +++ b/distro/src/main/assembly/plugin-kafka.xml @@ -73,6 +73,7 @@ com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:jar:${fasterxml.jackson.version} com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:jar:${fasterxml.jackson.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} commons-codec:commons-codec org.codehaus.woodstox:stax2-api com.fasterxml.woodstox:woodstox-core diff --git a/distro/src/main/assembly/plugin-kms.xml b/distro/src/main/assembly/plugin-kms.xml index 1529b20ad2..9f0dee2feb 100755 --- a/distro/src/main/assembly/plugin-kms.xml +++ b/distro/src/main/assembly/plugin-kms.xml @@ -65,6 +65,7 @@ org.noggit:noggit:jar:${noggit.version} org.apache.zookeeper:zookeeper:jar:${zookeeper.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/plugin-kylin.xml b/distro/src/main/assembly/plugin-kylin.xml index 6d7b29c8be..da87929930 100644 --- a/distro/src/main/assembly/plugin-kylin.xml +++ b/distro/src/main/assembly/plugin-kylin.xml @@ -63,6 +63,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/plugin-ozone.xml b/distro/src/main/assembly/plugin-ozone.xml index e6db381d8b..23dd5ec9c9 100644 --- a/distro/src/main/assembly/plugin-ozone.xml +++ b/distro/src/main/assembly/plugin-ozone.xml @@ -99,6 +99,7 @@ org.apache.zookeeper:zookeeper-jute:jar:${zookeeper.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.fasterxml.woodstox:woodstox-core:jar:${fasterxml.woodstox.version} org.codehaus.woodstox:stax2-api:jar:${codehaus.woodstox.stax2api.version} com.sun.jersey:jersey-core diff --git a/distro/src/main/assembly/plugin-presto.xml b/distro/src/main/assembly/plugin-presto.xml index 3818f45565..9cdfc9fb19 100644 --- a/distro/src/main/assembly/plugin-presto.xml +++ b/distro/src/main/assembly/plugin-presto.xml @@ -72,6 +72,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.sun.jersey:jersey-core com.sun.jersey:jersey-server commons-cli:commons-cli diff --git a/distro/src/main/assembly/plugin-solr.xml b/distro/src/main/assembly/plugin-solr.xml index 72fd83f55f..7cc3933a00 100644 --- a/distro/src/main/assembly/plugin-solr.xml +++ b/distro/src/main/assembly/plugin-solr.xml @@ -75,6 +75,7 @@ org.apache.orc:orc-shims:jar:${orc.version} io.airlift:aircompressor:jar:${aircompressor.version} org.apache.hadoop.thirdparty:hadoop-shaded-guava:jar:${hadoop-shaded-guava.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} diff --git a/distro/src/main/assembly/plugin-sqoop.xml b/distro/src/main/assembly/plugin-sqoop.xml index 071b873e16..dd66c095fa 100644 --- a/distro/src/main/assembly/plugin-sqoop.xml +++ b/distro/src/main/assembly/plugin-sqoop.xml @@ -59,6 +59,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/plugin-trino.xml b/distro/src/main/assembly/plugin-trino.xml index 5e629276af..28f1d2c990 100644 --- a/distro/src/main/assembly/plugin-trino.xml +++ b/distro/src/main/assembly/plugin-trino.xml @@ -60,6 +60,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.sun.jersey:jersey-core com.sun.jersey:jersey-server commons-cli:commons-cli diff --git a/distro/src/main/assembly/plugin-yarn.xml b/distro/src/main/assembly/plugin-yarn.xml index 6d469933f8..8dd15597bf 100644 --- a/distro/src/main/assembly/plugin-yarn.xml +++ b/distro/src/main/assembly/plugin-yarn.xml @@ -60,6 +60,7 @@ org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} org.noggit:noggit:jar:${noggit.version} org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} net.java.dev.jna:jna-platform:jar:${jna-platform.version} diff --git a/distro/src/main/assembly/storm-agent.xml b/distro/src/main/assembly/storm-agent.xml index 6c8cbd6a8a..66dc4d3e3c 100644 --- a/distro/src/main/assembly/storm-agent.xml +++ b/distro/src/main/assembly/storm-agent.xml @@ -82,6 +82,7 @@ com.fasterxml.jackson.core:jackson-databind com.fasterxml.jackson.core:jackson-core org.apache.solr:solr-solrj:jar:${solr.version} + org.apache.solr:solr-hadoop-auth-framework:jar:${solr.version} commons-codec:commons-codec com.kstruct:gethostname4j:jar:${kstruct.gethostname4j.version} net.java.dev.jna:jna:jar:${jna.version} diff --git a/pom.xml b/pom.xml index fa530c3fe6..8cc3a9fcc7 100644 --- a/pom.xml +++ b/pom.xml @@ -199,7 +199,7 @@ 2.5 2.0.13 2.0.13 - 8.11.3 + 8.11.4 reuseReports jacoco java diff --git a/security-admin/db/mysql/patches/076-add-delegation-token-tables.sql b/security-admin/db/mysql/patches/076-add-delegation-token-tables.sql new file mode 100644 index 0000000000..75b6d081a7 --- /dev/null +++ b/security-admin/db/mysql/patches/076-add-delegation-token-tables.sql @@ -0,0 +1,55 @@ +-- 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. + +DROP PROCEDURE IF EXISTS add_delegation_token_tables; + +DELIMITER ;; +CREATE PROCEDURE add_delegation_token_tables() BEGIN + + IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'x_ranger_dt_master_key') THEN + CREATE TABLE x_ranger_dt_master_key ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + key_id INT NOT NULL, + expiry_date BIGINT NOT NULL, + key_bytes BLOB NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_key_id (key_id) + ) ; + END IF; + + IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'x_ranger_delegation_token') THEN + CREATE TABLE x_ranger_delegation_token ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + sequence_number INT NOT NULL, + owner VARCHAR(255) NOT NULL, + renewer VARCHAR(255), + real_user VARCHAR(255), + issue_date BIGINT NOT NULL, + max_date BIGINT NOT NULL, + renew_date BIGINT NOT NULL, + master_key_id INT NOT NULL, + token_password BLOB NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_sequence_number (sequence_number) + ) ; + END IF; + +END;; + +DELIMITER ; + +CALL add_delegation_token_tables(); + +DROP PROCEDURE IF EXISTS add_delegation_token_tables; diff --git a/security-admin/db/oracle/patches/076-add-delegation-token-tables.sql b/security-admin/db/oracle/patches/076-add-delegation-token-tables.sql new file mode 100644 index 0000000000..bc333cbcfb --- /dev/null +++ b/security-admin/db/oracle/patches/076-add-delegation-token-tables.sql @@ -0,0 +1,48 @@ +-- 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. + +call spdropsequence('X_RANGER_DT_MASTER_KEY_SEQ'); +CREATE SEQUENCE X_RANGER_DT_MASTER_KEY_SEQ START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +call spdropsequence('X_RANGER_DELEGATION_TOKEN_SEQ'); +CREATE SEQUENCE X_RANGER_DELEGATION_TOKEN_SEQ START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; + +CREATE TABLE x_ranger_dt_master_key ( + id NUMBER(20) NOT NULL, + key_id NUMBER(10) NOT NULL, + expiry_date NUMBER(20) NOT NULL, + key_bytes BLOB NOT NULL, + create_time DATE DEFAULT SYSDATE, + PRIMARY KEY (id), + CONSTRAINT uk_dt_mk_key_id UNIQUE (key_id) +); + +CREATE TABLE x_ranger_delegation_token ( + id NUMBER(20) NOT NULL, + sequence_number NUMBER(10) NOT NULL, + owner VARCHAR2(255) NOT NULL, + renewer VARCHAR2(255), + real_user VARCHAR2(255), + issue_date NUMBER(20) NOT NULL, + max_date NUMBER(20) NOT NULL, + renew_date NUMBER(20) NOT NULL, + master_key_id NUMBER(10) NOT NULL, + token_password BLOB NOT NULL, + create_time DATE DEFAULT SYSDATE, + PRIMARY KEY (id), + CONSTRAINT uk_dt_seq_number UNIQUE (sequence_number) +); + +commit; diff --git a/security-admin/db/postgres/patches/076-add-delegation-token-tables.sql b/security-admin/db/postgres/patches/076-add-delegation-token-tables.sql new file mode 100644 index 0000000000..258c4d156a --- /dev/null +++ b/security-admin/db/postgres/patches/076-add-delegation-token-tables.sql @@ -0,0 +1,40 @@ +-- 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. + +CREATE SEQUENCE IF NOT EXISTS x_ranger_dt_master_key_seq; + +CREATE TABLE IF NOT EXISTS x_ranger_dt_master_key ( + id BIGINT DEFAULT nextval('x_ranger_dt_master_key_seq'::regclass) PRIMARY KEY, + key_id INT NOT NULL UNIQUE, + expiry_date BIGINT NOT NULL, + key_bytes BYTEA NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE SEQUENCE IF NOT EXISTS x_ranger_delegation_token_seq; + +CREATE TABLE IF NOT EXISTS x_ranger_delegation_token ( + id BIGINT DEFAULT nextval('x_ranger_delegation_token_seq'::regclass) PRIMARY KEY, + sequence_number INT NOT NULL UNIQUE, + owner VARCHAR(255) NOT NULL, + renewer VARCHAR(255), + real_user VARCHAR(255), + issue_date BIGINT NOT NULL, + max_date BIGINT NOT NULL, + renew_date BIGINT NOT NULL, + master_key_id INT NOT NULL, + token_password BYTEA NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/security-admin/db/sqlanywhere/patches/076-add-delegation-token-tables.sql b/security-admin/db/sqlanywhere/patches/076-add-delegation-token-tables.sql new file mode 100644 index 0000000000..4d92fb056b --- /dev/null +++ b/security-admin/db/sqlanywhere/patches/076-add-delegation-token-tables.sql @@ -0,0 +1,47 @@ +-- 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. + +call dbo.removeForeignKeysAndTable('x_ranger_dt_master_key') +GO + +CREATE TABLE dbo.x_ranger_dt_master_key ( + id bigint IDENTITY NOT NULL, + key_id int NOT NULL, + expiry_date bigint NOT NULL, + key_bytes varbinary(max) NOT NULL, + create_time datetime DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT x_ranger_dt_master_key_PK_id PRIMARY KEY CLUSTERED(id), + CONSTRAINT x_ranger_dt_master_key_UK_key_id UNIQUE(key_id) +) GO + +call dbo.removeForeignKeysAndTable('x_ranger_delegation_token') +GO + +CREATE TABLE dbo.x_ranger_delegation_token ( + id bigint IDENTITY NOT NULL, + sequence_number int NOT NULL, + owner varchar(255) NOT NULL, + renewer varchar(255), + real_user varchar(255), + issue_date bigint NOT NULL, + max_date bigint NOT NULL, + renew_date bigint NOT NULL, + master_key_id int NOT NULL, + token_password varbinary(max) NOT NULL, + create_time datetime DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT x_ranger_delegation_token_PK_id PRIMARY KEY CLUSTERED(id), + CONSTRAINT x_ranger_delegation_token_UK_seq UNIQUE(sequence_number) +) GO +EXIT diff --git a/security-admin/db/sqlserver/patches/076-add-delegation-token-tables.sql b/security-admin/db/sqlserver/patches/076-add-delegation-token-tables.sql new file mode 100644 index 0000000000..fc2fd10675 --- /dev/null +++ b/security-admin/db/sqlserver/patches/076-add-delegation-token-tables.sql @@ -0,0 +1,55 @@ +-- 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. + +SET ANSI_NULLS ON +SET QUOTED_IDENTIFIER ON +SET ANSI_PADDING ON +GO + +IF (OBJECT_ID('x_ranger_dt_master_key') IS NULL) +BEGIN + CREATE TABLE [dbo].[x_ranger_dt_master_key] ( + [id] [bigint] IDENTITY(1,1) NOT NULL, + [key_id] [int] NOT NULL, + [expiry_date] [bigint] NOT NULL, + [key_bytes] [varbinary](max) NOT NULL, + [create_time] [datetime] DEFAULT GETDATE(), + PRIMARY KEY CLUSTERED ([id] ASC), + CONSTRAINT [x_ranger_dt_mk_UK_key_id] UNIQUE NONCLUSTERED ([key_id] ASC) + ) +END +GO + +IF (OBJECT_ID('x_ranger_delegation_token') IS NULL) +BEGIN + CREATE TABLE [dbo].[x_ranger_delegation_token] ( + [id] [bigint] IDENTITY(1,1) NOT NULL, + [sequence_number] [int] NOT NULL, + [owner] [varchar](255) NOT NULL, + [renewer] [varchar](255) NULL, + [real_user] [varchar](255) NULL, + [issue_date] [bigint] NOT NULL, + [max_date] [bigint] NOT NULL, + [renew_date] [bigint] NOT NULL, + [master_key_id] [int] NOT NULL, + [token_password] [varbinary](max) NOT NULL, + [create_time] [datetime] DEFAULT GETDATE(), + PRIMARY KEY CLUSTERED ([id] ASC), + CONSTRAINT [x_ranger_dt_UK_seq_number] UNIQUE NONCLUSTERED ([sequence_number] ASC) + ) +END +GO + +exit diff --git a/security-admin/src/main/java/org/apache/ranger/biz/RangerDelegationTokenSecretManager.java b/security-admin/src/main/java/org/apache/ranger/biz/RangerDelegationTokenSecretManager.java new file mode 100644 index 0000000000..08789c1123 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/biz/RangerDelegationTokenSecretManager.java @@ -0,0 +1,323 @@ +/* + * 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.biz; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSecretManager; +import org.apache.hadoop.security.token.delegation.DelegationKey; +import org.apache.ranger.common.PropertiesUtil; +import org.apache.ranger.db.RangerDaoManager; +import org.apache.ranger.entity.XXRangerDTMasterKey; +import org.apache.ranger.entity.XXRangerDelegationToken; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +@Component +public class RangerDelegationTokenSecretManager + extends AbstractDelegationTokenSecretManager { + + private static final Logger LOG = LoggerFactory.getLogger(RangerDelegationTokenSecretManager.class); + + private static final long DEFAULT_KEY_UPDATE_INTERVAL_SEC = 86400; + private static final long DEFAULT_TOKEN_MAX_LIFETIME_SEC = 604800; + private static final long DEFAULT_TOKEN_RENEW_INTERVAL_SEC = 86400; + private static final long DEFAULT_TOKEN_REMOVER_SCAN_INTERVAL_SEC = 3600; + + @Autowired + private RangerDaoManager daoManager; + + @Autowired + @Qualifier("transactionManager") + private PlatformTransactionManager txManager; + + private final boolean enabled; + private volatile boolean started = false; + + public RangerDelegationTokenSecretManager() { + super( + PropertiesUtil.getLongProperty("ranger.admin.delegation-token.key.update-interval.sec", DEFAULT_KEY_UPDATE_INTERVAL_SEC) * 1000, + PropertiesUtil.getLongProperty("ranger.admin.delegation-token.max-lifetime.sec", DEFAULT_TOKEN_MAX_LIFETIME_SEC) * 1000, + PropertiesUtil.getLongProperty("ranger.admin.delegation-token.renew-interval.sec", DEFAULT_TOKEN_RENEW_INTERVAL_SEC) * 1000, + PropertiesUtil.getLongProperty("ranger.admin.delegation-token.removal-scan-interval.sec", DEFAULT_TOKEN_REMOVER_SCAN_INTERVAL_SEC) * 1000 + ); + this.enabled = PropertiesUtil.getBooleanProperty("ranger.admin.delegation-token.enabled", false); + } + + @PostConstruct + public void init() { + if (!enabled) { + LOG.info("Ranger Delegation Token support is disabled"); + return; + } + LOG.info("Initializing RangerDelegationTokenSecretManager"); + try { + loadFromDB(); + startThreads(); + started = true; + LOG.info("RangerDelegationTokenSecretManager started successfully"); + } catch (IOException e) { + LOG.error("Failed to start RangerDelegationTokenSecretManager. " + + "Delegation token support will be unavailable until restart.", e); + } + } + + @PreDestroy + public void shutdown() { + if (enabled) { + LOG.info("Shutting down RangerDelegationTokenSecretManager"); + try { + stopThreads(); + } catch (Exception e) { + LOG.warn("Error shutting down RangerDelegationTokenSecretManager", e); + } + } + } + + public boolean isEnabled() { + return enabled && started; + } + + @Override + public RangerDelegationTokenIdentifier createIdentifier() { + return new RangerDelegationTokenIdentifier(); + } + + public Token createDelegationToken(String owner, String renewer) throws IOException { + Text ownerText = new Text(owner); + Text renewerText = renewer != null ? new Text(renewer) : null; + RangerDelegationTokenIdentifier ident = new RangerDelegationTokenIdentifier(ownerText, renewerText, ownerText); + + Token token = new Token<>(ident, this); + + if (LOG.isDebugEnabled()) { + LOG.debug("Created delegation token for owner={}, renewer={}, sequenceNumber={}", owner, renewer, ident.getSequenceNumber()); + } + + return token; + } + + public long renewDelegationToken(Token token, String callerUser) throws IOException { + return renewToken(token, callerUser); + } + + public void cancelDelegationToken(Token token, String callerUser) throws IOException { + cancelToken(token, callerUser); + } + + public RangerDelegationTokenIdentifier verifyToken(Token token) throws IOException { + ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier()); + DataInputStream in = new DataInputStream(buf); + + RangerDelegationTokenIdentifier ident = createIdentifier(); + ident.readFields(in); + + byte[] expectedPassword = retrievePassword(ident); + + if (expectedPassword == null) { + throw new IOException("Token is invalid or expired"); + } + + if (!MessageDigest.isEqual(expectedPassword, token.getPassword())) { + throw new IOException("Token password does not match"); + } + + return ident; + } + + @Override + protected void storeNewMasterKey(DelegationKey key) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("==> storeNewMasterKey(keyId={})", key.getKeyId()); + } + try { + byte[] keyBytes = serializeDelegationKey(key); + executeInTransaction(status -> { + XXRangerDTMasterKey entity = new XXRangerDTMasterKey(); + entity.setKeyId(key.getKeyId()); + entity.setExpiryDate(key.getExpiryDate()); + entity.setKeyBytes(keyBytes); + daoManager.getXXRangerDTMasterKey().create(entity); + return null; + }); + } catch (Exception e) { + throw new IOException("Failed to store master key: keyId=" + key.getKeyId(), e); + } + } + + @Override + protected void removeStoredMasterKey(DelegationKey key) { + if (LOG.isDebugEnabled()) { + LOG.debug("==> removeStoredMasterKey(keyId={})", key.getKeyId()); + } + try { + executeInTransaction(status -> { + daoManager.getXXRangerDTMasterKey().deleteByKeyId(key.getKeyId()); + return null; + }); + } catch (Exception e) { + LOG.warn("Failed to remove master key: keyId=" + key.getKeyId(), e); + } + } + + @Override + protected void storeNewToken(RangerDelegationTokenIdentifier ident, long renewDate) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("==> storeNewToken(seqNum={}, owner={})", ident.getSequenceNumber(), ident.getOwner()); + } + try { + byte[] tokenPassword = retrievePassword(ident); + executeInTransaction(status -> { + XXRangerDelegationToken entity = new XXRangerDelegationToken(); + entity.setSequenceNumber(ident.getSequenceNumber()); + entity.setOwner(ident.getOwner().toString()); + entity.setRenewer(ident.getRenewer() != null ? ident.getRenewer().toString() : null); + entity.setRealUser(ident.getRealUser() != null ? ident.getRealUser().toString() : null); + entity.setIssueDate(ident.getIssueDate()); + entity.setMaxDate(ident.getMaxDate()); + entity.setRenewDate(renewDate); + entity.setMasterKeyId(ident.getMasterKeyId()); + entity.setTokenPassword(tokenPassword); + daoManager.getXXRangerDelegationToken().create(entity); + return null; + }); + } catch (Exception e) { + throw new IOException("Failed to store token: seqNum=" + ident.getSequenceNumber(), e); + } + } + + @Override + protected void updateStoredToken(RangerDelegationTokenIdentifier ident, long renewDate) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("==> updateStoredToken(seqNum={}, renewDate={})", ident.getSequenceNumber(), renewDate); + } + try { + executeInTransaction(status -> { + daoManager.getXXRangerDelegationToken().updateRenewDate(ident.getSequenceNumber(), renewDate); + return null; + }); + } catch (Exception e) { + throw new IOException("Failed to update token: seqNum=" + ident.getSequenceNumber(), e); + } + } + + @Override + protected void removeStoredToken(RangerDelegationTokenIdentifier ident) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("==> removeStoredToken(seqNum={})", ident.getSequenceNumber()); + } + try { + executeInTransaction(status -> { + daoManager.getXXRangerDelegationToken().deleteBySequenceNumber(ident.getSequenceNumber()); + return null; + }); + } catch (Exception e) { + LOG.warn("Failed to remove token: seqNum=" + ident.getSequenceNumber(), e); + } + } + + protected T executeInTransaction(TransactionCallback action) { + TransactionTemplate txTemplate = new TransactionTemplate(txManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + return txTemplate.execute(action); + } + + private void loadFromDB() { + LOG.info("Loading delegation token state from DB"); + + executeInTransaction(status -> { + List masterKeys = daoManager.getXXRangerDTMasterKey().findAll(); + if (masterKeys != null) { + for (XXRangerDTMasterKey entity : masterKeys) { + try { + DelegationKey key = deserializeDelegationKey(entity.getKeyBytes()); + addKey(key); + if (LOG.isDebugEnabled()) { + LOG.debug("Loaded master key: keyId={}", key.getKeyId()); + } + } catch (Exception e) { + LOG.error("Failed to load master key: keyId=" + entity.getKeyId(), e); + } + } + LOG.info("Loaded {} master keys from DB", masterKeys.size()); + } + + List tokens = daoManager.getXXRangerDelegationToken().findAll(); + if (tokens != null) { + for (XXRangerDelegationToken entity : tokens) { + try { + RangerDelegationTokenIdentifier ident = new RangerDelegationTokenIdentifier( + new Text(entity.getOwner()), + entity.getRenewer() != null ? new Text(entity.getRenewer()) : null, + entity.getRealUser() != null ? new Text(entity.getRealUser()) : null + ); + ident.setIssueDate(entity.getIssueDate()); + ident.setMaxDate(entity.getMaxDate()); + ident.setSequenceNumber(entity.getSequenceNumber()); + ident.setMasterKeyId(entity.getMasterKeyId()); + addPersistedDelegationToken(ident, entity.getRenewDate()); + if (LOG.isDebugEnabled()) { + LOG.debug("Loaded delegation token: seqNum={}, owner={}", entity.getSequenceNumber(), entity.getOwner()); + } + } catch (Exception e) { + LOG.error("Failed to load delegation token: seqNum=" + entity.getSequenceNumber(), e); + } + } + LOG.info("Loaded {} delegation tokens from DB", tokens.size()); + } + return null; + }); + } + + private static byte[] serializeDelegationKey(DelegationKey key) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(bos); + key.write(out); + out.flush(); + return bos.toByteArray(); + } + + private static DelegationKey deserializeDelegationKey(byte[] data) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + DataInputStream in = new DataInputStream(bis); + DelegationKey key = new DelegationKey(); + key.readFields(in); + return key; + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/db/RangerDaoManagerBase.java b/security-admin/src/main/java/org/apache/ranger/db/RangerDaoManagerBase.java index a268cb9113..34a6eceee4 100644 --- a/security-admin/src/main/java/org/apache/ranger/db/RangerDaoManagerBase.java +++ b/security-admin/src/main/java/org/apache/ranger/db/RangerDaoManagerBase.java @@ -325,5 +325,8 @@ public XXPolicyRefAccessTypeDao getXXPolicyRefAccessType() { public XXRMSResourceMappingDao getXXRMSResourceMapping() { return new XXRMSResourceMappingDao(this); } public XXResourceMappingDiffDao getXXResourceMappingDiff() { return new XXResourceMappingDiffDao(this); } + + public XXRangerDTMasterKeyDao getXXRangerDTMasterKey() { return new XXRangerDTMasterKeyDao(this); } + public XXRangerDelegationTokenDao getXXRangerDelegationToken() { return new XXRangerDelegationTokenDao(this); } } diff --git a/security-admin/src/main/java/org/apache/ranger/db/XXRangerDTMasterKeyDao.java b/security-admin/src/main/java/org/apache/ranger/db/XXRangerDTMasterKeyDao.java new file mode 100644 index 0000000000..da1f6ed945 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/db/XXRangerDTMasterKeyDao.java @@ -0,0 +1,67 @@ +/* + * 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.db; + +import java.util.List; + +import javax.persistence.NoResultException; + +import org.apache.ranger.common.DateUtil; +import org.apache.ranger.common.db.BaseDao; +import org.apache.ranger.entity.XXRangerDTMasterKey; +import org.springframework.stereotype.Service; + +@Service +public class XXRangerDTMasterKeyDao extends BaseDao { + + public XXRangerDTMasterKeyDao(RangerDaoManagerBase daoManager) { + super(daoManager); + } + + @Override + public XXRangerDTMasterKey create(XXRangerDTMasterKey obj) { + obj.setCreateTime(DateUtil.getUTCDate()); + return super.create(obj); + } + + public XXRangerDTMasterKey findByKeyId(int keyId) { + try { + return getEntityManager() + .createQuery("SELECT obj FROM XXRangerDTMasterKey obj WHERE obj.keyId = :keyId", tClass) + .setParameter("keyId", keyId) + .getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + public List findAll() { + return getEntityManager() + .createQuery("SELECT obj FROM XXRangerDTMasterKey obj ORDER BY obj.keyId", tClass) + .getResultList(); + } + + public void deleteByKeyId(int keyId) { + getEntityManager() + .createQuery("DELETE FROM XXRangerDTMasterKey obj WHERE obj.keyId = :keyId") + .setParameter("keyId", keyId) + .executeUpdate(); + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/db/XXRangerDelegationTokenDao.java b/security-admin/src/main/java/org/apache/ranger/db/XXRangerDelegationTokenDao.java new file mode 100644 index 0000000000..436a8fb81b --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/db/XXRangerDelegationTokenDao.java @@ -0,0 +1,75 @@ +/* + * 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.db; + +import java.util.List; + +import javax.persistence.NoResultException; + +import org.apache.ranger.common.DateUtil; +import org.apache.ranger.common.db.BaseDao; +import org.apache.ranger.entity.XXRangerDelegationToken; +import org.springframework.stereotype.Service; + +@Service +public class XXRangerDelegationTokenDao extends BaseDao { + + public XXRangerDelegationTokenDao(RangerDaoManagerBase daoManager) { + super(daoManager); + } + + @Override + public XXRangerDelegationToken create(XXRangerDelegationToken obj) { + obj.setCreateTime(DateUtil.getUTCDate()); + return super.create(obj); + } + + public XXRangerDelegationToken findBySequenceNumber(int sequenceNumber) { + try { + return getEntityManager() + .createQuery("SELECT obj FROM XXRangerDelegationToken obj WHERE obj.sequenceNumber = :seqNum", tClass) + .setParameter("seqNum", sequenceNumber) + .getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + public List findAll() { + return getEntityManager() + .createQuery("SELECT obj FROM XXRangerDelegationToken obj", tClass) + .getResultList(); + } + + public void deleteBySequenceNumber(int sequenceNumber) { + getEntityManager() + .createQuery("DELETE FROM XXRangerDelegationToken obj WHERE obj.sequenceNumber = :seqNum") + .setParameter("seqNum", sequenceNumber) + .executeUpdate(); + } + + public void updateRenewDate(int sequenceNumber, long renewDate) { + getEntityManager() + .createQuery("UPDATE XXRangerDelegationToken obj SET obj.renewDate = :renewDate WHERE obj.sequenceNumber = :seqNum") + .setParameter("renewDate", renewDate) + .setParameter("seqNum", sequenceNumber) + .executeUpdate(); + } +} 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 c03bb11296..bd838bea9c 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 @@ -123,11 +123,15 @@ public Long getId() { * AUTH_TYPE_TRUSTED_PROXY is an element of enum AuthType. Its value is "AUTH_TYPE_TRUSTED_PROXY". */ public static final int AUTH_TYPE_TRUSTED_PROXY = 4; + /** + * AUTH_TYPE_DELEGATION_TOKEN is an element of enum AuthType. Its value is "AUTH_TYPE_DELEGATION_TOKEN". + */ + public static final int AUTH_TYPE_DELEGATION_TOKEN = 5; /** * Max value for enum AuthType_MAX */ - public static final int AuthType_MAX = 4; + public static final int AuthType_MAX = 5; diff --git a/security-admin/src/main/java/org/apache/ranger/entity/XXRangerDTMasterKey.java b/security-admin/src/main/java/org/apache/ranger/entity/XXRangerDTMasterKey.java new file mode 100644 index 0000000000..2844da72d1 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/entity/XXRangerDTMasterKey.java @@ -0,0 +1,107 @@ +/* + * 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.entity; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name = "x_ranger_dt_master_key") +public class XXRangerDTMasterKey implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + @Id + @SequenceGenerator(name = "X_RANGER_DT_MASTER_KEY_SEQ", sequenceName = "X_RANGER_DT_MASTER_KEY_SEQ", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.AUTO, generator = "X_RANGER_DT_MASTER_KEY_SEQ") + @Column(name = "id") + protected Long id; + + @Column(name = "key_id", nullable = false, unique = true) + protected Integer keyId; + + @Column(name = "expiry_date", nullable = false) + protected Long expiryDate; + + @Lob + @Column(name = "key_bytes", nullable = false) + protected byte[] keyBytes; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "create_time") + protected Date createTime; + + public XXRangerDTMasterKey() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getKeyId() { + return keyId; + } + + public void setKeyId(Integer keyId) { + this.keyId = keyId; + } + + public Long getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(Long expiryDate) { + this.expiryDate = expiryDate; + } + + public byte[] getKeyBytes() { + return keyBytes; + } + + public void setKeyBytes(byte[] keyBytes) { + this.keyBytes = keyBytes; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + @Override + public String toString() { + return "XXRangerDTMasterKey [id=" + id + ", keyId=" + keyId + ", expiryDate=" + expiryDate + "]"; + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/entity/XXRangerDelegationToken.java b/security-admin/src/main/java/org/apache/ranger/entity/XXRangerDelegationToken.java new file mode 100644 index 0000000000..9406930277 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/entity/XXRangerDelegationToken.java @@ -0,0 +1,175 @@ +/* + * 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.entity; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name = "x_ranger_delegation_token") +public class XXRangerDelegationToken implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + @Id + @SequenceGenerator(name = "X_RANGER_DELEGATION_TOKEN_SEQ", sequenceName = "X_RANGER_DELEGATION_TOKEN_SEQ", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.AUTO, generator = "X_RANGER_DELEGATION_TOKEN_SEQ") + @Column(name = "id") + protected Long id; + + @Column(name = "sequence_number", nullable = false, unique = true) + protected Integer sequenceNumber; + + @Column(name = "owner", nullable = false) + protected String owner; + + @Column(name = "renewer") + protected String renewer; + + @Column(name = "real_user") + protected String realUser; + + @Column(name = "issue_date", nullable = false) + protected Long issueDate; + + @Column(name = "max_date", nullable = false) + protected Long maxDate; + + @Column(name = "renew_date", nullable = false) + protected Long renewDate; + + @Column(name = "master_key_id", nullable = false) + protected Integer masterKeyId; + + @Lob + @Column(name = "token_password", nullable = false) + protected byte[] tokenPassword; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "create_time") + protected Date createTime; + + public XXRangerDelegationToken() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getSequenceNumber() { + return sequenceNumber; + } + + public void setSequenceNumber(Integer sequenceNumber) { + this.sequenceNumber = sequenceNumber; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getRenewer() { + return renewer; + } + + public void setRenewer(String renewer) { + this.renewer = renewer; + } + + public String getRealUser() { + return realUser; + } + + public void setRealUser(String realUser) { + this.realUser = realUser; + } + + public Long getIssueDate() { + return issueDate; + } + + public void setIssueDate(Long issueDate) { + this.issueDate = issueDate; + } + + public Long getMaxDate() { + return maxDate; + } + + public void setMaxDate(Long maxDate) { + this.maxDate = maxDate; + } + + public Long getRenewDate() { + return renewDate; + } + + public void setRenewDate(Long renewDate) { + this.renewDate = renewDate; + } + + public Integer getMasterKeyId() { + return masterKeyId; + } + + public void setMasterKeyId(Integer masterKeyId) { + this.masterKeyId = masterKeyId; + } + + public byte[] getTokenPassword() { + return tokenPassword; + } + + public void setTokenPassword(byte[] tokenPassword) { + this.tokenPassword = tokenPassword; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + @Override + public String toString() { + return "XXRangerDelegationToken [id=" + id + ", sequenceNumber=" + sequenceNumber + + ", owner=" + owner + ", renewer=" + renewer + ", issueDate=" + issueDate + + ", maxDate=" + maxDate + ", renewDate=" + renewDate + ", masterKeyId=" + masterKeyId + "]"; + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/rest/DelegationTokenREST.java b/security-admin/src/main/java/org/apache/ranger/rest/DelegationTokenREST.java new file mode 100644 index 0000000000..9ca77e9731 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/rest/DelegationTokenREST.java @@ -0,0 +1,205 @@ +/* + * 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.rest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.apache.hadoop.security.AccessControlException; +import org.apache.hadoop.security.token.Token; +import org.apache.ranger.biz.RangerBizUtil; +import org.apache.ranger.biz.RangerDelegationTokenSecretManager; +import org.apache.ranger.common.RESTErrorUtil; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Path("delegation-token") +@Component +@Scope("request") +@Transactional(propagation = Propagation.REQUIRES_NEW) +public class DelegationTokenREST { + private static final Logger LOG = LoggerFactory.getLogger(DelegationTokenREST.class); + + private static final int MAX_RENEWER_LENGTH = 255; + + @Autowired + RangerDelegationTokenSecretManager secretManager; + + @Autowired + RangerBizUtil bizUtil; + + @Autowired + RESTErrorUtil restErrorUtil; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Map getDelegationToken(@QueryParam("renewer") String renewer, + @Context HttpServletRequest request) { + if (LOG.isDebugEnabled()) { + LOG.debug("==> DelegationTokenREST.getDelegationToken(renewer={})", renewer); + } + + if (!secretManager.isEnabled()) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + "Delegation token support is not enabled", true); + } + + String authenticatedUser = bizUtil.getCurrentUserLoginId(); + if (authenticatedUser == null) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_UNAUTHORIZED, + "Authentication required", true); + } + + if (renewer != null && renewer.length() > MAX_RENEWER_LENGTH) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_BAD_REQUEST, + "Renewer name is too long", true); + } + + try { + Token token = secretManager.createDelegationToken(authenticatedUser, renewer); + + Map result = new HashMap<>(); + result.put("urlString", token.encodeToUrlString()); + + LOG.info("Delegation token created for user={}, renewer={}", authenticatedUser, renewer); + + return result; + } catch (Exception e) { + LOG.error("Failed to create delegation token for user=" + authenticatedUser, e); + throw restErrorUtil.createRESTException("Failed to create delegation token"); + } + } + + @PUT + @Path("/renew") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Map renewDelegationToken(Map requestBody, + @Context HttpServletRequest request) { + if (LOG.isDebugEnabled()) { + LOG.debug("==> DelegationTokenREST.renewDelegationToken()"); + } + + if (!secretManager.isEnabled()) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + "Delegation token support is not enabled", true); + } + + String tokenEncoded = requestBody != null ? requestBody.get("token") : null; + if (tokenEncoded == null || tokenEncoded.isEmpty()) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_BAD_REQUEST, + "Token parameter is required", true); + } + + String authenticatedUser = bizUtil.getCurrentUserLoginId(); + if (authenticatedUser == null) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_UNAUTHORIZED, + "Authentication required", true); + } + + try { + Token token = new Token<>(); + token.decodeFromUrlString(tokenEncoded); + + long newExpiryTime = secretManager.renewDelegationToken(token, authenticatedUser); + + Map result = new HashMap<>(); + result.put("expirationTime", newExpiryTime); + + LOG.info("Delegation token renewed by user={}, newExpiryTime={}", authenticatedUser, newExpiryTime); + + return result; + } catch (AccessControlException e) { + LOG.warn("Delegation token renewal denied for user={}", authenticatedUser); + throw restErrorUtil.create403RESTException("Delegation token renewal denied"); + } catch (IOException e) { + LOG.warn("Delegation token renewal failed: {}", e.getMessage()); + throw restErrorUtil.createRESTException(HttpServletResponse.SC_UNAUTHORIZED, + "Delegation token is invalid or expired", true); + } catch (Exception e) { + LOG.error("Failed to renew delegation token", e); + throw restErrorUtil.createRESTException("Internal error during delegation token renewal"); + } + } + + @PUT + @Path("/cancel") + @Consumes(MediaType.APPLICATION_JSON) + public void cancelDelegationToken(Map requestBody, + @Context HttpServletRequest request) { + if (LOG.isDebugEnabled()) { + LOG.debug("==> DelegationTokenREST.cancelDelegationToken()"); + } + + if (!secretManager.isEnabled()) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, + "Delegation token support is not enabled", true); + } + + String tokenEncoded = requestBody != null ? requestBody.get("token") : null; + if (tokenEncoded == null || tokenEncoded.isEmpty()) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_BAD_REQUEST, + "Token parameter is required", true); + } + + String authenticatedUser = bizUtil.getCurrentUserLoginId(); + if (authenticatedUser == null) { + throw restErrorUtil.createRESTException(HttpServletResponse.SC_UNAUTHORIZED, + "Authentication required", true); + } + + try { + Token token = new Token<>(); + token.decodeFromUrlString(tokenEncoded); + + secretManager.cancelDelegationToken(token, authenticatedUser); + + LOG.info("Delegation token cancelled by user={}", authenticatedUser); + } catch (AccessControlException e) { + LOG.warn("Delegation token cancellation denied for user={}", authenticatedUser); + throw restErrorUtil.create403RESTException("Delegation token cancellation denied"); + } catch (IOException e) { + LOG.warn("Delegation token cancellation failed: {}", e.getMessage()); + throw restErrorUtil.createRESTException(HttpServletResponse.SC_UNAUTHORIZED, + "Delegation token is invalid or expired", true); + } catch (Exception e) { + LOG.error("Failed to cancel delegation token", e); + throw restErrorUtil.createRESTException("Internal error during delegation token cancellation"); + } + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerDelegationTokenAuthFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerDelegationTokenAuthFilter.java new file mode 100644 index 0000000000..51785023ed --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerDelegationTokenAuthFilter.java @@ -0,0 +1,121 @@ +/* + * 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.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.apache.hadoop.security.token.Token; +import org.apache.ranger.biz.RangerDelegationTokenSecretManager; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.web.filter.GenericFilterBean; + +public class RangerDelegationTokenAuthFilter extends GenericFilterBean { + private static final Logger LOG = LoggerFactory.getLogger(RangerDelegationTokenAuthFilter.class); + + public static final String HEADER_DELEGATION_TOKEN = "X-Delegation-Token-Encoded"; + public static final String PARAM_DELEGATION_TOKEN = "delegationToken"; + + private static final String DEFAULT_ROLE = "ROLE_USER"; + + @Autowired + RangerDelegationTokenSecretManager secretManager; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + if (!secretManager.isEnabled()) { + chain.doFilter(request, response); + return; + } + + String tokenEncoded = httpRequest.getHeader(HEADER_DELEGATION_TOKEN); + if (StringUtils.isEmpty(tokenEncoded)) { + tokenEncoded = httpRequest.getParameter(PARAM_DELEGATION_TOKEN); + } + + if (StringUtils.isEmpty(tokenEncoded)) { + chain.doFilter(request, response); + return; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("==> RangerDelegationTokenAuthFilter: found delegation token in request for URI={}", httpRequest.getRequestURI()); + } + + try { + Token token = new Token<>(); + token.decodeFromUrlString(tokenEncoded); + + RangerDelegationTokenIdentifier ident = secretManager.verifyToken(token); + String userName = ident.getUser().getShortUserName(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Delegation token verified for user={}", userName); + } + + final List grantedAuths = new ArrayList<>(); + grantedAuths.add(new SimpleGrantedAuthority(DEFAULT_ROLE)); + + final UserDetails principal = new User(userName, "", grantedAuths); + final Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths); + WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest); + ((AbstractAuthenticationToken) authentication).setDetails(webDetails); + + SecurityContextHolder.getContext().setAuthentication(authentication); + httpRequest.setAttribute("delegationTokenEnabled", true); + httpRequest.getSession(true); + + if (LOG.isDebugEnabled()) { + LOG.debug("<== RangerDelegationTokenAuthFilter: authenticated user={} via delegation token", userName); + } + + chain.doFilter(request, response); + } catch (Exception e) { + LOG.warn("Delegation token authentication failed: {}", e.getMessage()); + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Delegation token authentication failed"); + } + } +} 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 71d7af0d11..d16b4c287a 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 @@ -166,6 +166,8 @@ private int getAuthType(HttpServletRequest request) { if (ssoEnabled) { authType = XXAuthSession.AUTH_TYPE_SSO; + } else if (request.getAttribute("delegationTokenEnabled") != null && Boolean.valueOf(String.valueOf(request.getAttribute("delegationTokenEnabled")))) { + authType = XXAuthSession.AUTH_TYPE_DELEGATION_TOKEN; } else if (request.getAttribute("spnegoEnabled") != null && Boolean.valueOf(String.valueOf(request.getAttribute("spnegoEnabled")))){ if (request.getAttribute("trustedProxyEnabled") != null && Boolean.valueOf(String.valueOf(request.getAttribute("trustedProxyEnabled")))) { if (logger.isDebugEnabled()) { diff --git a/security-admin/src/main/resources/META-INF/persistence.xml b/security-admin/src/main/resources/META-INF/persistence.xml index 827a312fdf..21027da7f8 100644 --- a/security-admin/src/main/resources/META-INF/persistence.xml +++ b/security-admin/src/main/resources/META-INF/persistence.xml @@ -79,6 +79,8 @@ org.apache.ranger.entity.XXServiceVersionInfo org.apache.ranger.entity.XXPluginInfo org.apache.ranger.entity.XXUgsyncAuditInfo + org.apache.ranger.entity.XXRangerDTMasterKey + org.apache.ranger.entity.XXRangerDelegationToken NONE 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 d922d2721d..260cad52d2 100644 --- a/security-admin/src/main/resources/conf.dist/security-applicationContext.xml +++ b/security-admin/src/main/resources/conf.dist/security-applicationContext.xml @@ -65,6 +65,7 @@ http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd"> + @@ -105,6 +106,9 @@ http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd"> + + + diff --git a/security-admin/src/main/webapp/scripts/utils/XAEnums.js b/security-admin/src/main/webapp/scripts/utils/XAEnums.js index f81fa16197..bcc4c0c59b 100644 --- a/security-admin/src/main/webapp/scripts/utils/XAEnums.js +++ b/security-admin/src/main/webapp/scripts/utils/XAEnums.js @@ -154,7 +154,8 @@ define(function(require) { AUTH_TYPE_PASSWORD:{value:1, label:'Username/Password', rbkey:'xa.enum.AuthType.AUTH_TYPE_PASSWORD', tt: 'lbl.AuthType_AUTH_TYPE_PASSWORD'}, AUTH_TYPE_KERBEROS:{value:2, label:'Kerberos', rbkey:'xa.enum.AuthType.AUTH_TYPE_KERBEROS', tt: 'lbl.AuthType_AUTH_TYPE_KERBEROS'}, AUTH_TYPE_SSO:{value:3, label:'SingleSignOn', rbkey:'xa.enum.AuthType.AUTH_TYPE_SSO', tt: 'lbl.AuthType_AUTH_TYPE_SSO'}, - AUTH_TYPE_TRUSTED_PROXY:{value:4, label:'Trusted Proxy', rbkey:'xa.enum.AuthType.AUTH_TYPE_TRUSTED_PROXY', tt: 'lbl.AuthType_AUTH_TYPE_TRUSTED_PROXY'} + AUTH_TYPE_TRUSTED_PROXY:{value:4, label:'Trusted Proxy', rbkey:'xa.enum.AuthType.AUTH_TYPE_TRUSTED_PROXY', tt: 'lbl.AuthType_AUTH_TYPE_TRUSTED_PROXY'}, + AUTH_TYPE_DELEGATION_TOKEN:{value:5, label:'Delegation Token', rbkey:'xa.enum.AuthType.AUTH_TYPE_DELEGATION_TOKEN', tt: 'lbl.AuthType_AUTH_TYPE_DELEGATION_TOKEN'} }); XAEnums.BooleanValue = mergeParams(XAEnums.BooleanValue, { diff --git a/security-admin/src/test/java/org/apache/ranger/biz/TestRangerDelegationTokenSecretManager.java b/security-admin/src/test/java/org/apache/ranger/biz/TestRangerDelegationTokenSecretManager.java new file mode 100644 index 0000000000..ccffd2c1ce --- /dev/null +++ b/security-admin/src/test/java/org/apache/ranger/biz/TestRangerDelegationTokenSecretManager.java @@ -0,0 +1,244 @@ +/* + * 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.biz; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.Collections; + +import org.apache.hadoop.security.token.Token; +import org.apache.ranger.db.RangerDaoManager; +import org.apache.ranger.db.XXRangerDTMasterKeyDao; +import org.apache.ranger.db.XXRangerDelegationTokenDao; +import org.apache.ranger.entity.XXRangerDTMasterKey; +import org.apache.ranger.entity.XXRangerDelegationToken; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class TestRangerDelegationTokenSecretManager { + + @Mock + private RangerDaoManager daoManager; + + @Mock + private XXRangerDTMasterKeyDao masterKeyDao; + + @Mock + private XXRangerDelegationTokenDao tokenDao; + + private RangerDelegationTokenSecretManager secretManager; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + secretManager = new TestableSecretManager(); + + Mockito.when(daoManager.getXXRangerDTMasterKey()).thenReturn(masterKeyDao); + Mockito.when(daoManager.getXXRangerDelegationToken()).thenReturn(tokenDao); + Mockito.when(masterKeyDao.findAll()).thenReturn(Collections.emptyList()); + Mockito.when(tokenDao.findAll()).thenReturn(Collections.emptyList()); + Mockito.when(masterKeyDao.create(Mockito.any(XXRangerDTMasterKey.class))) + .thenAnswer(inv -> inv.getArgument(0)); + Mockito.when(tokenDao.create(Mockito.any(XXRangerDelegationToken.class))) + .thenAnswer(inv -> inv.getArgument(0)); + + java.lang.reflect.Field daoField = RangerDelegationTokenSecretManager.class.getDeclaredField("daoManager"); + daoField.setAccessible(true); + daoField.set(secretManager, daoManager); + + secretManager.startThreads(); + } + + @Test + public void testCreateAndVerifyToken() throws Exception { + Token token = secretManager.createDelegationToken("testUser", "yarn"); + + assertNotNull(token); + assertNotNull(token.getIdentifier()); + assertNotNull(token.getPassword()); + assertTrue(token.getPassword().length > 0); + + RangerDelegationTokenIdentifier ident = secretManager.verifyToken(token); + assertNotNull(ident); + assertEquals("testUser", ident.getOwner().toString()); + assertEquals("yarn", ident.getRenewer().toString()); + } + + @Test + public void testVerifyTokenWithWrongPassword() throws Exception { + Token token = secretManager.createDelegationToken("testUser", "yarn"); + + Token tamperedToken = new Token<>( + token.getIdentifier(), + new byte[]{1, 2, 3, 4}, // wrong password + token.getKind(), + token.getService() + ); + + try { + secretManager.verifyToken(tamperedToken); + fail("Should have thrown IOException for wrong password"); + } catch (IOException e) { + assertTrue(e.getMessage().contains("password does not match")); + } + } + + @Test + public void testRenewTokenByDesignatedRenewer() throws Exception { + Token token = secretManager.createDelegationToken("owner", "yarn"); + + long newExpiry = secretManager.renewDelegationToken(token, "yarn"); + assertTrue(newExpiry > System.currentTimeMillis()); + } + + @Test + public void testRenewTokenByNonRenewer() throws Exception { + Token token = secretManager.createDelegationToken("owner", "yarn"); + + try { + secretManager.renewDelegationToken(token, "attacker"); + fail("Should have thrown IOException for non-renewer"); + } catch (Exception e) { + // Expected: Hadoop throws AccessControlException wrapped in IOException + } + } + + @Test + public void testCancelTokenByOwner() throws Exception { + Token token = secretManager.createDelegationToken("owner", "yarn"); + + secretManager.cancelDelegationToken(token, "owner"); + + try { + secretManager.verifyToken(token); + fail("Should have thrown IOException for cancelled token"); + } catch (IOException e) { + // Expected + } + } + + @Test + public void testCancelTokenByRenewer() throws Exception { + Token token = secretManager.createDelegationToken("owner", "yarn"); + + secretManager.cancelDelegationToken(token, "yarn"); + + try { + secretManager.verifyToken(token); + fail("Should have thrown IOException for cancelled token"); + } catch (IOException e) { + // Expected + } + } + + @Test + public void testCancelTokenByNonOwner() throws Exception { + Token token = secretManager.createDelegationToken("owner", "yarn"); + + try { + secretManager.cancelDelegationToken(token, "attacker"); + fail("Should have thrown exception for non-owner/non-renewer cancel"); + } catch (Exception e) { + // Expected: Hadoop throws AccessControlException + } + } + + @Test + public void testCreateTokenWithNullRenewer() throws Exception { + Token token = secretManager.createDelegationToken("testUser", null); + + assertNotNull(token); + + RangerDelegationTokenIdentifier ident = secretManager.verifyToken(token); + assertEquals("testUser", ident.getOwner().toString()); + } + + @Test + public void testMultipleTokensIndependent() throws Exception { + Token token1 = secretManager.createDelegationToken("user1", "yarn"); + Token token2 = secretManager.createDelegationToken("user2", "yarn"); + + RangerDelegationTokenIdentifier ident1 = secretManager.verifyToken(token1); + RangerDelegationTokenIdentifier ident2 = secretManager.verifyToken(token2); + + assertEquals("user1", ident1.getOwner().toString()); + assertEquals("user2", ident2.getOwner().toString()); + + secretManager.cancelDelegationToken(token1, "user1"); + + try { + secretManager.verifyToken(token1); + fail("Cancelled token should not verify"); + } catch (IOException e) { + // Expected + } + + // Token2 still valid + assertNotNull(secretManager.verifyToken(token2)); + } + + @Test + public void testTokenRoundTripEncoding() throws Exception { + Token token = secretManager.createDelegationToken("testUser", "yarn"); + + String encoded = token.encodeToUrlString(); + Token decoded = new Token<>(); + decoded.decodeFromUrlString(encoded); + + RangerDelegationTokenIdentifier ident = secretManager.verifyToken(decoded); + assertEquals("testUser", ident.getOwner().toString()); + } + + @Test + public void testDbPersistenceOnCreate() throws Exception { + secretManager.createDelegationToken("testUser", "yarn"); + + Mockito.verify(masterKeyDao, Mockito.atLeastOnce()).create(Mockito.any(XXRangerDTMasterKey.class)); + + Mockito.verify(tokenDao).create(Mockito.any(XXRangerDelegationToken.class)); + } + + /** + * Testable subclass that bypasses PropertiesUtil config loading + * and executes DB callbacks directly without TransactionTemplate. + */ + private static class TestableSecretManager extends RangerDelegationTokenSecretManager { + TestableSecretManager() { + super(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + protected T executeInTransaction(org.springframework.transaction.support.TransactionCallback action) { + return action.doInTransaction(null); + } + } +} diff --git a/security-admin/src/test/java/org/apache/ranger/rest/TestDelegationTokenREST.java b/security-admin/src/test/java/org/apache/ranger/rest/TestDelegationTokenREST.java new file mode 100644 index 0000000000..f3a3e0ce1c --- /dev/null +++ b/security-admin/src/test/java/org/apache/ranger/rest/TestDelegationTokenREST.java @@ -0,0 +1,358 @@ +/* + * 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.rest; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.WebApplicationException; + +import org.apache.hadoop.security.AccessControlException; +import org.apache.hadoop.security.token.Token; +import org.apache.ranger.biz.RangerBizUtil; +import org.apache.ranger.biz.RangerDelegationTokenSecretManager; +import org.apache.ranger.common.RESTErrorUtil; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TestDelegationTokenREST { + + @Mock + private RangerDelegationTokenSecretManager secretManager; + + @Mock + private RangerBizUtil bizUtil; + + @Mock + private RESTErrorUtil restErrorUtil; + + @Mock + private HttpServletRequest request; + + @InjectMocks + private DelegationTokenREST delegationTokenREST = new DelegationTokenREST(); + + @Before + public void setup() { + Mockito.lenient().when(restErrorUtil.createRESTException(Mockito.anyInt(), Mockito.anyString(), Mockito.anyBoolean())) + .thenAnswer(invocation -> { + int status = invocation.getArgument(0); + return new WebApplicationException(javax.ws.rs.core.Response.status(status).build()); + }); + Mockito.lenient().when(restErrorUtil.createRESTException(Mockito.anyString())) + .thenAnswer(invocation -> new WebApplicationException( + javax.ws.rs.core.Response.status(HttpServletResponse.SC_BAD_REQUEST).build())); + Mockito.lenient().when(restErrorUtil.create403RESTException(Mockito.anyString())) + .thenAnswer(invocation -> new WebApplicationException( + javax.ws.rs.core.Response.status(HttpServletResponse.SC_FORBIDDEN).build())); + } + + // --- Get tests --- + + @Test + public void testGetDelegationToken_disabled() { + Mockito.when(secretManager.isEnabled()).thenReturn(false); + + try { + delegationTokenREST.getDelegationToken("yarn", request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getResponse().getStatus()); + } + } + + @Test + public void testGetDelegationToken_noAuth() { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn(null); + + try { + delegationTokenREST.getDelegationToken("yarn", request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, e.getResponse().getStatus()); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testGetDelegationToken_success() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + + Token mockToken = Mockito.mock(Token.class); + Mockito.when(mockToken.encodeToUrlString()).thenReturn("encodedTokenString"); + Mockito.when(secretManager.createDelegationToken("testUser", "yarn")).thenReturn(mockToken); + + Map result = delegationTokenREST.getDelegationToken("yarn", request); + + assertEquals("encodedTokenString", result.get("urlString")); + } + + @Test + public void testGetDelegationToken_internalError() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.when(secretManager.createDelegationToken(Mockito.anyString(), Mockito.anyString())) + .thenThrow(new IOException("test error")); + + try { + delegationTokenREST.getDelegationToken("yarn", request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + // error thrown via restErrorUtil + } + } + + @Test + public void testGetDelegationToken_renewerTooLong() { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + + String longRenewer = new String(new char[300]).replace('\0', 'a'); + + try { + delegationTokenREST.getDelegationToken(longRenewer, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getResponse().getStatus()); + } + } + + // --- Renew tests --- + + @Test + public void testRenewDelegationToken_disabled() { + Mockito.when(secretManager.isEnabled()).thenReturn(false); + + Map body = new HashMap<>(); + body.put("token", "someToken"); + + try { + delegationTokenREST.renewDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getResponse().getStatus()); + } + } + + @Test + public void testRenewDelegationToken_nullToken() { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + + try { + delegationTokenREST.renewDelegationToken(null, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getResponse().getStatus()); + } + } + + @Test + public void testRenewDelegationToken_noAuth() { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn(null); + + Map body = new HashMap<>(); + body.put("token", "someToken"); + + try { + delegationTokenREST.renewDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, e.getResponse().getStatus()); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testRenewDelegationToken_success() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.when(secretManager.renewDelegationToken(Mockito.any(Token.class), Mockito.eq("testUser"))) + .thenReturn(9999999L); + + Token fakeToken = new Token<>(); + String encoded = fakeToken.encodeToUrlString(); + + Map body = new HashMap<>(); + body.put("token", encoded); + Map result = delegationTokenREST.renewDelegationToken(body, request); + + assertEquals(9999999L, result.get("expirationTime")); + } + + @Test + @SuppressWarnings("unchecked") + public void testRenewDelegationToken_passesCallerIdentity() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.when(secretManager.renewDelegationToken(Mockito.any(Token.class), Mockito.eq("testUser"))) + .thenReturn(1L); + + Token fakeToken = new Token<>(); + Map body = new HashMap<>(); + body.put("token", fakeToken.encodeToUrlString()); + + delegationTokenREST.renewDelegationToken(body, request); + + Mockito.verify(secretManager).renewDelegationToken(Mockito.any(Token.class), Mockito.eq("testUser")); + } + + @Test + @SuppressWarnings("unchecked") + public void testRenewDelegationToken_accessDenied() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.when(secretManager.renewDelegationToken(Mockito.any(Token.class), Mockito.anyString())) + .thenThrow(new AccessControlException("not the designated renewer")); + + Token fakeToken = new Token<>(); + Map body = new HashMap<>(); + body.put("token", fakeToken.encodeToUrlString()); + + try { + delegationTokenREST.renewDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_FORBIDDEN, e.getResponse().getStatus()); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testRenewDelegationToken_invalidToken() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.when(secretManager.renewDelegationToken(Mockito.any(Token.class), Mockito.anyString())) + .thenThrow(new IOException("token is expired")); + + Token fakeToken = new Token<>(); + Map body = new HashMap<>(); + body.put("token", fakeToken.encodeToUrlString()); + + try { + delegationTokenREST.renewDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, e.getResponse().getStatus()); + } + } + + // --- Cancel tests --- + + @Test + public void testCancelDelegationToken_disabled() { + Mockito.when(secretManager.isEnabled()).thenReturn(false); + + Map body = new HashMap<>(); + body.put("token", "someToken"); + + try { + delegationTokenREST.cancelDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getResponse().getStatus()); + } + } + + @Test + public void testCancelDelegationToken_noAuth() { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn(null); + + Map body = new HashMap<>(); + body.put("token", "someToken"); + + try { + delegationTokenREST.cancelDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, e.getResponse().getStatus()); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testCancelDelegationToken_passesCallerIdentity() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + + Token fakeToken = new Token<>(); + Map body = new HashMap<>(); + body.put("token", fakeToken.encodeToUrlString()); + + delegationTokenREST.cancelDelegationToken(body, request); + + Mockito.verify(secretManager).cancelDelegationToken(Mockito.any(Token.class), Mockito.eq("testUser")); + } + + @Test + @SuppressWarnings("unchecked") + public void testCancelDelegationToken_accessDenied() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.doThrow(new AccessControlException("not authorized")) + .when(secretManager).cancelDelegationToken(Mockito.any(Token.class), Mockito.anyString()); + + Token fakeToken = new Token<>(); + Map body = new HashMap<>(); + body.put("token", fakeToken.encodeToUrlString()); + + try { + delegationTokenREST.cancelDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_FORBIDDEN, e.getResponse().getStatus()); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testCancelDelegationToken_invalidToken() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(bizUtil.getCurrentUserLoginId()).thenReturn("testUser"); + Mockito.doThrow(new IOException("token does not exist")) + .when(secretManager).cancelDelegationToken(Mockito.any(Token.class), Mockito.anyString()); + + Token fakeToken = new Token<>(); + Map body = new HashMap<>(); + body.put("token", fakeToken.encodeToUrlString()); + + try { + delegationTokenREST.cancelDelegationToken(body, request); + fail("Expected WebApplicationException"); + } catch (WebApplicationException e) { + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, e.getResponse().getStatus()); + } + } +} diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerDelegationTokenAuthFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerDelegationTokenAuthFilter.java new file mode 100644 index 0000000000..3e16a94ad5 --- /dev/null +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerDelegationTokenAuthFilter.java @@ -0,0 +1,202 @@ +/* + * 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.filter; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.apache.ranger.biz.RangerDelegationTokenSecretManager; +import org.apache.ranger.plugin.util.RangerDelegationTokenIdentifier; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +@RunWith(MockitoJUnitRunner.class) +public class TestRangerDelegationTokenAuthFilter { + + @Mock + private RangerDelegationTokenSecretManager secretManager; + + @InjectMocks + private RangerDelegationTokenAuthFilter filter = new RangerDelegationTokenAuthFilter(); + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private FilterChain chain; + + @Before + public void setup() { + SecurityContextHolder.clearContext(); + } + + @After + public void teardown() { + SecurityContextHolder.clearContext(); + } + + @Test + public void testPassThrough_whenDisabled() throws IOException, ServletException { + Mockito.when(secretManager.isEnabled()).thenReturn(false); + + filter.doFilter(request, response, chain); + + Mockito.verify(chain).doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + public void testPassThrough_whenAlreadyAuthenticated() throws IOException, ServletException { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken("existingUser", "pass")); + + filter.doFilter(request, response, chain); + + Mockito.verify(chain).doFilter(request, response); + assertEquals("existingUser", SecurityContextHolder.getContext().getAuthentication().getName()); + } + + @Test + public void testPassThrough_whenNoTokenPresent() throws IOException, ServletException { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + Mockito.when(request.getHeader(RangerDelegationTokenAuthFilter.HEADER_DELEGATION_TOKEN)).thenReturn(null); + Mockito.when(request.getParameter(RangerDelegationTokenAuthFilter.PARAM_DELEGATION_TOKEN)).thenReturn(null); + + filter.doFilter(request, response, chain); + + Mockito.verify(chain).doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @SuppressWarnings("unchecked") + public void testAuthentication_validTokenInHeader() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + + Token fakeToken = new Token<>(); + String encoded = fakeToken.encodeToUrlString(); + + Mockito.when(request.getHeader(RangerDelegationTokenAuthFilter.HEADER_DELEGATION_TOKEN)) + .thenReturn(encoded); + + RangerDelegationTokenIdentifier ident = new RangerDelegationTokenIdentifier( + new Text("tokenOwner"), new Text("yarn"), new Text("tokenOwner")); + Mockito.when(secretManager.verifyToken(Mockito.any(Token.class))).thenReturn(ident); + + filter.doFilter(request, response, chain); + + Mockito.verify(chain).doFilter(request, response); + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + assertNotNull(auth); + assertTrue(auth.isAuthenticated()); + assertEquals("tokenOwner", auth.getName()); + } + + @Test + @SuppressWarnings("unchecked") + public void testAuthentication_validTokenInParam() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + + Token fakeToken = new Token<>(); + String encoded = fakeToken.encodeToUrlString(); + + Mockito.when(request.getHeader(RangerDelegationTokenAuthFilter.HEADER_DELEGATION_TOKEN)) + .thenReturn(null); + Mockito.when(request.getParameter(RangerDelegationTokenAuthFilter.PARAM_DELEGATION_TOKEN)) + .thenReturn(encoded); + + RangerDelegationTokenIdentifier ident = new RangerDelegationTokenIdentifier( + new Text("paramUser"), new Text("yarn"), new Text("paramUser")); + Mockito.when(secretManager.verifyToken(Mockito.any(Token.class))).thenReturn(ident); + + filter.doFilter(request, response, chain); + + Mockito.verify(chain).doFilter(request, response); + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + assertNotNull(auth); + assertEquals("paramUser", auth.getName()); + assertTrue(auth.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_USER"))); + } + + @Test + @SuppressWarnings("unchecked") + public void testAuthentication_invalidToken_returns401() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + + Token fakeToken = new Token<>(); + String encoded = fakeToken.encodeToUrlString(); + + Mockito.when(request.getHeader(RangerDelegationTokenAuthFilter.HEADER_DELEGATION_TOKEN)) + .thenReturn(encoded); + Mockito.when(secretManager.verifyToken(Mockito.any(Token.class))) + .thenThrow(new IOException("Token password does not match")); + + filter.doFilter(request, response, chain); + + Mockito.verify(chain, Mockito.never()).doFilter(request, response); + Mockito.verify(response).sendError( + Mockito.eq(HttpServletResponse.SC_UNAUTHORIZED), + Mockito.anyString()); + } + + @Test + @SuppressWarnings("unchecked") + public void testAuthentication_setsRequestAttribute() throws Exception { + Mockito.when(secretManager.isEnabled()).thenReturn(true); + + Token fakeToken = new Token<>(); + String encoded = fakeToken.encodeToUrlString(); + + Mockito.when(request.getHeader(RangerDelegationTokenAuthFilter.HEADER_DELEGATION_TOKEN)) + .thenReturn(encoded); + + RangerDelegationTokenIdentifier ident = new RangerDelegationTokenIdentifier( + new Text("testUser"), new Text("yarn"), new Text("testUser")); + Mockito.when(secretManager.verifyToken(Mockito.any(Token.class))).thenReturn(ident); + + filter.doFilter(request, response, chain); + + Mockito.verify(request).setAttribute("delegationTokenEnabled", true); + } +}