You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ma...@apache.org on 2023/10/13 23:11:25 UTC

[ranger] branch master updated (db665595f -> b2b5c5bc7)

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

madhan pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git


    from db665595f RANGER-4451: when the last service is removed from zone, the zone should not be deleted
     new 7d46e05ff RANGER-4465: Python client for managing users, groups, user-group associations
     new b2b5c5bc7 RANGER-4472: updated getResourceACLs() handling of tags associated with the resource and its descendent

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../model/validation/RangerServiceDefHelper.java   |  54 +++++-
 .../ranger/plugin/policyengine/PolicyEngine.java   |  19 +-
 .../policyengine/RangerPolicyEngineImpl.java       |  48 ++++-
 .../RangerDefaultPolicyEvaluator.java              |   5 +
 .../policyevaluator/RangerPolicyEvaluator.java     |   2 +
 .../ranger/plugin/policyengine/TestPolicyACLs.java |   7 +
 .../policyengine/resource_hierarchy_tags.json      |  85 +++++++++
 .../test_aclprovider_resource_hierarchy_tags.json  | 181 +++++++++++++++++++
 intg/src/main/python/README.md                     | 140 ++++++++++++++-
 .../client/ranger_user_mgmt_client.py              | 194 +++++++++++++++++++++
 .../python/apache_ranger/model/ranger_user_mgmt.py | 119 +++++++++++++
 intg/src/main/python/setup.py                      |   2 +-
 .../sample-client/src/main/python/user_mgmt.py     | 137 +++++++++++++++
 13 files changed, 983 insertions(+), 10 deletions(-)
 create mode 100644 agents-common/src/test/resources/policyengine/resource_hierarchy_tags.json
 create mode 100644 agents-common/src/test/resources/policyengine/test_aclprovider_resource_hierarchy_tags.json
 create mode 100644 intg/src/main/python/apache_ranger/client/ranger_user_mgmt_client.py
 create mode 100644 intg/src/main/python/apache_ranger/model/ranger_user_mgmt.py
 create mode 100644 ranger-examples/sample-client/src/main/python/user_mgmt.py


[ranger] 02/02: RANGER-4472: updated getResourceACLs() handling of tags associated with the resource and its descendent

Posted by ma...@apache.org.
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

commit b2b5c5bc7f587e70f82b956526a5e6b71f3f6a94
Author: Madhan Neethiraj <ma...@apache.org>
AuthorDate: Thu Oct 12 08:58:51 2023 -0700

    RANGER-4472: updated getResourceACLs() handling of tags associated with the resource and its descendent
---
 .../model/validation/RangerServiceDefHelper.java   |  54 +++++-
 .../ranger/plugin/policyengine/PolicyEngine.java   |  19 ++-
 .../policyengine/RangerPolicyEngineImpl.java       |  48 +++++-
 .../RangerDefaultPolicyEvaluator.java              |   5 +
 .../policyevaluator/RangerPolicyEvaluator.java     |   2 +
 .../ranger/plugin/policyengine/TestPolicyACLs.java |   7 +
 .../policyengine/resource_hierarchy_tags.json      |  85 ++++++++++
 .../test_aclprovider_resource_hierarchy_tags.json  | 181 +++++++++++++++++++++
 8 files changed, 394 insertions(+), 7 deletions(-)

diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/model/validation/RangerServiceDefHelper.java b/agents-common/src/main/java/org/apache/ranger/plugin/model/validation/RangerServiceDefHelper.java
index c1388abc2..6cc93ad6d 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/model/validation/RangerServiceDefHelper.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/model/validation/RangerServiceDefHelper.java
@@ -188,6 +188,10 @@ public class RangerServiceDefHelper {
 		return _delegate.getResourceHierarchies(policyType);
 	}
 
