This is an automated email from the ASF dual-hosted git repository.

madhan 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 f510319fb RANGER-3855: added RangerMultiSourceUserStoreRetriever 
implementation
f510319fb is described below

commit f510319fb23bc23c71e08780e0b59d502b9590d3
Author: Eckman, Barbara <barbara_eck...@cable.comcast.com>
AuthorDate: Thu Nov 17 16:11:45 2022 -0500

    RANGER-3855: added RangerMultiSourceUserStoreRetriever implementation
    
    Signed-off-by: Madhan Neethiraj <mad...@apache.org>
---
 .../externalretrievers/GetFromDataFile.java        |  75 +++++
 .../externalretrievers/GetFromURL.java             | 224 +++++++++++++
 .../contextenricher/externalretrievers/LICENSE     | 202 ++++++++++++
 .../contextenricher/externalretrievers/NOTICE      |  18 +
 .../contextenricher/externalretrievers/README.md   | 137 ++++++++
 .../RangerMultiSourceUserStoreRetriever.java       | 365 +++++++++++++++++++++
 .../ranger/plugin/util/RangerRolesProvider.java    |   2 +-
 .../apache/ranger/plugin/util/RangerRolesUtil.java |   2 +-
 dev-support/spotbugsIncludeFile.xml                |   1 +
 9 files changed, 1024 insertions(+), 2 deletions(-)

diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/GetFromDataFile.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/GetFromDataFile.java
new file mode 100644
index 000000000..93cf38aac
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/GetFromDataFile.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.plugin.contextenricher.externalretrievers;
+
+import org.apache.ranger.plugin.contextenricher.RangerAbstractContextEnricher;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class GetFromDataFile {
+    private static final Logger LOG = 
LoggerFactory.getLogger(GetFromDataFile.class);
+
+    public Map<String, Map<String, String>> getFromDataFile(String dataFile, 
String attrName) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> getFromDataFile(dataFile={}, attrName={})", 
dataFile, attrName);
+        }
+
+        Map<String, Map<String, String>> ret = new HashMap<>();
+
+        // create an instance so that readProperties() can be used!
+        RangerAbstractContextEnricher ce = new RangerAbstractContextEnricher() 
{
+            @Override
+            public void enrich(RangerAccessRequest rangerAccessRequest) {
+            }
+        };
+
+        Properties prop = ce.readProperties(dataFile);
+
+        if (prop == null) {
+            LOG.warn("getFromDataFile({}, {}): failed to read file", dataFile, 
attrName);
+        } else {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("read from datafile {}: {}", dataFile, prop);
+            }
+
+            // reformat UserAttrsProp into UserStore format:
+            // format of UserAttrsProp: Map<String, String>
+            // format of UserStore: Map<String, Map<String, String>>
+            for (String user : prop.stringPropertyNames()) {
+                Map<String, String> userAttrs = new HashMap<>();
+
+                userAttrs.put(attrName, prop.getProperty(user));
+
+                ret.put(user, userAttrs);
+            }
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== getFromDataFile(dataFile={}, attrName={}): ret={}", 
dataFile, attrName, ret);
+        }
+
+        return ret;
+    }
+}
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/GetFromURL.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/GetFromURL.java
new file mode 100644
index 000000000..f9eae3574
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/GetFromURL.java
@@ -0,0 +1,224 @@
+/*
+ * 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.externalretrievers;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.apache.hadoop.util.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class GetFromURL {
+    private static final Logger LOG = 
LoggerFactory.getLogger(GetFromURL.class);
+
+    private final Gson gson = new Gson();
+
+    public Map<String, Map<String, String>> getFromURL(String url, String 
configFile) throws Exception {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> getFromURL(url={}, configFile={})", url, 
configFile);
+        }
+
+        String         token   = getBearerToken(configFile);
+        HttpUriRequest request = RequestBuilder.get().setUri(url)
+                                                     
.setHeader(HttpHeaders.AUTHORIZATION, token)
+                                                     
.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
+                                                     .build();
+        Map<String, Map<String, String>> ret;
+
+        try (CloseableHttpClient   httpClient = HttpClients.createDefault();
+             CloseableHttpResponse response   = httpClient.execute(request)) {
+            if (response == null) {
+                throw new IOException("getFromURL(" + url + ") failed: null 
response");
+            }
+
+            int statusCode = response.getStatusLine().getStatusCode();
+
+            if (statusCode != HttpStatus.SC_OK) {
+                throw new IOException("getFromURL(" + url + ") failed: http 
status=" + response.getStatusLine());
+            }
+
+            HttpEntity                             httpEntity     = 
response.getEntity();
+            String                                 stringResult   = 
EntityUtils.toString(httpEntity);
+            Map                                    resultMap      = 
gson.fromJson(stringResult, Map.class);
+            Map<String, Map<String, List<String>>> userAttrValues = 
(Map<String, Map<String, List<String>>>) resultMap.get("body");
+
+            ret = toUserAttributes(userAttrValues);
+
+            // and ensure response body is fully consumed
+            EntityUtils.consume(httpEntity);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== getFromURL(url={}, configFile={}): ret={}", url, 
configFile, ret);
+        }
+
+        return ret;
+    }
+
+    private String getBearerToken(String configFile) throws Exception {
+        String                    secrets    = getSecretsFromFile(configFile);
+        JsonObject                jsonObject = gson.fromJson(secrets, 
JsonObject.class);
+        String                    tokenURL   = 
jsonObject.get("tokenUrl").getAsString(); // retrieve tokenURL and create a new 
HttpPost object with it:
+        List<Map<String, String>> headers    = 
gson.fromJson(jsonObject.getAsJsonArray("headers"), List.class);
+        List<Map<String, String>> params     = 
gson.fromJson(jsonObject.getAsJsonArray("params"), List.class);
+        List<NameValuePair>       nvPairs    = new ArrayList<>();
+        HttpPost                  httpPost   = new HttpPost(tokenURL);
+
+        // add headers to httpPost object:
+        for (Map<String, String> header : headers) {
+            for (Map.Entry<String, String> e : header.entrySet()) {
+                httpPost.setHeader(e.getKey(), e.getValue());
+            }
+        }
+
+        // add params to httpPost entity:
+        for (Map<String, String> param : params) {
+            for (Map.Entry<String, String> e : param.entrySet()) {
+                nvPairs.add(new BasicNameValuePair(e.getKey(), e.getValue()));
+            }
+        }
+
+        httpPost.setEntity(new UrlEncodedFormEntity(nvPairs, 
StandardCharsets.UTF_8));
+
+        String ret;
+
+        // execute httpPost:
+        try (CloseableHttpClient   httpClient = HttpClients.createDefault();
+             CloseableHttpResponse response   = httpClient.execute(httpPost)) {
+            if (response == null) {
+                throw new IOException("getBearerToken(" + configFile + ") 
failed: null response");
+            }
+
+            int statusCode = response.getStatusLine().getStatusCode();
+
+            if (statusCode != HttpStatus.SC_OK) {
+                throw new IOException("getBearerToken(" + configFile + ") 
failed: http status=" + response.getStatusLine());
+            }
+
+            HttpEntity          httpEntity   = response.getEntity();
+            String              stringResult = 
EntityUtils.toString(httpEntity);
+            Map<String, Object> resultMap    = gson.fromJson(stringResult, 
Map.class);
+            String              token        = 
resultMap.get("access_token").toString();
+
+            ret = "Bearer " + token;
+
+            // and ensure response body is fully consumed
+            EntityUtils.consume(httpEntity);
+        }
+
+        return ret;
+    }
+
+    private Map<String, Map<String, String>> toUserAttributes(Map<String, 
Map<String, List<String>>> userAttrValues){
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> toUserAttributes(userAttrValues={})", 
userAttrValues);
+        }
+
+        Map<String, Map<String, String>> ret = new HashMap<>();
+
+        for (Map.Entry<String, Map<String, List<String>>> userEntry : 
userAttrValues.entrySet()) {
+            String                    user       = userEntry.getKey();
+            Map<String, List<String>> attrValues = userEntry.getValue();
+            Map<String, String>       userAttrs  = new HashMap<>();
+
+            for (Map.Entry<String, List<String>> attrEntry : 
attrValues.entrySet()) {
+                String       attrName = attrEntry.getKey();
+                List<String> values   = attrEntry.getValue();
+
+                userAttrs.put(attrName, String.join(",", values));
+            }
+
+            ret.put(user, userAttrs);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== toUserAttributes(userAttrValues={}): ret={}", 
userAttrValues, ret);
+        }
+
+        return ret;
+    }
+
+    private String getSecretsFromFile(String configFile) throws IOException {
+        String ret = decodeSecrets(new 
String(Files.readAllBytes(Paths.get(configFile))));
+
+        verifyToken(ret);
+
+        return ret;
+    }
+
+    private String decodeSecrets(String encodedSecrets) {
+        return new String(Base64.getDecoder().decode(encodedSecrets));
+    }
+
+    private void verifyToken(String secrets) throws IOException {
+        String     errorMessage = "";
+        JsonObject jsonObject   = gson.fromJson(secrets, JsonObject.class);
+
+        // verify all necessary items are there
+        if (jsonObject.get("tokenUrl") == null) {
+            errorMessage += "tokenUrl must be specified in the config file; ";
+        }
+
+        if (jsonObject.get("headers") == null) {
+            errorMessage += "headers must be specified in the config file; ";
+        } else { // verify that Content-type, if included, is 
application/x-www-form-urlencoded
+            List<Map<String, String>> headers = 
gson.fromJson(jsonObject.getAsJsonArray("headers"), List.class);
+
+            for (Map<String, String> header : headers) {
+                if (header.containsKey("Content-Type") && 
!StringUtils.equalsIgnoreCase(header.get("Content-Type"), 
"application/x-www-form-urlencoded")) {
+                    errorMessage += "Content-Type, if specified, must be 
\"application/x-www-form-urlencoded\"; ";
+                }
+            }
+        }
+
+        if (jsonObject.get("params") == null) {
+            errorMessage += "params must be specified in the config file; ";
+        }
+
+        if (!errorMessage.equals("")) {
+            throw new IOException(errorMessage);
+        }
+    }
+}
+
+
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/LICENSE
 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/NOTICE
 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/NOTICE
new file mode 100644
index 000000000..b5c81eb4d
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/NOTICE
@@ -0,0 +1,18 @@
+Apache Ranger External User Store Retriever and Apache Ranger Role User Store 
Retriever
+
+Copyright 2022 Comcast Cable Communications Management, LLC
+
+Licensed 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.
+
+PDX-License-Identifier: Apache-2.0
+
+This product includes software developed at Comcast (http://www.comcast.com/).
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/README.md
 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/README.md
new file mode 100644
index 000000000..c874419eb
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/README.md
@@ -0,0 +1,137 @@
+
+````text
+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.
+````
+
+# Ranger External User Store Retrievers
+
+A library to retrieve userStore entries from sources external to the Ranger 
Admin User Store. The top level class is called 
RangerMultiSourceUserStoreRetriever.
+
+## Business Value
+
+A counterpart of RangerAdminUserStoreRetriever, instead of retrieving items 
from the internal Ranger userStore,
+RangerMultiSourceUserStoreRetriever retrieves userStore entries from other 
sources.  The userStore entries will persist
+while the plugin is up, and will be refreshed at configured intervals.
+
+This enables ABAC (Attribute-based Access Control) based on the user's 
attributes that are retrieved.
+For example, the API could return the set of business partners that the user 
has been granted access to.
+Thus, a row filter policy can be built using a condition like this:
+````text
+${{USER.partner}}.includes(partner)
+````
+where partner is the name of a column in a hive table.  This enables
+row filter policies in which a user might match multiple conditions, which is 
not possible with out-of-the-box Ranger.
+
+## Currently Supported Sources
+
+### Arbitrary API calls (source name: "api")
+
+This code enables additions to the UserStore to be retrieved simply by 
creating an API which
+returns data in userStore format, and including attrName, userStoreURL, and 
optionally
+configFile and dataFile as contextEnricher options in the host plugin's 
service definition.
+
+#### Configuration Items
+
+Configurations are specified in the host plugin's service definition, as 
enricherOptions in the contextEnricher
+definition.  Configurations specific to this source type:
+
+**attrName** is the attribute whose values are mapped to the user, i.e., the 
key to be used in the userStore:
+user -> **attrName** -> attrValues.
+In Ranger policies it appears in ${{USER.**attrName**}} syntax, eg 
${{USER.partner}}
+
+**userStoreURL** is the URL from which to retrieve the user-to-attribute 
mapping.
+
+**dataFile** is a local java properties file from which the user-to-attribute 
mapping can be retrieved. It is optional,
+and intended to be used primarily in development.
+
+**configFile** is the name of the file containing the Base64-encoded secrets 
needed as inputs to retrieve
+the Bearer Token needed for access to the userStoreURL. It is optional, for 
security reasons.  If configFile doesn't
+appear in the EnricherOptions, a default value is constructed as 
"/var/ranger/security/"+attrName+".conf".
+
+The config file is a required JSON file which must contain:
+- **tokenUrl** : the name of the url to call to retrieve the Bearer Token
+- **headers**: list of key-value pairs representing names and values of http 
headers for the call to the tokenUrl.
+  Note: Content-Type is assumed to be "application/x-www-form-urlencoded".  
Inclusion of a different content-type header in the config file will cause a 
400 error.
+- **params**: name-value pairs to be added as parameters to the URI's query 
portion
+
+Here are the contents of an **example configFile**:
+```json
+{
+  "tokenUrl": "https://security.mycompany.com/token.oauth2";,
+  "headers": [
+    { "Content-Type": "application/x-www-form-urlencoded" } ,
+    { "Accept":       "application/json" }
+  ],
+  "params": [
+    { "client_id":     "my_user_name" },
+    { "client_secret": "***************" },
+    { "grant_type":    "client_credentials" },
+    { "scope":         "my_project" }
+  ]
+}
+```
+
+### RangerRoles (source name: "role")
+
+In this case, attributes are retrieved internally from Ranger, based on the
+roles of which the user is a member.  No additional coding is needed.
+
+#### Configuration Items
+
+Configurations are specified in the host plugin's service definition, as 
enricherOptions in the contextEnricher
+definition.  Instead of external storage configurations (eg URL, datafile), 
configurations
+specify how to retrieve the roles of interest:
+
+**attrName** is the attribute whose values are mapped to the user, i.e., the 
key to be used in the userStore:
+user -> **attrName** -> attrValues.  It is also the string used to identify 
the role of interest.  By convention,
+role names are assumed to have this structure:  _attrName.attrValue_,  e.g., 
salesRegion.northeast.
+
+## Service Definition Configurations
+In order to ensure that all new userStore entries are retained, there must be 
a single userStoreRetrieverClass
+and a single userStore for all retrievers.
+
+**Options at the Context Enricher Level:**
+
+**userStoreRetrieverClassName** is the name of the context enricher that calls 
all subsequent retriever methods.
+**userStoreRefresherPollingInterval** defines the interval at which the 
userStoreRefresher polls its source, seeking data changes since it was last 
refreshed.
+
+Within the options for this enricher, configurations for the individual 
retrievers are given in a special format.
+The option key is "retrieverX_*sourceType*", where X is a sequential integer 
and sourceType is (currently)
+either "api" or "role".  The option value is a string containing 
configurations for the individual retrievers, as outlined above,
+specified in a comma-separated Java Property-like format.
+
+Here is the relevant section of a **sample host plugin's service definition**. 
 Two api retrievers and two role retrievers
+are specified.
+
+```json
+{
+  "contextEnrichers": [
+    {
+      "itemId":   1,
+      "name":     "RangerMultiSourceUserStoreRetriever",
+      "enricher": 
"org.apache.ranger.plugin.contextenricher.RangerUserStoreEnricher",
+      "enricherOptions": {
+        "userStoreRetrieverClassName":       
"org.apache.ranger.plugin.contextenricher.externalretrievers.RangerMultiSourceUserStoreRetriever",
+        "userStoreRefresherPollingInterval": "60000",
+        "retriever0_api":                    
"attrName=partner,userStoreURL=http://localhost:8000/security/getPartnersByUser";,
+        "retriever1_api":                    
"attrName=ownedResources,dataFile=/var/ranger/data/userOwnerResource.txt",
+        "retriever2_role":                   "attrName=salesRegion",
+        "retriever3_role":                   "attrName=sensitivityLevel"
+      }
+    }
+  ]
+}
+```
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/RangerMultiSourceUserStoreRetriever.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/RangerMultiSourceUserStoreRetriever.java
new file mode 100644
index 000000000..7e2462814
--- /dev/null
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/contextenricher/externalretrievers/RangerMultiSourceUserStoreRetriever.java
@@ -0,0 +1,365 @@
+/*
+ * 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.externalretrievers;
+
+import org.apache.ranger.admin.client.RangerAdminClient;
+import org.apache.ranger.plugin.contextenricher.RangerUserStoreRetriever;
+import org.apache.ranger.plugin.util.RangerRoles;
+import org.apache.ranger.plugin.util.RangerRolesUtil;
+import org.apache.ranger.plugin.util.RangerUserStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+// Options examples for individual retrievers:
+//   "retriever0_api":  
"attrName=partner,userStoreURL=http://localhost:8000/security/getPartnersByUser";,
+//   "retriever1_role": "attrName=employee"
+
+public class RangerMultiSourceUserStoreRetriever extends 
RangerUserStoreRetriever {
+    private static final Logger LOG = 
LoggerFactory.getLogger(RangerMultiSourceUserStoreRetriever.class);
+
+    private static final Pattern PATTERN_ROLE_RETRIEVER_NAME = 
Pattern.compile("\\d+_role");
+
+    private Map<String, Map<String,String>> retrieverOptions = 
Collections.emptyMap();
+    private RangerAdminClient               adminClient      = null;
+    private RangerUserStore                 userStore        = null;
+    private RangerRolesUtil                 rolesUtil        = new 
RangerRolesUtil(new RangerRoles());
+
+    // options come from service-def
+    @Override
+    public void init(Map<String, String> options) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> init(options={})", options);
+        }
+
+        try {
+            retrieverOptions = toRetrieverOptions(options);
+
+            if (hasAnyRoleRetriever()) {
+                adminClient = pluginContext.createAdminClient(pluginConfig);
+            }
+        } catch (Exception e) {
+            LOG.error("init() failed", e);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== init(options={})", options);
+        }
+    }
+
+    @Override
+    public RangerUserStore retrieveUserStoreInfo(long lastKnownVersion, long 
lastActivationTimeInMillis) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("=> retrieveUserStoreInfo(lastKnownVersion={}, 
lastActivationTimeInMillis={})", lastKnownVersion, lastActivationTimeInMillis);
+        }
+
+        // if there are any role type retrievers, get rangerRoles; otherwise 
don't bother
+        if (adminClient != null) {
+            try {
+                RangerRoles roles        = rolesUtil.getRoles();
+                long        rolesVersion = roles.getRoleVersion() != null ? 
roles.getRoleVersion() : -1;
+                RangerRoles updatedRoles = 
adminClient.getRolesIfUpdated(rolesVersion, lastActivationTimeInMillis);
+
+                if (updatedRoles != null) {
+                    rolesUtil = new RangerRolesUtil(updatedRoles);
+                }
+            } catch (Exception e) {
+                LOG.error("retrieveUserStoreInfo(lastKnownVersion={}) failed 
to retrieve roles", lastKnownVersion, e);
+            }
+        }
+
+        Map<String, Map<String, String>> userAttrs = null;
+
+        try {
+            userAttrs = retrieveAll();
+        } catch (Exception e) {
+            LOG.error("retrieveUserStoreInfo(lastKnownVersion={}) failed", 
lastKnownVersion, e);
+        }
+
+        if (userAttrs != null) {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("retrieveUserStoreInfo(lastKnownVersion={}): 
user-attributes={}", lastKnownVersion, userAttrs);
+            }
+
+            userStore = new RangerUserStore();
+
+            userStore.setUserStoreVersion(System.currentTimeMillis());
+            userStore.setUserAttrMapping(userAttrs);
+        } else {
+            LOG.error("retrieveUserStoreInfo(lastKnownVersion={}): failed to 
retrieve user-attributes", lastKnownVersion);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== retrieveUserStoreInfo(lastKnownVersion={}, 
lastActivationTimeInMillis={}): ret={}", lastKnownVersion, 
lastActivationTimeInMillis, userStore);
+        }
+
+        return userStore;
+    }
+
+    private Map<String, Map<String,String>> toRetrieverOptions(Map<String, 
String> enricherOptions) throws Exception {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> toRetrieverOptions({})", enricherOptions);
+        }
+
+        Map<String, Map<String, String>> ret = new HashMap<>();
+
+        for (Map.Entry<String, String> entry : enricherOptions.entrySet()) {
+            String retrieverName = entry.getKey();
+
+            if (retrieverName.startsWith("retriever")) {
+                String retrieverOptions = entry.getValue();
+
+                ret.put(retrieverName, toRetrieverOptions(retrieverName, 
retrieverOptions));
+            }
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== toRetrieverOptions({}): ret={}", enricherOptions, 
ret);
+        }
+
+        return ret;
+    }
+
+    // Managing options for various retrievals
+    private Map<String, String> toRetrieverOptions(String name, String 
options) throws Exception {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> toRetrieverOptions(name={}, options={})", name, 
options);
+        }
+
+        Properties prop = new Properties();
+
+        options = options.replaceAll("\\s", "");
+        options = options.replaceAll(",", "\n");
+
+        try {
+            prop.load(new StringReader(options));
+        } catch (Exception e) {
+            LOG.error("toRetrieverOptions(name={}, options={}): failed to 
parse retriever options", name, options, e);
+
+            throw new Exception(name + ": failed to parse retriever options: " 
+ options, e);
+        }
+
+        Map<String, String> ret = new HashMap<>();
+
+        for (String key : prop.stringPropertyNames()) {
+            ret.put(key, prop.getProperty(key));
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== toRetrieverOptions(name={}, options={}): ret={}", 
name, options, ret);
+        }
+
+        return ret;
+    }
+
+    private boolean hasAnyRoleRetriever() {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> hasAnyRoleRetriever()");
+        }
+
+        boolean ret = false;
+
+        for (String retrieverName : retrieverOptions.keySet()) {
+            Matcher matcher = 
PATTERN_ROLE_RETRIEVER_NAME.matcher(retrieverName);
+
+            if (matcher.find()) {
+                ret = true;
+
+                break;
+            }
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== hasAnyRoleRetriever(): ret={}", ret);
+        }
+
+        return ret;
+    }
+
+    // top-level retrieval management
+    private Map<String, Map<String, String>> retrieveAll() throws Exception {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> retrieveAll()");
+        }
+
+        Map<String, Map<String, String>> ret = new HashMap<>();
+
+        for (Map.Entry<String, Map<String, String>> entry : 
retrieverOptions.entrySet()) {
+            String                           name    = entry.getKey();
+            Map<String, String>              options = entry.getValue();
+            String                           source  = 
name.replaceAll("\\w+_","");
+            Map<String, Map<String, String>> userAttrs;
+
+            switch (source) {
+                case "api":
+                    userAttrs = retrieveUserAttributes(name, options);
+                break;
+
+                case "role":
+                    userAttrs = retrieveUserAttrFromRoles(name, options);
+                break;
+
+                default:
+                    throw new Exception("unrecognized retriever source '" + 
source + "'. Valid values: api, role");
+            }
+
+            mergeUserAttributes(userAttrs, ret);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== retrieveAll(): ret={}", ret);
+        }
+
+        return ret;
+    }
+
+    // external retrieval
+    private Map<String, Map<String, String>> retrieveUserAttributes(String 
retrieverName, Map<String, String> options) throws Exception {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> retrieveUserAttributes(name={}, options={})", 
retrieverName, options);
+        }
+
+        String attrName = options.get("attrName");
+        String url      = options.get("userStoreURL");
+        String dataFile = options.get("dataFile");
+
+        if (attrName == null) {
+            throw new Exception(retrieverName + ": attrName must be specified 
in retriever options");
+        }
+
+        if (url == null && dataFile == null) {
+            throw new Exception(retrieverName + ": url or dataFile must be 
specified in retriever options");
+        }
+
+        Map<String, Map<String, String>> ret;
+
+        if (url != null) {
+            GetFromURL gu = new GetFromURL();
+
+            String configFile = options.getOrDefault("configFile", 
"/var/ranger/security/" + attrName + ".conf");
+
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("{}: configFile={}", retrieverName, configFile);
+            }
+
+            ret = gu.getFromURL(url, configFile);  // get user-Attrs mapping 
in UserStore format from an API call
+
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("loaded attribute {} from URL {}: {}", attrName, 
url, ret);
+            }
+        } else {
+            GetFromDataFile gf = new GetFromDataFile();
+
+            ret = gf.getFromDataFile(dataFile, attrName);
+
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("loaded attribute {} from file {}: {}", attrName, 
dataFile, ret);
+            }
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== retrieveUserAttributes(name={}, options={}): 
ret={}", retrieverName, options, ret);
+        }
+
+        return ret;
+    }
+
+    // role-based retrieval
+    /** retrieveSingleRoleUserAttrMapping:
+     *
+     * @param options includes the attribute name of interest, from which to 
create the UserStore attribute name
+     *      and to identify the role of interest.
+     * @return  In UserStore format, maps from user to attrName to attribute 
values
+     *
+     * rangerRoles: one object for each role; contains set of users who are 
members.  The important feature here
+     *      *    is that it maps roles to users.  
rolesUtil.getUserRoleMapping() returns the reverse:
+     *      *    maps users to roles that they are members of. This is closer 
to the UserStore format.
+     */
+    public Map<String, Map<String, String>> retrieveUserAttrFromRoles(String 
retrieverName, Map<String, String> options) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("==> retrieveUserAttrFromRoles(name={}, options={})", 
retrieverName, options);
+        }
+
+        Map<String, Map<String, String>> ret         = new HashMap<>();
+        Map<String, Set<String>>         userToRoles = 
rolesUtil.getUserRoleMapping();
+        String                           attrName    = options.get("attrName");
+        String                           rolePrefix  = attrName + ".";
+        Pattern                          pattern     = Pattern.compile("^.*" + 
rolePrefix + ".*$");
+
+        for (Map.Entry<String, Set<String>> entry : userToRoles.entrySet()) {
+            String       user       = entry.getKey();
+            Set<String>  roles      = entry.getValue();
+            List<String> attrValues = new ArrayList<>();
+
+            for (String role : roles) {
+                Matcher matcher = pattern.matcher(role);
+
+                if (matcher.find()) {
+                    String value = matcher.group().replace(rolePrefix, "");
+
+                    attrValues.add(value);
+                }
+            }
+
+            if (!attrValues.isEmpty()) {
+                Map<String, String> userAttrs = new HashMap<>();
+
+                userAttrs.put(attrName, String.join(",", attrValues));
+
+                ret.put(user, userAttrs);
+            }
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("<== retrieveUserAttrFromRoles(name={}, options={}): 
ret={}", retrieverName, options, ret);
+        }
+
+        return ret;
+    }
+
+    private void mergeUserAttributes(Map<String, Map<String, String>> source, 
Map<String, Map<String, String>> dest) {
+        if (dest.size() == 0) {
+            dest.putAll(source);
+        } else {
+            for (Map.Entry<String, Map<String, String>> e : source.entrySet()) 
{
+                String              userName  = e.getKey();
+                Map<String, String> userAttrs = e.getValue();
+
+                if (dest.containsKey(userName)) {
+                    Map<String, String> existingAttrs = dest.get(userName);
+
+                    existingAttrs.putAll(userAttrs);
+                } else {
+                    dest.put(userName, userAttrs);
+                }
+            }
+        }
+    }
+}
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesProvider.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesProvider.java
index 6efd13f80..bea82f5b8 100644
--- 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesProvider.java
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesProvider.java
@@ -139,7 +139,7 @@ public class RangerRolesProvider {
                                plugIn.setRoles(roles);
                                rangerUserGroupRolesSetInPlugin = true;
                                
setLastActivationTimeInMillis(System.currentTimeMillis());
-                               lastKnownRoleVersion = roles.getRoleVersion();
+                               lastKnownRoleVersion = roles.getRoleVersion() 
!= null ? roles.getRoleVersion() : -1;;
                        } else {
                                if (!rangerUserGroupRolesSetInPlugin && 
!serviceDefSetInPlugin) {
                                        plugIn.setRoles(null);
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesUtil.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesUtil.java
index f785e1186..40b2652a9 100644
--- 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesUtil.java
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRolesUtil.java
@@ -44,7 +44,7 @@ public class RangerRolesUtil {
     public RangerRolesUtil(RangerRoles roles) {
         if (roles != null) {
             this.roles  = roles;
-            roleVersion = roles.getRoleVersion();
+            roleVersion = roles.getRoleVersion() != null ? 
roles.getRoleVersion() : -1;
 
             if (CollectionUtils.isNotEmpty(roles.getRangerRoles())) {
                 for (RangerRole role : roles.getRangerRoles()) {
diff --git a/dev-support/spotbugsIncludeFile.xml 
b/dev-support/spotbugsIncludeFile.xml
index 3621e8c08..9a0a9261a 100644
--- a/dev-support/spotbugsIncludeFile.xml
+++ b/dev-support/spotbugsIncludeFile.xml
@@ -45,6 +45,7 @@
         <Bug pattern="NM_SAME_SIMPLE_NAME_AS_SUPERCLASS" />
         <Bug pattern="IL_INFINITE_RECURSIVE_LOOP" />
         <Bug pattern="DMI_RANDOM_USED_ONLY_ONCE" />
+        <Bug pattern="UI_INHERITANCE_UNSAFE_GETRESOURCE" />
       </Or>
     </Not>
   </Match>

Reply via email to