You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by sp...@apache.org on 2020/03/09 20:50:30 UTC
[ranger] branch master updated: RANGER-2723: Support ldap attribute
based document level control for solr plugin
This is an automated email from the ASF dual-hosted git repository.
spolavarapu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 2ec95d5 RANGER-2723: Support ldap attribute based document level control for solr plugin
2ec95d5 is described below
commit 2ec95d58a9e6b674b7d374f6b1a7a98eeb76195e
Author: Sailaja Polavarapu <sp...@cloudera.com>
AuthorDate: Mon Mar 9 13:50:03 2020 -0700
RANGER-2723: Support ldap attribute based document level control for solr plugin
---
.../admin/client/AbstractRangerAdminClient.java | 5 +
.../ranger/admin/client/RangerAdminClient.java | 3 +
.../ranger/admin/client/RangerAdminRESTClient.java | 82 ++++
.../RangerAdminUserStoreRetriever.java | 74 +++
.../contextenricher/RangerUserStoreEnricher.java | 505 +++++++++++++++++++++
.../contextenricher/RangerUserStoreRetriever.java | 64 +++
.../plugin/util/RangerAccessRequestUtil.java | 16 +
.../apache/ranger/plugin/util/RangerRESTUtils.java | 3 +
.../solr/authorizer/FieldToAttributeMapping.java | 106 +++++
.../solr/authorizer/RangerSolrAuthorizer.java | 265 ++++++++++-
.../solr/authorizer/SubsetQueryPlugin.java | 97 ++++
11 files changed, 1202 insertions(+), 18 deletions(-)
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 87d0190..1ad5ec0 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
@@ -111,4 +111,9 @@ public abstract class AbstractRangerAdminClient implements RangerAdminClient {
public List<String> getTagTypes(String tagTypePattern) throws Exception {
return null;
}
+
+ @Override
+ public RangerUserStore getUserStoreIfUpdated(long lastKnownUserStoreVersion, long lastActivationTimeInMillis) throws Exception {
+ 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 58eb00a..22a8121 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
@@ -27,6 +27,7 @@ import org.apache.ranger.plugin.util.GrantRevokeRoleRequest;
import org.apache.ranger.plugin.util.RangerRoles;
import org.apache.ranger.plugin.util.ServicePolicies;
import org.apache.ranger.plugin.util.ServiceTags;
+import org.apache.ranger.plugin.util.RangerUserStore;
import java.util.List;
@@ -61,4 +62,6 @@ public interface RangerAdminClient {
List<String> getTagTypes(String tagTypePattern) throws Exception;
+ RangerUserStore getUserStoreIfUpdated(long lastKnownUserStoreVersion, long lastActivationTimeInMillis) 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 e5f9747..479a50c 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
@@ -908,4 +908,86 @@ public class RangerAdminRESTClient extends AbstractRangerAdminClient {
return ret;
}
+ @Override
+ public RangerUserStore getUserStoreIfUpdated(long lastKnownUserStoreVersion, long lastActivationTimeInMillis) throws Exception {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerAdminRESTClient.getUserStoreIfUpdated(" + lastKnownUserStoreVersion + ", " + lastActivationTimeInMillis + ")");
+ }
+
+ final RangerUserStore ret;
+ final UserGroupInformation user = MiscUtil.getUGILoginUser();
+ final boolean isSecureMode = user != null && UserGroupInformation.isSecurityEnabled();
+ final ClientResponse response;
+
+ Map<String, String> queryParams = new HashMap<String, String>();
+ queryParams.put(RangerRESTUtils.REST_PARAM_LAST_KNOWN_USERSTORE_VERSION, Long.toString(lastKnownUserStoreVersion));
+ queryParams.put(RangerRESTUtils.REST_PARAM_LAST_ACTIVATION_TIME, Long.toString(lastActivationTimeInMillis));
+ queryParams.put(RangerRESTUtils.REST_PARAM_PLUGIN_ID, pluginId);
+ 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);
+ }
+ PrivilegedAction<ClientResponse> action = new PrivilegedAction<ClientResponse>() {
+ public ClientResponse run() {
+ ClientResponse clientRes = null;
+ String relativeURL = RangerRESTUtils.REST_URL_SERVICE_SERCURE_GET_USERSTORE + serviceNameUrlParam;
+ try {
+ clientRes = restClient.get(relativeURL, queryParams);
+ } catch (Exception e) {
+ LOG.error("Failed to get response, Error is : "+e.getMessage());
+ }
+ return clientRes;
+ }
+ };
+ response = user.doAs(action);
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Checking UserStore updated as user : " + user);
+ }
+ String relativeURL = RangerRESTUtils.REST_URL_SERVICE_SERCURE_GET_USERSTORE + serviceNameUrlParam;
+ response = restClient.get(relativeURL, queryParams);
+ }
+
+ if (response == null || response.getStatus() == HttpServletResponse.SC_NOT_MODIFIED) {
+ if (response == null) {
+ LOG.error("Error getting UserStore; Received NULL response!!. secureMode=" + isSecureMode + ", user=" + user + ", serviceName=" + serviceName);
+ } else {
+ RESTResponse resp = RESTResponse.fromClientResponse(response);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("No change in UserStore. secureMode=" + isSecureMode + ", user=" + user
+ + ", response=" + resp + ", serviceName=" + serviceName
+ + ", " + "lastKnownUserStoreVersion=" + lastKnownUserStoreVersion
+ + ", " + "lastActivationTimeInMillis=" + lastActivationTimeInMillis);
+ }
+ }
+ ret = null;
+ } else if (response.getStatus() == HttpServletResponse.SC_OK) {
+ ret = response.getEntity(RangerUserStore.class);
+ } else if (response.getStatus() == HttpServletResponse.SC_NOT_FOUND) {
+ ret = null;
+ LOG.error("Error getting UserStore; service not found. secureMode=" + isSecureMode + ", user=" + user
+ + ", response=" + response.getStatus() + ", serviceName=" + serviceName
+ + ", " + "lastKnownUserStoreVersion=" + lastKnownUserStoreVersion
+ + ", " + "lastActivationTimeInMillis=" + lastActivationTimeInMillis);
+ String exceptionMsg = response.hasEntity() ? response.getEntity(String.class) : null;
+
+ RangerServiceNotFoundException.throwExceptionIfServiceNotFound(serviceName, exceptionMsg);
+
+ LOG.warn("Received 404 error code with body:[" + exceptionMsg + "], Ignoring");
+ } else {
+ RESTResponse resp = RESTResponse.fromClientResponse(response);
+ LOG.warn("Error getting UserStore. secureMode=" + isSecureMode + ", user=" + user + ", response=" + resp + ", serviceName=" + serviceName);
+ ret = null;
+ }
+
+ if(LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerAdminRESTClient.getUserStoreIfUpdated(" + lastKnownUserStoreVersion + ", " + lastActivationTimeInMillis + "): ");
+ }
+
+ return ret;
+ }
+
}
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerAdminUserStoreRetriever.java b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerAdminUserStoreRetriever.java
new file mode 100644
index 0000000..ed96336
--- /dev/null
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerAdminUserStoreRetriever.java
@@ -0,0 +1,74 @@
+/*
+ * 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.contextenricher;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.ranger.admin.client.RangerAdminClient;
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.RangerUserStore;
+
+import java.nio.channels.ClosedByInterruptException;
+import java.util.Map;
+
+public class RangerAdminUserStoreRetriever extends RangerUserStoreRetriever {
+ private static final Log LOG = LogFactory.getLog(RangerAdminUserStoreRetriever.class);
+
+ private RangerAdminClient adminClient;
+
+ @Override
+ public void init(Map<String, String> options) {
+
+ if (StringUtils.isNotBlank(serviceName) && serviceDef != null && StringUtils.isNotBlank(appId)) {
+ RangerPluginConfig pluginConfig = super.pluginConfig;
+
+ if (pluginConfig == null) {
+ pluginConfig = new RangerPluginConfig(serviceDef.getName(), serviceName, appId, null, null, null);
+ }
+
+ adminClient = RangerBasePlugin.createAdminClient(pluginConfig);
+ } else {
+ LOG.error("FATAL: Cannot find service/serviceDef to use for retrieving userstore. Will NOT be able to retrieve userstore.");
+ }
+ }
+
+ @Override
+ public RangerUserStore retrieveUserStoreInfo(long lastKnownVersion, long lastActivationTimeInMillis) throws Exception {
+
+ RangerUserStore rangerUserStore = null;
+
+ if (adminClient != null) {
+ try {
+ rangerUserStore = adminClient.getUserStoreIfUpdated(lastKnownVersion, lastActivationTimeInMillis);
+ } catch (ClosedByInterruptException closedByInterruptException) {
+ LOG.error("UserStore-retriever thread was interrupted while blocked on I/O");
+ throw new InterruptedException();
+ } catch (Exception e) {
+ LOG.error("UserStore-retriever encounterd exception, exception=", e);
+ LOG.error("Returning null userstore info");
+ }
+ }
+ return rangerUserStore;
+ }
+
+}
+
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerUserStoreEnricher.java b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerUserStoreEnricher.java
new file mode 100644
index 0000000..fd4e11e
--- /dev/null
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerUserStoreEnricher.java
@@ -0,0 +1,505 @@
+/*
+ * 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.contextenricher;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.apache.ranger.plugin.service.RangerAuthContext;
+import org.apache.ranger.plugin.util.DownloaderTask;
+import org.apache.ranger.plugin.util.DownloadTrigger;
+import org.apache.ranger.plugin.util.RangerUserStore;
+import org.apache.ranger.plugin.util.RangerPerfTracer;
+import org.apache.ranger.plugin.util.RangerAccessRequestUtil;
+import org.apache.ranger.plugin.util.RangerServiceNotFoundException;
+
+import java.io.File;
+import java.io.Reader;
+import java.io.Writer;
+import java.io.FileWriter;
+import java.io.FileReader;
+import java.util.Timer;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class RangerUserStoreEnricher extends RangerAbstractContextEnricher {
+ private static final Log LOG = LogFactory.getLog(RangerUserStoreEnricher.class);
+
+ private static final Log PERF_SET_USERSTORE_LOG = RangerPerfTracer.getPerfLogger("userstoreenricher.setuserstore");
+
+
+ private static final String USERSTORE_REFRESHER_POLLINGINTERVAL_OPTION = "userStoreRefresherPollingInterval";
+ private static final String USERSTORE_RETRIEVER_CLASSNAME_OPTION = "userStoreRetrieverClassName";
+
+ private RangerUserStoreRefresher userStoreRefresher;
+ private RangerUserStoreRetriever userStoreRetriever;
+ private RangerUserStore rangerUserStore;
+ private boolean disableCacheIfServiceNotFound = true;
+
+ private final BlockingQueue<DownloadTrigger> userStoreDownloadQueue = new LinkedBlockingQueue<>();
+ private Timer userStoreDownloadTimer;
+
+ @Override
+ public void init() {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreEnricher.init()");
+ }
+
+ super.init();
+
+ String userStoreRetrieverClassName = getOption(USERSTORE_RETRIEVER_CLASSNAME_OPTION);
+
+ long pollingIntervalMs = getLongOption(USERSTORE_REFRESHER_POLLINGINTERVAL_OPTION, 3600 * 1000);
+
+ if (StringUtils.isNotBlank(userStoreRetrieverClassName)) {
+
+ try {
+ @SuppressWarnings("unchecked")
+ Class<RangerUserStoreRetriever> userStoreRetriverClass = (Class<RangerUserStoreRetriever>) Class.forName(userStoreRetrieverClassName);
+
+ userStoreRetriever = userStoreRetriverClass.newInstance();
+
+ } catch (ClassNotFoundException exception) {
+ LOG.error("Class " + userStoreRetrieverClassName + " not found, exception=" + exception);
+ } catch (ClassCastException exception) {
+ LOG.error("Class " + userStoreRetrieverClassName + " is not a type of RangerUserStoreRetriever, exception=" + exception);
+ } catch (IllegalAccessException exception) {
+ LOG.error("Class " + userStoreRetrieverClassName + " illegally accessed, exception=" + exception);
+ } catch (InstantiationException exception) {
+ LOG.error("Class " + userStoreRetrieverClassName + " could not be instantiated, exception=" + exception);
+ }
+
+ if (userStoreRetriever != null) {
+ String propertyPrefix = "ranger.plugin." + serviceDef.getName();
+ disableCacheIfServiceNotFound = getBooleanConfig(propertyPrefix + ".disable.cache.if.servicenotfound", true);
+ String cacheDir = getConfig(propertyPrefix + ".policy.cache.dir", null);
+ String cacheFilename = String.format("%s_%s_userstore.json", appId, serviceName);
+
+ cacheFilename = cacheFilename.replace(File.separatorChar, '_');
+ cacheFilename = cacheFilename.replace(File.pathSeparatorChar, '_');
+
+ String cacheFile = cacheDir == null ? null : (cacheDir + File.separator + cacheFilename);
+
+ userStoreRetriever.setServiceName(serviceName);
+ userStoreRetriever.setServiceDef(serviceDef);
+ userStoreRetriever.setAppId(appId);
+ userStoreRetriever.setPluginConfig(getPluginConfig());
+ userStoreRetriever.init(enricherDef.getEnricherOptions());
+
+ userStoreRefresher = new RangerUserStoreRefresher(userStoreRetriever, this, -1L, userStoreDownloadQueue, cacheFile);
+
+ try {
+ userStoreRefresher.populateUserStoreInfo();
+ } catch (Throwable exception) {
+ LOG.error("Exception when retrieving userstore information for this enricher", exception);
+ }
+
+ userStoreRefresher.setDaemon(true);
+ userStoreRefresher.startRefresher();
+
+ userStoreDownloadTimer = new Timer("userStoreDownloadTimer", true);
+
+ try {
+ userStoreDownloadTimer.schedule(new DownloaderTask(userStoreDownloadQueue), pollingIntervalMs, pollingIntervalMs);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Scheduled userStoreDownloadRefresher to download userstore every " + pollingIntervalMs + " milliseconds");
+ }
+ } catch (IllegalStateException exception) {
+ LOG.error("Error scheduling userStoreDownloadTimer:", exception);
+ LOG.error("*** UserStore information will NOT be downloaded every " + pollingIntervalMs + " milliseconds ***");
+ userStoreDownloadTimer = null;
+ }
+ }
+ } else {
+ LOG.error("No value specified for " + USERSTORE_RETRIEVER_CLASSNAME_OPTION + " in the RangerUserStoreEnricher options");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreEnricher.init()");
+ }
+ }
+
+ @Override
+ public void enrich(RangerAccessRequest request) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreEnricher.enrich(" + request + ")");
+ }
+
+ enrich(request, null);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreEnricher.enrich(" + request + ")");
+ }
+ }
+
+ @Override
+ public void enrich(RangerAccessRequest request, Object dataStore) {
+
+ // Unused by Solr plugin as document level authorization gets RangerUserStore from AuthContext
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreEnricher.enrich(" + request + ") with dataStore:[" + dataStore + "]");
+ }
+ final RangerUserStore rangerUserStore;
+
+ if (dataStore instanceof RangerUserStore) {
+ rangerUserStore = (RangerUserStore) dataStore;
+ } else {
+ rangerUserStore = this.rangerUserStore;
+
+ if (dataStore != null) {
+ LOG.warn("Incorrect type of dataStore :[" + dataStore.getClass().getName() + "], falling back to original enrich");
+ }
+ }
+
+ RangerAccessRequestUtil.setRequestUserStoreInContext(request.getContext(), rangerUserStore);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreEnricher.enrich(" + request + ") with dataStore:[" + dataStore + "])");
+ }
+ }
+
+ public RangerUserStore getRangerUserStore() {return this.rangerUserStore;}
+
+ public void setRangerUserStore(final RangerUserStore rangerUserStore) {
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreEnricher.setRangerUserStore(rangerUserStore=" + rangerUserStore + ")");
+ }
+
+ if (rangerUserStore == null) {
+ LOG.info("UserStore information is null for service " + serviceName);
+ this.rangerUserStore = null;
+ } else {
+ RangerPerfTracer perf = null;
+
+ if(RangerPerfTracer.isPerfTraceEnabled(PERF_SET_USERSTORE_LOG)) {
+ perf = RangerPerfTracer.getPerfTracer(PERF_SET_USERSTORE_LOG, "RangerUserStoreEnricher.setRangerUserStore(newUserStoreVersion=" + rangerUserStore.getUserStoreVersion() + ")");
+ }
+
+ this.rangerUserStore = rangerUserStore;
+ RangerPerfTracer.logAlways(perf);
+ }
+
+ setRangerUserStoreInPlugin();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreEnricher.setRangerUserStore(rangerUserStore=" + rangerUserStore + ")");
+ }
+
+ }
+
+ @Override
+ public boolean preCleanup() {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreEnricher.preCleanup()");
+ }
+
+ super.preCleanup();
+
+ if (userStoreDownloadTimer != null) {
+ userStoreDownloadTimer.cancel();
+ userStoreDownloadTimer = null;
+ }
+
+ if (userStoreRefresher != null) {
+ userStoreRefresher.cleanup();
+ userStoreRefresher = null;
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreEnricher.preCleanup() : result=" + true);
+ }
+ return true;
+ }
+
+ private void setRangerUserStoreInPlugin() {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> setRangerUserStoreInPlugin()");
+ }
+
+ RangerAuthContext authContext = getAuthContext();
+
+ if (authContext != null) {
+ authContext.addOrReplaceRequestContextEnricher(this, rangerUserStore);
+
+ notifyAuthContextChanged();
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== setRangerUserStoreInPlugin()");
+ }
+ }
+
+ static class RangerUserStoreRefresher extends Thread {
+ private static final Log LOG = LogFactory.getLog(RangerUserStoreRefresher.class);
+ private static final Log PERF_REFRESHER_INIT_LOG = RangerPerfTracer.getPerfLogger("userstore.init");
+
+ //private final RangerAdminClient adminClient;
+ private final RangerUserStoreRetriever userStoreRetriever;
+ private final RangerUserStoreEnricher userStoreEnricher;
+ private long lastKnownVersion;
+ private final BlockingQueue<DownloadTrigger> userStoreDownloadQueue;
+ private long lastActivationTimeInMillis;
+
+ private final String cacheFile;
+ private boolean hasProvidedUserStoreToReceiver;
+ private Gson gson;
+
+ RangerUserStoreRefresher(RangerUserStoreRetriever userStoreRetriever, RangerUserStoreEnricher userStoreEnricher,
+ long lastKnownVersion, BlockingQueue<DownloadTrigger> userStoreDownloadQueue,
+ String cacheFile) {
+ this.userStoreRetriever = userStoreRetriever;
+ this.userStoreEnricher = userStoreEnricher;
+ this.lastKnownVersion = lastKnownVersion;
+ this.userStoreDownloadQueue = userStoreDownloadQueue;
+ this.cacheFile = cacheFile;
+ try {
+ gson = new GsonBuilder().setDateFormat("yyyyMMdd-HH:mm:ss.SSS-Z").create();
+ } catch(Throwable excp) {
+ LOG.fatal("failed to create GsonBuilder object", excp);
+ }
+ }
+
+ public long getLastActivationTimeInMillis() {
+ return lastActivationTimeInMillis;
+ }
+
+ public void setLastActivationTimeInMillis(long lastActivationTimeInMillis) {
+ this.lastActivationTimeInMillis = lastActivationTimeInMillis;
+ }
+
+ @Override
+ public void run() {
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreRefresher().run()");
+ }
+
+ while (true) {
+
+ try {
+ RangerPerfTracer perf = null;
+
+ if(RangerPerfTracer.isPerfTraceEnabled(PERF_REFRESHER_INIT_LOG)) {
+ perf = RangerPerfTracer.getPerfTracer(PERF_REFRESHER_INIT_LOG,
+ "RangerUserStoreRefresher.run(lastKnownVersion=" + lastKnownVersion + ")");
+ }
+ DownloadTrigger trigger = userStoreDownloadQueue.take();
+ populateUserStoreInfo();
+ trigger.signalCompletion();
+
+ RangerPerfTracer.log(perf);
+
+ } catch (InterruptedException excp) {
+ LOG.debug("RangerUserStoreRefresher().run() : interrupted! Exiting thread", excp);
+ break;
+ }
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreRefresher().run()");
+ }
+ }
+
+ private void populateUserStoreInfo() throws InterruptedException {
+
+ RangerUserStore rangerUserStore = null;
+ if (userStoreEnricher != null) {
+ try {
+ rangerUserStore = userStoreRetriever.retrieveUserStoreInfo(lastKnownVersion, lastActivationTimeInMillis);
+
+ if (rangerUserStore == null) {
+ if (!hasProvidedUserStoreToReceiver) {
+ rangerUserStore = loadFromCache();
+ }
+ }
+
+ if (rangerUserStore != null) {
+ userStoreEnricher.setRangerUserStore(rangerUserStore);
+ if (rangerUserStore.getUserStoreVersion() != -1L) {
+ saveToCache(rangerUserStore);
+ }
+ LOG.info("RangerUserStoreRefresher.populateUserStoreInfo() - Updated userstore-cache to new version, lastKnownVersion=" + lastKnownVersion + "; newVersion="
+ + (rangerUserStore.getUserStoreVersion() == null ? -1L : rangerUserStore.getUserStoreVersion()));
+ hasProvidedUserStoreToReceiver = true;
+ lastKnownVersion = rangerUserStore.getUserStoreVersion() == null ? -1L : rangerUserStore.getUserStoreVersion();
+ setLastActivationTimeInMillis(System.currentTimeMillis());
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("RangerUserStoreRefresher.populateUserStoreInfo() - No need to update userstore-cache. lastKnownVersion=" + lastKnownVersion);
+ }
+ }
+ } catch (RangerServiceNotFoundException snfe) {
+ LOG.error("Caught ServiceNotFound exception :", snfe);
+
+ // Need to clean up local userstore cache
+ if (userStoreEnricher.disableCacheIfServiceNotFound) {
+ disableCache();
+ setLastActivationTimeInMillis(System.currentTimeMillis());
+ lastKnownVersion = -1L;
+ }
+ } catch (InterruptedException interruptedException) {
+ throw interruptedException;
+ } catch (Exception e) {
+ LOG.error("Encountered unexpected exception. Ignoring", e);
+ }
+ } else {
+ LOG.error("RangerUserStoreRefresher.populateUserStoreInfo() - no userstore receiver to update userstore-cache");
+ }
+ }
+
+ public void cleanup() {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreRefresher.cleanup()");
+ }
+
+ stopRefresher();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreRefresher.cleanup()");
+ }
+ }
+
+ public void startRefresher() {
+ try {
+ super.start();
+ } catch (Exception excp) {
+ LOG.error("RangerUserStoreRefresher.startRetriever() - failed to start, exception=" + excp);
+ }
+ }
+
+ public void stopRefresher() {
+
+ if (super.isAlive()) {
+ super.interrupt();
+
+ try {
+ super.join();
+ } catch (InterruptedException excp) {
+ LOG.error("RangerUserStoreRefresher.stopRefresher(): error while waiting for thread to exit", excp);
+ }
+ }
+ }
+
+
+ private RangerUserStore loadFromCache() {
+ RangerUserStore rangerUserStore = null;
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreRefreher.loadFromCache()");
+ }
+
+ File cacheFile = StringUtils.isEmpty(this.cacheFile) ? null : new File(this.cacheFile);
+
+ if (cacheFile != null && cacheFile.isFile() && cacheFile.canRead()) {
+ Reader reader = null;
+
+ try {
+ reader = new FileReader(cacheFile);
+
+ rangerUserStore = gson.fromJson(reader, RangerUserStore.class);
+
+ } catch (Exception excp) {
+ LOG.error("failed to load userstore information from cache file " + cacheFile.getAbsolutePath(), excp);
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (Exception excp) {
+ LOG.error("error while closing opened cache file " + cacheFile.getAbsolutePath(), excp);
+ }
+ }
+ }
+ } else {
+ LOG.warn("cache file does not exist or not readable '" + (cacheFile == null ? null : cacheFile.getAbsolutePath()) + "'");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreRefreher.loadFromCache()");
+ }
+
+ return rangerUserStore;
+ }
+
+ public void saveToCache(RangerUserStore rangerUserStore) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreRefreher.saveToCache()");
+ }
+
+ if (rangerUserStore != null) {
+ File cacheFile = StringUtils.isEmpty(this.cacheFile) ? null : new File(this.cacheFile);
+
+ if (cacheFile != null) {
+ Writer writer = null;
+
+ try {
+ writer = new FileWriter(cacheFile);
+
+ gson.toJson(rangerUserStore, writer);
+ } catch (Exception excp) {
+ LOG.error("failed to save userstore information to cache file '" + cacheFile.getAbsolutePath() + "'", excp);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (Exception excp) {
+ LOG.error("error while closing opened cache file '" + cacheFile.getAbsolutePath() + "'", excp);
+ }
+ }
+ }
+ }
+ } else {
+ LOG.info("userstore information is null. Nothing to save in cache");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreRefreher.saveToCache()");
+ }
+ }
+
+ private void disableCache() {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("==> RangerUserStoreRefreher.disableCache()");
+ }
+
+ File cacheFile = StringUtils.isEmpty(this.cacheFile) ? null : new File(this.cacheFile);
+ if (cacheFile != null && cacheFile.isFile() && cacheFile.canRead()) {
+ LOG.warn("Cleaning up local userstore cache");
+ String renamedCacheFile = cacheFile.getAbsolutePath() + "_" + System.currentTimeMillis();
+ if (!cacheFile.renameTo(new File(renamedCacheFile))) {
+ LOG.error("Failed to move " + cacheFile.getAbsolutePath() + " to " + renamedCacheFile);
+ } else {
+ LOG.warn("moved " + cacheFile.getAbsolutePath() + " to " + renamedCacheFile);
+ }
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("No local userstore cache found. No need to disable it!");
+ }
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("<== RangerUserStoreRefreher.disableCache()");
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerUserStoreRetriever.java b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerUserStoreRetriever.java
new file mode 100644
index 0000000..1addbc4
--- /dev/null
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/RangerUserStoreRetriever.java
@@ -0,0 +1,64 @@
+/*
+ * 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.contextenricher;
+
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.model.RangerServiceDef;
+import org.apache.ranger.plugin.util.RangerUserStore;
+
+import java.util.Map;
+
+public abstract class RangerUserStoreRetriever {
+
+ protected String serviceName;
+ protected RangerServiceDef serviceDef;
+ protected String appId;
+ protected RangerPluginConfig pluginConfig;
+
+ public abstract void init(Map<String, String> options);
+
+ public abstract RangerUserStore retrieveUserStoreInfo(long lastKnownVersion, long lastActivationTimeInMillis) throws Exception;
+
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public RangerServiceDef getServiceDef() {
+ return serviceDef;
+ }
+
+ public void setServiceDef(RangerServiceDef serviceDef) {
+ this.serviceDef = serviceDef;
+ }
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ public void setPluginConfig(RangerPluginConfig pluginConfig) { this.pluginConfig = pluginConfig; }
+}
\ No newline at end of file
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerAccessRequestUtil.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerAccessRequestUtil.java
index bd980ce..bc52bdb 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerAccessRequestUtil.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerAccessRequestUtil.java
@@ -38,6 +38,7 @@ public class RangerAccessRequestUtil {
public static final String KEY_CONTEXT_TAG_OBJECT = "TAG_OBJECT";
public static final String KEY_CONTEXT_RESOURCE = "RESOURCE";
public static final String KEY_CONTEXT_REQUESTED_RESOURCES = "REQUESTED_RESOURCES";
+ public static final String KEY_CONTEXT_USERSTORE = "USERSTORE";
public static final String KEY_TOKEN_NAMESPACE = "token:";
public static final String KEY_USER = "USER";
public static final String KEY_OWNER = "OWNER";
@@ -158,4 +159,19 @@ public class RangerAccessRequestUtil {
Object ret = getTokenFromContext(context, KEY_ROLES);
return ret != null ? (Set<String>) ret : Collections.EMPTY_SET;
}
+
+ public static void setRequestUserStoreInContext(Map<String, Object> context, RangerUserStore rangerUserStore) {
+ context.put(KEY_CONTEXT_USERSTORE, rangerUserStore);
+ }
+
+ public static RangerUserStore getRequestUserStoreFromContext(Map<String, Object> context) {
+ RangerUserStore ret = null;
+ Object val = context.get(KEY_CONTEXT_USERSTORE);
+
+ if(val instanceof RangerUserStore) {
+ ret = (RangerUserStore) val;
+ }
+
+ return ret;
+ }
}
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 0b492ab..3e402aa 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
@@ -76,6 +76,9 @@ public class RangerRESTUtils {
public static final String REST_PARAM_LAST_KNOWN_ROLE_VERSION = "lastKnownRoleVersion";
+ public static final String REST_PARAM_LAST_KNOWN_USERSTORE_VERSION = "lastKnownUserStoreVersion";
+ public static final String REST_URL_SERVICE_SERCURE_GET_USERSTORE = "/service/xusers/secure/download/";
+
private static final int MAX_PLUGIN_ID_LEN = 255;
public static final String REST_PARAM_CLUSTER_NAME = "clusterName";
diff --git a/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/FieldToAttributeMapping.java b/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/FieldToAttributeMapping.java
new file mode 100644
index 0000000..ba5f7d4
--- /dev/null
+++ b/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/FieldToAttributeMapping.java
@@ -0,0 +1,106 @@
+/*
+ * 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.authorization.solr.authorizer;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to hold the configuration which maps fields in Solr to one or more
+ * LDAP attributes. Includes details such as whether empty values in the doc should be permitted, whether there is a
+ * default value for all users, whether any RegExs should be applied and any extra options that should be passed
+ * to the fq
+ */
+public class FieldToAttributeMapping {
+
+ private static final Splitter ATTR_NAME_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
+
+ private final String fieldName;
+ private final Collection<String> attributes;
+ private final FilterType filterType;
+ private final boolean acceptEmpty;
+ private final String allUsersValue;
+ private final Pattern attrValueRegex;
+ private final String extraOpts;
+
+ /**
+ * The four filter types currently supported, AND, OR, LessThanOrEqualTo (LTE), GreaterThanOrEqualTo (GTE)
+ * Expected to be expanded in the future
+ */
+ enum FilterType {
+ AND,
+ OR,
+ LTE,
+ GTE
+ }
+
+ /**
+ * @param fieldName The field being mapped
+ * @param ldapAttributeNames comma delimited list of attributes which will be used to acquire values for this field
+ * @param filterType filter type can be any one of {@link FilterType}
+ * @param acceptEmpty true if an empty value in the Solr field should be counted as a match (i.e. doc returned)
+ * @param allUsersValue the value which the field may contain that would indicate that all users should see this doc
+ * @param valueFilterRegex String representation of a {@link Pattern} that will be applied to attributes retrieved
+ * from the attribute source. Note: If match groups are used, the last non-null match-group
+ * will be applied as the value for this filter
+ * @param extraOpts Any extra options that should be passed to the filter as constructed before appending to the fq
+ */
+ public FieldToAttributeMapping(String fieldName, String ldapAttributeNames, String filterType, boolean acceptEmpty, String allUsersValue, String valueFilterRegex, String extraOpts) {
+ this.fieldName = fieldName;
+ this.attributes = Collections.unmodifiableSet(Sets.newHashSet(ATTR_NAME_SPLITTER.split(ldapAttributeNames)));
+ this.filterType = FilterType.valueOf(filterType);
+ this.acceptEmpty = acceptEmpty;
+ this.allUsersValue = allUsersValue;
+ this.attrValueRegex = Pattern.compile(valueFilterRegex);
+ this.extraOpts = extraOpts;
+ }
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ public Collection<String> getAttributes() {
+ return attributes;
+ }
+
+ public FilterType getFilterType() {
+ return filterType;
+ }
+
+ public boolean getAcceptEmpty() {
+ return acceptEmpty;
+ }
+
+ public String getAllUsersValue() {
+ return allUsersValue;
+ }
+
+ public String getExtraOpts() {
+ return extraOpts;
+ }
+
+ public Pattern getAttrValueRegex() {
+ return attrValueRegex;
+ }
+}
diff --git a/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/RangerSolrAuthorizer.java b/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/RangerSolrAuthorizer.java
index 4538a5b..30b9861 100644
--- a/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/RangerSolrAuthorizer.java
+++ b/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/RangerSolrAuthorizer.java
@@ -21,21 +21,30 @@ package org.apache.ranger.authorization.solr.authorizer;
import java.io.IOException;
import java.security.Principal;
-import java.util.ArrayList;
+import java.util.Map;
import java.util.Date;
+import java.util.Set;
+import java.util.Iterator;
import java.util.Enumeration;
+import java.util.Collection;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.HashSet;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ranger.audit.provider.MiscUtil;
+import org.apache.ranger.plugin.contextenricher.RangerContextEnricher;
+import org.apache.ranger.plugin.contextenricher.RangerUserStoreEnricher;
import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl;
import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl;
import org.apache.ranger.plugin.policyengine.RangerAccessResult;
+import org.apache.ranger.plugin.service.RangerAuthContext;
import org.apache.ranger.plugin.service.RangerBasePlugin;
import org.apache.ranger.plugin.util.RangerPerfTracer;
import org.apache.solr.common.SolrException;
@@ -85,6 +94,20 @@ public class RangerSolrAuthorizer extends SearchComponent implements Authorizati
public static final String ACCESS_TYPE_OTHERS = "others";
public static final String ACCESS_TYPE_ADMIN = "solr_admin";
+ public static final String ATTRS_ENABLED_PROP = "attrs_enabled";
+ public static final String FIELD_ATTR_MAPPINGS = "field_attr_mappings";
+ public static final String FIELD_FILTER_TYPE = "filter_type";
+ public static final String ATTR_NAMES = "attr_names";
+ public static final String PERMIT_EMPTY_VALUES = "permit_empty";
+ public static final String ALL_USERS_VALUE = "all_users_value";
+ public static final String ATTRIBUTE_FILTER_REGEX = "value_filter_regex";
+ public static final String AND_OP_QPARSER = "andQParser";
+ public static final String EXTRA_OPTS = "extra_opts";
+
+ private List<FieldToAttributeMapping> fieldAttributeMappings = new LinkedList<>();
+
+ private String andQParserName;
+
private static volatile RangerBasePlugin solrPlugin = null;
private RangerSolrAuditHandler auditHandler = null;
@@ -100,6 +123,7 @@ public class RangerSolrAuthorizer extends SearchComponent implements Authorizati
private String tokenCountField;
private boolean allowMissingValue;
private String qParserName;
+ private boolean attrsEnabled;
private enum MatchType {
DISJUNCTIVE,
@@ -123,9 +147,39 @@ public class RangerSolrAuthorizer extends SearchComponent implements Authorizati
this.allowMissingValue = params.getBool(ALLOW_MISSING_VAL_PROP, false);
this.tokenCountField = params.get(TOKEN_COUNT_PROP, DEFAULT_TOKEN_COUNT_FIELD_PROP);
}
+
+ this.attrsEnabled = params.getBool(ATTRS_ENABLED_PROP, false);
+
logger.info("RangerSolrAuthorizer.init(): authField={" + authField + "}, allRolesToken={" + allRolesToken +
"}, enabled={" + enabled + "}, matchType={" + matchMode + "}, qParserName={" + qParserName +
- "}, allowMissingValue={" + allowMissingValue + "}, tokenCountField={" + tokenCountField + "}");
+ "}, allowMissingValue={" + allowMissingValue + "}, tokenCountField={" + tokenCountField + "}, attrsEnabled={" + attrsEnabled + "}");
+
+ if (attrsEnabled) {
+
+ if (params.get(FIELD_ATTR_MAPPINGS) != null) {
+ logger.info("Solr params = " + params.get(FIELD_ATTR_MAPPINGS));
+
+ NamedList mappings = checkAndGet(args, FIELD_ATTR_MAPPINGS);
+
+ Iterator<Map.Entry<String, NamedList>> iter = mappings.iterator();
+ while (iter.hasNext()) {
+ Map.Entry<String, NamedList> entry = iter.next();
+ String solrFieldName = entry.getKey();
+ String attributeNames = checkAndGet(entry.getValue(), ATTR_NAMES);
+ String filterType = checkAndGet(entry.getValue(), FIELD_FILTER_TYPE);
+ boolean acceptEmpty = false;
+ if (entry.getValue().getBooleanArg(PERMIT_EMPTY_VALUES) != null) {
+ acceptEmpty = entry.getValue().getBooleanArg(PERMIT_EMPTY_VALUES);
+ }
+ String allUsersValue = getWithDefault(entry.getValue(), ALL_USERS_VALUE, "");
+ String regex = getWithDefault(entry.getValue(), ATTRIBUTE_FILTER_REGEX, "");
+ String extraOpts = getWithDefault(entry.getValue(), EXTRA_OPTS, "");
+ FieldToAttributeMapping mapping = new FieldToAttributeMapping(solrFieldName, attributeNames, filterType, acceptEmpty, allUsersValue, regex, extraOpts);
+ fieldAttributeMappings.add(mapping);
+ }
+ }
+ this.andQParserName = this.<String>checkAndGet(args, AND_OP_QPARSER).trim();
+ }
}
/*
@@ -313,6 +367,9 @@ public class RangerSolrAuthorizer extends SearchComponent implements Authorizati
@Override
public void prepare(ResponseBuilder rb) throws IOException {
if (!enabled) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Solr Document level Authorization is not enabled!");
+ }
return;
}
@@ -321,24 +378,53 @@ public class RangerSolrAuthorizer extends SearchComponent implements Authorizati
return;
}
- Set<String> roles = getRolesForUser(userName);
- if (roles != null && !roles.isEmpty()) {
- String filterQuery;
- if (matchMode == MatchType.DISJUNCTIVE) {
- filterQuery = getDisjunctiveFilterQueryStr(roles);
- } else {
- filterQuery = getConjunctiveFilterQueryStr(roles);
- }
- ModifiableSolrParams newParams = new ModifiableSolrParams(rb.req.getParams());
- newParams.add("fq", filterQuery);
- rb.req.setParams(newParams);
+ if (attrsEnabled) {
if (logger.isDebugEnabled()) {
- logger.debug("Adding filter query {" + filterQuery + "} for user {" + userName + "} with roles {" + roles + "}");
+ logger.debug("Checking Ldap attributes to be added to the query filter");
+ }
+ if (getUserStoreEnricher() == null || getUserStoreEnricher().getRangerUserStore() == null) {
+ logger.error("No User store enricher to read the ldap attributes");
+ return;
}
+ // Ranger UserStore info for user/group attributes
+ Map<String, Map<String, String>> userAttrMapping = getUserStoreEnricher().getRangerUserStore().getUserAttrMapping();
+ if (MapUtils.isNotEmpty(userAttrMapping)) {
+ ModifiableSolrParams newParams = new ModifiableSolrParams(rb.req.getParams());
+ Map<String, String> userAttributes = userAttrMapping.get(userName);
+ for (FieldToAttributeMapping mapping : fieldAttributeMappings) {
+ String filterQuery = buildFilterQueryString(userName, userAttributes, mapping);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Adding filter clause : {}" + filterQuery);
+ }
+ newParams.add("fq", filterQuery);
+ }
+ rb.req.setParams(newParams);
+ }
} else {
- throw new SolrException(SolrException.ErrorCode.UNAUTHORIZED,
- "Request from user: " + userName + " rejected because user is not associated with any roles");
+ if (logger.isDebugEnabled()) {
+ logger.debug("Checking User roles to be added to the query filter");
+ }
+
+ Set<String> roles = getRolesForUser(userName);
+ if (roles != null && !roles.isEmpty()) {
+ String filterQuery;
+ if (matchMode == MatchType.DISJUNCTIVE) {
+ filterQuery = getDisjunctiveFilterQueryStr(roles);
+ } else {
+ filterQuery = getConjunctiveFilterQueryStr(roles);
+ }
+ ModifiableSolrParams newParams = new ModifiableSolrParams(rb.req.getParams());
+ newParams.add("fq", filterQuery);
+ rb.req.setParams(newParams);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Adding filter query {" + filterQuery + "} for user {" + userName + "} with roles {" + roles + "}");
+ }
+
+ } else {
+ throw new SolrException(SolrException.ErrorCode.UNAUTHORIZED,
+ "Request from user: " + userName + " rejected because user is not associated with any roles");
+ }
}
}
@@ -596,4 +682,147 @@ public class RangerSolrAuthorizer extends SearchComponent implements Authorizati
return userName;
}
+
+ private RangerUserStoreEnricher getUserStoreEnricher() {
+ RangerUserStoreEnricher ret = null;
+ RangerAuthContext authContext = solrPlugin.getCurrentRangerAuthContext();
+
+ if (authContext != null) {
+ Map<RangerContextEnricher, Object> contextEnricherMap = authContext.getRequestContextEnrichers();
+
+ if (MapUtils.isNotEmpty(contextEnricherMap)) {
+ Set<RangerContextEnricher> contextEnrichers = contextEnricherMap.keySet();
+
+ for (RangerContextEnricher enricher : contextEnrichers) {
+ if (enricher instanceof RangerUserStoreEnricher) {
+ ret = (RangerUserStoreEnricher) enricher;
+
+ break;
+ }
+ }
+ }
+ }
+ return ret;
+ }
+
+ private <T> T checkAndGet(NamedList args, String key) {
+ logger.info("checkAndGet() " + key);
+ return (T) Preconditions.checkNotNull(args.get(key));
+ }
+
+ private <T> T getWithDefault(NamedList args, String key, T defaultValue) {
+ T value = (T) args.get(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ return value;
+ }
+ }
+
+ private String buildFilterQueryString(String userName, Map<String, String> userAttributes, FieldToAttributeMapping mapping) {
+ String fieldName = mapping.getFieldName();
+ Collection<String> attributeValues = getUserAttributesForField(userName, userAttributes, mapping);
+ switch (mapping.getFilterType()) {
+ case OR:
+ return buildSimpleORFilterQuery(fieldName, attributeValues, mapping.getAcceptEmpty(), mapping.getAllUsersValue(), mapping.getExtraOpts());
+ case AND:
+ return buildSubsetFilterQuery(fieldName, attributeValues, mapping.getAcceptEmpty(), mapping.getAllUsersValue(), mapping.getExtraOpts());
+ case GTE:
+ return buildGreaterThanFilterQuery(fieldName, attributeValues, mapping.getAcceptEmpty(), mapping.getAllUsersValue(), mapping.getExtraOpts());
+ case LTE:
+ return buildLessThanFilterQuery(fieldName, attributeValues, mapping.getAcceptEmpty(), mapping.getAllUsersValue(), mapping.getExtraOpts());
+ default:
+ return null;
+ }
+ }
+
+ private Collection<String> getUserAttributesForField(String userName, Map<String, String> userAttributes, FieldToAttributeMapping mapping) {
+ Set<String> userAttributesSubset = new HashSet<>();
+ if (CollectionUtils.isNotEmpty(mapping.getAttributes())) {
+ if (mapping.getAttributes().contains("groups")) {
+ userAttributesSubset.addAll(getGroupsForUser(userName));
+ }
+ }
+ for (String attributeName : mapping.getAttributes()) {
+ userAttributesSubset.add(userAttributes.get(attributeName));
+ }
+ return userAttributesSubset;
+ }
+
+ private String buildSimpleORFilterQuery(String fieldName, Collection<String> attributeValues, boolean allowEmptyField, String allUsersValue, String extraOpts) {
+ StringBuilder s = new StringBuilder();
+ for (String attributeValue : attributeValues) {
+ s.append(fieldName).append(":\"").append(attributeValue).append("\" ");
+ }
+ if (allUsersValue != null && !allUsersValue.equals("")) {
+ s.append(fieldName).append(":\"").append(allUsersValue).append("\" ");
+ }
+ if (allowEmptyField) {
+ s.append("(*:* AND -").append(fieldName).append(":*) ");
+ }
+ if (extraOpts != null && !extraOpts.equals("")) {
+ s.append(extraOpts + " ");
+ }
+ s.deleteCharAt(s.length() - 1);
+ return s.toString();
+ }
+
+ private String buildSubsetFilterQuery(String fieldName, Collection<String> attributeValues, boolean allowEmptyField, String allUsersValue, String extraOpts) {
+ StringBuilder s = new StringBuilder();
+ s.append("{!").append(andQParserName)
+ .append(" set_field=").append(fieldName)
+ .append(" set_value=").append(Joiner.on(',').join(attributeValues));
+ if (allUsersValue != null && !allUsersValue.equals("")) {
+ s.append(" wildcard_token=").append(allUsersValue);
+ }
+ if (allowEmptyField) {
+ s.append(" allow_missing_val=true");
+ } else {
+ s.append(" allow_missing_val=false");
+ }
+ if (extraOpts != null && !extraOpts.equals("")) {
+ s.append(" " + extraOpts);
+ }
+ s.append("}");
+ return s.toString();
+ }
+
+ private String buildGreaterThanFilterQuery(String fieldName, Collection<String> attributeValues, boolean allowEmptyField, String allUsersValue, String extraOpts) {
+ String value;
+ if (attributeValues.size() == 1) {
+ value = attributeValues.iterator().next();
+ } else if (allUsersValue != null && !allUsersValue.equals("")) {
+ value = allUsersValue;
+ } else {
+ throw new IllegalArgumentException("Greater Than Filter Query cannot be built for field " + fieldName);
+ }
+ StringBuilder extraClause = new StringBuilder();
+ if (allowEmptyField) {
+ extraClause.append(" (*:* AND -").append(fieldName).append(":*)");
+ }
+ if (extraOpts != null && !extraOpts.equals("")) {
+ extraClause.append(" ").append(extraOpts);
+ }
+ return fieldName + ":[" + value + " TO *]" + extraClause.toString();
+ }
+
+ private String buildLessThanFilterQuery(String fieldName, Collection<String> attributeValues, boolean allowEmptyField, String allUsersValue, String extraOpts) {
+ String value;
+ if (attributeValues.size() == 1) {
+ value = attributeValues.iterator().next();
+ } else if (allUsersValue != null && !allUsersValue.equals("")) {
+ value = allUsersValue;
+ } else {
+ throw new IllegalArgumentException("Less Than Filter Query cannot be built for field " + fieldName);
+ }
+ StringBuilder extraClause = new StringBuilder();
+ if (allowEmptyField) {
+ extraClause.append(" (*:* AND -").append(fieldName).append(":*)");
+ }
+ if (extraOpts != null && !extraOpts.equals("")) {
+ extraClause.append(" ").append(extraOpts);
+ }
+ return fieldName + ":[* TO " + value + "]" + extraClause.toString();
+ }
+
}
diff --git a/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/SubsetQueryPlugin.java b/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/SubsetQueryPlugin.java
new file mode 100644
index 0000000..1ea4d28
--- /dev/null
+++ b/plugin-solr/src/main/java/org/apache/ranger/authorization/solr/authorizer/SubsetQueryPlugin.java
@@ -0,0 +1,97 @@
+/*
+ * 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.authorization.solr.authorizer;
+
+import com.google.common.base.Preconditions;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.CoveringQuery;
+import org.apache.lucene.search.LongValuesSource;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.WildcardQuery;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.search.QParser;
+import org.apache.solr.search.QParserPlugin;
+import org.apache.solr.search.SyntaxError;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * A custom {@linkplain QParserPlugin} which supports subset queries on a given Solr index.
+ * This filter accepts the name of the field whose value should be used for subset matching
+ * and the set against which subset queries are to be run ( as a comma separated string values).
+ */
+public class SubsetQueryPlugin extends QParserPlugin {
+ public static final String SETVAL_PARAM_NAME = "set_value";
+ public static final String SETVAL_FIELD_NAME = "set_field";
+ public static final String COUNT_FIELD_NAME = "count_field";
+ public static final String MISSING_VAL_ALLOWED = "allow_missing_val";
+ public static final String WILDCARD_CHAR = "wildcard_token";
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void init(NamedList arg0) {
+ }
+
+ @Override
+ public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
+ return new QParser(qstr, localParams, params, req) {
+
+ @Override
+ public Query parse() throws SyntaxError {
+ String fieldName = Preconditions.checkNotNull(localParams.get(SETVAL_FIELD_NAME));
+ String countFieldName = Preconditions.checkNotNull(localParams.get(COUNT_FIELD_NAME));
+ boolean allowMissingValues = Boolean.parseBoolean(Preconditions.checkNotNull(localParams.get(MISSING_VAL_ALLOWED)));
+ String wildcardToken = localParams.get(WILDCARD_CHAR);
+
+ LongValuesSource minimumNumberMatch = LongValuesSource.fromIntField(countFieldName);
+ Collection<Query> queries = new ArrayList<>();
+
+ String fieldVals = Preconditions.checkNotNull(localParams.get(SETVAL_PARAM_NAME));
+ for (String v : fieldVals.split(",")) {
+ queries.add(new TermQuery(new Term(fieldName, v)));
+ }
+ if (wildcardToken != null && !wildcardToken.equals("")) {
+ queries.add(new TermQuery(new Term(fieldName, wildcardToken)));
+ }
+ if (allowMissingValues) {
+ // To construct this query we need to do a little trick tho construct a test for an empty field as follows:
+ // (*:* AND -fieldName:*) ==> parses as: (+*:* -fieldName:*)
+ // It is a feature of Lucene that pure negative queries are not allowed (although Solr allows them as a top level construct)
+ // therefore we need to AND with *:*
+ // We can then pass this BooleanQuery to the CoveringQuery as one of its allowed matches.
+ BooleanQuery.Builder builder = new BooleanQuery.Builder();
+ builder.add(new BooleanClause(new MatchAllDocsQuery(), BooleanClause.Occur.SHOULD));
+ builder.add(new BooleanClause(new WildcardQuery(new Term(fieldName, "*")), BooleanClause.Occur.MUST_NOT));
+
+ queries.add(builder.build());
+ }
+ return new CoveringQuery(queries, minimumNumberMatch);
+ }
+ };
+ }
+
+}