+	public Set<Set<String>> getResourceHierarchyKeys(Integer policyType) {
+		return _delegate.getResourceHierarchyKeys(policyType);
+	}
+
 	public Set<List<RangerResourceDef>> filterHierarchies_containsOnlyMandatoryResources(Integer policyType) {
 		Set<List<RangerResourceDef>> hierarchies = getResourceHierarchies(policyType);
 		Set<List<RangerResourceDef>> result = new HashSet<List<RangerResourceDef>>(hierarchies.size());
@@ -200,6 +204,32 @@ public class RangerServiceDefHelper {
 		return result;
 	}
 
+	public boolean isValidHierarchy(Integer policyType, Collection<String> keys, boolean requireExactMatch) {
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("==> isValidHierarchy(policyType=" + policyType + ", keys=" + StringUtils.join(keys, ", ") + ", requireExactMatch=" + requireExactMatch + ")");
+		}
+
+		boolean ret = false;
+
+		for (Set<String> hierarchyKeys : getResourceHierarchyKeys(policyType)) {
+			if (requireExactMatch) {
+				ret = hierarchyKeys.equals(keys);
+			} else {
+				ret = hierarchyKeys.containsAll(keys);
+			}
+
+			if (ret) {
+				break;
+			}
+		}
+
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("<== isValidHierarchy(policyType=" + policyType + ", keys=" + StringUtils.join(keys, ", ") + ", requireExactMatch=" + requireExactMatch + "): ret=" + ret);
+		}
+
+		return ret;
+	}
+
 	public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType, Collection<String> keys) {
 		if (LOG.isDebugEnabled()) {
 			LOG.debug("==> getResourceHierarchies(policyType=" + policyType + ", keys=" + StringUtils.join(keys, ",") + ")");
@@ -265,7 +295,7 @@ public class RangerServiceDefHelper {
 	 * @param hierarchy
 	 * @return
 	 */
-	public Set<String> getAllResourceNames(List<RangerResourceDef> hierarchy) {
+	public static Set<String> getAllResourceNames(List<RangerResourceDef> hierarchy) {
 		Set<String> result = new HashSet<String>(hierarchy.size());
 		for (RangerResourceDef resourceDef : hierarchy) {
 			result.add(resourceDef.getName());
@@ -322,6 +352,7 @@ public class RangerServiceDefHelper {
 	static class Delegate {
 		final RangerServiceDef _serviceDef;
 		final Map<Integer, Set<List<RangerResourceDef>>> _hierarchies = new HashMap<>();
+		final Map<Integer, Set<Set<String>>>             _hierarchyKeys = new HashMap<>();
 		final Map<Integer, Map<String, RangerResourceDef>> _wildcardEnabledResourceDefs = new HashMap<>();
 		final Date _serviceDefFreshnessDate;
 		final String _serviceName;
@@ -347,14 +378,23 @@ public class RangerServiceDefHelper {
 				if(graph != null) {
 					Map<String, RangerResourceDef> resourceDefMap = getResourcesAsMap(resources);
 					if (isValid(graph, resourceDefMap)) {
-						Set<List<RangerResourceDef>> hierarchies = getHierarchies(graph, resourceDefMap);
+						Set<List<RangerResourceDef>> hierarchies  = getHierarchies(graph, resourceDefMap);
+						Set<Set<String>>             hierachyKeys = new HashSet<>(hierarchies.size());
+
+						for (List<RangerResourceDef> hierarchy : hierarchies) {
+							hierachyKeys.add(Collections.unmodifiableSet(getAllResourceNames(hierarchy)));
+						}
+
 						_hierarchies.put(policyType, Collections.unmodifiableSet(hierarchies));
+						_hierarchyKeys.put(policyType, Collections.unmodifiableSet(hierachyKeys));
 					} else {
 						isValid = false;
 						_hierarchies.put(policyType, EMPTY_RESOURCE_HIERARCHY);
+						_hierarchyKeys.put(policyType, Collections.emptySet());
 					}
 				} else {
 					_hierarchies.put(policyType, EMPTY_RESOURCE_HIERARCHY);
+					_hierarchyKeys.put(policyType, Collections.emptySet());
 				}
 			}
 
@@ -402,6 +442,16 @@ public class RangerServiceDefHelper {
 			return ret;
 		}
 
+		public Set<Set<String>> getResourceHierarchyKeys(Integer policyType) {
+			if (policyType == null || policyType == RangerPolicy.POLICY_TYPE_AUDIT) {
+				policyType = RangerPolicy.POLICY_TYPE_ACCESS;
+			}
+
+			Set<Set<String>> ret = _hierarchyKeys.get(policyType);
+
+			return ret != null ? ret : Collections.emptySet();
+		}
+
 		public String getServiceName() {
 			return _serviceName;
 		}
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/PolicyEngine.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/PolicyEngine.java
index 04f010a03..3373dbae9 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/PolicyEngine.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/PolicyEngine.java
@@ -48,6 +48,7 @@ import org.apache.ranger.plugin.util.RangerPolicyDeltaUtil;
 import org.apache.ranger.plugin.util.RangerResourceEvaluatorsRetriever;
 import org.apache.ranger.plugin.util.RangerReadWriteLock;
 import org.apache.ranger.plugin.util.RangerRoles;
+import org.apache.ranger.plugin.util.ServiceDefUtil;
 import org.apache.ranger.plugin.util.ServicePolicies;
 import org.apache.ranger.plugin.util.StringTokenReplacer;
 import org.slf4j.Logger;
@@ -59,6 +60,7 @@ public class PolicyEngine {
     private static final Logger PERF_POLICYENGINE_INIT_LOG       = RangerPerfTracer.getPerfLogger("policyengine.init");
     private static final Logger PERF_POLICYENGINE_REBALANCE_LOG  = RangerPerfTracer.getPerfLogger("policyengine.rebalance");
 
+    private final RangerServiceDefHelper              serviceDefHelper;
     private final RangerPolicyRepository              policyRepository;
     private final RangerPolicyRepository              tagPolicyRepository;
     private final List<RangerContextEnricher>         allContextEnrichers;
@@ -119,6 +121,8 @@ public class PolicyEngine {
         return policyRepository.getPolicyVersion();
     }
 
+    public RangerServiceDefHelper getServiceDefHelper() { return serviceDefHelper; }
+
     public RangerPolicyRepository getPolicyRepository() {
         return policyRepository;
     }
@@ -229,6 +233,7 @@ public class PolicyEngine {
         }
 
         policyRepository = new RangerPolicyRepository(servicePolicies, this.pluginContext);
+        serviceDefHelper = new RangerServiceDefHelper(policyRepository.getServiceDef(), false);
 
         ServicePolicies.TagPolicies tagPolicies = servicePolicies.getTagPolicies();
 
@@ -482,9 +487,16 @@ public class PolicyEngine {
     }
 
     synchronized static private void buildImpliedAccessGrants(ServicePolicies servicePolicies) {
-        buildImpliedAccessGrants(servicePolicies.getServiceDef());
-        if (servicePolicies.getTagPolicies() != null) {
-            buildImpliedAccessGrants(servicePolicies.getTagPolicies().getServiceDef());
+        RangerServiceDef serviceDef = servicePolicies.getServiceDef();
+
+        if (serviceDef != null) {
+            buildImpliedAccessGrants(ServiceDefUtil.normalize(serviceDef));
+
+            RangerServiceDef tagServiceDef = servicePolicies.getTagPolicies() != null ? servicePolicies.getTagPolicies().getServiceDef() : null;
+
+            if (tagServiceDef != null) {
+                buildImpliedAccessGrants(ServiceDefUtil.normalizeAccessTypeDefs(ServiceDefUtil.normalize(tagServiceDef), serviceDef.getName()));
+            }
         }
     }
 
@@ -567,6 +579,7 @@ public class PolicyEngine {
     private PolicyEngine(final PolicyEngine other, ServicePolicies servicePolicies) {
         this.useForwardedIPAddress = other.useForwardedIPAddress;
         this.trustedProxyAddresses = other.trustedProxyAddresses;
+        this.serviceDefHelper      = other.serviceDefHelper;
         this.pluginContext         = other.pluginContext;
         this.lock                  = other.lock;
 
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerPolicyEngineImpl.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerPolicyEngineImpl.java
index 8ba0b1a7f..20400fdfa 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerPolicyEngineImpl.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerPolicyEngineImpl.java
@@ -282,6 +282,17 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 
 
 			for (int policyType : policyTypes) {
+				// if resource isn't applicable for the policyType, skip evaluating policies and gathering ACLs
+				// for example, following resources are not applicable for listed policy-types
+				//   - database: masking/row-filter policies
+				//   - table:    masking policies
+				//   - column:   row-filter policies
+				boolean requireExactMatch = (policyType == RangerPolicy.POLICY_TYPE_DATAMASK) || (policyType == RangerPolicy.POLICY_TYPE_ROWFILTER);
+
+				if (!policyEngine.getServiceDefHelper().isValidHierarchy(policyType, request.getResource().getKeys(), requireExactMatch)) {
+					continue;
+				}
+
 				List<RangerPolicyEvaluator> allEvaluators           = new ArrayList<>();
 				Map<Long, MatchType>        tagMatchTypeMap         = new HashMap<>();
 				Set<Long>                   policyIdForTemporalTags = new HashSet<>();
@@ -312,7 +323,7 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 					MatchType matchType = tagMatchTypeMap.get(evaluator.getPolicyId());
 
 					boolean isMatched = false;
-					boolean isConditionalMatch = false;
+					boolean isConditionalMatch = evaluator.getPolicyConditionsCount() > 0;
 
 					if (matchType == null) {
 						for (RangerPolicyResourceEvaluator resourceEvaluator : evaluator.getResourceEvaluators()) {
@@ -1007,7 +1018,8 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 					RangerTagForEval tag = tagEvaluator.getTag();
 
 					allEvaluators.add(evaluator);
-					tagMatchTypeMap.put(evaluator.getPolicyId(), tag.getMatchType());
+
+					updateMatchTypeForTagEvaluator(tagEvaluator, tagMatchTypeMap);
 
 					if (CollectionUtils.isNotEmpty(tag.getValidityPeriods())) {
 						policyIdForTemporalTags.add(evaluator.getPolicyId());
@@ -1021,6 +1033,38 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 		}
 	}
 
+	// Multiple tags can be mapped to a tag-based policy. In such cases, use the match-type of the tag having the highest precedence
+	// Consider following tags:
+	//  table  table1      has tag SENSITIVE(level=normal)
+	//  column table1.col1 has tag SENSITIVE(level=high)
+	//
+	// Following 2 tags will be matched for table1:
+	//  SENSITIVE(level=normal) with MatchType.SELF
+	//  SENSITIVE(level=high)   with MatchType.DESCENDANT
+	//
+	// Following 2 tags will be matched for table1.col1:
+	//  SENSITIVE(level=high)   with MatchType.SELF
+	//  SENSITIVE(level=normal) with MatchType.SELF_AND_ALL_DESCENDANTS
+	//
+	// In these cases, matchType SELF should be used for policy evaluation
+	//
+	private void updateMatchTypeForTagEvaluator(PolicyEvaluatorForTag tag, Map<Long, MatchType> tagMatchTypeMap) {
+		Long      evaluatorId = tag.getEvaluator().getPolicyId();
+		MatchType existing    = tagMatchTypeMap.get(evaluatorId);
+
+		if (existing != MatchType.SELF) {
+			MatchType matchType = tag.getTag().getMatchType();
+
+			if (existing == null || existing == MatchType.NONE || matchType == MatchType.SELF || matchType == MatchType.SELF_AND_ALL_DESCENDANTS) {
+				tagMatchTypeMap.put(evaluatorId, matchType);
+			} else if (matchType == MatchType.ANCESTOR) {
+				if (existing == MatchType.DESCENDANT) {
+					tagMatchTypeMap.put(evaluatorId, MatchType.SELF_AND_ALL_DESCENDANTS);
+				}
+			}
+		}
+	}
+
 	private void getResourceAccessInfoForZone(RangerAccessRequest request, RangerResourceAccessInfo ret, String zoneName) {
 		final RangerPolicyRepository matchedRepository = policyEngine.getRepositoryForZone(zoneName);
 
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerDefaultPolicyEvaluator.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerDefaultPolicyEvaluator.java
index bf7ebe86a..8e908f6a9 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerDefaultPolicyEvaluator.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerDefaultPolicyEvaluator.java
@@ -90,6 +90,11 @@ public class RangerDefaultPolicyEvaluator extends RangerAbstractPolicyEvaluator
 
 	boolean isUseAclSummaryForEvaluation() { return useAclSummaryForEvaluation; }
 
+	@Override
+	public int getPolicyConditionsCount() {
+		return conditionEvaluators.size();
+	}
+
 	@Override
 	public int getCustomConditionsCount() {
 		return customConditionsCount;
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerPolicyEvaluator.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerPolicyEvaluator.java
index dcaae9ff1..0d4886c57 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerPolicyEvaluator.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerPolicyEvaluator.java
@@ -92,6 +92,8 @@ public interface RangerPolicyEvaluator {
 
 	int getEvalOrder();
 
+	int getPolicyConditionsCount();
+
 	int getCustomConditionsCount();
 
 	int getValidityScheduleEvaluatorsCount();
diff --git a/agents-common/src/test/java/org/apache/ranger/plugin/policyengine/TestPolicyACLs.java b/agents-common/src/test/java/org/apache/ranger/plugin/policyengine/TestPolicyACLs.java
index 196755c6e..9a69efcba 100644
--- a/agents-common/src/test/java/org/apache/ranger/plugin/policyengine/TestPolicyACLs.java
+++ b/agents-common/src/test/java/org/apache/ranger/plugin/policyengine/TestPolicyACLs.java
@@ -94,6 +94,13 @@ public class TestPolicyACLs {
 		runTestsFromResourceFiles(tests);
 	}
 
+	@Test
+	public void testResourceACLs_resource_hierarchy_tags() throws Exception {
+		String[] tests = {"/policyengine/test_aclprovider_resource_hierarchy_tags.json"};
+
+		runTestsFromResourceFiles(tests);
+	}
+
 	private void runTestsFromResourceFiles(String[] resourceNames) throws Exception {
 		for(String resourceName : resourceNames) {
 			InputStream       inStream = this.getClass().getResourceAsStream(resourceName);
diff --git a/agents-common/src/test/resources/policyengine/resource_hierarchy_tags.json b/agents-common/src/test/resources/policyengine/resource_hierarchy_tags.json
new file mode 100644
index 000000000..33c7204af
--- /dev/null
+++ b/agents-common/src/test/resources/policyengine/resource_hierarchy_tags.json
@@ -0,0 +1,85 @@
+{
+    "op":          "add_or_update",
+    "tagModel":    "resource_private",
+    "serviceName": "cl1_hive",
+    "tagDefinitions": {
+      "1": {
+        "id":            1,
+        "guid":          "tag-def-1",
+        "name":          "SENSITIVE",
+        "attributeDefs": [ { "name": "level", "type": "string" } ]
+      }
+    },
+    "tags": {
+      "1": {
+        "id":         1,
+        "guid":       "tag-1",
+        "type":       "SENSITIVE",
+        "attributes": { "level": "normal" }
+      },
+      "2": {
+        "id":         2,
+        "guid":       "tag-2",
+        "type":       "SENSITIVE",
+        "attributes": { "level": "high" }
+      },
+      "3": {
+        "id":         3,
+        "guid":       "tag-3",
+        "type":       "SENSITIVE",
+        "attributes": { "level": "top" }
+      },
+      "4": {
+        "id":         4,
+        "guid":       "tag-4",
+        "type":       "SENSITIVE",
+        "attributes": { "level": "top" }
+      },
+      "5": {
+        "id":         5,
+        "guid":       "tag-5",
+        "type":       "SENSITIVE",
+        "attributes": { "level": "top" }
+      }
+    },
+    "serviceResources": [
+      {
+        "id":               1,
+        "guid":             "resource-1",
+        "serviceName":      "cl1_hive",
+        "resourceElements": { "database": { "values": [ "db1" ] },  "table": { "values": [ "tbl1" ] } }
+      },
+      {
+        "id":               2,
+        "guid":             "resource-2",
+        "serviceName":      "cl1_hive",
+        "resourceElements": { "database": { "values": [ "db1" ] },  "table": { "values": [ "tbl1" ] },  "column": { "values": [ "SSN" ] } }
+      },
+      {
+        "id":               3,
+        "guid":             "resource-3",
+        "serviceName":      "cl1_hive",
+        "resourceElements": { "database": { "values": [ "db1" ] },  "table": { "values": [ "tbl1" ] },  "column": { "values": [ "Age" ] } }
+      },
+      {
+        "id":               4,
+        "guid":             "resource-4",
+        "serviceName":      "cl1_hive",
+        "resourceElements": { "database": { "values": [ "db1" ] },  "table": { "values": [ "tbl1" ] },  "column": { "values": [ "Name" ] } }
+      },
+      {
+        "id":               5,
+        "guid":             "resource-5",
+        "serviceName":      "cl1_hive",
+        "resourceElements": { "database": { "values": [ "db2" ] }, "table": { "values": [ "*" ] }, "column": { "values": [ "*" ] } }
+      }
+    ],
+    "resourceToTagIds": {
+      "1": [ 1 ],
+      "2": [ 2 ],
+      "3": [ 3 ],
+      "4": [ 4 ],
+      "5": [ 5 ]
+    }
+}
+
diff --git a/agents-common/src/test/resources/policyengine/test_aclprovider_resource_hierarchy_tags.json b/agents-common/src/test/resources/policyengine/test_aclprovider_resource_hierarchy_tags.json
new file mode 100644
index 000000000..27152ba42
--- /dev/null
+++ b/agents-common/src/test/resources/policyengine/test_aclprovider_resource_hierarchy_tags.json
@@ -0,0 +1,181 @@
+{
+  "testCases": [
+    {
+      "name": "Test multiple tag instances for resource hierarchy",
+
+      "servicePolicies": {
+        "serviceName": "hivedev",
+        "serviceDef": {
+          "name": "hive", "id": 3,
+          "resources": [
+            { "name": "database", "level": 1,                       "mandatory": true, "lookupSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Hive Database", "description": "Hive Database" },
+            { "name": "table",    "level": 2, "parent": "database", "mandatory": true, "lookupSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Hive Table", "description": "Hive Table" },
+            { "name": "column",   "level": 3, "parent": "table",    "mandatory": true, "lookupSupported": true, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": true }, "label": "Hive Column", "description": "Hive Column" }
+          ],
+          "accessTypes": [
+            { "name": "select", "label": "Select" },
+            { "name": "update", "label": "Update" },
+            { "name": "create", "label": "Create" },
+            { "name": "drop",   "label": "Drop" },
+            { "name": "alter",  "label": "Alter" },
+            { "name": "index",  "label": "Index" },
+            { "name": "lock",   "label": "Lock" },
+            { "name": "all",    "label": "All" }
+          ],
+          "policyConditions": [
+            { "itemId": 1, "name": "expression", "evaluator": "org.apache.ranger.plugin.conditionevaluator.RangerScriptConditionEvaluator", "evaluatorOptions": { "engineName": "JavaScript", "ui.isMultiline": "true" }, "label": "Enter boolean expression", "description": "Boolean expression" }
+          ],
+          "dataMaskDef": {
+            "maskTypes": [
+              { "itemId": 1,  "name": "MASK",      "label": "Mask",       "description": "Replace lowercase with 'x', uppercase with 'X', digits with '0'" },
+              { "itemId": 2,  "name": "SHUFFLE",   "label": "Shuffle",    "description": "Randomly shuffle the contents" },
+              { "itemId": 3,  "name": "MASK_HASH", "label": "Hash",       "description": "Hash value of the contents" },
+              { "itemId": 4,  "name": "MASK_NONE", "label": "No masking", "description": "Unmasked value of the contents" },
+              { "itemId": 10, "name": "NULL",      "label": "NULL",       "description": "Replace with NULL" }
+            ],
+            "accessTypes":[
+              { "name": "select", "label": "Select" }
+            ],
+            "resources":[
+              { "name": "database", "matcherOptions": { "wildCard": false } },
+              { "name": "table",    "matcherOptions": { "wildCard": false } },
+              { "name": "column",   "matcherOptions": { "wildCard": false } }
+            ]
+          },
+          "rowFilterDef": {
+            "accessTypes":[
+              { "name": "select", "label": "Select"}
+            ],
+            "resources":[
+              { "name": "database", "matcherOptions": { "wildCard": false } },
+              { "name": "table",    "matcherOptions": { "wildCard": false } }
+            ]
+          }
+        },
+        "policies": [
+        ],
+        "tagPolicies": {
+          "serviceName": "tagdev",
+          "serviceDef": {
+            "name": "tag", "id": 100,
+            "resources": [
+              { "itemId": 1, "name": "tag", "type": "string", "level": 1, "parent": "", "mandatory": true, "lookupSupported": true, "recursiveSupported": false, "excludesSupported": false, "matcher": "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", "matcherOptions": { "wildCard": true, "ignoreCase": false }, "label": "TAG", "description": "TAG" }
+            ],
+            "accessTypes": [
+              { "itemId": 1, "name": "hive:select", "label": "hive:select" },
+              { "itemId": 2, "name": "hive:update", "label": "hive:update" },
+              { "itemId": 3, "name": "hive:create", "label": "hive:create" },
+              { "itemId": 4, "name": "hive:drop",   "label": "hive:drop" },
+              { "itemId": 5, "name": "hive:alter",  "label": "hive:alter" },
+              { "itemId": 6, "name": "hive:index",  "label": "hive:index" },
+              { "itemId": 7, "name": "hive:lock",   "label": "hive:lock" },
+              { "itemId": 8, "name": "hive:all",    "label": "hive:all",
+                "impliedGrants": [ "hive:select", "hive:update", "hive:create", "hive:drop", "hive:alter", "hive:index", "hive:lock" ] }
+            ],
+            "dataMaskDef": {
+              "resources":[
+                { "name": "tag" }
+              ]
+            },
+            "contextEnrichers": [
+              { "itemId": 1, "name": "TagEnricher", "enricher": "org.apache.ranger.plugin.contextenricher.RangerTagEnricher", "enricherOptions": { "tagRetrieverClassName": "org.apache.ranger.plugin.contextenricher.RangerFileBasedTagRetriever", "tagRefresherPollingInterval": 60000, "serviceTagsFileName": "/policyengine/resource_hierarchy_tags.json" } }
+            ],
+            "policyConditions": [
+              { "itemId": 1, "name": "expression",     "evaluator": "org.apache.ranger.plugin.conditionevaluator.RangerScriptConditionEvaluator",         "evaluatorOptions": { "engineName": "JavaScript", "ui.isMultiline": "true" },    "label": "Enter boolean expression",       "description": "Boolean expression" },
+              { "itemId": 2, "name": "enforce-expiry", "evaluator": "org.apache.ranger.plugin.conditionevaluator.RangerScriptTemplateConditionEvaluator", "evaluatorOptions": { "scriptTemplate": "ctx.isAccessedAfter('expiry_date');" }, "label": "Deny access after expiry_date?", "description": "Deny access after expiry_date? (yes/no)" },
+              { "itemId": 3, "name": "ip-range",       "evaluator": "org.apache.ranger.plugin.conditionevaluator.RangerIpMatcher",                        "evaluatorOptions": { },                                                         "label": "IP Address Range",               "description": "IP Address Range" }
+            ]
+          },
+          "policies": [
+            { "id": 101, "name": "SENSITIVE", "isEnabled": true, "isAuditEnabled": true, "policyType": 0,
+              "resources": { "tag": { "values": [ "SENSITIVE" ], "isRecursive": false } },
+              "policyItems": [
+                {"accesses": [{"type": "hive:select", "isAllowed": true}], "users": [ "test-user"] }
+              ]
+            },
+            { "id": 102, "name": "mask: SENSITIVE(level=normal)", "isEnabled": true, "isAuditEnabled": true, "policyType": 1,
+              "resources": { "tag": { "values": [ "SENSITIVE" ], "isRecursive": false } },
+              "conditions": [ { "type": "expression", "values": [ "TAG.level == 'normal'" ] } ],
+              "dataMaskPolicyItems": [
+                { "accesses": [ { "type": "hive:select", "isAllowed": true } ], "users": [ "test-user"], "dataMaskInfo": { "dataMaskType": "SHUFFLE"}}
+              ]
+            },
+            { "id": 103, "name": "mask: SENSITIVE(level=high)", "isEnabled": true, "isAuditEnabled": true, "policyType": 1,
+              "resources": { "tag": { "values": [ "SENSITIVE" ], "isRecursive": false } },
+              "conditions": [ { "type": "expression", "values": [ "TAG.level == 'high'" ] } ],
+              "dataMaskPolicyItems": [
+                { "accesses": [ { "type": "hive:select", "isAllowed": true } ], "users": [ "test-user"], "dataMaskInfo": { "dataMaskType": "MASK"}}
+              ]
+            },
+            { "id": 104, "name": "mask: SENSITIVE(level=top)", "isEnabled": true, "isAuditEnabled": true, "policyType": 1,
+              "resources": { "tag": { "values": [ "SENSITIVE" ], "isRecursive": false } },
+              "conditions": [ { "type": "expression", "values": [ "TAG.level == 'top'" ] } ],
+              "dataMaskPolicyItems": [
+                { "accesses": [ { "type": "hive:select", "isAllowed": true } ], "users": [ "test-user"], "dataMaskInfo": { "dataMaskType": "MASK_HASH"}}
+              ]
+            }
+          ]
+        }
+      },
+      "tests": [
+        { "name":            "table: db1.tbl1",
+          "resource":        { "elements": { "database": "db1", "table": "tbl1" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } }
+        },
+        { "name":            "column: db1.tbl1.SSN",
+          "resource":        { "elements": { "database": "db1", "table": "tbl1", "column": "SSN" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } },
+          "dataMasks": [
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true }
+          ]
+        },
+        { "name":            "column: db1.tbl1.Age",
+          "resource":        { "elements": { "database": "db1", "table": "tbl1", "column": "Age" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } },
+          "dataMasks": [
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true }
+          ]
+        },
+        { "name":            "column: db1.tbl1.Name",
+          "resource":        { "elements": { "database": "db1", "table": "tbl1", "column": "Name" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } },
+          "dataMasks": [
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true }
+          ]
+        },
+        { "name":            "database: db2",
+          "resource":        { "elements": { "database": "db2" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } }
+        },
+        { "name":            "table: db2.tbl1",
+          "resource":        { "elements": { "database": "db2", "table": "tbl1" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } }
+        },
+        { "name":            "column: db2.tbl1.Name",
+          "resource":        { "elements": { "database": "db2", "table": "tbl1", "column": "Name" } },
+          "userPermissions": { "test-user": { "select":  { "result": 1, "isFinal": true } } },
+          "dataMasks": [
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK" },      "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "SHUFFLE" },   "isConditional": true },
+            {"users": [ "test-user" ], "groups": [], "roles": [], "accessTypes": [ "select" ], "maskInfo": { "dataMaskType": "MASK_HASH" }, "isConditional": true }
+          ]
+        }
+      ]
+    }
+  ]
+}


[ranger] 01/02: RANGER-4465: Python client for managing users, groups, user-group associations

Posted by ma...@apache.org.
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

commit 7d46e05ff7f98c082a12505c3641c1a06f839f87
Author: Madhan Neethiraj <ma...@apache.org>
AuthorDate: Sun Oct 8 12:57:20 2023 -0700

    RANGER-4465: Python client for managing users, groups, user-group associations
---
 intg/src/main/python/README.md                     | 140 ++++++++++++++-
 .../client/ranger_user_mgmt_client.py              | 194 +++++++++++++++++++++
 .../python/apache_ranger/model/ranger_user_mgmt.py | 119 +++++++++++++
 intg/src/main/python/setup.py                      |   2 +-
 .../sample-client/src/main/python/user_mgmt.py     | 137 +++++++++++++++
 5 files changed, 589 insertions(+), 3 deletions(-)

diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md
index 0af1aa093..90e8ff6b8 100644
--- a/intg/src/main/python/README.md
+++ b/intg/src/main/python/README.md
@@ -36,7 +36,7 @@ Verify if apache-ranger client is installed:
 
 Package      Version
 ------------ ---------
-apache-ranger 0.0.11
+apache-ranger 0.0.12
 ```
 
 ## Usage
@@ -229,4 +229,140 @@ kms_client.delete_key(key_name)
 print('delete_key(' + key_name + ')')
 ```
 
-For more examples, checkout `sample-client` python  project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python/sample_client.py) module.
+```python test_ranger_user_mgmt.py```
+```python
+# test_ranger_user_mgmt.py
+from apache_ranger.client.ranger_client           import *
+from apache_ranger.utils                          import *
+from apache_ranger.model.ranger_user_mgmt         import *
+from apache_ranger.client.ranger_user_mgmt_client import *
+from datetime                                     import datetime
+
+##
+## Step 1: create a client to connect to Apache Ranger
+##
+ranger_url  = 'http://localhost:6080'
+ranger_auth = ('admin', 'rangerR0cks!')
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# ranger_auth = HTTPKerberosAuth()
+#
+# For HTTP Basic authentication
+#
+# ranger_auth = ('admin', 'rangerR0cks!')
+
+ranger    = RangerClient(ranger_url, ranger_auth)
+user_mgmt = RangerUserMgmtClient(ranger)
+
+
+
+##
+## Step 2: Let's call User Management APIs
+##
+
+print('\nListing users')
+
+users = user_mgmt.find_users()
+
+print(f'    {len(users.list)} users found')
+
+for user in users.list:
+    print(f'        id: {user.id}, name: {user.name}')
+
+
+print('\nListing groups')
+
+groups = user_mgmt.find_groups()
+
+print(f'    {len(groups.list)} groups found')
+
+for group in groups.list:
+    print(f'        id: {group.id}, name: {group.name}')
+
+print('\nListing group-users')
+
+group_users = user_mgmt.find_group_users()
+
+print(f'    {len(group_users.list)} group-users found')
+
+for group_user in group_users.list:
+    print(f'        id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}')
+
+
+now = datetime.now()
+
+name_suffix = '-' + now.strftime('%Y%m%d-%H%M%S-%f')
+user_name   = 'test-user' + name_suffix
+group_name  = 'test-group' + name_suffix
+
+
+user = RangerUser({ 'name': user_name, 'firstName': user_name, 'lastName': 'user', 'emailAddress': user_name + '@test.org', 'password': 'Welcome1', 'userRoleList': [ 'ROLE_USER' ], 'otherAttributes': '{ "dept": "test" }' })
+
+print(f'\nCreating user: name={user.name}')
+
+created_user = user_mgmt.create_user(user)
+
+print(f'    created user: {created_user}')
+
+
+group = RangerGroup({ 'name': group_name, 'otherAttributes': '{ "dept": "test" }' })
+
+print(f'\nCreating group: name={group.name}')
+
+created_group = user_mgmt.create_group(group)
+
+print(f'    created group: {created_group}')
+
+
+group_user = RangerGroupUser({ 'name': created_group.name, 'parentGroupId': created_group.id, 'userId': created_user.id })
+
+print(f'\nAdding user {created_user.name} to group {created_group.name}')
+
+created_group_user = user_mgmt.create_group_user(group_user)
+
+print(f'    created group-user: {created_group_user}')
+
+
+print('\nListing group-users')
+
+group_users = user_mgmt.find_group_users()
+
+print(f'    {len(group_users.list)} group-users found')
+
+for group_user in group_users.list:
+    print(f'        id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}')
+
+
+print(f'\nListing users for group {group.name}')
+
+users = user_mgmt.get_users_in_group(group.name)
+
+print(f'    users: {users}')
+
+
+print(f'\nListing groups for user {user.name}')
+
+groups = user_mgmt.get_groups_for_user(user.name)
+
+print(f'    groups: {groups}')
+
+
+print(f'\nDeleting group-user {created_group_user.id}')
+
+user_mgmt.delete_group_user_by_id(created_group_user.id)
+
+
+print(f'\nDeleting group {group.name}')
+
+user_mgmt.delete_group_by_id(created_group.id, True)
+
+
+print(f'\nDeleting user {user.name}')
+
+user_mgmt.delete_user_by_id(created_user.id, True)
+```
+
+For more examples, checkout `sample-client` python  project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module.
diff --git a/intg/src/main/python/apache_ranger/client/ranger_user_mgmt_client.py b/intg/src/main/python/apache_ranger/client/ranger_user_mgmt_client.py
new file mode 100644
index 000000000..3a0b44278
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/client/ranger_user_mgmt_client.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+
+import logging
+from apache_ranger.model.ranger_user_mgmt import *
+from apache_ranger.utils                  import *
+
+LOG = logging.getLogger(__name__)
+
+class RangerUserMgmtClient:
+    def __init__(self, ranger_client):
+        self.client_http = ranger_client.client_http
+
+    def create_user(self, user):
+        resp = self.client_http.call_api(RangerUserMgmtClient.CREATE_USER, request_data=user)
+
+        return type_coerce(resp, RangerUser)
+
+    def update_user_by_id(self, user_id, user):
+        resp = self.client_http.call_api(RangerUserMgmtClient.UPDATE_USER.format_path({'id': user_id}), request_data=user)
+
+        return type_coerce(resp, RangerUser)
+
+    def delete_user_by_id(self, user_id, is_force_delete=False):
+        self.client_http.call_api(RangerUserMgmtClient.DELETE_USER.format_path({'id': user_id}), query_params={'forceDelete': is_force_delete})
+
+    def get_user_by_id(self, user_id):
+        resp = self.client_http.call_api(RangerUserMgmtClient.GET_USER_BY_ID.format_path({'id': user_id}))
+
+        return type_coerce(resp, RangerUser)
+
+    def get_user(self, user_name):
+        resp = self.find_users({ 'name': user_name })
+
+        if resp is not None and resp.list is not None:
+            for user in resp.list:
+                if user.name == user_name:
+                    return user
+
+        return None
+
+    def get_groups_for_user(self, name):
+        user = self.get_user(name)
+
+        if user is not None and user.groupNameList is not None:
+            ret = user.groupNameList
+        else:
+            ret = None
+
+        return ret
+
+    def find_users(self, filter=None):
+        resp = self.client_http.call_api(RangerUserMgmtClient.FIND_USERS, filter)
+
+        vList = PList(resp)
+
+        vList.list = resp.get('vXUsers')
+
+        vList.type_coerce_list(RangerUser)
+
+        return vList
+
+
+    def create_group(self, group):
+        resp = self.client_http.call_api(RangerUserMgmtClient.CREATE_GROUP, request_data=group)
+
+        return type_coerce(resp, RangerGroup)
+
+    def update_group_by_id(self, group_id, group):
+        resp = self.client_http.call_api(RangerUserMgmtClient.UPDATE_GROUP.format_path({'id': group_id}), request_data=group)
+
+        return type_coerce(resp, RangerGroup)
+
+    def delete_group_by_id(self, group_id, is_force_delete=False):
+        self.client_http.call_api(RangerUserMgmtClient.DELETE_GROUP.format_path({'id': group_id}), query_params={'forceDelete': is_force_delete})
+
+    def get_group_by_id(self, group_id):
+        resp = self.client_http.call_api(RangerUserMgmtClient.GET_GROUP_BY_ID.format_path({'id': group_id}))
+
+        return type_coerce(resp, RangerGroup)
+
+    def get_group(self, group_name):
+        resp = self.find_groups({ 'name': group_name })
+
+        if resp is not None and resp.list is not None:
+            for group in resp.list:
+                if group.name == group_name:
+                    return group
+
+        return None
+
+    def get_users_in_group(self, name):
+        group_users = self.get_group_users_for_group(name)
+
+        if group_users is not None and group_users.users is not None:
+            ret = []
+            for user in group_users.users:
+                ret.append(user.name)
+        else:
+            ret = None
+
+        return ret
+
+    def find_groups(self, filter=None):
+        resp = self.client_http.call_api(RangerUserMgmtClient.FIND_GROUPS, filter)
+
+        vList = PList(resp)
+
+        vList.list = resp.get('vXGroups')
+
+        vList.type_coerce_list(RangerGroup)
+
+        return vList
+
+
+    def create_group_user(self, group_user):
+        resp = self.client_http.call_api(RangerUserMgmtClient.CREATE_GROUP_USER, request_data=group_user)
+
+        return type_coerce(resp, RangerGroupUser)
+
+    def update_group_user(self, group_user):
+        resp = self.client_http.call_api(RangerUserMgmtClient.UPDATE_GROUP_USER, request_data=group_user)
+
+        return type_coerce(resp, RangerGroupUser)
+
+    def delete_group_user_by_id(self, group_user_id):
+        self.client_http.call_api(RangerUserMgmtClient.DELETE_GROUP_USER.format_path({'id': group_user_id}))
+
+    def find_group_users(self, filter=None):
+        resp = self.client_http.call_api(RangerUserMgmtClient.FIND_GROUP_USERS, filter)
+
+        vList = PList(resp)
+
+        vList.list = resp.get('vXGroupUsers')
+
+        vList.type_coerce_list(RangerGroupUser)
+
+        return vList
+
+    def get_group_users_for_group(self, name):
+        resp = self.client_http.call_api(RangerUserMgmtClient.GET_GROUP_USERS_FOR_GROUP.format_path({'name': name}))
+
+        return type_coerce(resp, RangerGroupUsers)
+
+    # URIs
+    URI_XUSERS_BASE                  = 'service/xusers'
+    URI_XUSERS_USERS                 = URI_XUSERS_BASE + '/users'
+    URI_XUSERS_SECURE_USERS          = URI_XUSERS_BASE + '/secure/users'
+    URI_XUSERS_SECURE_USER_BY_ID     = URI_XUSERS_SECURE_USERS + '/{id}'
+    URI_XUSERS_DELETE_USER           = URI_XUSERS_USERS + '/{id}'
+    URI_XUSERS_GROUPS                = URI_XUSERS_BASE + '/groups'
+    URI_XUSERS_SECURE_GROUPS         = URI_XUSERS_BASE + '/secure/groups'
+    URI_XUSERS_SECURE_GROUP_BY_ID    = URI_XUSERS_SECURE_GROUPS + '/{id}'
+    URI_XUSERS_DELETE_GROUP          = URI_XUSERS_GROUPS + '/{id}'
+    URI_XUSERS_GROUP_USERS           = URI_XUSERS_BASE + '/groupusers'
+    URI_XUSERS_GROUP_USER_BY_ID      = URI_XUSERS_GROUP_USERS + '/{id}'
+    URI_XUSERS_GROUP_USERS_FOR_GROUP = URI_XUSERS_GROUP_USERS + '/groupName/{name}'
+
+
+    # APIs
+    CREATE_USER    = API(URI_XUSERS_SECURE_USERS, HttpMethod.POST, HTTPStatus.OK)
+    UPDATE_USER    = API(URI_XUSERS_SECURE_USER_BY_ID, HttpMethod.PUT, HTTPStatus.OK)
+    DELETE_USER    = API(URI_XUSERS_DELETE_USER, HttpMethod.DELETE, HTTPStatus.NO_CONTENT)
+    GET_USER_BY_ID = API(URI_XUSERS_SECURE_USER_BY_ID, HttpMethod.GET, HTTPStatus.OK)
+    FIND_USERS     = API(URI_XUSERS_USERS, HttpMethod.GET, HTTPStatus.OK)
+
+    CREATE_GROUP    = API(URI_XUSERS_SECURE_GROUPS, HttpMethod.POST, HTTPStatus.OK)
+    UPDATE_GROUP    = API(URI_XUSERS_SECURE_GROUP_BY_ID, HttpMethod.PUT, HTTPStatus.OK)
+    DELETE_GROUP    = API(URI_XUSERS_DELETE_GROUP, HttpMethod.DELETE, HTTPStatus.NO_CONTENT)
+    GET_GROUP_BY_ID = API(URI_XUSERS_SECURE_GROUP_BY_ID, HttpMethod.GET, HTTPStatus.OK)
+    FIND_GROUPS     = API(URI_XUSERS_GROUPS, HttpMethod.GET, HTTPStatus.OK)
+
+    CREATE_GROUP_USER = API(URI_XUSERS_GROUP_USERS, HttpMethod.POST, HTTPStatus.OK)
+    UPDATE_GROUP_USER = API(URI_XUSERS_GROUP_USERS, HttpMethod.PUT, HTTPStatus.OK)
+    DELETE_GROUP_USER = API(URI_XUSERS_GROUP_USER_BY_ID, HttpMethod.DELETE, HTTPStatus.NO_CONTENT)
+    FIND_GROUP_USERS  = API(URI_XUSERS_GROUP_USERS, HttpMethod.GET, HTTPStatus.OK)
+
+    GET_GROUP_USERS_FOR_GROUP = API(URI_XUSERS_GROUP_USERS_FOR_GROUP, HttpMethod.GET, HTTPStatus.OK)
diff --git a/intg/src/main/python/apache_ranger/model/ranger_user_mgmt.py b/intg/src/main/python/apache_ranger/model/ranger_user_mgmt.py
new file mode 100644
index 000000000..244ed55fc
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/model/ranger_user_mgmt.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+
+from apache_ranger.model.ranger_base import *
+from apache_ranger.utils             import *
+
+class RangerUser(RangerBase):
+    def __init__(self, attrs=None):
+        if attrs is None:
+            attrs = {}
+
+        RangerBaseModelObject.__init__(self, attrs)
+
+        self.id              = attrs.get('id')
+        self.createDate      = attrs.get('createDate')
+        self.updateDate      = attrs.get('updateDate')
+        self.owner           = attrs.get('owner')
+        self.updatedBy       = attrs.get('updatedBy')
+
+        self.name            = attrs.get('name')
+        self.description     = attrs.get('description')
+        self.firstName       = attrs.get('firstName')
+        self.lastName        = attrs.get("lastName")
+        self.emailAddress    = attrs.get('emailAddress')
+        self.password        = attrs.get('password')
+        self.credStoreId     = attrs.get("credStoreId")
+        self.status          = attrs.get('status')
+        self.isVisible       = attrs.get('isVisible')
+        self.userSource      = attrs.get('userSource')
+        self.userRoleList    = attrs.get('userRoleList')
+        self.otherAttributes = attrs.get('otherAttributes')
+        self.syncSource      = attrs.get('syncSource')
+        self.groupIdList     = attrs.get('groupIdList')
+        self.groupNameList   = attrs.get('groupNameList')
+
+        if self.status is None:
+            self.status = 1
+
+        if self.userRoleList is None:
+            self.userRoleList = [ 'ROLE_USER' ]
+
+
+class RangerGroup(RangerBase):
+    def __init__(self, attrs=None):
+        if attrs is None:
+            attrs = {}
+
+        RangerBaseModelObject.__init__(self, attrs)
+
+        self.id              = attrs.get('id')
+        self.createDate      = attrs.get('createDate')
+        self.updateDate      = attrs.get('updateDate')
+        self.owner           = attrs.get('owner')
+        self.updatedBy       = attrs.get('updatedBy')
+
+        self.name            = attrs.get('name')
+        self.description     = attrs.get('description')
+        self.groupType       = attrs.get('groupType')
+        self.groupSource     = attrs.get("groupSource")
+        self.credStoreId     = attrs.get("credStoreId")
+        self.isVisible       = attrs.get('isVisible')
+        self.otherAttributes = attrs.get('otherAttributes')
+        self.syncSource      = attrs.get('syncSource')
+
+class RangerGroupUser(RangerBase):
+    def __init__(self, attrs=None):
+        if attrs is None:
+            attrs = {}
+
+        RangerBaseModelObject.__init__(self, attrs)
+
+        self.id              = attrs.get('id')
+        self.createDate      = attrs.get('createDate')
+        self.updateDate      = attrs.get('updateDate')
+        self.owner           = attrs.get('owner')
+        self.updatedBy       = attrs.get('updatedBy')
+
+        self.name            = attrs.get('name')
+        self.parentGroupId   = attrs.get('parentGroupId')
+        self.userId          = attrs.get('userId')
+
+
+class RangerGroupUsers(RangerBase):
+    def __init__(self, attrs=None):
+        if attrs is None:
+            attrs = {}
+
+        RangerBaseModelObject.__init__(self, attrs)
+
+        self.id              = attrs.get('id')
+        self.createDate      = attrs.get('createDate')
+        self.updateDate      = attrs.get('updateDate')
+        self.owner           = attrs.get('owner')
+        self.updatedBy       = attrs.get('updatedBy')
+
+        self.group = attrs.get('xgroupInfo')
+        self.users = attrs.get('xuserInfo')
+
+    def type_coerce_attrs(self):
+        super(RangerGroupUsers, self).type_coerce_attrs()
+
+        self.group = type_coerce(self.group, RangerGroup)
+        self.users = type_coerce_list(self.users, RangerUser)
diff --git a/intg/src/main/python/setup.py b/intg/src/main/python/setup.py
index c7dfaa0b4..89db40e64 100644
--- a/intg/src/main/python/setup.py
+++ b/intg/src/main/python/setup.py
@@ -27,7 +27,7 @@ with open("README.md", "r") as fh:
 
 setup(
     name="apache-ranger",
-    version="0.0.11",
+    version="0.0.12",
     author="Apache Ranger",
     author_email="dev@ranger.apache.org",
     description="Apache Ranger Python client",
diff --git a/ranger-examples/sample-client/src/main/python/user_mgmt.py b/ranger-examples/sample-client/src/main/python/user_mgmt.py
new file mode 100644
index 000000000..97bbd05eb
--- /dev/null
+++ b/ranger-examples/sample-client/src/main/python/user_mgmt.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+
+from apache_ranger.client.ranger_client           import *
+from apache_ranger.utils                          import *
+from apache_ranger.model.ranger_user_mgmt         import *
+from apache_ranger.client.ranger_user_mgmt_client import *
+from datetime                                     import datetime
+
+## create a client to connect to Apache Ranger admin server
+ranger_url  = 'http://localhost:6080'
+ranger_auth = ('admin', 'rangerR0cks!')
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# ranger_auth = HTTPKerberosAuth()
+
+
+print(f'\nUsing Ranger at {ranger_url}');
+
+ranger = RangerClient(ranger_url, ranger_auth)
+
+user_mgmt = RangerUserMgmtClient(ranger)
+
+
+print('\nListing users')
+
+users = user_mgmt.find_users()
+
+print(f'    {len(users.list)} users found')
+
+for user in users.list:
+    print(f'        id: {user.id}, name: {user.name}')
+
+
+print('\nListing groups')
+
+groups = user_mgmt.find_groups()
+
+print(f'    {len(groups.list)} groups found')
+
+for group in groups.list:
+    print(f'        id: {group.id}, name: {group.name}')
+
+print('\nListing group-users')
+
+group_users = user_mgmt.find_group_users()
+
+print(f'    {len(group_users.list)} group-users found')
+
+for group_user in group_users.list:
+    print(f'        id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}')
+
+
+now = datetime.now()
+
+name_suffix = '-' + now.strftime('%Y%m%d-%H%M%S-%f')
+user_name   = 'test-user' + name_suffix
+group_name  = 'test-group' + name_suffix
+
+
+user = RangerUser({ 'name': user_name, 'firstName': user_name, 'lastName': 'user', 'emailAddress': user_name + '@test.org', 'password': 'Welcome1', 'userRoleList': [ 'ROLE_USER' ], 'otherAttributes': '{ "dept": "test" }' })
+
+print(f'\nCreating user: name={user.name}')
+
+created_user = user_mgmt.create_user(user)
+
+print(f'    created user: {created_user}')
+
+
+group = RangerGroup({ 'name': group_name, 'otherAttributes': '{ "dept": "test" }' })
+
+print(f'\nCreating group: name={group.name}')
+
+created_group = user_mgmt.create_group(group)
+
+print(f'    created group: {created_group}')
+
+
+group_user = RangerGroupUser({ 'name': created_group.name, 'parentGroupId': created_group.id, 'userId': created_user.id })
+
+print(f'\nAdding user {created_user.name} to group {created_group.name}')
+
+created_group_user = user_mgmt.create_group_user(group_user)
+
+print(f'    created group-user: {created_group_user}')
+
+
+print('\nListing group-users')
+
+group_users = user_mgmt.find_group_users()
+
+print(f'    {len(group_users.list)} group-users found')
+
+for group_user in group_users.list:
+    print(f'        id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}')
+
+
+print(f'\nListing users for group {group.name}')
+
+group_users = user_mgmt.get_group_users_for_group(group.name)
+
+print(f'    group: {group_users.group}')
+print(f'    users: {group_users.users}')
+
+
+print(f'\nDeleting group-user {created_group_user.id}')
+
+user_mgmt.delete_group_user_by_id(created_group_user.id)
+
+
+print(f'\nDeleting group {group.name}')
+
+user_mgmt.delete_group_by_id(created_group.id, True)
+
+
+print(f'\nDeleting user {user.name}')
+
+user_mgmt.delete_user_by_id(created_user.id, True)