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 2022/08/11 20:52:45 UTC

[ranger] branch master updated (a064f8d91 -> 4ec6c5599)

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 a064f8d91 RANGER-3848: Enable auto-renew for kerberos in Java client
     new 026be3eeb RANGER-3828: plugin-nestedstructure with tagsync AtlasNestedStructureResourceMapper class
     new 4ec6c5599 RANGER-3816: getResourceACLs() updated to handle macros in resource values

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:
 .../policyengine/RangerPolicyEngineImpl.java       |  59 ++--
 .../plugin/policyengine/RangerResourceACLs.java    |   7 +-
 .../RangerAbstractPolicyEvaluator.java             | 106 +++++-
 .../policyevaluator/RangerPolicyEvaluator.java     |   5 +-
 .../RangerDefaultPolicyResourceMatcher.java        |   4 +
 .../plugin/store/EmbeddedServiceDefsUtil.java      |   7 +-
 .../plugin/util/RangerRequestExprResolver.java     |  41 +++
 .../ranger-servicedef-nestedstructure.json         | 186 +++++++++++
 .../ranger/plugin/policyengine/TestPolicyACLs.java |   6 +-
 .../policyengine/test_aclprovider_default.json     |  32 ++
 dev-support/ranger-pmd-ruleset.xml                 |   1 +
 {docs => plugin-nestedstructure}/.gitignore        |   0
 {docs => plugin-nestedstructure}/LICENSE           |   0
 plugin-nestedstructure/NOTICE                      |  18 +
 plugin-nestedstructure/README.md                   | 168 ++++++++++
 .../conf/log4j.properties                          |  35 +-
 .../conf/ranger-nestedstructure-audit.xml          |  36 +-
 .../conf/ranger-nestedstructure-policymgr-ssl.xml  |  50 +++
 .../conf/ranger-nestedstructure-security.xml       |  41 +--
 plugin-nestedstructure/pom.xml                     | 111 +++++++
 .../nestedstructure/authorizer/AccessResult.java   |  54 +++
 .../nestedstructure/authorizer/DataMasker.java     | 283 ++++++++++++++++
 .../authorizer/FieldLevelAccess.java               |  62 ++++
 .../authorizer/JsonManipulator.java                | 168 ++++++++++
 .../nestedstructure/authorizer/MaskTypes.java      |  54 +++
 .../authorizer/MaskingException.java               |  32 ++
 .../authorizer/NestedStructureAccessType.java      |  45 +++
 .../authorizer/NestedStructureAuditHandler.java    |  95 ++++++
 .../authorizer/NestedStructureAuthorizer.java      | 285 ++++++++++++++++
 .../authorizer/NestedStructureResource.java        |  46 +++
 .../authorizer/NestedStructureService.java         |  37 +++
 .../authorizer/RecordFilterJavaScript.java         | 110 +++++++
 .../nestedstructure/authorizer/ExampleClient.java  |  47 +++
 .../nestedstructure/authorizer/TestDataMasker.java | 266 +++++++++++++++
 .../authorizer/TestJsonManipulator.java            | 363 +++++++++++++++++++++
 .../authorizer/TestNestedStructureAuthorizer.java  | 105 ++++++
 .../authorizer/TestRecordFilterJavaScript.java     |  66 ++++
 .../test/resources/servicedef-nestedstructure.json | 186 +++++++++++
 .../src/test/resources/test_customer_records.json  | 164 ++++++++++
 pom.xml                                            |  14 +
 .../AtlasNestedStructureResourceMapper.java        |  95 ++++++
 .../nestedstructureplugin/ResourceTests.java       | 139 ++++++++
 42 files changed, 3522 insertions(+), 107 deletions(-)
 create mode 100644 agents-common/src/main/resources/service-defs/ranger-servicedef-nestedstructure.json
 copy {docs => plugin-nestedstructure}/.gitignore (100%)
 copy {docs => plugin-nestedstructure}/LICENSE (100%)
 create mode 100644 plugin-nestedstructure/NOTICE
 create mode 100644 plugin-nestedstructure/README.md
 copy security-admin/db/mysql/init/reset_db_with_seed.sh => plugin-nestedstructure/conf/log4j.properties (52%)
 mode change 100755 => 100644
 copy ranger-examples/plugin-sampleapp/conf/ranger-sampleapp-audit.xml => plugin-nestedstructure/conf/ranger-nestedstructure-audit.xml (66%)
 create mode 100644 plugin-nestedstructure/conf/ranger-nestedstructure-policymgr-ssl.xml
 copy ranger-examples/plugin-sampleapp/conf/ranger-sampleapp-security.xml => plugin-nestedstructure/conf/ranger-nestedstructure-security.xml (66%)
 create mode 100644 plugin-nestedstructure/pom.xml
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/AccessResult.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/DataMasker.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/FieldLevelAccess.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/JsonManipulator.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskTypes.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskingException.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAccessType.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuditHandler.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuthorizer.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureResource.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureService.java
 create mode 100644 plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
 create mode 100644 plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/ExampleClient.java
 create mode 100644 plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestDataMasker.java
 create mode 100644 plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestJsonManipulator.java
 create mode 100644 plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestNestedStructureAuthorizer.java
 create mode 100644 plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
 create mode 100644 plugin-nestedstructure/src/test/resources/servicedef-nestedstructure.json
 create mode 100644 plugin-nestedstructure/src/test/resources/test_customer_records.json
 create mode 100644 tagsync/src/main/java/org/apache/ranger/tagsync/nestedstructureplugin/AtlasNestedStructureResourceMapper.java
 create mode 100644 tagsync/src/test/java/org/apache/ranger/tagsync/nestedstructureplugin/ResourceTests.java


[ranger] 02/02: RANGER-3816: getResourceACLs() updated to handle macros in resource values

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 4ec6c55993bd01e02aa56980c037ed1c2ca92649
Author: Madhan Neethiraj <ma...@apache.org>
AuthorDate: Fri Jul 1 15:31:02 2022 -0700

    RANGER-3816: getResourceACLs() updated to handle macros in resource values
---
 .../policyengine/RangerPolicyEngineImpl.java       |  59 +++++++-----
 .../plugin/policyengine/RangerResourceACLs.java    |   7 +-
 .../RangerAbstractPolicyEvaluator.java             | 106 ++++++++++++++++++++-
 .../policyevaluator/RangerPolicyEvaluator.java     |   5 +-
 .../RangerDefaultPolicyResourceMatcher.java        |   4 +
 .../plugin/util/RangerRequestExprResolver.java     |  41 ++++++++
 .../ranger/plugin/policyengine/TestPolicyACLs.java |   6 +-
 .../policyengine/test_aclprovider_default.json     |  32 +++++++
 8 files changed, 227 insertions(+), 33 deletions(-)

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 3ae0add51..23db18f3a 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
@@ -36,6 +36,7 @@ import org.apache.ranger.plugin.policyengine.RangerResourceACLs.RowFilterResult;
 import org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator;
 import org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.RangerPolicyResourceEvaluator;
 import org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.PolicyACLSummary;
+import org.apache.ranger.plugin.policyresourcematcher.RangerPolicyResourceMatcher;
 import org.apache.ranger.plugin.policyresourcematcher.RangerPolicyResourceMatcher.MatchType;
 import org.apache.ranger.plugin.service.RangerDefaultRequestProcessor;
 import org.apache.ranger.plugin.util.GrantRevokeRequest;
@@ -311,39 +312,43 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 					MatchType matchType = tagMatchTypeMap.get(evaluator.getPolicyId());
 
 					boolean isMatched = false;
+					boolean isConditionalMatch = false;
 
 					if (matchType == null) {
 						for (RangerPolicyResourceEvaluator resourceEvaluator : evaluator.getResourceEvaluators()) {
-							matchType = resourceEvaluator.getPolicyResourceMatcher().getMatchType(request.getResource(), request.getContext());
+							RangerPolicyResourceMatcher matcher = resourceEvaluator.getPolicyResourceMatcher();
 
-							if (request.getResourceMatchingScope() == RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS) {
-								isMatched = matchType != MatchType.NONE;
-							} else {
-								isMatched = matchType == MatchType.SELF || matchType == MatchType.SELF_AND_ALL_DESCENDANTS;
-							}
+							matchType = matcher.getMatchType(request.getResource(), request.getContext());
+							isMatched = isMatch(matchType, request.getResourceMatchingScope());
 
 							if (isMatched) {
+								isConditionalMatch = false;
+
 								break;
+							} else if (matcher.getNeedsDynamicEval() && !isConditionalMatch) {
+								MatchType dynWildCardMatch = resourceEvaluator.getMacrosReplaceWithWildcardMatcher(policyEngine).getMatchType(request.getResource(), request.getContext());
+
+								isConditionalMatch = isMatch(dynWildCardMatch, request.getResourceMatchingScope());
 							}
 						}
 					} else {
-						if (request.getResourceMatchingScope() == RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS) {
-							isMatched = matchType != MatchType.NONE;
-						} else {
-							isMatched = matchType == MatchType.SELF || matchType == MatchType.SELF_AND_ALL_DESCENDANTS;
-						}
+						isMatched = isMatch(matchType, request.getResourceMatchingScope());
 					}
 
-					if (!isMatched) {
+					if (!isMatched && !isConditionalMatch) {
 						continue;
 					}
 
+					if (!isConditionalMatch) {
+						isConditionalMatch = policyIdForTemporalTags.contains(evaluator.getPolicyId()) || evaluator.getValidityScheduleEvaluatorsCount() != 0;
+					}
+
 					if (policyType == RangerPolicy.POLICY_TYPE_ACCESS) {
-						updateFromPolicyACLs(evaluator, policyIdForTemporalTags, ret);
+						updateFromPolicyACLs(evaluator, isConditionalMatch, ret);
 					} else if (policyType == RangerPolicy.POLICY_TYPE_ROWFILTER) {
-						updateRowFiltersFromPolicy(evaluator, policyIdForTemporalTags, ret);
+						updateRowFiltersFromPolicy(evaluator, isConditionalMatch, ret);
 					} else if (policyType == RangerPolicy.POLICY_TYPE_DATAMASK) {
-						updateDataMasksFromPolicy(evaluator, policyIdForTemporalTags, ret);
+						updateDataMasksFromPolicy(evaluator, isConditionalMatch, ret);
 					}
 				}
 
@@ -1172,15 +1177,13 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 		return policyEngine.getPluginContext().getConfig().getIsFallbackSupported();
 	}
 
-	private void updateFromPolicyACLs(RangerPolicyEvaluator evaluator, Set<Long> policyIdForTemporalTags, RangerResourceACLs resourceACLs) {
+	private void updateFromPolicyACLs(RangerPolicyEvaluator evaluator, boolean isConditional, RangerResourceACLs resourceACLs) {
 		PolicyACLSummary aclSummary = evaluator.getPolicyACLSummary();
 
 		if (aclSummary == null) {
 			return;
 		}
 
-		boolean isConditional = policyIdForTemporalTags.contains(evaluator.getPolicyId()) || evaluator.getValidityScheduleEvaluatorsCount() != 0;
-
 		for (Map.Entry<String, Map<String, PolicyACLSummary.AccessResult>> userAccessInfo : aclSummary.getUsersAccessInfo().entrySet()) {
 			final String userName = userAccessInfo.getKey();
 
@@ -1248,12 +1251,10 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 		}
 	}
 
-	private void updateRowFiltersFromPolicy(RangerPolicyEvaluator evaluator, Set<Long> policyIdForTemporalTags, RangerResourceACLs resourceACLs) {
+	private void updateRowFiltersFromPolicy(RangerPolicyEvaluator evaluator, boolean isConditional, RangerResourceACLs resourceACLs) {
 		PolicyACLSummary aclSummary = evaluator.getPolicyACLSummary();
 
 		if (aclSummary != null) {
-			boolean isConditional = policyIdForTemporalTags.contains(evaluator.getPolicyId()) || evaluator.getValidityScheduleEvaluatorsCount() != 0;
-
 			for (RowFilterResult rowFilterResult : aclSummary.getRowFilters()) {
 				rowFilterResult = copyRowFilter(rowFilterResult);
 
@@ -1266,12 +1267,10 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 		}
 	}
 
-	private void updateDataMasksFromPolicy(RangerPolicyEvaluator evaluator, Set<Long> policyIdForTemporalTags, RangerResourceACLs resourceACLs) {
+	private void updateDataMasksFromPolicy(RangerPolicyEvaluator evaluator, boolean isConditional, RangerResourceACLs resourceACLs) {
 		PolicyACLSummary aclSummary = evaluator.getPolicyACLSummary();
 
 		if (aclSummary != null) {
-			boolean isConditional = policyIdForTemporalTags.contains(evaluator.getPolicyId()) || evaluator.getValidityScheduleEvaluatorsCount() != 0;
-
 			for (DataMaskResult dataMaskResult : aclSummary.getDataMasks()) {
 				dataMaskResult = copyDataMask(dataMaskResult);
 
@@ -1312,6 +1311,18 @@ public class RangerPolicyEngineImpl implements RangerPolicyEngine {
 		return values != null ? new HashSet<>(values) : null;
 	}
 
+	private boolean isMatch(MatchType matchType, RangerAccessRequest.ResourceMatchingScope matchingScope) {
+		final boolean ret;
+
+		if (matchingScope == RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS) {
+			ret = matchType != MatchType.NONE;
+		} else {
+			ret = matchType == MatchType.SELF || matchType == MatchType.SELF_AND_ALL_DESCENDANTS;
+		}
+
+		return ret;
+	}
+
 	private static class ServiceConfig {
 		private final Set<String> auditExcludedUsers;
 		private final Set<String> auditExcludedGroups;
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerResourceACLs.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerResourceACLs.java
index aa49507b8..e0c0f94a9 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerResourceACLs.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerResourceACLs.java
@@ -40,6 +40,7 @@ import java.util.Objects;
 import java.util.Set;
 
 import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_ALLOWED;
+import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_CONDITIONAL;
 import static org.apache.ranger.plugin.policyevaluator.RangerPolicyEvaluator.ACCESS_DENIED;
 
 public class RangerResourceACLs {
@@ -116,7 +117,7 @@ public class RangerResourceACLs {
 			accessResult = new AccessResult(access, policy);
 
 			userAccessInfo.put(accessType, accessResult);
-		} else {
+		} else if (access != ACCESS_CONDITIONAL) {
 			accessResult.setResult(access);
 			accessResult.setPolicy(policy);
 		}
@@ -137,7 +138,7 @@ public class RangerResourceACLs {
 			accessResult = new AccessResult(access, policy);
 
 			groupAccessInfo.put(accessType, accessResult);
-		} else {
+		} else if (access != ACCESS_CONDITIONAL) {
 			accessResult.setResult(access);
 			accessResult.setPolicy(policy);
 		}
@@ -158,7 +159,7 @@ public class RangerResourceACLs {
 			accessResult = new AccessResult(access, policy);
 
 			roleAccessInfo.put(accessType, accessResult);
-		} else {
+		} else if (access != ACCESS_CONDITIONAL) {
 			accessResult.setResult(access);
 			accessResult.setPolicy(policy);
 		}
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerAbstractPolicyEvaluator.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerAbstractPolicyEvaluator.java
index c16d2acb0..159617b39 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerAbstractPolicyEvaluator.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyevaluator/RangerAbstractPolicyEvaluator.java
@@ -25,18 +25,24 @@ import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyResource;
 import org.apache.ranger.plugin.model.RangerServiceDef;
 import org.apache.ranger.plugin.model.RangerServiceDef.RangerResourceDef;
 import org.apache.ranger.plugin.model.validation.RangerServiceDefHelper;
+import org.apache.ranger.plugin.policyengine.PolicyEngine;
 import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
 import org.apache.ranger.plugin.policyengine.RangerPluginContext;
 import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions;
 import org.apache.ranger.plugin.policyresourcematcher.RangerDefaultPolicyResourceMatcher;
 import org.apache.ranger.plugin.policyresourcematcher.RangerPolicyResourceMatcher;
+import org.apache.ranger.plugin.resourcematcher.RangerAbstractResourceMatcher;
 import org.apache.ranger.plugin.resourcematcher.RangerResourceMatcher;
+import org.apache.ranger.plugin.util.RangerRequestExprResolver;
 import org.apache.ranger.plugin.util.ServiceDefUtil;
+import org.apache.ranger.plugin.util.StringTokenReplacer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
@@ -47,6 +53,18 @@ public abstract class RangerAbstractPolicyEvaluator implements RangerPolicyEvalu
 
 	private static final AtomicLong NEXT_RESOURCE_EVALUATOR_ID = new AtomicLong(1);
 
+	private final static Map<String, Object> WILDCARD_EVAL_CONTEXT = new HashMap<String, Object>() {
+		@Override
+		public boolean containsKey(Object key) { return true; }
+
+		@Override
+		public Object get(Object key) { return RangerAbstractResourceMatcher.WILDCARD_ASTERISK; }
+	};
+
+	static {
+		WILDCARD_EVAL_CONTEXT.put(RangerAbstractResourceMatcher.WILDCARD_ASTERISK, RangerAbstractResourceMatcher.WILDCARD_ASTERISK);
+	}
+
 	private   RangerPolicy                        policy;
 	private   RangerServiceDef                    serviceDef;
 	private   boolean                             needsDynamicEval = false;
@@ -235,11 +253,61 @@ public abstract class RangerAbstractPolicyEvaluator implements RangerPolicyEvalu
 		return sb;
 	}
 
+	private Map<String, RangerPolicyResource> getPolicyResourcesWithMacrosReplaced(Map<String, RangerPolicyResource> resources, PolicyEngine policyEngine) {
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("==> RangerAbstractPolicyEvaluator.getPolicyResourcesWithMacrosReplaced(" + resources + ")");
+		}
+
+		final Map<String, RangerPolicyResource> ret;
+		final Collection<String>                resourceKeys = resources == null ? null : resources.keySet();
+
+		if (CollectionUtils.isNotEmpty(resourceKeys)) {
+			ret = new HashMap<>();
+
+			for (String resourceName : resourceKeys) {
+				RangerPolicyResource resourceValues = resources.get(resourceName);
+				List<String>         values         = resourceValues == null ? null : resourceValues.getValues();
+
+				if (CollectionUtils.isNotEmpty(values)) {
+					StringTokenReplacer tokenReplacer = policyEngine.getStringTokenReplacer(resourceName);
+
+					List<String> modifiedValues = new ArrayList<>();
+
+					for (String value : values) {
+						RangerRequestExprResolver exprResolver  = new RangerRequestExprResolver(value, serviceDef.getName());
+						String                    modifiedValue = exprResolver.resolveExpressions(WILDCARD_EVAL_CONTEXT);
+
+						if (tokenReplacer != null) {
+							modifiedValue = tokenReplacer.replaceTokens(modifiedValue, WILDCARD_EVAL_CONTEXT);
+						}
+
+						modifiedValues.add(modifiedValue);
+					}
+
+					RangerPolicyResource modifiedPolicyResource = new RangerPolicyResource(modifiedValues, resourceValues.getIsExcludes(), resourceValues.getIsRecursive());
+
+					ret.put(resourceName, modifiedPolicyResource);
+				} else {
+					ret.put(resourceName, resourceValues);
+				}
+			}
+		} else {
+			ret = resources;
+		}
+
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("<== RangerAbstractPolicyEvaluator.getPolicyResourcesWithMacrosReplaced(" + resources  + "): " + ret);
+		}
+
+		return ret;
+	}
+
 	public class RangerDefaultPolicyResourceEvaluator implements RangerPolicyResourceEvaluator {
-		private final long                               id;
-		private final Map<String, RangerPolicyResource>  resource;
-		private final RangerDefaultPolicyResourceMatcher resourceMatcher;
-		private final RangerResourceDef                  leafResourceDef;
+		private final    long                               id;
+		private final    Map<String, RangerPolicyResource>  resource;
+		private final    RangerDefaultPolicyResourceMatcher resourceMatcher;
+		private final    RangerResourceDef                  leafResourceDef;
+		private volatile RangerDefaultPolicyResourceMatcher macrosReplacedWithWildcardMatcher;
 
 		public RangerDefaultPolicyResourceEvaluator(long id, Map<String, RangerPolicyResource> resource, int policyType, RangerServiceDef serviceDef, RangerServiceDefHelper serviceDefHelper) {
 			this.id              = id;
@@ -268,6 +336,36 @@ public abstract class RangerAbstractPolicyEvaluator implements RangerPolicyEvalu
 			return resourceMatcher;
 		}
 
+		@Override
+		public RangerPolicyResourceMatcher getMacrosReplaceWithWildcardMatcher(PolicyEngine policyEngine) {
+			RangerDefaultPolicyResourceMatcher ret = this.macrosReplacedWithWildcardMatcher;
+
+			if (ret == null) {
+				synchronized (this) {
+					ret = this.macrosReplacedWithWildcardMatcher;
+
+					if (ret == null) {
+						if (resourceMatcher.getNeedsDynamicEval()) {
+							Map<String, RangerPolicyResource> updatedResource = getPolicyResourcesWithMacrosReplaced(resource, policyEngine);
+
+							ret = new RangerDefaultPolicyResourceMatcher();
+
+							ret.setPolicyResources(updatedResource, resourceMatcher.getPolicyType());
+							ret.setServiceDef(serviceDef);
+							ret.setServiceDefHelper(resourceMatcher.getServiceDefHelper());
+							ret.init();
+						} else {
+							ret = resourceMatcher;
+						}
+
+						this.macrosReplacedWithWildcardMatcher = ret;
+					}
+				}
+			}
+
+			return ret;
+		}
+
 		@Override
 		public Map<String, RangerPolicyResource> getPolicyResource() {
 			return resource;
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 d1c2f7cde..f130e2491 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
@@ -39,6 +39,8 @@ import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyItemAccess;
 import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyResource;
 import org.apache.ranger.plugin.model.RangerPolicy.RangerRowFilterPolicyItem;
 import org.apache.ranger.plugin.model.RangerServiceDef;
+import org.apache.ranger.plugin.policyengine.PolicyEngine;
+import org.apache.ranger.plugin.policyengine.RangerResourceAccessInfo;
 import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
 import org.apache.ranger.plugin.policyengine.RangerAccessResult;
 import org.apache.ranger.plugin.policyengine.RangerAccessResource;
@@ -46,7 +48,6 @@ import org.apache.ranger.plugin.policyengine.RangerPolicyEngine;
 import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions;
 import org.apache.ranger.plugin.policyengine.RangerResourceACLs.DataMaskResult;
 import org.apache.ranger.plugin.policyengine.RangerResourceACLs.RowFilterResult;
-import org.apache.ranger.plugin.policyengine.RangerResourceAccessInfo;
 import org.apache.ranger.plugin.policyresourcematcher.RangerResourceEvaluator;
 import org.apache.ranger.plugin.policyresourcematcher.RangerPolicyResourceMatcher;
 
@@ -641,5 +642,7 @@ public interface RangerPolicyEvaluator {
 		}
 
 		RangerPolicyEvaluator getPolicyEvaluator();
+
+		RangerPolicyResourceMatcher getMacrosReplaceWithWildcardMatcher(PolicyEngine policyEngine);
 	}
 }
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/policyresourcematcher/RangerDefaultPolicyResourceMatcher.java b/agents-common/src/main/java/org/apache/ranger/plugin/policyresourcematcher/RangerDefaultPolicyResourceMatcher.java
index 15f2522db..78e2f1884 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/policyresourcematcher/RangerDefaultPolicyResourceMatcher.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/policyresourcematcher/RangerDefaultPolicyResourceMatcher.java
@@ -101,6 +101,10 @@ public class RangerDefaultPolicyResourceMatcher implements RangerPolicyResourceM
 
     public int getPolicyType() { return policyType; }
 
+    public RangerServiceDefHelper getServiceDefHelper() {
+        return serviceDefHelper;
+    }
+
     @Override
     public RangerServiceDef getServiceDef() {
         return serviceDef;
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRequestExprResolver.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRequestExprResolver.java
index 4e5d949de..3a183cff6 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRequestExprResolver.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRequestExprResolver.java
@@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
 
 import javax.script.ScriptEngine;
 import java.util.Collection;
+import java.util.Map;
 import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -54,6 +55,46 @@ public class RangerRequestExprResolver {
         }
     }
 
+    /*
+     * replaces expressions in this.str with corresponding values in exprValues map (argument).
+     * For example, given the following:
+     *   1. this.str has value:   "dept = '${{USER.dept}}'"
+     *   2. exprValues has value: { "dept": "marketing" }
+     * This method returns: "dept = 'marketing'"
+     */
+    public String resolveExpressions(Map<String, Object> exprValues) {
+        String ret = str;
+
+        if (hasTokens) {
+            StringBuffer sb      = new StringBuffer();
+            Matcher      matcher = PATTERN.matcher(str);
+
+            while (matcher.find()) {
+                String expr = matcher.group(REGEX_GROUP_EXPR);
+                String val  = Objects.toString(exprValues.get(expr));
+
+                matcher.appendReplacement(sb, val);
+            }
+
+            matcher.appendTail(sb);
+
+            ret = sb.toString();
+
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("RangerRequestExprResolver.resolveExpressions(" + str + "): ret=" + ret);
+            }
+        }
+
+        return  ret;
+    }
+
+    /*
+     * replaces expressions in this.str with corresponding values in request (argument).
+     * For example, given the following:
+     *   1. this.str has value:                         "dept = '${{USER.dept}}'"
+     *   2. request.user has attribute dept with value: "marketing"
+     * This method returns: "dept = 'marketing'"
+     */
     public String resolveExpressions(RangerAccessRequest request) {
         String ret = str;
 
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 b6135b096..a7f48bb33 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
@@ -267,7 +267,11 @@ public class TestPolicyACLs {
 				} else if (!(MapUtils.isEmpty(acls.getRoleACLs()) && MapUtils.isEmpty(oneTest.rolePermissions))) {
 					roleACLsMatched = false;
 				}
-				assertTrue("getResourceACLs() failed! " + testCase.name + ":" + oneTest.name, userACLsMatched && groupACLsMatched && roleACLsMatched && rowFiltersMatched && dataMaskingMatched);
+				assertTrue("getResourceACLs() failed! " + testCase.name + ":" + oneTest.name + " - userACLsMatched", userACLsMatched);
+				assertTrue("getResourceACLs() failed! " + testCase.name + ":" + oneTest.name + " - groupACLsMatched", groupACLsMatched);
+				assertTrue("getResourceACLs() failed! " + testCase.name + ":" + oneTest.name + " - roleACLsMatched", roleACLsMatched);
+				assertTrue("getResourceACLs() failed! " + testCase.name + ":" + oneTest.name + " - rowFiltersMatched", rowFiltersMatched);
+				assertTrue("getResourceACLs() failed! " + testCase.name + ":" + oneTest.name + " - dataMaskingMatched", dataMaskingMatched);
 			}
 		}
 	}
diff --git a/agents-common/src/test/resources/policyengine/test_aclprovider_default.json b/agents-common/src/test/resources/policyengine/test_aclprovider_default.json
index 37f4f3c2d..8b799acff 100644
--- a/agents-common/src/test/resources/policyengine/test_aclprovider_default.json
+++ b/agents-common/src/test/resources/policyengine/test_aclprovider_default.json
@@ -394,6 +394,30 @@
                 "users": ["user1", "user2"]
               }
             ]
+          },
+          { "id": 21, "name": "db=user_{USER}, table=*, column=*", "isEnabled": true, "isAuditEnabled":  true, "isDenyAllElse": false,
+            "resources": { "database": { "values": [ "user_{USER}*" ] }, "table": { "values": [ "*" ] }, "column": { "values": [ "*" ] } },
+            "policyItems": [
+              { "accesses": [ { "type": "select" }, { "type": "update" } ],
+                "groups": [ "public" ]
+              }
+            ]
+          },
+          { "id": 22, "name": "db=dept_${{USER.dept}}, table=*, column=*", "isEnabled": true, "isAuditEnabled":  true, "isDenyAllElse": false,
+            "resources": { "database": { "values": [ "dept_${{USER.dept}}" ] }, "table": { "values": [ "*" ] }, "column": { "values": [ "*" ] } },
+            "policyItems": [
+              { "accesses": [ { "type": "select" } ],
+                "groups": [ "public", "engg" ]
+              }
+            ]
+          },
+          { "id": 23, "name": "db=dept_engg, table=*, column=*", "isEnabled": true, "isAuditEnabled":  true, "isDenyAllElse": false,
+            "resources": { "database": { "values": [ "dept_engg" ] }, "table": { "values": [ "*" ] }, "column": { "values": [ "*" ] } },
+            "policyItems": [
+              { "accesses": [ { "type": "select" } ],
+                "groups": [ "engg" ]
+              }
+            ]
           }
         ],
         "tagPolicies": {
@@ -557,6 +581,14 @@
       },
 
       "tests": [
+        { "name": "{USER} macro in database name",
+          "resource": { "elements": { "database": "user_madhan", "table": "test_tbl1" } },
+          "groupPermissions": { "public":  { "select":  { "result": 2, "isFinal": true }, "update": { "result": 2, "isFinal": true } } }
+        },
+        { "name": "${{USER.dept}} macro in database name",
+          "resource": { "elements": { "database": "dept_engg", "table": "test_tbl1" } },
+          "groupPermissions": { "public":  { "select":  { "result": 2, "isFinal": true } }, "engg":  { "select":  { "result": 1, "isFinal": true } } }
+        },
       {
         "name": "denyAllElse-test",
         "resource": {"elements":{"database":"denyAllElse", "table":"table-1", "column": "column-1" }},


[ranger] 01/02: RANGER-3828: plugin-nestedstructure with tagsync AtlasNestedStructureResourceMapper class

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 026be3eebd8a7cdd0d7f473674ce43550669a9b0
Author: Eckman, Barbara <ba...@cable.comcast.com>
AuthorDate: Wed Jul 13 18:52:51 2022 -0400

    RANGER-3828: plugin-nestedstructure with tagsync AtlasNestedStructureResourceMapper class
---
 .../plugin/store/EmbeddedServiceDefsUtil.java      |   7 +-
 .../ranger-servicedef-nestedstructure.json         | 186 +++++++++++
 dev-support/ranger-pmd-ruleset.xml                 |   1 +
 plugin-nestedstructure/.gitignore                  |   1 +
 plugin-nestedstructure/LICENSE                     | 202 ++++++++++++
 plugin-nestedstructure/NOTICE                      |  18 +
 plugin-nestedstructure/README.md                   | 168 ++++++++++
 plugin-nestedstructure/conf/log4j.properties       |  37 +++
 .../conf/ranger-nestedstructure-audit.xml          |  39 +++
 .../conf/ranger-nestedstructure-policymgr-ssl.xml  |  50 +++
 .../conf/ranger-nestedstructure-security.xml       |  62 ++++
 plugin-nestedstructure/pom.xml                     | 111 +++++++
 .../nestedstructure/authorizer/AccessResult.java   |  54 +++
 .../nestedstructure/authorizer/DataMasker.java     | 283 ++++++++++++++++
 .../authorizer/FieldLevelAccess.java               |  62 ++++
 .../authorizer/JsonManipulator.java                | 168 ++++++++++
 .../nestedstructure/authorizer/MaskTypes.java      |  54 +++
 .../authorizer/MaskingException.java               |  32 ++
 .../authorizer/NestedStructureAccessType.java      |  45 +++
 .../authorizer/NestedStructureAuditHandler.java    |  95 ++++++
 .../authorizer/NestedStructureAuthorizer.java      | 285 ++++++++++++++++
 .../authorizer/NestedStructureResource.java        |  46 +++
 .../authorizer/NestedStructureService.java         |  37 +++
 .../authorizer/RecordFilterJavaScript.java         | 110 +++++++
 .../nestedstructure/authorizer/ExampleClient.java  |  47 +++
 .../nestedstructure/authorizer/TestDataMasker.java | 266 +++++++++++++++
 .../authorizer/TestJsonManipulator.java            | 363 +++++++++++++++++++++
 .../authorizer/TestNestedStructureAuthorizer.java  | 105 ++++++
 .../authorizer/TestRecordFilterJavaScript.java     |  66 ++++
 .../test/resources/servicedef-nestedstructure.json | 186 +++++++++++
 .../src/test/resources/test_customer_records.json  | 164 ++++++++++
 pom.xml                                            |  14 +
 .../AtlasNestedStructureResourceMapper.java        |  95 ++++++
 .../nestedstructureplugin/ResourceTests.java       | 139 ++++++++
 34 files changed, 3597 insertions(+), 1 deletion(-)

diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java b/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java
index db629c85d..79e09a1a2 100755
--- a/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java
@@ -49,7 +49,7 @@ public class EmbeddedServiceDefsUtil {
 
 
 	// following servicedef list should be reviewed/updated whenever a new embedded service-def is added
-	public static final String DEFAULT_BOOTSTRAP_SERVICEDEF_LIST = "tag,hdfs,hbase,hive,kms,knox,storm,yarn,kafka,solr,atlas,nifi,nifi-registry,sqoop,kylin,elasticsearch,presto,trino,ozone,kudu,schema-registry";
+	public static final String DEFAULT_BOOTSTRAP_SERVICEDEF_LIST = "tag,hdfs,hbase,hive,kms,knox,storm,yarn,kafka,solr,atlas,nifi,nifi-registry,sqoop,kylin,elasticsearch,presto,trino,ozone,kudu,schema-registry,nestedstructure";
 	private static final String PROPERTY_SUPPORTED_SERVICE_DEFS = "ranger.supportedcomponents";
 	private Set<String> supportedServiceDefs;
 	public static final String EMBEDDED_SERVICEDEF_TAG_NAME  = "tag";
@@ -75,6 +75,7 @@ public class EmbeddedServiceDefsUtil {
 	public static final String EMBEDDED_SERVICEDEF_TRINO_NAME  = "trino";
 	public static final String EMBEDDED_SERVICEDEF_OZONE_NAME  = "ozone";
 	public static final String EMBEDDED_SERVICEDEF_KUDU_NAME  = "kudu";
+	public static final String EMBEDDED_SERVICEDEF_NESTEDSTRUCTURE_NAME  = "nestedstructure";
 
 	public static final String PROPERTY_CREATE_EMBEDDED_SERVICE_DEFS = "ranger.service.store.create.embedded.service-defs";
 
@@ -120,6 +121,7 @@ public class EmbeddedServiceDefsUtil {
 	private RangerServiceDef trinoServiceDef;
 	private RangerServiceDef ozoneServiceDef;
 	private RangerServiceDef kuduServiceDef;
+	private RangerServiceDef nestedStructureServiveDef;
 
 	private RangerServiceDef tagServiceDef;
 
@@ -171,6 +173,7 @@ public class EmbeddedServiceDefsUtil {
 			prestoServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_PRESTO_NAME);
 			ozoneServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_OZONE_NAME);
 			kuduServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_KUDU_NAME);
+			nestedStructureServiveDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_NESTEDSTRUCTURE_NAME);
 
 			// Ensure that tag service def is updated with access types of all service defs
 			store.updateTagServiceDefForAccessTypes();
@@ -258,6 +261,8 @@ public class EmbeddedServiceDefsUtil {
 
 	public long getKuduServiceDefId() { return getId(kuduServiceDef); }
 
+	public long getNestedStructureServiceDefId() { return getId(nestedStructureServiveDef); }
+
 	public RangerServiceDef getEmbeddedServiceDef(String defType) throws Exception {
 		RangerServiceDef serviceDef=null;
 		if(StringUtils.isNotEmpty(defType)){
diff --git a/agents-common/src/main/resources/service-defs/ranger-servicedef-nestedstructure.json b/agents-common/src/main/resources/service-defs/ranger-servicedef-nestedstructure.json
new file mode 100644
index 000000000..dc6b1d32d
--- /dev/null
+++ b/agents-common/src/main/resources/service-defs/ranger-servicedef-nestedstructure.json
@@ -0,0 +1,186 @@
+{
+  "name":        "nestedstructure",
+  "displayName": "nestedstructure",
+  "implClass":   "",
+  "label":       "NestedStructure",
+  "description": "Plugin to enforce READ and WRITE access control on nested structures such as JSON response objects from microservice API resource calls",
+  "options": {
+    "enableDenyAndExceptionsInPolicies": "true"
+  },
+  "configs": [
+    { "itemId": 1, "name": "commonNameForCertificate",   "type": "string", "mandatory": false },
+    { "itemId": 2, "name": "policy.download.auth.users", "type": "string", "mandatory": false }
+  ],
+  "resources": [
+    {
+      "itemId":                 1,
+      "name":                   "schema",
+      "type":                   "string",
+      "level":                  10,
+      "mandatory":              true,
+      "lookupSupported":        false,
+      "recursiveSupported":     false,
+      "excludesSupported":      true,
+      "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+      "matcherOptions":         { "wildCard": "true", "ignoreCase": "true" },
+      "label":                  "NestedStructure Schema",
+      "description":            "Schema of the nested structure returned from Microservice GET, etc",
+      "accessTypeRestrictions": [],
+      "isValidLeaf":            true
+    },
+    {
+      "itemId":                  2,
+      "name":                   "field",
+      "type":                   "string",
+      "level":                  20,
+      "parent":                 "schema",
+      "mandatory":              true,
+      "lookupSupported":        false,
+      "recursiveSupported":     false,
+      "excludesSupported":      true,
+      "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+      "matcherOptions":         { "wildCard": "true", "ignoreCase": "true" },
+      "label":                  "NestedStructure Schema Field",
+      "description":            "NestedStructure Schema Field",
+      "accessTypeRestrictions": [],
+      "isValidLeaf":            true
+    }
+  ],
+  "accessTypes": [
+    { "itemId": 1, "name": "read",  "label": "Read" },
+    { "itemId": 2, "name": "write", "label": "Write" }
+  ],
+  "policyConditions": [],
+  "contextEnrichers": [],
+  "enums":            [],
+  "dataMaskDef": {
+    "maskTypes": [
+      {
+        "itemId":          1,
+        "name":            "MASK",
+        "label":           "Redact",
+        "description":     "Replace lowercase with 'x', uppercase with 'X', digits with '0'",
+        "transformer":     "mask({field})",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          2,
+        "name":            "MASK_SHOW_LAST_4",
+        "label":           "Partial mask: show last 4",
+        "description":     "Show last 4 characters; replace rest with 'x'",
+        "transformer":     "mask_show_last_n({field}, 4, 'x', 'x', 'x', -1, '1')",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          3,
+        "name":            "MASK_SHOW_FIRST_4",
+        "label":           "Partial mask: show first 4",
+        "description":     "Show first 4 characters; replace rest with 'x'",
+        "transformer":     "mask_show_first_n({field}, 4, 'x', 'x', 'x', -1, '1')",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          4,
+        "name":            "MASK_HASH",
+        "label":           "Hash",
+        "description":     "Hash the value",
+        "transformer":     "mask_hash({field})",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          5,
+        "name":            "MASK_NULL",
+        "label":           "Nullify",
+        "description":     "Replace with NULL",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          6,
+        "name":            "MASK_NONE",
+        "label":           "Unmasked (retain original value)",
+        "description":     "No masking",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          12,
+        "name":            "MASK_DATE_SHOW_YEAR",
+        "label":           "Date: show only year",
+        "description":     "Date: show only year",
+        "transformer":     "mask({field}, 'x', 'x', 'x', -1, '1', 1, 0, -1)",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          13,
+        "name":            "CUSTOM",
+        "label":           "Custom",
+        "description":     "Custom",
+        "dataMaskOptions": {}
+      }
+    ],
+    "accessTypes": [
+      { "itemId": 1, "name":  "read", "label": "Read" }
+    ],
+    "resources": [
+      {
+        "itemId":                 1,
+        "name":                   "schema",
+        "type":                   "string",
+        "level":                  10,
+        "mandatory":              true,
+        "lookupSupported":        false,
+        "recursiveSupported":     false,
+        "excludesSupported":      false,
+        "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+        "matcherOptions":         { "wildCard": "false", "ignoreCase": "true" },
+        "uiHint":                 "{ \"singleValue\":true }",
+        "label":                  "NestedStructure Schema",
+        "description":            "NestedStructure Schema returned from Microservice GET, etc",
+        "accessTypeRestrictions": [],
+        "isValidLeaf":            false
+      },
+      {
+        "itemId":                 2,
+        "name":                   "field",
+        "type":                   "string",
+        "level":                  20,
+        "parent":                 "schema",
+        "mandatory":              true,
+        "lookupSupported":        false,
+        "recursiveSupported":     false,
+        "excludesSupported":      false,
+        "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+        "matcherOptions":         { "wildCard": "false", "ignoreCase": "true" },
+        "uiHint":                 "{ \"singleValue\":true }",
+        "label":                  "NestedStructure Schema Field",
+        "description":            "NestedStructure Schema Field",
+        "accessTypeRestrictions": [],
+        "isValidLeaf":            true
+      }
+    ]
+  },
+  "rowFilterDef": {
+    "accessTypes": [
+      { "itemId": 1, "name": "read",  "label": "Read"  },
+      { "itemId": 2, "name": "write", "label": "Write" }
+    ],
+    "resources": [
+      {
+        "itemId":                 1,
+        "name":                   "schema",
+        "type":                   "string",
+        "level":                  10,
+        "mandatory":              true,
+        "lookupSupported":        false,
+        "recursiveSupported":     false,
+        "excludesSupported":      false,
+        "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+        "matcherOptions":         { "wildCard": "false", "ignoreCase": "true" },
+        "uiHint":                 "{ \"singleValue\":true }",
+        "label":                  "NestedStructure Schema",
+        "description":            "NestedStructure Schema returned from Microservice GET, etc",
+        "accessTypeRestrictions": [],
+        "isValidLeaf":            true
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/dev-support/ranger-pmd-ruleset.xml b/dev-support/ranger-pmd-ruleset.xml
index 88d77f236..2899f7ca9 100644
--- a/dev-support/ranger-pmd-ruleset.xml
+++ b/dev-support/ranger-pmd-ruleset.xml
@@ -94,6 +94,7 @@
     <exclude name="AvoidReassigningLoopVariables" />
     <exclude name="JUnitTestContainsTooManyAsserts" />
     <exclude name="JUnitTestsShouldIncludeAssert" />
+    <exclude name="JUnit4TestShouldUseTestAnnotation" />
     <exclude name="UseAssertSameInsteadOfAssertTrue" />
     <exclude name="UseAssertNullInsteadOfAssertTrue" />
     <exclude name="UseAssertTrueInsteadOfAssertEquals" />
diff --git a/plugin-nestedstructure/.gitignore b/plugin-nestedstructure/.gitignore
new file mode 100644
index 000000000..b83d22266
--- /dev/null
+++ b/plugin-nestedstructure/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/plugin-nestedstructure/LICENSE b/plugin-nestedstructure/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/plugin-nestedstructure/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/plugin-nestedstructure/NOTICE b/plugin-nestedstructure/NOTICE
new file mode 100644
index 000000000..8772a4fba
--- /dev/null
+++ b/plugin-nestedstructure/NOTICE
@@ -0,0 +1,18 @@
+Apache Ranger Nestedstructure Plugin
+
+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.
+S
+PDX-License-Identifier: Apache-2.0
+
+This product includes software developed at Comcast (http://www.comcast.com/).
diff --git a/plugin-nestedstructure/README.md b/plugin-nestedstructure/README.md
new file mode 100644
index 000000000..ea878f6a2
--- /dev/null
+++ b/plugin-nestedstructure/README.md
@@ -0,0 +1,168 @@
+<!---
+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-api-plugin
+
+## License
+Licensed under the Apache License, Version 2.0.   You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+## Usage
+
+Simply call `NestedStructureAuthorizer.authorize()` to determine if the user has access to the JSON record and fields.
+The result will indicate if the user is authorized. The result also contains the JSON string the user is authorized view, for example by masking fields as specified in Apache Ranger policies.
+```java
+String      schema     = "json_object.cxt.cmt.product.vnull3";
+String      userName   = "beckma200";
+Set<String> userGroups = new HashSet<>();
+String      jsonString = ...;
+
+AccessResult result = NestedStructureAuthorizer.getInstance().authorize(schema, userName, userGroups, jsonString, NestedStructureAccessType.READ);
+
+String authorizedJsonString = result.hasAccess() ? result.getJson() : null;
+```
+
+An example client is included at `org.apache.ranger.authorization.nestedstructure.authorizer.ExampleClient`
+
+## Configuration Items
+The classpath needs to contain 3 files, `ranger-nestedstructure-audit.xml`,
+`ranger-nestedstructure-policymgr-ssl.xml`, and `ranger-nestedstructure-security.xml`.
+Each of these files need to edited in each deployment.
+Other required files do not need edits and are included in the jar file.
+
+**ranger-nestedstructure-security.xml**
+- `ranger.plugin.nestedstructure.policy.rest.url` should be set to the correct audit location (prod vs integration).
+
+
+**ranger-nestedstructure-audit.xml**
+- `xasecure.audit.destination.solr.urls` should be set to the correct audit location (prod vs integration).
+
+**ranger-nestedstructure-policymgr-ssl.xml**
+- `xasecure.policymgr.clientssl.keystore` should be set to location of the `ranger-plugin-keystore.p12` file.
+- `xasecure.policymgr.clientssl.keystore.credential.file` should be set to the location of `ranger.jceks` file.
+- `xasecure.policymgr.clientssl.truststore` should be set to location of the `global-truststore.p12` file.
+- `xasecure.policymgr.clientssl.truststore.credential.file` should be set to the location of the `ranger.jceks` file.
+
+
+## Example of Permissions of Masking Different Fields
+
+```json
+{
+  "store": {
+    "book": [
+      {
+        "category": "reference",
+        "author": "Nigel Rees",
+        "title": "Sayings of the Century",
+        "price": 8.95
+      },
+      {
+        "category": "fiction",
+        "author": "Evelyn Waugh",
+        "title": "Sword of Honour",
+        "price": 12.99
+      },
+      {
+        "category": "fiction",
+        "author": "Herman Melville",
+        "title": "Moby Dick",
+        "isbn": "0-553-21311-3",
+        "price": 8.99
+        },
+      {
+        "category": "fiction",
+        "author": "J. R. R. Tolkien",
+        "title": "The Lord of the Rings",
+        "isbn": "0-395-19395-8",
+        "price": 22.99
+      }
+    ],
+    "bicycle": {
+      "color": "red",
+      "price": 19.95
+    }
+  },
+  "expensive": 10
+}
+```
+
+##### Arrays
+Arrays require the user to specify that all elements of the array should be considered. The addition of an asterisk `*` is required.
+To restrict by book price, specify one of the following values in Apache Ranger Policy for resource `field`:
+
+- `store.book[*]price < 100`
+- `store.book.*.price < 100`
+
+##### Maps
+Simple dot `.` syntax is all that is required.
+To restrict the color of the bicycle use in Ranger
+`store.bicycle.color`
+
+### Masking
+Only primitive types (numbers, booleans, and strings) can be masked.
+Elements inside arrays and maps will be masked at a field level.
+
+Note that at this time, masking a container is NOT possible.  Each element has to be individually masked.
+
+If the mask type is not applicable to the datatype, a default mask of `NULL` will be used.
+
+#### Mask Types
+* MASK
+  * Replaces entire String with `*`.
+  * Replaces Number with `-11111`
+  * Ensures resulting String has length of 5 of more
+  * Replaces Booleans with false
+  * Supported types: String, Boolean, Number
+* MASK_SHOW_LAST_4
+  * Replaces all but the last four characters of a string with `x`
+  * Supported types: String
+* MASK_SHOW_FIRST_4
+  * Replaces all except the first four characters of a string with `x`
+  * Supported types: String
+* MASK_HASH
+  * Replaces string with a SHA256 hash of the string
+  * Supported types: String
+* CUSTOM
+  * Replaces value with a custom specified value of the same type
+  * Supported types: String, Boolean, Number
+* MASK_NULL
+  * Replaces value with `null`
+  * Supported types: String, Boolean, Number
+* MASK_NONE
+  * Returns the value without changing it
+  * Supported types: String, Boolean, Number
+* MASK_DATE_SHOW_YEAR
+  * Replaces a parsable date with only the year parsed from the date.
+  * The table below lists the supported date formats.
+  * For more information on date formats see [DateFormatter](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) documentation.
+  * <table>
+     <tr><th>Format</th><th>Description</th><th>Example</th></tr>
+     <tr><td>BASIC_ISO_DATE</td><td>Basic ISO date</td><td>'20111203'</td></tr>
+     <tr><td>ISO_LOCAL_DATE	ISO</td><td>Local Date</td><td>'2011-12-03'</td></tr>
+     <tr><td>ISO_OFFSET_DATE</td><td>ISO Date with offset</td><td>''2011-12-03+01:00'</td></tr>
+     <tr><td>ISO_DATE</td><td>ISO Date with or without offset</td><td>'2011-12-03+01:00'; '2011-12-03'</td></tr>
+     <tr><td>ISO_LOCAL_DATE_TIME</td><td>ISO Local Date and Time</td><td>'2011-12-03T10:15:30'</td></tr>
+     <tr><td>ISO_OFFSET_DATE_TIME</td><td>Date Time with Offset</td><td>'2011-12-03T10:15:30+01:00'</td></tr>
+     <tr><td>ISO_ZONED_DATE_TIME</td><td>Zoned Date Time</td><td>'2011-12-03T10:15:30+01:00[Europe/Paris]'</td></tr>
+     <tr><td>ISO_DATE_TIME</td><td>Date and time with ZoneId</td><td>'2011-12-03T10:15:30+01:00[Europe/Paris]'</td></tr>
+     <tr><td>ISO_ORDINAL_DATE</td><td>Year and day of year/td><td>'2012-337'</td></tr>
+     <tr><td>ISO_WEEK_DATE</td><td>Year and Week</td><td>'2012-W48-6'</td></tr>
+     <tr><td>ISO_INSTANT</td><td>Date and Time of an Instant</td><td>'2011-12-03T10:15:30Z'</td></tr>
+     <tr><td>RFC_1123_DATE_TIME</td><td>RFC 1123 / RFC 822</td><td>'Tue, 3 Jun 2008 11:05:30 GMT'</td></tr>
+     </table>
+
diff --git a/plugin-nestedstructure/conf/log4j.properties b/plugin-nestedstructure/conf/log4j.properties
new file mode 100644
index 000000000..f01052b34
--- /dev/null
+++ b/plugin-nestedstructure/conf/log4j.properties
@@ -0,0 +1,37 @@
+# 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.
+
+log4j.rootLogger=WARN,CONSOLE,ROLLING
+
+#log4j.logger.org.apache=WARN
+#log4j.logger.org.omg=WARN
+#log4j.logger.com.amazonaws.services.kinesis=DEBUG
+
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
+
+# Pattern to output the caller's file name and line number.
+log4j.appender.CONSOLE.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
+
+log4j.appender.ROLLING=org.apache.log4j.RollingFileAppender
+log4j.appender.ROLLING.File=kvs-gateway.log
+
+log4j.appender.ROLLING.MaxFileSize=100MB
+# Keep one backup file
+log4j.appender.ROLLING.MaxBackupIndex=1
+
+log4j.appender.ROLLING.layout=org.apache.log4j.PatternLayout
+log4j.appender.ROLLING.layout.ConversionPattern=%p %t %c - %m%n
+
diff --git a/plugin-nestedstructure/conf/ranger-nestedstructure-audit.xml b/plugin-nestedstructure/conf/ranger-nestedstructure-audit.xml
new file mode 100644
index 000000000..9eb0b22f6
--- /dev/null
+++ b/plugin-nestedstructure/conf/ranger-nestedstructure-audit.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration xmlns:xi="http://www.w3.org/2001/XInclude">
+	<!-- Ranger audit provider configuration -->
+	<property>
+		<name>xasecure.audit.destination.solr</name>
+		<value>true</value>
+	</property>
+	<property>
+		<name>xasecure.audit.destination.solr.async.max.queue.size</name>
+		<value>100</value>
+	</property>
+	<property>
+		<name>xasecure.audit.destination.solr.async.max.flush.interval.ms</name>
+		<value>1000</value>
+	</property>
+	<property>
+		<name>xasecure.audit.destination.solr.ssl.checkPeerName</name>
+		<value>false</value>
+	</property>
+	<property>
+		<name>xasecure.audit.destination.solr.urls</name>
+		<value></value>
+	</property>
+</configuration>
\ No newline at end of file
diff --git a/plugin-nestedstructure/conf/ranger-nestedstructure-policymgr-ssl.xml b/plugin-nestedstructure/conf/ranger-nestedstructure-policymgr-ssl.xml
new file mode 100644
index 000000000..092546f53
--- /dev/null
+++ b/plugin-nestedstructure/conf/ranger-nestedstructure-policymgr-ssl.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+        <!--
+          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.
+        -->
+
+<configuration>
+    <property>
+        <name>xasecure.policymgr.clientssl.keystore</name>
+        <value>/var/ranger/security/ranger-plugin-keystore.p12</value>
+    </property>
+    <property>
+        <name>xasecure.policymgr.clientssl.keystore.credential.file</name>
+        <value>jceks://file/var/ranger/security/ranger.jceks</value>
+    </property>
+    <property>
+        <name>xasecure.policymgr.clientssl.keystore.password</name>
+        <value>none</value>
+    </property>
+    <property>
+        <name>xasecure.policymgr.clientssl.keystore.type</name>
+        <value>pkcs12</value>
+    </property>
+    <property>
+        <name>xasecure.policymgr.clientssl.truststore</name>
+        <value>/var/ranger/security/global-truststore.p12</value>
+    </property>
+   <property>
+        <name>xasecure.policymgr.clientssl.truststore.credential.file</name>
+        <value>jceks://file/var/ranger/security/ranger.jceks</value>
+    </property>
+   <property>
+        <name>xasecure.policymgr.clientssl.truststore.password</name>
+        <value>none</value>
+    </property>
+   <property>
+        <name>xasecure.policymgr.clientssl.truststore.type</name>
+        <value>pkcs12</value>
+    </property>
+</configuration>
\ No newline at end of file
diff --git a/plugin-nestedstructure/conf/ranger-nestedstructure-security.xml b/plugin-nestedstructure/conf/ranger-nestedstructure-security.xml
new file mode 100644
index 000000000..9be584186
--- /dev/null
+++ b/plugin-nestedstructure/conf/ranger-nestedstructure-security.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<configuration xmlns:xi="http://www.w3.org/2001/XInclude">
+	<property>
+		<name>ranger.plugin.nestedstructure.policy.rest.url</name>
+		<value></value>
+		<description>
+			URL to Ranger Admin
+		</description>
+	</property>
+	<property>
+		<name>ranger.plugin.nestedstructure.service.name</name>
+		<value>privacera_nestedstructure</value>
+		<description>
+			Name of the Ranger service containing policies for this SampleApp instance
+		</description>
+	</property>
+	<property>
+		<name>ranger.plugin.nestedstructure.policy.source.impl</name>
+		<value>org.apache.ranger.admin.client.RangerAdminRESTClient</value>
+		<description>
+			Class to retrieve policies from the source
+		</description>
+	</property>
+	<property>
+		<name>ranger.plugin.nestedstructure.policy.cache.dir</name>
+			<value>/tmp</value>
+		<description>
+			Directory where Ranger policies are cached after successful retrieval from the source
+		</description>
+	</property>
+	<property>
+		<name>ranger.plugin.nestedstructure.policy.rest.client.connection.timeoutMs</name>
+		<value>120000</value>
+		<description>
+			RangerRestClient Connection Timeout in Milli Seconds
+		</description>
+	</property>
+	<property>
+		<name>ranger.plugin.nestedstructure.policy.rest.client.read.timeoutMs</name>
+		<value>30000</value>
+		<description>
+			RangerRestClient read Timeout in Milli Seconds
+		</description>
+	</property>
+</configuration>
\ No newline at end of file
diff --git a/plugin-nestedstructure/pom.xml b/plugin-nestedstructure/pom.xml
new file mode 100644
index 000000000..63f57ea06
--- /dev/null
+++ b/plugin-nestedstructure/pom.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ranger</artifactId>
+        <groupId>org.apache.ranger</groupId>
+        <version>3.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ranger-nestedstructure-plugin</artifactId>
+    <name>NestedStructure Security Plugin</name>
+    <description>NestedStructure Security Plugin</description>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.ranger</groupId>
+                <artifactId>ranger-plugins-common</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>commons-codec</groupId>
+                <artifactId>commons-codec</artifactId>
+                <version>1.11</version>
+            </dependency>
+            <dependency>
+                <groupId>commons-lang</groupId>
+                <artifactId>commons-lang</artifactId>
+                <version>2.6</version>
+            </dependency>
+            <dependency>
+                <groupId>commons-logging</groupId>
+                <artifactId>commons-logging</artifactId>
+                <version>1.1.1</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.logging.log4j</groupId>
+                <artifactId>log4j-core</artifactId>
+                <version>${log4j2.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.code.gson</groupId>
+                <artifactId>gson</artifactId>
+                <version>${gson.version}</version>
+                <scope>compile</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.jayway.jsonpath</groupId>
+                <artifactId>json-path</artifactId>
+                <version>2.4.0</version>
+            </dependency>
+            <dependency>
+                <groupId>org.openjdk.nashorn</groupId>
+                <artifactId>nashorn-core</artifactId>
+                <version>15.4</version>
+            </dependency>
+            <dependency>
+                <groupId>org.testng</groupId>
+                <artifactId>testng</artifactId>
+                <version>${testng.version}</version>
+                <scope>test</scope>
+            </dependency>
+        </dependencies>
+
+        <build>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven.surefire.plugin.version}</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>3.3</version>
+                    <inherited>true</inherited>
+                    <configuration>
+                        <useIncrementalCompilation>false</useIncrementalCompilation>
+                        <source>${javac.source.version}</source>
+                        <target>${javac.target.version}</target>
+                        <encoding>UTF-8</encoding>
+                        <maxmem>1024m</maxmem>
+                        <fork>true</fork>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </build>
+</project>
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/AccessResult.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/AccessResult.java
new file mode 100644
index 000000000..bf4a32501
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/AccessResult.java
@@ -0,0 +1,54 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The response of the API when checking for authorizing and masking fields that are not authorized.
+ **/
+public class AccessResult {
+    private final boolean         hasAccess;
+    private final String          json;
+    private final List<Exception> errors = new ArrayList<>();
+
+    public AccessResult(boolean hasAccess, String json) {
+        this.hasAccess = hasAccess;
+        this.json      = json;
+    }
+
+    public boolean hasAccess() {
+        return hasAccess;
+    }
+
+    public String getJson() {
+        return json;
+    }
+
+    public boolean hasErrors() { return errors.size() > 0; }
+
+    public AccessResult addError(Exception e){
+        errors.add(e);
+
+        return this;
+    }
+
+    public List<Exception> getErrors() { return errors; }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/DataMasker.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/DataMasker.java
new file mode 100644
index 000000000..f630799b0
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/DataMasker.java
@@ -0,0 +1,283 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Masks the 3 primitive types of json: strings, numbers, and booleans
+ * Not all mask types are supported on each type.  For example hashing a boolean doesn't make sense.
+ * When masking dates, a subset of the date formats defined in {@link DateTimeFormatter} are allowed.
+ * See DataMasker.SUPPORTED_DATE_FORMATS for the list of supported formats.
+ */
+public class DataMasker {
+    /**
+     * The default numberic value when masking and no other value is defined.
+     */
+    static final Number DEFAULT_NUMBER_MASK = new Long(-11111);
+
+    /**
+     * The default boolean value when masking and no other value is defined.
+     */
+    static final Boolean DEFAULT_BOOLEAN_MASK = false;
+
+    /**
+     * A list of the supported date formats that can be masked.
+     */
+    static final List<DateTimeFormatter> SUPPORTED_DATE_FORMATS;
+
+    static {
+        SUPPORTED_DATE_FORMATS = Arrays.asList(
+                DateTimeFormatter.BASIC_ISO_DATE,
+                DateTimeFormatter.ISO_LOCAL_DATE,
+                DateTimeFormatter.ISO_OFFSET_DATE,
+                DateTimeFormatter.ISO_DATE,
+                DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+                DateTimeFormatter.ISO_OFFSET_DATE_TIME,
+                DateTimeFormatter.ISO_ZONED_DATE_TIME,
+                DateTimeFormatter.ISO_DATE_TIME,
+                DateTimeFormatter.ISO_ORDINAL_DATE,
+                DateTimeFormatter.ISO_WEEK_DATE,
+                DateTimeFormatter.ISO_INSTANT,
+                DateTimeFormatter.RFC_1123_DATE_TIME
+        );
+    }
+
+    /**
+     * Masks a boolean value if applicable
+     * @param value input value
+     * @param maskType type of masking
+     * @param customMaskValueStr string representation of a boolean if applicable
+     * @return the masked value
+     */
+    public static Boolean maskBoolean(Boolean value, String maskType, String customMaskValueStr) {
+        if (maskType == null){
+            throw new MaskingException("boolean doesn't support mask type: " + maskType);
+        }
+
+        final Boolean ret;
+
+        switch (maskType) {
+            case MaskTypes.MASK:
+                ret = DEFAULT_BOOLEAN_MASK;
+                break;
+
+            case MaskTypes.MASK_NULL:
+                // replace with NULL
+                ret = null;
+                break;
+
+            case MaskTypes.MASK_NONE:
+                // noop, same as no mask
+                ret = value;
+                break;
+
+            case MaskTypes.CUSTOM: {
+                Boolean customMaskValue = DEFAULT_BOOLEAN_MASK;
+
+                try {
+                    customMaskValue = Boolean.parseBoolean(customMaskValueStr);
+                } catch (Exception e) {
+                    // ignore
+                }
+
+                // already done by the policy
+                ret = customMaskValue;
+            }
+                break;
+
+            default:
+                // raise error, error message "unknown mask type"
+                throw new MaskingException("boolean doesn't support mask type: " + maskType);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Masks a number value if applicable
+     * @param value input value of the number
+     * @param maskType type of masking
+     * @param customMaskValueStr string representation of a number if application
+     * @return the masked value
+     */
+    public static Number maskNumber(Number value, String maskType, String customMaskValueStr) {
+        if (maskType == null){
+            throw new MaskingException("number doesn't support mask type: " + maskType);
+        }
+
+        final Number ret;
+
+        switch (maskType) {
+            case MaskTypes.MASK:
+                ret= DEFAULT_NUMBER_MASK;
+                break;
+
+            case MaskTypes.MASK_NULL:
+                // replace with NULL
+                ret = null;
+                break;
+
+            case MaskTypes.MASK_NONE:
+                // noop, same as no mask
+                ret = value;
+                break;
+
+            case MaskTypes.CUSTOM: {
+                try {
+                    ret = Long.parseLong(customMaskValueStr);
+                } catch (Exception e) {
+                    throw new MaskingException("unable to extract number from custom mask value: " + customMaskValueStr, e);
+                }
+            }
+                break;
+
+            default:
+                // raise error, error message "unknown mask type"
+                throw new MaskingException("number doesn't support mask type: " + maskType);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Masks a string value if applicable
+     * @param value the input value of the string
+     * @param maskType type of masking
+     * @param customMaskValue string value if using custom masking
+     * @return the masked value
+     */
+    public static String maskString(String value, String maskType, String customMaskValue) {
+        if (maskType == null){
+            throw new MaskingException("string doesn't support mask type: " + maskType);
+        }
+
+        final String ret;
+
+        switch (maskType) {
+            case MaskTypes.MASK:
+                ret = generateMask(value);
+                break;
+
+            case MaskTypes.MASK_SHOW_LAST_4:
+                // "Show last 4 characters; replace rest with 'x'"
+                ret = showLastFour(value);
+                break;
+
+            case MaskTypes.MASK_SHOW_FIRST_4:
+                // "Show first 4 characters; replace rest with 'x'",
+                ret = showFirstFour(value);
+                break;
+
+            case MaskTypes.MASK_HASH:
+                // "Hash the value"
+                //can't hash null, so give it a consistent null string to hash
+                ret = DigestUtils.sha256Hex(value == null ? "null" : value);
+                break;
+
+            case MaskTypes.MASK_NULL:
+                // replace with NULL
+                ret = null;
+                break;
+
+            case MaskTypes.MASK_NONE:
+                // noop, same as no mask
+                ret = value;
+                break;
+
+            case MaskTypes.MASK_DATE_SHOW_YEAR:
+                //"Date: show only year",
+                ret = maskYear(value);
+                break;
+
+            case MaskTypes.CUSTOM:
+                // already done by the policy
+                ret = customMaskValue;
+                break;
+
+            default:
+                // raise error, error message "unknown mask type"
+                throw new MaskingException("string doesn't support mask type: " + maskType);
+        }
+
+        return ret;
+    }
+
+    private static String generateMask(String value) {
+        // to do : take an object as param, identify whether it's an array, string or number;
+        // number return -1111111111; array : mask each element
+        //number of **** will be between MIN and MAX MASK LENGTH
+        int maskedValueLen = StringUtils.length(value);
+
+        if (maskedValueLen < MaskTypes.MIN_MASK_LENGTH) {
+            maskedValueLen = MaskTypes.MIN_MASK_LENGTH;
+        } else if (maskedValueLen > MaskTypes.MAX_MASK_LENGTH) {
+            maskedValueLen = MaskTypes.MAX_MASK_LENGTH;
+        }
+
+        return StringUtils.repeat("*", maskedValueLen);
+    }
+
+    private static String showLastFour(String value){
+        int length = StringUtils.length(value);
+
+        return length <= 4 ? value : StringUtils.repeat("x", length - 4) + value.substring(length - 4);
+    }
+
+    private static String showFirstFour(String value){
+        int length = StringUtils.length(value);
+
+        return length <= 4 ? value : value.substring(0, 4) + StringUtils.repeat("x", length - 4);
+    }
+
+    private static String maskYear(String value){
+        String ret = null;
+
+        if (StringUtils.isEmpty(value)) {
+            ret = value;
+        } else {
+            for (DateTimeFormatter dateFormat : SUPPORTED_DATE_FORMATS) {
+                try {
+                    TemporalAccessor temporalAccessor = dateFormat.parse(value);
+                    LocalDate        localDateTime    = LocalDate.from(temporalAccessor);
+
+                    ret = localDateTime.format(DateTimeFormatter.ofPattern("yyyy"));
+
+                    break;
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+
+            if (ret == null) {
+                throw new MaskingException("Unable to mask year, unsupported date format: " + value +
+                        ". See documentation for supported date formats.");
+            }
+        }
+
+        return ret;
+    }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/FieldLevelAccess.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/FieldLevelAccess.java
new file mode 100644
index 000000000..0d27d36ec
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/FieldLevelAccess.java
@@ -0,0 +1,62 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+/**
+ * An internal structure.  It notes if a specific field is authorized, is masked and what type of masking it is.
+ * If written in scala, this would have been a case class.
+ **/
+class FieldLevelAccess {
+    final String  field;
+    final boolean hasAccess;
+    final Long    maskPolicyId;
+    final boolean isMasked;
+    final String  customMaskedValue;
+    final String  maskType;
+
+    public FieldLevelAccess(String field, boolean hasAccess, Long maskPolicyId, boolean isMasked,
+                            String maskType, String customMaskedValue) {
+        this.field             = field;
+        this.hasAccess         = hasAccess;
+        this.maskPolicyId      = maskPolicyId;
+        this.isMasked          = isMasked;
+        this.maskType          = maskType;
+        this.customMaskedValue = customMaskedValue;
+    }
+
+    public String getCustomMaskedValue() {
+        return customMaskedValue;
+    }
+
+    public String getField() {
+        return field;
+    }
+
+    public boolean isHasAccess() {
+        return hasAccess;
+    }
+
+    public Long getMaskPolicyId() {
+        return maskPolicyId;
+    }
+
+    public boolean isMasked() {
+        return isMasked;
+    }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/JsonManipulator.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/JsonManipulator.java
new file mode 100644
index 000000000..57030f277
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/JsonManipulator.java
@@ -0,0 +1,168 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.DocumentContext;
+import com.jayway.jsonpath.JsonPath;
+import com.jayway.jsonpath.Option;
+
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+* Accepts a json string, parses it into a {@link DocumentContext}.
+ * Individual fields can be updated in the {@link DocumentContext}.
+ * And a new json string can be obtained.
+**/
+public class JsonManipulator {
+    /**
+    the overall document
+     **/
+    private final DocumentContext documentContext;
+
+    /**
+    a {@link DocumentContext} that specializes in returning field names (not values)
+     **/
+    private final DocumentContext fieldContextDocument;
+
+    private final Set<String> fields;
+
+
+    /**
+     *
+     * @param jsonString json to be parsed and masked
+     */
+    public JsonManipulator(String jsonString) {
+        checkIsValidJson(jsonString);
+
+        documentContext = JsonPath.parse(jsonString);
+
+        //"$..*" - give me everything
+        Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build();
+
+        fieldContextDocument = JsonPath.using(conf).parse(jsonString);
+
+        List<String> leafPathList = fieldContextDocument.read("$..*");
+
+        //remove non-leaf nodes from the list
+        //if element n starts with element n-1, then n-1 is a leaf node and should be removed
+        Collections.sort(leafPathList);
+
+        List<String> filteredList = new ArrayList<>();
+
+        for (int i = 0; i < leafPathList.size(); i++) {
+            String current = leafPathList.get(i);
+
+            if ((i + 1) < leafPathList.size()) {
+                String next = leafPathList.get(i + 1);
+
+                if (!next.startsWith(current)) {
+                    filteredList.add(current);
+                }
+            } else {
+                filteredList.add(current);
+            }
+        }
+
+        leafPathList = filteredList;
+
+        Stream<String> newList = leafPathList.stream().map(path -> {
+            return path.replaceAll("\\[[0-9]+\\]", ".*") //removes "[0]" replaces with .*
+                    .replaceAll("\\$\\['", "") //removes "$['"
+                    .replaceAll("'\\]\\['", ".") //removes "']['"
+                    .replaceAll("\\*\\['", "*.") //removes *['
+                    .replaceAll("'\\]", ""); //removes trailing "']"
+        });
+
+        fields = newList.collect(Collectors.toSet());
+    }
+
+    /**
+     *
+      * @return The names of all the edge fields in the {@link DocumentContext}.
+     * Note that is a value is nested (ie it is of type map) that it is not returned.
+     * For example if the full field set was Set(address, address.city, address.street, address.state),
+     * only Set(address.city, address.street, address.state) would be returned
+     */
+    public Set<String> getFields() { return fields; }
+
+    /**
+     * Does the actual masking of values.
+     * @param fieldAccess
+     */
+    public void maskFields(List<FieldLevelAccess> fieldAccess){
+        Stream<FieldLevelAccess> maskedFields = fieldAccess.stream().filter(fa -> fa.hasAccess && fa.isMasked);
+
+        maskedFields.forEach(fa -> {
+            //System.out.println( " attribute " + fa.field + " masked ? " + (fa.isMasked? "yes":"no"));
+
+            getMatchingFields(fa.field).forEach(fullFieldPath -> {
+                final Object realValue = documentContext.read(fullFieldPath);
+                final Object maskedValue;
+
+                //I know I could use polymorphism to not have different methods
+                //but I prefer the readability and the clarity of different method names
+                if (realValue instanceof String) {
+                    maskedValue = DataMasker.maskString((String)realValue, fa.maskType, fa.customMaskedValue);
+                } else if (realValue instanceof Number) {
+                    maskedValue = DataMasker.maskNumber((Number)realValue, fa.maskType, fa.customMaskedValue);
+                } else if (realValue instanceof Boolean) {
+                    maskedValue = DataMasker.maskBoolean((Boolean)realValue, fa.maskType, fa.customMaskedValue);
+                } else {
+                    throw new MaskingException("unable to determine field type: " + realValue);
+                }
+
+                documentContext.set(fullFieldPath, maskedValue);
+            });
+        });
+    }
+
+    /**
+     * @return the current/updated json string of the {@link DocumentContext} that is being worked on
+     */
+    public String getJsonString(){return documentContext.jsonString();}
+
+
+    /**
+     * Used for testing
+     * @param fullPath
+     * @return the value at a specific path
+     */
+    String readString(String fullPath){
+        return documentContext.read(fullPath).toString();
+    }
+
+    private void checkIsValidJson(String jsonString) {
+        try {
+            JsonParser.parseString(jsonString); // throws JsonSyntaxException
+        } catch (JsonSyntaxException e) {
+            throw new MaskingException("invalid input json; unable to mask", e);
+        }
+    }
+
+    private List<String> getMatchingFields(String fieldPath){
+        return fieldContextDocument.read("$."+fieldPath);
+    }
+}
+
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskTypes.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskTypes.java
new file mode 100644
index 000000000..6464cfc06
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskTypes.java
@@ -0,0 +1,54 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+/**
+ * Defines the types of masks that are supported.
+ */
+public interface MaskTypes {
+    String MASK                = "MASK";
+    String MASK_SHOW_LAST_4    = "MASK_SHOW_LAST_4";
+    String MASK_SHOW_FIRST_4   = "MASK_SHOW_FIRST_4";
+    String MASK_HASH           = "MASK_HASH";
+    String MASK_NULL           = "MASK_NULL";
+    String MASK_NONE           = "MASK_NONE";
+    String MASK_DATE_SHOW_YEAR = "MASK_DATE_SHOW_YEAR";
+    String CUSTOM              = "CUSTOM";
+    int    MIN_MASK_LENGTH     = 5;
+    int    MAX_MASK_LENGTH     = 30;
+
+    /**
+     * These formats can be masked into yyyy, '2012'
+     <table>
+     <tr><th>Format</th><th>Description</th><th>Example</th></tr>
+     <tr><td>BASIC_ISO_DATE</td><td>Basic ISO date</td><td>'20111203'</td></tr>
+     <tr><td>ISO_LOCAL_DATE	ISO</td><td>Local Date</td><td>'2011-12-03'</td></tr>
+     <tr><td>ISO_OFFSET_DATE</td><td>ISO Date with offset</td><td>''2011-12-03+01:00'</td></tr>
+     <tr><td>ISO_DATE</td><td>ISO Date with or without offset</td><td>'2011-12-03+01:00'; '2011-12-03'</td></tr>
+     <tr><td>ISO_LOCAL_DATE_TIME</td><td>ISO Local Date and Time</td><td>'2011-12-03T10:15:30'</td></tr>
+     <tr><td>ISO_OFFSET_DATE_TIME</td><td>Date Time with Offset</td><td>'2011-12-03T10:15:30+01:00'</td></tr>
+     <tr><td>ISO_ZONED_DATE_TIME</td><td>Zoned Date Time</td><td>'2011-12-03T10:15:30+01:00[Europe/Paris]'</td></tr>
+     <tr><td>ISO_DATE_TIME</td><td>Date and time with ZoneId</td><td>'2011-12-03T10:15:30+01:00[Europe/Paris]'</td></tr>
+     <tr><td>ISO_ORDINAL_DATE</td><td>Year and day of year/td><td>'2012-337'</td></tr>
+     <tr><td>ISO_WEEK_DATE</td><td>Year and Week</td><td>'2012-W48-6'</td></tr>
+     <tr><td>ISO_INSTANT</td><td>Date and Time of an Instant</td><td>'2011-12-03T10:15:30Z'</td></tr>
+     <tr><td>RFC_1123_DATE_TIME</td><td>RFC 1123 / RFC 822</td><td>'Tue, 3 Jun 2008 11:05:30 GMT'</td></tr>
+     </table>
+     */
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskingException.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskingException.java
new file mode 100644
index 000000000..316973db7
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/MaskingException.java
@@ -0,0 +1,32 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+/**
+ * The most common {@link Exception} this project throws.
+ **/
+public class MaskingException extends RuntimeException {
+    public MaskingException(String message){
+        super(message);
+    }
+
+    public MaskingException(String message, Exception e) {
+        super(message, e);
+    }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAccessType.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAccessType.java
new file mode 100644
index 000000000..92f099051
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAccessType.java
@@ -0,0 +1,45 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.apache.commons.lang3.StringUtils;
+
+public enum NestedStructureAccessType {
+    READ("read"),
+    WRITE("write");
+
+    private final String value;
+
+    public static NestedStructureAccessType getAccessType(String name) {
+        for (NestedStructureAccessType accessType : NestedStructureAccessType.values()) {
+            if (StringUtils.equalsIgnoreCase(accessType.value, name) ||
+                StringUtils.equalsIgnoreCase(accessType.name(), name)) {
+                return accessType;
+            }
+        }
+
+        return null;
+    }
+
+    NestedStructureAccessType(String value) {
+        this.value = value;
+    }
+
+    public String getValue() { return value; }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuditHandler.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuditHandler.java
new file mode 100644
index 000000000..1203ee2c1
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuditHandler.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.ranger.audit.model.AuthzAuditEvent;
+import org.apache.ranger.plugin.audit.RangerDefaultAuditHandler;
+import org.apache.ranger.plugin.model.RangerPolicy;
+import org.apache.ranger.plugin.policyengine.RangerAccessResult;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class NestedStructureAuditHandler extends RangerDefaultAuditHandler {
+    public static final String  ACCESS_TYPE_ROWFILTER = "ROW_FILTER";
+
+    List<AuthzAuditEvent> auditEvents  = null;
+    boolean               deniedExists = false;
+
+    public NestedStructureAuditHandler(Configuration config) {
+        super(config);
+    }
+
+    @Override
+    public void processResult(RangerAccessResult result) {
+        if (result.getIsAudited()) {
+            AuthzAuditEvent auditEvent = createAuditEvent(result);
+
+            if (auditEvent != null) {
+                if (auditEvents == null) {
+                    auditEvents = new ArrayList<>();
+                }
+
+                auditEvents.add(auditEvent);
+
+                if (auditEvent.getAccessResult() == 0) {
+                    deniedExists = true;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void processResults(Collection<RangerAccessResult> results) {
+        for (RangerAccessResult result : results) {
+            processResult(result);
+        }
+    }
+
+    public void flushAudit() {
+        if (auditEvents != null) {
+            for (AuthzAuditEvent auditEvent : auditEvents) {
+                if (deniedExists && auditEvent.getAccessResult() != 0) { // if deny exists, skip logging for allowed results
+                    continue;
+                }
+
+                super.logAuthzAudit(auditEvent);
+            }
+        }
+    }
+
+    private AuthzAuditEvent createAuditEvent(RangerAccessResult result) {
+        AuthzAuditEvent ret = super.getAuthzEvents(result);
+
+        if (ret != null) {
+            int policyType = result.getPolicyType();
+
+            if (policyType == RangerPolicy.POLICY_TYPE_DATAMASK && result.isMaskEnabled()) {
+                ret.setAccessType(result.getMaskType());
+            } else if (policyType == RangerPolicy.POLICY_TYPE_ROWFILTER) {
+                ret.setAccessType(ACCESS_TYPE_ROWFILTER);
+            }
+        }
+
+        return ret;
+    }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuthorizer.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuthorizer.java
new file mode 100644
index 000000000..bd2f509e7
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuthorizer.java
@@ -0,0 +1,285 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl;
+import org.apache.ranger.plugin.policyengine.RangerAccessResult;
+import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.RangerRoles;
+import org.apache.ranger.plugin.util.ServicePolicies;
+import org.apache.ranger.plugin.util.ServiceTags;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public class NestedStructureAuthorizer {
+    static private final Logger logger = LoggerFactory.getLogger(NestedStructureAuthorizer.class);
+
+    private static final String RANGER_CMT_SERVICETYPE = "nestedstructure";
+    private static final String RANGER_CMT_APPID       = "nestedstructure";
+
+    private static volatile NestedStructureAuthorizer instance;
+
+    private final RangerBasePlugin plugin;
+
+    private NestedStructureAuthorizer() {
+        plugin = new RangerBasePlugin(RANGER_CMT_SERVICETYPE, RANGER_CMT_APPID);
+
+        plugin.init();
+    }
+
+    // for testing purpose only
+    public NestedStructureAuthorizer(ServicePolicies policies, ServiceTags tags, RangerRoles roles) {
+        RangerPolicyEngineOptions options = new RangerPolicyEngineOptions();
+
+        options.disablePolicyRefresher    = true;
+        options.disableUserStoreRetriever = true;
+        options.disableTagRetriever       = true;
+
+        RangerPluginConfig pluginConfig = new RangerPluginConfig(RANGER_CMT_SERVICETYPE, policies.getServiceName(), RANGER_CMT_APPID, null, null, options);
+
+        plugin = new RangerBasePlugin(pluginConfig, policies, tags, roles);
+    }
+
+    public static NestedStructureAuthorizer getInstance() {
+        NestedStructureAuthorizer ret = NestedStructureAuthorizer.instance;
+
+        if (ret == null) {
+            synchronized (NestedStructureAuthorizer.class) {
+                ret = NestedStructureAuthorizer.instance;
+
+                if (ret == null) {
+                    NestedStructureAuthorizer.instance = ret = new NestedStructureAuthorizer();
+                }
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     *
+     * @param schema atlas schema name
+     * @param user atlas user name
+     * @param userGroups atlas user groups
+     * @param json the json of the record to be evaluated
+     * @param accessType access type requested; must be included in NestedStructureAccessType.
+     * @return if the user is authorized to access this data.  If there is no authorization, null is returned.
+     * If there is partial authorization, a modified/masked json blob is returned
+     */
+    public AccessResult authorize(String schema, String user, Set<String> userGroups, String json, NestedStructureAccessType accessType) {
+        AccessResult ret;
+
+        NestedStructureAuditHandler auditHandler = new NestedStructureAuditHandler(plugin.getConfig());
+
+        try {
+            ret = privateAuthorize(schema, user, userGroups, json, accessType, auditHandler);
+        } catch (Exception e) {
+            logger.warn("exception during processing, user: " + user + "\n json: " + json, e);
+
+            ret = new AccessResult(false, null).addError(e);
+        } finally {
+            auditHandler.flushAudit();
+        }
+
+        return ret;
+    }
+
+    private AccessResult privateAuthorize(String schema, String user, Set<String> userGroups, String json, NestedStructureAccessType accessType, NestedStructureAuditHandler auditHandler) {
+        final AccessResult ret;
+
+        if (!hasAccessToSchemaOrAnyField(schema, user, userGroups, accessType, auditHandler)) {
+            ret = new AccessResult(false, null);
+        } else if (!hasAccessToRecord(schema, user, userGroups, json, accessType, auditHandler)) {
+            ret = new AccessResult(false, null);
+        } else {
+            boolean                accessDenied    = false;
+            JsonManipulator        jsonManipulator = new JsonManipulator(json);
+            List<FieldLevelAccess> fieldResults    = new ArrayList<>();
+
+            //check each field individually - both if the user has access and if so, what masking is required
+            for (String field : jsonManipulator.getFields()) {
+                FieldLevelAccess fieldAccess =  hasFieldAccess(schema, user, userGroups, field, accessType, auditHandler);
+
+                fieldResults.add(fieldAccess);
+
+                if (!fieldAccess.hasAccess) {
+                    accessDenied = true;
+
+                    break;
+                }
+            }
+
+            //the user must have access to all fields.
+            // if the user doesn't have access to one of the fields return an empty/false AccessResult
+            if (accessDenied) {
+                ret = new AccessResult(false, null);
+            } else {
+                jsonManipulator.maskFields(fieldResults);
+
+                ret = new AccessResult(true, jsonManipulator.getJsonString());
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Checks to see that the user has access to the specific field in this schema
+     * @param schema atlas schema name
+     * @param user atlas user name
+     * @param userGroups atlas user groups
+     * @param fld field name
+     * @param accessType access type requested; must be included in NestedStructureAccessType.
+     * @return a pojo describing access level and masking
+     */
+    private FieldLevelAccess hasFieldAccess(String schema, String user, Set<String> userGroups, String fld, NestedStructureAccessType accessType, NestedStructureAuditHandler auditHandler) {
+        String atlasString = fld.replaceAll("\\.\\[\\*\\]\\.'", ".") //removes ".[*]."
+                                .replaceAll("\\.\\*\\.", "."); //removes ".*."
+
+        //   RangerAccessResource fldResource = new NestedStructure_Resource(Optional.of("json_object.cxt.cmt.product.vnull3"), Optional.of("partner"));
+        NestedStructureResource resource = new NestedStructureResource(Optional.of(schema), Optional.of(atlasString));
+        RangerAccessRequest     request  = new RangerAccessRequestImpl(resource, accessType.getValue(), user, userGroups, null);
+        RangerAccessResult      result   = plugin.isAccessAllowed(request, auditHandler);
+
+        if (result == null){
+            throw new MaskingException("unable to determine access");
+        }
+
+        boolean          hasAccess = result.getIsAccessDetermined() && result.getIsAllowed();
+        FieldLevelAccess ret;
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("checking at line 123 " + accessType + " access to " + schema + "." + fld + " as " + atlasString + " for user: " + user +
+                    " has access ? " + (hasAccess ? "yes" : "no") + " policyId:  " + result.getPolicyId());
+        }
+
+        if (!hasAccess) {
+            ret = new FieldLevelAccess(fld, hasAccess, -1L, true, null, null);
+        } else {
+            RangerAccessResult maskResult = plugin.evalDataMaskPolicies(request, null);
+
+            if (maskResult == null) {
+                throw new MaskingException("unable to determine access");
+            }
+
+            boolean isMasked     = maskResult.isMaskEnabled();
+            Long    maskPolicyId = maskResult.getPolicyId();
+
+            // generate audit log for masking only when masking is enabled for the field
+            if (isMasked) {
+                auditHandler.processResult(maskResult);
+            }
+
+            if (logger.isDebugEnabled()) {
+                String maskPolicy = isMasked ? (" policyId:  " + maskPolicyId) : "";
+
+                logger.debug("attribute " + fld + " as " + atlasString + " masked ? " + (isMasked ? "yes" : "no") + maskPolicy);
+            }
+
+            ret = new FieldLevelAccess(fld, hasAccess, maskPolicyId, isMasked, maskResult.getMaskType(), maskResult.getMaskedValue());
+        }
+
+        return ret;
+    }
+
+    /**
+     * record-level filtering of schema
+     * note that while determining the filter to apply for a table, Apache Ranger policy engine evaluates
+     * the policy-items in the order listed in the policy. The filter specified in the first policy-item
+     * that matches the access-request (i.e. user/groups) will be used in the query.
+     * @param schema atlas schema name
+     * @param user atlas user name
+     * @param userGroups atlas user groups
+     * @param jsonString the json payload that needs to be evaluated
+     * @param accessType access type requested; must be included in NestedStructureAccessType.
+     * @return if the user is authorized to view this particular record
+     */
+
+    private boolean hasAccessToRecord(String schema, String user, Set<String> userGroups, String jsonString, NestedStructureAccessType accessType, NestedStructureAuditHandler auditHandler) {
+        boolean                 ret      = true;
+        NestedStructureResource resource = new NestedStructureResource(Optional.of(schema));
+        RangerAccessRequest     request  = new RangerAccessRequestImpl(resource, accessType.getValue(), user, userGroups, null);
+        RangerAccessResult      result   = plugin.evalRowFilterPolicies(request, null);
+
+        if (result == null) {
+            throw new MaskingException("unable to determine access");
+        }
+
+        if (result.isRowFilterEnabled()) {
+            String filterExpr = result.getFilterExpr();
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("row level filter enabled with expression: " + filterExpr);
+            }
+
+            ret = RecordFilterJavaScript.filterRow(user, filterExpr, jsonString);
+
+            // generate audit log only when row-filter denies access to the record
+            if (!ret) {
+                result.setIsAllowed(false);
+
+                auditHandler.processResult(result);
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Checks to see if this user has any access at all to this schema
+     * @param schema atlas schema name
+     * @param user atlas user name
+     * @param accessType access type requested; must be included in NestedStructureAccessType.
+     * @return if the user has access to this schema
+     */
+    private boolean hasAccessToSchemaOrAnyField(String schema, String user, Set<String> userGroups, NestedStructureAccessType accessType, NestedStructureAuditHandler auditHandler) {
+        NestedStructureResource resource = new NestedStructureResource(Optional.of(schema));
+        RangerAccessRequestImpl request  = new RangerAccessRequestImpl(resource, accessType.getValue(), user, userGroups, null);
+
+        request.setResourceMatchingScope(RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS);
+
+        RangerAccessResult result = plugin.isAccessAllowed(request, null);
+
+        if (result == null){
+            throw new MaskingException("unable to determine access");
+        }
+
+        boolean ret = result.getIsAccessDetermined() && result.getIsAllowed();
+
+        // generate audit log when the user doesn't have access to any field within the schema
+        if (!ret) {
+            auditHandler.processResult(result);
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("checking LINE 202 " + accessType + " access to " + schema + " for user: " + user + " has access ? "
+                    + (ret ? "yes" : "no") + " policyId:  " + result.getPolicyId());
+        }
+
+        return ret;
+    }
+}
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureResource.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureResource.java
new file mode 100644
index 000000000..ff4706519
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureResource.java
@@ -0,0 +1,46 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl;
+
+import java.util.Optional;
+
+public class NestedStructureResource extends RangerAccessResourceImpl {
+  public static final String KEY_SCHEMA = "schema";
+  public static final String KEY_FIELD  = "field";
+
+  public NestedStructureResource(Optional<String> schema, Optional<String> field) {
+        if (schema.isPresent()) {
+            setValue(KEY_SCHEMA, schema.get());
+        }
+
+        if (field.isPresent()) {
+            setValue(KEY_FIELD, field.get());
+        }
+    }
+
+    public NestedStructureResource(Optional<String> schema) {
+        if (schema.isPresent()) {
+            setValue(KEY_SCHEMA, schema.get());
+        }
+    }
+}
+
+
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureService.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureService.java
new file mode 100644
index 000000000..15b6f21c9
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureService.java
@@ -0,0 +1,37 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.apache.ranger.plugin.service.RangerBaseService;
+import org.apache.ranger.plugin.service.ResourceLookupContext;
+
+import java.util.List;
+import java.util.Map;
+
+public class NestedStructureService extends RangerBaseService {
+    @Override
+    public Map<String, Object> validateConfig()  {
+        return null;
+    }
+
+    @Override
+    public List<String> lookupResource(ResourceLookupContext resourceLookupContext)  {
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
new file mode 100644
index 000000000..77767767c
--- /dev/null
+++ b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
@@ -0,0 +1,110 @@
+/**
+ *
+ *
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import jdk.nashorn.api.scripting.ClassFilter;
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.script.Bindings;
+import javax.script.ScriptEngine;
+
+/**
+ * Executes an injected javascript command to determine if the user has access to the selected record
+ */
+public class RecordFilterJavaScript {
+    private static final Logger logger = LoggerFactory.getLogger(RecordFilterJavaScript.class);
+
+    /**
+     * javascript primitive imports that the nashorn engine needs to function properly, e.g., with "includes"
+     */
+    private static final String NASHORN_POLYFILL_ARRAY_PROTOTYPE_INCLUDES  = "if (!Array.prototype.includes) " +
+            "{ Object.defineProperty(Array.prototype, 'includes', { value: function(valueToFind, fromIndex) " +
+            "{ if (this == null) { throw new TypeError('\"this\" is null or not defined'); } var o = Object(this); " +
+            "var len = o.length >>> 0; if (len === 0) { return false; } var n = fromIndex | 0; " +
+            "var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); " +
+            "function sameValueZero(x, y) { return x === y || (typeof x === 'number' && typeof y === 'number' " +
+            "&& isNaN(x) && isNaN(y)); } while (k < len) { if (sameValueZero(o[k], valueToFind)) { return true; } k++; }" +
+            " return false; } }); }";
+
+
+    /**
+     * This class filter prevents javascript from importing, using or reflecting any java classes
+     * Helps keep javascript clean of injections.  It also contains other checks to ensure that injected
+     * javascript is reasonably safe.
+     */
+    static class SecurityFilter implements ClassFilter {
+        @Override
+        public boolean exposeToScripts(String s) {
+            return false;
+        }
+
+        /**
+         *
+          * @param filterExpr the javascript to check if it contains potentially harmful commands
+         * @return if this script is likely bad
+         */
+        boolean containsMalware(String filterExpr){
+            //this.engine is the javascript notation for getting access to runtime that is executing the script
+            //more checks can be added here
+            return filterExpr.contains("this.engine");
+        }
+    }
+
+
+    public static boolean filterRow(String user, String filterExpr, String jsonString) {
+        SecurityFilter securityFilter = new SecurityFilter();
+
+        if (securityFilter.containsMalware(filterExpr)) {
+            throw new MaskingException("cannot process filter expression due to security concern \"this.engine\": " + filterExpr);
+        }
+
+        NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+        ScriptEngine               engine  = factory.getScriptEngine(securityFilter);
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("filterExpr: " + filterExpr);
+        }
+
+        // convert the given JSON string to JavaScript object, which the filterExpr expects, and then exec the filterExpr
+        String script = " jsonAttr = JSON.parse(jsonString); " + NASHORN_POLYFILL_ARRAY_PROTOTYPE_INCLUDES + " " + filterExpr;
+
+        try {
+            Bindings bindings = engine.createBindings();
+
+            bindings.put("jsonString", jsonString);
+            bindings.put("user", user);
+
+            boolean hasAccess = (boolean) engine.eval(script, bindings);
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("row filter access=" + hasAccess);
+            }
+
+            return hasAccess;
+        } catch (Exception e) {
+            throw new MaskingException("unable to properly evaluate filter expression: " + filterExpr, e);
+        }
+    }
+}
+
+ 
\ No newline at end of file
diff --git a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/ExampleClient.java b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/ExampleClient.java
new file mode 100644
index 000000000..af32aff99
--- /dev/null
+++ b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/ExampleClient.java
@@ -0,0 +1,47 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+
+import java.util.Set;
+
+public class ExampleClient {
+    public static void main( String[] args) {
+        String      schema     = "json_object.foo.v1";
+        String      userName   = "someuser";
+        Set<String> userGroups = null;
+        String      jsonString = "{\n" +
+                "    \"foo\": \"12345678\",\n" +
+                "    \"address\": {\n" +
+                "      \"city\": \"philadelphia\" \n" +
+                "    },\n" +
+                "    \"bar\": \"snafu\" \n" +
+                "}\n";
+
+        AccessResult result = NestedStructureAuthorizer.getInstance().authorize(schema, userName, userGroups, jsonString, NestedStructureAccessType.READ);
+
+        System.out.println("has errors: "+ result.hasErrors());
+        System.out.println("hasAccess: "+ result.hasAccess());
+        System.out.println("authorizedJson: "+ result.getJson());
+
+        result.getErrors().stream().forEach(e-> e.printStackTrace());
+
+        System.out.println("done");
+    }
+}
diff --git a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestDataMasker.java b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestDataMasker.java
new file mode 100644
index 000000000..e070fa24f
--- /dev/null
+++ b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestDataMasker.java
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.apache.ranger.authorization.nestedstructure.authorizer.MaskTypes.*;
+
+public class TestDataMasker {
+
+    @DataProvider(name = "testMaskProvider")
+    public Object[][] dpMethod(){
+        return new Object[][] {
+                {"1234567890", MASK, null, "**********"},
+                {"1", MASK, null, "*****"},
+                {"", MASK, null, "*****"},
+                {null, MASK, null, "*****"},
+                {"1234567890", MASK_SHOW_LAST_4, null, "xxxxxx7890"},
+
+                {null, MASK_SHOW_LAST_4, null, null},
+                {"1", MASK_SHOW_LAST_4, null, "1"},
+                {"12", MASK_SHOW_LAST_4, null, "12"},
+                {"abc", MASK_SHOW_LAST_4, null, "abc"},
+                {"abcd", MASK_SHOW_LAST_4, null, "abcd"},
+                {"abcde", MASK_SHOW_LAST_4, null, "xbcde"},
+                {"abcde1234567890a", MASK_SHOW_LAST_4, null, "xxxxxxxxxxxx890a"},
+
+                {null, MASK_SHOW_FIRST_4, null, null},
+                {"1", MASK_SHOW_FIRST_4, null, "1"},
+                {"12", MASK_SHOW_FIRST_4, null, "12"},
+                {"abc", MASK_SHOW_FIRST_4, null, "abc"},
+                {"abcd", MASK_SHOW_FIRST_4, null, "abcd"},
+                {"abcde", MASK_SHOW_FIRST_4, null, "abcdx"},
+                {"abcde1234567890a", MASK_SHOW_FIRST_4, null, "abcdxxxxxxxxxxxx"},
+
+                {null, MASK_NULL, null, null},
+                {"1", MASK_NULL, null, null},
+                {"12", MASK_NULL, null, null},
+                {"abc", MASK_NULL, null, null},
+                {"abcd", MASK_NULL, null, null},
+                {"abcde", MASK_NULL, null, null},
+                {"abcde1234567890a", MASK_NULL, null, null},
+
+                {null, CUSTOM, null, null},
+                {"1", CUSTOM, null, null},
+                {"12", CUSTOM, "woot", "woot"},
+                {"abc", CUSTOM, "woot", "woot"},
+                {"abcd", CUSTOM, "woot", "woot"},
+                {"abcde", CUSTOM, "woot", "woot"},
+                {"abcde1234567890a", CUSTOM, "woot", "woot"},
+
+                {"1234567890", MASK, null, "**********"},
+                {"1234567890", MASK, null, "**********"},
+                {"1234567890", MASK, null, "**********"},
+                {"1", MASK, null, "*****"},
+                {"1B", MASK, null, "*****"},
+                {"akdkajkjkdfsjkdfsjklfjkfjkljkjklfjkldfsdfjkldfjkldfsjkdfjkljkljklf", MASK, null, "******************************"},
+        };
+    }
+    @Test(dataProvider = "testMaskProvider")
+    void testMask(String value, String maskType, String customValue, String result){
+        Assert.assertEquals(DataMasker.maskString(value, maskType, customValue), result);
+    }
+
+    @DataProvider(name = "shaProvider")
+    public Object[][] shaProvider() {
+        return new Object[][]{
+                {"1234567890"},
+                {null},
+                {""},
+                {"djfklasfjkjkdsjadsjkladfsjkl;adfsjewi9etwigodsfojkkmcv  " +
+                        "]e3djfjkadsjkfls;jkfjdsfkj;kldsjfdsl;jfas" +
+                        "dsfjadsl;fjdsklfjewfl fjdsjw fkl;jfkldsj9049023902390234902349023490" +
+                        "]389439023490234890234890234890234890fjfsjdfsjkldsjkldfsjkdfsjklef" +
+                        "ershjewjrjkl;erwjkl;erwijo23490234890234890fjkdfsjkdfsjkadsf" +
+                        "23490234890890dfiudfsjkdfsjkldfsjkl90890234890234890fdjklfj!@#%^))(*&^%$(" +
+                        ")(*&^%$#!@#$%^&*()(*&^%$@#$%^&*((*&^%$!@#$%^&*((*&^%$@#$%^&*()(*&^%$@#$%^"},
+                {"fdjkls"},
+                {"    "}
+        };
+    }
+
+    @Test(dataProvider = "shaProvider")
+    void testShaMask(String value){
+        String masked = DataMasker.maskString(value, MASK_HASH, null);
+        Assert.assertEquals(masked.length(), 64);
+        Assert.assertTrue(isHexadecimal(masked));
+    }
+
+    private static final Pattern HEXADECIMAL_PATTERN = Pattern.compile("\\p{XDigit}+");
+
+    private boolean isHexadecimal(String input) {
+        final Matcher matcher = HEXADECIMAL_PATTERN.matcher(input);
+        return matcher.matches();
+    }
+
+    @DataProvider(name = "badMasks")
+    public Object[][] badMasks(){
+        return new Object[][] {
+                {"1234567890", null, null, "**********"},
+                {"1", null, null, "*****"},
+
+                {null, "", null, null},
+                {"1", "", null, "1"},
+                {"12", "", null, "12"},
+
+                {"abcd", "mask", null, "abcd"},
+                {"abcde", "mask", null, "abcdx"},
+                {"abcde1234567890a", "mask", null, "abcdxxxxxxxxxxxx"},
+
+        };
+    }
+    @Test(expectedExceptions = { MaskingException.class }, dataProvider = "badMasks")
+    void testInvalidMask(String value, String maskType, String customValue, String result){
+        DataMasker.maskString(value, maskType, customValue);
+    }
+
+    @DataProvider(name = "dateformats")
+    public Object[][] dateformats(){
+        return new Object[][] {
+                {"", MASK_DATE_SHOW_YEAR, null, ""},
+                {null, MASK_DATE_SHOW_YEAR, null, null},
+                {"20111203", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2011-12-03", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2011-12-03+01:00", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2012-12-03+01:00", MASK_DATE_SHOW_YEAR, null, "2012"},
+                {"2011-12-03T10:15:30", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2011-12-03T10:15:30+01:00", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2015-12-03T10:15:30+01:00[Europe/Paris]", MASK_DATE_SHOW_YEAR, null, "2015"},
+                {"2012-12-03T10:15:30+01:00[Europe/Paris]", MASK_DATE_SHOW_YEAR, null, "2012"},
+                {"2012-337", MASK_DATE_SHOW_YEAR, null, "2012"},
+                {"2012-W48-6", MASK_DATE_SHOW_YEAR, null, "2012"},
+                {"Tue, 3 Jun 2008 11:05:30 GMT", MASK_DATE_SHOW_YEAR, null, "2008"},
+                {"3 Jun 2008 11:05:30 GMT", MASK_DATE_SHOW_YEAR, null, "2008"}
+        };
+    }
+    @Test(dataProvider = "dateformats")
+    void testMaskYear(String value, String maskType, String customValue, String result){
+        Assert.assertEquals(DataMasker.maskString(value, maskType, customValue), result);
+    }
+
+    @DataProvider(name = "dateformatsBad")
+    public Object[][] dateformatsBad(){
+        return new Object[][] {
+                {" ", MASK_DATE_SHOW_YEAR, null, ""},
+                {"null", MASK_DATE_SHOW_YEAR, null, null},
+                {"2011120354", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2011--12-03", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2011-12 01:00", MASK_DATE_SHOW_YEAR, null, "2011"},
+                {"2012-12-03T+01:00", MASK_DATE_SHOW_YEAR, null, "2012"},
+                {"2011-12-0310:15:30", MASK_DATE_SHOW_YEAR, null, "2011"},
+        };
+    }
+    @Test(expectedExceptions = { MaskingException.class }, dataProvider = "dateformatsBad")
+    void testMaskYearBad(String value, String maskType, String customValue, String result){
+        DataMasker.maskString(value, maskType, customValue);
+    }
+
+    @DataProvider(name = "booleans2")
+    public Object[][] booleans(){
+        return new Object[][] {
+                {true, MASK, null, false},
+                {false, MASK, null, false},
+
+                {true, CUSTOM, "true", true},
+                {false, CUSTOM, "true", true},
+                {true, CUSTOM, "false", false},
+                {false, CUSTOM, "false", false},
+
+                {true, MASK_NULL, "true", null},
+                {false, MASK_NULL, "false", null},
+
+                {true, MASK_NONE, null, true},
+                {false, MASK_NONE, null, false},
+        };
+    }
+    @Test(dataProvider = "booleans2")
+    void testMaskBooleans(Boolean value, String maskType, String customValue, Boolean result){
+        Assert.assertEquals(DataMasker.maskBoolean(value, maskType, customValue), result);
+    }
+
+    @DataProvider(name = "booleansBad")
+    public Object[][] booleansBad(){
+        return new Object[][] {
+                {false, MASK_DATE_SHOW_YEAR, null, null},
+                {false, MASK_HASH, null, null},
+                {true, MASK_HASH, null, null},
+                {false, null, null, null},
+        };
+    }
+    @Test(expectedExceptions = { MaskingException.class }, dataProvider = "booleansBad")
+    void testMaskBooleansBad(Boolean value, String maskType, String customValue, Boolean result){
+        DataMasker.maskBoolean(value, maskType, customValue);
+    }
+
+
+    @DataProvider(name = "numbers")
+    public Object[][] numbers(){
+        return new Object[][] {
+                {0, MASK, null, DataMasker.DEFAULT_NUMBER_MASK},
+                {-101, MASK, null, DataMasker.DEFAULT_NUMBER_MASK},
+                {1.215, MASK, null, DataMasker.DEFAULT_NUMBER_MASK},
+                {12345648976453L, MASK, null, DataMasker.DEFAULT_NUMBER_MASK},
+
+                {0, MASK_NULL, null, null},
+                {-101, MASK_NULL, null, null},
+                {1.215, MASK_NULL, null, null},
+                {12345648976453L, MASK_NULL, null, null},
+
+                {0, MASK_NONE, null, 0},
+                {-101, MASK_NONE, null, -101},
+                {1.215, MASK_NONE, null, 1.215},
+                {12345648976453L, MASK_NONE, null, 12345648976453L},
+
+                {0, CUSTOM, "100", 100},
+                {-101, CUSTOM, "202", 202},
+                {1.215, CUSTOM, "303", 303},
+                {12345648976453L, CUSTOM, "-404", -404},
+        };
+    }
+    @Test(dataProvider = "numbers")
+    void testNumbers(Number value, String maskType, String customValue, Number result){
+        DataMasker.maskNumber(value, maskType, customValue);
+    }
+
+    @DataProvider(name = "numbersBad")
+    public Object[][] numbersBad(){
+        return new Object[][] {
+                {1, MASK_DATE_SHOW_YEAR, null, null},
+                {null, MASK_HASH, null, null},
+                {1000, MASK_HASH, null, null},
+                {1001.012345, null, null, null},
+
+                {1, CUSTOM, "", null},
+                {null, CUSTOM, "null", null},
+                {1000, CUSTOM, "102456fdafdasfda45fghnhjjuio", null},
+                {1001.012345, CUSTOM, "a big brown bear came lolloping over the mountain", null},
+        };
+    }
+    @Test(expectedExceptions = { MaskingException.class }, dataProvider = "numbersBad")
+    void testNumbersBad(Number value, String maskType, String customValue, Boolean result){
+        DataMasker.maskNumber(value, maskType, customValue);
+    }
+}
diff --git a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestJsonManipulator.java b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestJsonManipulator.java
new file mode 100644
index 000000000..79af9f650
--- /dev/null
+++ b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestJsonManipulator.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import com.google.gson.JsonParser;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import static org.apache.ranger.authorization.nestedstructure.authorizer.MaskTypes.*;
+
+public class TestJsonManipulator {
+    static final String testString1 = "{\n" +
+            "    \"customerRelationshipId\": \"42207ad4\",\n" +
+            "    \"accountnumber\": \"12345678\",\n" +
+            "    \"partner\": \"dance\",\n" +
+            "    \"acquisitionDate\": \"2018-02-01\",\n" +
+            "    \"customerSubtype\": [\n" +
+            "      \"type1\",\n" +
+            "      \"type2\"\n" +
+            "    ],\n" +
+            "    \"address\": {\n" +
+            "      \"addressLine1\": \"123 Main St\",\n" +
+            "      \"addressLine2\": \"Apt1\",\n" +
+            "      \"city\": \"philadelphia\",\n" +
+            "      \"state\": \"PA\",\n" +
+            "      \"zipCode\": \"19019\",\n" +
+            "      \"zipCodePlus4\": \"1111\",\n" +
+            "      \"country\": \"USA\"\n" +
+            "    }\n" +
+            "}\n";
+
+
+    static final String bigTester = "{" +
+            "    \"someString\": \"42207ad4-590e-4d5d-a65f-6a4ccddca9e3002\"," +
+            "    \"someNumber\": 12345678," +
+            "    \"someBoolean\": true," +
+            "    \"stringArray\": [\"thing1\", \"thing2\"]," +
+            "    \"numberArray\": [1, 2, 3]," +
+            "    \"booleanArray\": [true, false, true]," +
+            "    \"aMap\": {" +
+            "      \"mapString\": \"123 Main St\"," +
+            "      \"mapBoolean\": false," +
+            "      \"mapNumber\": 987," +
+            "      \"mapStrinArray\": [\"one\", \"two\"]," +
+            "      \"mapMap\": {\"mapMapString\": \"19019\"}" +
+            "    }\n" +
+            "}\n";
+
+
+    @Test
+    public void testFieldNames1() {
+        JsonManipulator js = new JsonManipulator("{foo: 1}");
+
+        Assert.assertEquals(js.getFields().size(), 1);
+    }
+
+    @Test
+    public void testFieldNames2() {
+        JsonManipulator js = new JsonManipulator("{foo: 1, bar: 2}");
+
+        Assert.assertEquals(js.getFields().size(), 2);
+    }
+
+    @Test
+    public void testFieldNamesBigger() {
+        JsonManipulator js = new JsonManipulator(testString1);
+
+        System.out.println(js.getFields());
+
+        Set<String> fields = js.getFields();
+
+        Assert.assertTrue(fields.contains("customerRelationshipId"));
+        Assert.assertTrue(fields.contains("partner"));
+        Assert.assertTrue(fields.contains("acquisitionDate"));
+        Assert.assertTrue(fields.contains("customerSubtype.*"));
+        Assert.assertTrue(fields.contains("accountnumber"));
+        Assert.assertTrue(fields.contains("address.addressLine1"));
+        Assert.assertTrue(fields.contains("address.addressLine2"));
+        Assert.assertTrue(fields.contains("address.city"));
+        Assert.assertTrue(fields.contains("address.zipCode"));
+        Assert.assertTrue(fields.contains("address.zipCodePlus4"));
+        Assert.assertTrue(fields.contains("address.state"));
+        Assert.assertTrue(fields.contains("address.country"));
+        Assert.assertEquals(js.getFields().size(), 12);
+    }
+
+    @Test(expectedExceptions = {MaskingException.class})
+    public void invalidJson() {
+        JsonManipulator js = new JsonManipulator("{foo:\"bar\"");
+
+        Assert.assertEquals(js.getFields().size(), 1);
+    }
+
+    @Test
+    public void testToString() {
+        String          condensedString = JsonParser.parseString(testString1).toString();
+        JsonManipulator man             = new JsonManipulator(condensedString);
+
+        Assert.assertEquals(man.getJsonString(), condensedString);
+    }
+
+    @Test
+    public void testBigTesterFieldNames() {
+        JsonManipulator man = new JsonManipulator(bigTester);
+
+        Assert.assertEquals(man.getFields().size(), 11);
+    }
+
+    @DataProvider(name = "simpleMasks")
+    public Object[][] simpleMasks(){
+        return new Object[][] {
+                //basic string masking
+                {"{\"string1\":\"value1\"}",
+                        Arrays.asList(
+                        new FieldLevelAccess("string1", true, 1L, true, MASK_NULL, "customValue")),
+                        "{\"string1\":null}"},
+                {"{\"string1\":\"value1\"}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, CUSTOM, "customValue")),
+                        "{\"string1\":\"customValue\"}"},
+                {"{\"string1\":\"value1\"}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_NONE, "customValue")),
+                        "{\"string1\":\"value1\"}"},
+                {"{\"string1\":\"value1\"}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_SHOW_FIRST_4, "customValue")),
+                        "{\"string1\":\"valuxx\"}"},
+                {"{\"string1\":\"value1\"}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_SHOW_LAST_4, "customValue")),
+                        "{\"string1\":\"xxlue1\"}"},
+                {"{\"string1\":\"value1\"}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_HASH, "customValue")),
+                        "{\"string1\":\"3c9683017f9e4bf33d0fbedd26bf143fd72de9b9dd145441b75f0604047ea28e\"}"},
+                {"{\"string1\":\"2021-12-25\"}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_DATE_SHOW_YEAR, "customValue")),
+                        "{\"string1\":\"2021\"}"},
+
+                //basic number masking
+                {"{\"string1\":123456}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_NULL, "100")),
+                        "{\"string1\":null}"},
+                {"{\"string1\":123456}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, CUSTOM, "100")),
+                        "{\"string1\":100}"},
+                {"{\"string1\":123456}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_NONE, "100")),
+                        "{\"string1\":123456}"},
+                {"{\"string1\":123456}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK, "100")),
+                        "{\"string1\":-11111}"},
+
+                //basic boolean masking
+                {"{\"string1\":true}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_NULL, "true")),
+                        "{\"string1\":null}"},
+                {"{\"string1\":true}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, CUSTOM, "false")),
+                        "{\"string1\":false}"},
+                {"{\"string1\":true}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK_NONE, "true")),
+                        "{\"string1\":true}"},
+                {"{\"string1\":true}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1", true, 1L, true, MASK, "true")),
+                        "{\"string1\":false}"},
+
+                //array string masking
+                {"{\"string1\":[\"aaaaaaa\",\"bbbbbbb\"]}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1.*", true, 1L, true, MASK_NULL, "true")),
+                        "{\"string1\":[null,null]}"},
+                {"{\"string1\":[\"aaaaaaa\",\"bbbbbbb\"]}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1.*", true, 1L, true, CUSTOM, "false")),
+                        "{\"string1\":[\"false\",\"false\"]}"},
+                {"{\"string1\":[\"aaaaaaa\",\"bbbbbbb\"]}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1.*", true, 1L, true, MASK_NONE, "true")),
+                        "{\"string1\":[\"aaaaaaa\",\"bbbbbbb\"]}"},
+                {"{\"string1\":[\"aaaaaaa\",\"bbbbbbbbbb\"]}",
+                        Arrays.asList(
+                                new FieldLevelAccess("string1.*", true, 1L, true, MASK, "true")),
+                        "{\"string1\":[\"*******\",\"**********\"]}"},
+
+
+        };
+    }
+
+    @Test(dataProvider = "simpleMasks")
+    void testSimpleMasks(String json, List<FieldLevelAccess> fieldAccess, String outputJson){
+        JsonManipulator man = new JsonManipulator(json);
+
+        man.maskFields(fieldAccess);
+
+        Assert.assertEquals(man.getJsonString(), outputJson);
+    }
+
+    @DataProvider(name = "complexMasks")
+    public Object[][] complexMasks(){
+        return new Object[][] {
+                //test masking two fields
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("someNumber", true, 1L, true, CUSTOM, "555"),
+                                new FieldLevelAccess("someBoolean", true, 1L, true, CUSTOM, "false")),
+                        "someNumber", "555"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("someNumber", true, 1L, true, CUSTOM, "555"),
+                                new FieldLevelAccess("someBoolean", true, 1L, true, CUSTOM, "false")),
+                        "someBoolean", "false"},
+
+
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("someString", true, 1L, true, CUSTOM, "555")),
+                        "someString", "555"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("someNumber", true, 1L, true, CUSTOM, "555")),
+                        "someNumber", "555"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("stringArray.*", true, 1L, true, CUSTOM, "555")),
+                        "stringArray.*", "[\"555\",\"555\"]"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("booleanArray.*", true, 1L, true, CUSTOM, "false")),
+                        "booleanArray.*", "[false,false,false]"},
+
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapString", true, 1L, true, CUSTOM, "foo")),
+                        "aMap.mapString", "foo"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapBoolean", true, 1L, true, CUSTOM, "true")),
+                        "aMap.mapBoolean", "true"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapNumber", true, 1L, true, CUSTOM, "444")),
+                        "aMap.mapNumber", "444"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapStrinArray.*", true, 1L, true, CUSTOM, "baa")),
+                        "aMap.mapStrinArray.*", "[\"baa\",\"baa\"]"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapMap.mapMapString", true, 1L, true, CUSTOM, "444qqq")),
+                        "aMap.mapMap.mapMapString", "444qqq"},
+
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapMap.mapMapString", false, 1L, true, CUSTOM, "444qqq")),
+                        "aMap.mapMap.mapMapString", "19019"},
+                {bigTester,
+                        Arrays.asList(
+                                new FieldLevelAccess("aMap.mapMap.mapMapString", true, 1L, false, CUSTOM, "444qqq")),
+                        "aMap.mapMap.mapMapString", "19019"},
+        };
+    }
+
+    @Test(dataProvider = "complexMasks")
+    void testComplexMasks(String json, List<FieldLevelAccess> fieldAccess, String fieldName, String value){
+        JsonManipulator man = new JsonManipulator(json);
+        man.maskFields(fieldAccess);
+        Assert.assertEquals(man.readString(fieldName), value);
+    }
+
+    @Test
+    void testRecordsInArray(){
+        String json = "{\n" +
+                "  \"modifiedTimestamp\": \"2000-01-23T04:56:07.000Z\",\n" +
+                "  \"source\": [\n" +
+                "    {\n" +
+                "      \"sourceId\": \"123456\",\n" +
+                "     \"sourceType\": \"a type\",\n" +
+                "      \"sourceType2\": \"type two\",\n" +
+                "      \"sourceSystem\": \"Source System\"\n" +
+                "    }\n" +
+                "  ],\n" +
+                "  \"channel\": \"channel 4\",\n" +
+                "  \"transactionStatus\": \"SUCCESS\",\n" +
+                "  \"customAttributes\": [\n" +
+                "    {\n" +
+                "      \"key\": \"new\",\n" +
+                "      \"value\": \"value1\"\n" +
+                "    }\n" +
+                "  ],\n" +
+                "  \"modifiedBy\": \"batchJob\"\n" +
+                "}";
+        JsonManipulator man = new JsonManipulator(json);
+        Assert.assertEquals(man.getFields().size(), 10);
+    }
+
+    @Test
+    void testRecordsInArray2(){
+        String json = "{\n" +
+                "  \"modifiedTimestamp\": \"2000-01-23T04:56:07.000Z\",\n" +
+                "  \"source\": [\n" +
+                "    {\n" +
+                "      \"sourceId\": \"123456\",\n" +
+                "     \"sourceType\": \"a type\",\n" +
+                "      \"sourceType2\": \"type two\",\n" +
+                "      \"sourceSystem\": \"Source System\"\n" +
+                "    }\n" +
+                "  ],\n" +
+                "  \"channel\": \"channel 4\",\n" +
+                "  \"transactionStatus\": \"SUCCESS\",\n" +
+                "  \"customAttributes\": [\n" +
+                "    {\n" +
+                "      \"key\": \"new\",\n" +
+                "      \"value\": \"value1\"\n" +
+                "    },\n" +
+                "    {\n" +
+                "      \"key\": \"new22\",\n" +
+                "      \"value\": \"value22\"\n" +
+                "    }\n" +
+                "  ],\n" +
+                "  \"modifiedBy\": \"batchJob\"\n" +
+                "}";
+        JsonManipulator man = new JsonManipulator(json);
+        Assert.assertEquals(man.getFields().size(), 10);
+
+        FieldLevelAccess fieldAccess = new FieldLevelAccess("customAttributes.*.key",
+                true, 1L, true, CUSTOM, "THEMASK");
+        man.maskFields(Arrays.asList(fieldAccess));
+        Assert.assertEquals(man.readString("customAttributes.[0].key"), "THEMASK");
+    }
+}
diff --git a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestNestedStructureAuthorizer.java b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestNestedStructureAuthorizer.java
new file mode 100644
index 000000000..461e9e972
--- /dev/null
+++ b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestNestedStructureAuthorizer.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.plugin.model.RangerServiceDef;
+import org.apache.ranger.plugin.util.RangerRoles;
+import org.apache.ranger.plugin.util.ServicePolicies;
+import org.apache.ranger.plugin.util.ServiceTags;
+
+import java.io.*;
+import java.util.List;
+import java.util.Set;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+
+public class TestNestedStructureAuthorizer {
+    static Gson gsonBuilder;
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        gsonBuilder = new GsonBuilder().setDateFormat("yyyyMMdd-HH:mm:ss.SSSZ")
+                                       .setPrettyPrinting()
+                                       .create();
+    }
+
+    @Test
+    public void test_customer_records() {
+        runTestsFromResourceFile("/test_customer_records.json");
+    }
+
+    private void runTestsFromResourceFile(String resourceName) {
+        try(InputStream       inStream = this.getClass().getResourceAsStream(resourceName);
+            InputStreamReader reader   = new InputStreamReader(inStream)) {
+            runTests(reader, resourceName);
+        } catch (IOException excp) {
+            // ignore
+        }
+    }
+
+    private void runTests(InputStreamReader reader, String testName) {
+        NestedStructureTestCase testCase = gsonBuilder.fromJson(reader, NestedStructureTestCase.class);
+
+        assertTrue("invalid input: " + testName, testCase != null && testCase.policies != null && testCase.tests != null);
+
+        if (testCase.policies.getServiceDef() == null && StringUtils.isNotBlank(testCase.serviceDefFilename)) {
+            try (InputStream       inStream   = this.getClass().getResourceAsStream(testCase.serviceDefFilename);
+                 InputStreamReader sdefReader = new InputStreamReader(inStream)) {
+                testCase.policies.setServiceDef(gsonBuilder.fromJson(sdefReader, RangerServiceDef.class));
+            } catch (IOException excp) {
+                // ignore
+            }
+        }
+
+        NestedStructureAuthorizer authorizer = new NestedStructureAuthorizer(testCase.policies, testCase.tags, testCase.roles);
+
+        for (NestedStructureTestCase.TestData test : testCase.tests) {
+            AccessResult expected = test.result;
+            AccessResult result   = authorizer.authorize(test.schema, test.user, test.userGroups, test.json, NestedStructureAccessType.getAccessType(test.accessType));
+
+            assertEquals(test.name + ": hasAccess doesn't match: expected=" + expected.hasAccess() + ", actual=" + result.hasAccess(), expected.hasAccess(), result.hasAccess());
+            assertEquals(test.name + ": json doesn't match: expected=" + expected.getJson() + ", actual=" + result.getJson(), expected.getJson(), result.getJson());
+        }
+    }
+
+    static class NestedStructureTestCase {
+        public ServicePolicies policies;
+        public ServiceTags     tags;
+        public RangerRoles     roles;
+        public List<TestData>  tests;
+        public String          serviceDefFilename;
+
+        class TestData {
+            public String       name;
+            public String       schema;
+            public String       json;
+            public String       user;
+            public Set<String>  userGroups;
+            public String       accessType;
+            public AccessResult result;
+        }
+    }
+}
diff --git a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
new file mode 100644
index 000000000..9cb161b8d
--- /dev/null
+++ b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authorization.nestedstructure.authorizer;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+
+public class TestRecordFilterJavaScript {
+
+    @Test(expectedExceptions = {MaskingException.class})
+    public void testArbitraryCommand(){
+        RecordFilterJavaScript.filterRow("user","this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
+                TestJsonManipulator.bigTester);
+    }
+
+    @Test
+    public void testAccessJava() {
+        try {
+            RecordFilterJavaScript.filterRow("user", "bufferedWriter = new java.io.BufferedWriter(new java.io.FileWriter('omg.txt'));\n" +
+                    "            bufferedWriter.write(\"Writing line one to file\"); bufferedWriter.close;", TestJsonManipulator.bigTester);
+
+        } catch (MaskingException e) {
+            Assert.assertTrue(e.getCause() instanceof RuntimeException);
+            Assert.assertTrue(e.getCause().getCause() instanceof ClassNotFoundException);
+        }
+        Assert.assertFalse(Files.exists(Paths.get("omg.txt")));
+    }
+
+    @AfterTest
+    public void deleteTestFile() throws IOException {
+        Files.deleteIfExists(Paths.get("omg.txt"));
+    }
+
+    @Test
+    public void testBasicFilters(){
+        Assert.assertEquals(RecordFilterJavaScript.filterRow("user", "jsonAttr.partner.equals('dance')", TestJsonManipulator.testString1), true);
+        Assert.assertEquals(RecordFilterJavaScript.filterRow("user", "jsonAttr.address.zipCode.equals('19019')", TestJsonManipulator.testString1), true);
+        Assert.assertEquals(RecordFilterJavaScript.filterRow("user", "jsonAttr.aMap.mapNumber > 5", TestJsonManipulator.bigTester), true);
+
+        Assert.assertEquals(RecordFilterJavaScript.filterRow("user", "jsonAttr.partner.equals('cox')", TestJsonManipulator.testString1), false);
+    }
+}
+
diff --git a/plugin-nestedstructure/src/test/resources/servicedef-nestedstructure.json b/plugin-nestedstructure/src/test/resources/servicedef-nestedstructure.json
new file mode 100644
index 000000000..9cf6ba85b
--- /dev/null
+++ b/plugin-nestedstructure/src/test/resources/servicedef-nestedstructure.json
@@ -0,0 +1,186 @@
+{
+  "name":        "nestedstructure",
+  "displayName": "nestedstructure",
+  "implClass":   "",
+  "label":       "NestedStructure",
+  "description": "Plugin to enforce read AND WRITE access control on nested structures such as JSON response objects from microservice API resource calls",
+  "options": {
+    "enableDenyAndExceptionsInPolicies": "true"
+  },
+  "configs": [
+    { "itemId": 1, "name": "commonNameForCertificate",   "type": "string", "mandatory": false },
+    { "itemId": 2, "name": "policy.download.auth.users", "type": "string", "mandatory": false }
+  ],
+  "resources": [
+    {
+      "itemId":                 1,
+      "name":                   "schema",
+      "type":                   "string",
+      "level":                  10,
+      "mandatory":              true,
+      "lookupSupported":        false,
+      "recursiveSupported":     false,
+      "excludesSupported":      true,
+      "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+      "matcherOptions":         { "wildCard": "true", "ignoreCase": "true" },
+      "label":                  "NestedStructure Schema",
+      "description":            "Schema of the nested structure returned from Microservice GET, etc",
+      "accessTypeRestrictions": [],
+      "isValidLeaf":            true
+    },
+    {
+      "itemId":                  2,
+      "name":                   "field",
+      "type":                   "string",
+      "level":                  20,
+      "parent":                 "schema",
+      "mandatory":              true,
+      "lookupSupported":        false,
+      "recursiveSupported":     false,
+      "excludesSupported":      true,
+      "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+      "matcherOptions":         { "wildCard": "true", "ignoreCase": "true" },
+      "label":                  "NestedStructure Schema Field",
+      "description":            "NestedStructure Schema Field",
+      "accessTypeRestrictions": [],
+      "isValidLeaf":            true
+    }
+  ],
+  "accessTypes": [
+    { "itemId": 1, "name": "read",  "label": "Read" },
+    { "itemId": 2, "name": "write", "label": "Write" }
+  ],
+  "policyConditions": [],
+  "contextEnrichers": [],
+  "enums":            [],
+  "dataMaskDef": {
+    "maskTypes": [
+      {
+        "itemId":          1,
+        "name":            "MASK",
+        "label":           "Redact",
+        "description":     "Replace lowercase with 'x', uppercase with 'X', digits with '0'",
+        "transformer":     "mask({field})",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          2,
+        "name":            "MASK_SHOW_LAST_4",
+        "label":           "Partial mask: show last 4",
+        "description":     "Show last 4 characters; replace rest with 'x'",
+        "transformer":     "mask_show_last_n({field}, 4, 'x', 'x', 'x', -1, '1')",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          3,
+        "name":            "MASK_SHOW_FIRST_4",
+        "label":           "Partial mask: show first 4",
+        "description":     "Show first 4 characters; replace rest with 'x'",
+        "transformer":     "mask_show_first_n({field}, 4, 'x', 'x', 'x', -1, '1')",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          4,
+        "name":            "MASK_HASH",
+        "label":           "Hash",
+        "description":     "Hash the value",
+        "transformer":     "mask_hash({field})",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          5,
+        "name":            "MASK_NULL",
+        "label":           "Nullify",
+        "description":     "Replace with NULL",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          6,
+        "name":            "MASK_NONE",
+        "label":           "Unmasked (retain original value)",
+        "description":     "No masking",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          12,
+        "name":            "MASK_DATE_SHOW_YEAR",
+        "label":           "Date: show only year",
+        "description":     "Date: show only year",
+        "transformer":     "mask({field}, 'x', 'x', 'x', -1, '1', 1, 0, -1)",
+        "dataMaskOptions": {}
+      },
+      {
+        "itemId":          13,
+        "name":            "CUSTOM",
+        "label":           "Custom",
+        "description":     "Custom",
+        "dataMaskOptions": {}
+      }
+    ],
+    "accessTypes": [
+      { "itemId": 1, "name":  "read", "label": "Read" }
+    ],
+    "resources": [
+      {
+        "itemId":                 1,
+        "name":                   "schema",
+        "type":                   "string",
+        "level":                  10,
+        "mandatory":              true,
+        "lookupSupported":        false,
+        "recursiveSupported":     false,
+        "excludesSupported":      false,
+        "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+        "matcherOptions":         { "wildCard": "false", "ignoreCase": "true" },
+        "uiHint":                 "{ \"singleValue\":true }",
+        "label":                  "NestedStructure Schema",
+        "description":            "NestedStructure Schema returned from Microservice GET, etc",
+        "accessTypeRestrictions": [],
+        "isValidLeaf":            false
+      },
+      {
+        "itemId":                 2,
+        "name":                   "field",
+        "type":                   "string",
+        "level":                  20,
+        "parent":                 "schema",
+        "mandatory":              true,
+        "lookupSupported":        false,
+        "recursiveSupported":     false,
+        "excludesSupported":      false,
+        "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+        "matcherOptions":         { "wildCard": "false", "ignoreCase": "true" },
+        "uiHint":                 "{ \"singleValue\":true }",
+        "label":                  "NestedStructure Schema Field",
+        "description":            "NestedStructure Schema Field",
+        "accessTypeRestrictions": [],
+        "isValidLeaf":            true
+      }
+    ]
+  },
+  "rowFilterDef": {
+    "accessTypes": [
+      { "itemId": 1, "name": "read",  "label": "Read"  },
+      { "itemId": 2, "name": "write", "label": "Write" }
+    ],
+    "resources": [
+      {
+        "itemId":                 1,
+        "name":                   "schema",
+        "type":                   "string",
+        "level":                  10,
+        "mandatory":              true,
+        "lookupSupported":        false,
+        "recursiveSupported":     false,
+        "excludesSupported":      false,
+        "matcher":                "org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher",
+        "matcherOptions":         { "wildCard": "false", "ignoreCase": "true" },
+        "uiHint":                 "{ \"singleValue\":true }",
+        "label":                  "NestedStructure Schema",
+        "description":            "NestedStructure Schema returned from Microservice GET, etc",
+        "accessTypeRestrictions": [],
+        "isValidLeaf":            true
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/plugin-nestedstructure/src/test/resources/test_customer_records.json b/plugin-nestedstructure/src/test/resources/test_customer_records.json
new file mode 100644
index 000000000..343fe7737
--- /dev/null
+++ b/plugin-nestedstructure/src/test/resources/test_customer_records.json
@@ -0,0 +1,164 @@
+{
+  "name":     "test access control policies for nestedstructure",
+  "comments": [
+    "tests on authorizing access to customer Json records, along with fields masking and record filtering",
+    "record structure:",
+    " { ",
+    "   id:           1,",
+    "   name:         customerName,",
+    "   phone:        phoneNumber,",
+    "   email:        emailAddress,",
+    "   address:      { line1: streetName, line2: landmark, city: myCity, state: myState, zipCode: myZipCode }",
+    "   lastOrderDate: 2022/07/16,",
+    "   recentOrders: [",
+    "     {",
+    "       orderId:     1,",
+    "       orderDate:   2022/06/14,",
+    "       orderAmount: 504.76",
+    "     }",
+    "     {",
+    "       orderId:     2,",
+    "       orderDate:   2022/06/06,",
+    "       orderAmount: 321.98",
+    "     }",
+    "   ]",
+    " }"
+  ],
+  "policies": {
+    "serviceName":   "dev_nestedstructure",
+    "serviceId":     1,
+    "policyVersion": 1,
+    "auditMode": "audit-none",
+    "policies": [
+      {
+        "id": 1, "name":"ACCESS: schema=customer, field=*",
+        "resources": { "schema": { "values": [ "customer" ] }, "field": { "values":  [ "*" ] } },
+        "policyItems":[
+          { "accesses": [ { "type": "read" } ], "users": [ "user1" ], "groups": [ "analysts" ] }
+        ]
+      },
+      {
+        "id": 2, "name":"ACCESS: schema=customer, field=id,name,lastOrderDate,recentOrders",
+        "resources": { "schema": { "values": [ "customer" ] }, "field": { "values":  [ "id", "name", "lastOrderDate", "recentOrders.*" ] } },
+        "policyItems":[
+          { "accesses": [ { "type": "read" } ], "groups": [ "csr" ] }
+        ]
+      },
+      {
+        "id": 101, "name":"MASKING: schema=customer, field=lastOrderDate", "policyType": 1,
+        "resources": { "schema": { "values": [ "customer" ] }, "field": { "values": [ "lastOrderDate"] } },
+        "dataMaskPolicyItems":[
+          { "accesses": [ { "type": "read" } ], "groups": [ "analysts" ], "dataMaskInfo": { "dataMaskType": "MASK_NULL" } },
+          { "accesses": [ { "type": "read" } ], "groups": [ "csr" ],      "dataMaskInfo": { "dataMaskType": "MASK_DATE_SHOW_YEAR" } }
+        ]
+      },
+      {
+        "id": 102, "name":"MASKING: schema=customer, field=recentOrders.orderDate", "policyType": 1,
+        "resources": { "schema": { "values": [ "customer" ] }, "field": { "values": [ "recentOrders.orderDate"] } },
+        "dataMaskPolicyItems":[
+          { "accesses": [ { "type": "read" } ], "groups": [ "analysts" ], "dataMaskInfo": { "dataMaskType": "MASK_NULL" } },
+          { "accesses": [ { "type": "read" } ], "groups": [ "csr" ],      "dataMaskInfo": { "dataMaskType": "MASK_DATE_SHOW_YEAR" } }
+        ]
+      },
+      {
+        "id": 201, "name":"FILTER: schema=customer", "policyType": 2,
+        "resources": { "schema": { "values": [ "customer" ] } },
+        "rowFilterPolicyItems":[
+          { "accesses": [ { "type": "read" } ], "groups": [ "region-ca" ], "rowFilterInfo": { "filterExpr": "jsonAttr.address.state == 'CA'" } },
+          { "accesses": [ { "type": "read" } ], "groups": [ "region-wa" ], "rowFilterInfo": { "filterExpr": "jsonAttr.address.state == 'WA'" } }
+        ]
+      }
+    ]
+  },
+  "tags": { },
+  "roles": {
+    "roleVersion": 1
+  },
+  "serviceDefFilename": "/servicedef-nestedstructure.json",
+  "tests": [
+    {
+      "name":   "ALLOW user1 to read schema=customer, fields=[id,email,address,lastOrderDate]",
+      "user":   "user1", "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"1\",\"name\":\"CA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"CA\"},\"lastOrderDate\":\"2022-07-16\"}",
+      "result": {
+        "hasAccess": true,
+        "json":     "{\"id\":\"1\",\"name\":\"CA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"CA\"},\"lastOrderDate\":\"2022-07-16\"}",
+        "errors":   [ ]
+      }
+    },
+    {
+      "name":   "ALLOW user in groups=[csr] to read schema=customer, with masked-year for lastOrderDate and recentOrders.orderDate",
+      "user":   "some-user", "userGroups": [ "csr" ], "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"1\",\"name\":\"CA-Customer\",\"lastOrderDate\":\"2022-07-16\",\"recentOrders\":[{\"orderId\":1,\"orderDate\":\"2022-06-14\",\"orderAmount\":504.76},{\"orderId\":2,\"orderDate\":\"2022-06-06\",\"orderAmount\":321.98}]}",
+      "result": {
+        "hasAccess": true,
+        "json":      "{\"id\":\"1\",\"name\":\"CA-Customer\",\"lastOrderDate\":\"2022\",\"recentOrders\":[{\"orderId\":1,\"orderDate\":\"2022\",\"orderAmount\":504.76},{\"orderId\":2,\"orderDate\":\"2022\",\"orderAmount\":321.98}]}",
+        "errors":    [ ]
+      }
+    },
+    {
+      "name":   "DENY user in groups=[csr] to read schema=customer, fields=[email,address]",
+      "user":   "some-user", "userGroups":  [ "csr" ], "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"1\",\"name\":\"CA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"CA\"},\"lastOrderDate\":\"2022-07-16\"}",
+      "result": {
+        "hasAccess": false,
+        "json":      null,
+        "errors":    [ ]
+      }
+    },
+    {
+      "name":   "ALLOW user in groups=[analysts, region-ca] to read schema=customer, having address.state=CA, with masked-null for lastOrderDate",
+      "user":   "some-user", "userGroups": [ "analysts", "region-ca" ], "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"1\",\"name\":\"CA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"CA\"},\"lastOrderDate\":\"2022-07-16\",\"recentOrders\":[{\"orderId\":1,\"orderDate\":\"2022-06-14\",\"orderAmount\":504.76},{\"orderId\":2,\"orderDate\":\"2022-06-06\",\"orderAmount\":321.98}]}",
+      "result": {
+        "hasAccess": true,
+        "json":      "{\"id\":\"1\",\"name\":\"CA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"CA\"},\"lastOrderDate\":null,\"recentOrders\":[{\"orderId\":1,\"orderDate\":null,\"orderAmount\":504.76},{\"orderId\":2,\"orderDate\":null,\"orderAmount\":321.98}]}",
+        "errors":    [ ]
+      }
+    },
+    {
+      "name":   "DENY user in groups=[analysts, region-ca] to read schema=customer, having address.state=WA",
+      "user":   "some-user", "userGroups": [ "analysts", "region-ca" ], "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"2\",\"name\":\"WA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"WA\"},\"lastOrderDate\":\"2022-07-16\"}",
+      "result": {
+        "hasAccess": false,
+        "json":      null,
+        "errors":    [ ]
+      }
+    },
+    {
+      "name":   "ALLOW user in groups=[analysts, region-wa] to read schema=customer, having address.state=WA, with masked-null for lastOrderDate",
+      "user":   "some-user", "userGroups": [ "analysts", "region-wa" ], "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"2\",\"name\":\"WA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"WA\"},\"lastOrderDate\":\"2022-07-16\",\"recentOrders\":[{\"orderId\":1,\"orderDate\":\"2022-06-14\",\"orderAmount\":504.76},{\"orderId\":2,\"orderDate\":\"2022-06-06\",\"orderAmount\":321.98}]}",
+      "result": {
+        "hasAccess": true,
+        "json":      "{\"id\":\"2\",\"name\":\"WA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"WA\"},\"lastOrderDate\":null,\"recentOrders\":[{\"orderId\":1,\"orderDate\":null,\"orderAmount\":504.76},{\"orderId\":2,\"orderDate\":null,\"orderAmount\":321.98}]}",
+        "errors":    [ ]
+      }
+    },
+    {
+      "name":   "DENY user in groups=[analysts, region-wa] to read schema=customer, having address.state=CA",
+      "user":   "some-user", "userGroups": [ "analysts", "region-wa" ], "accessType": "read",
+      "schema": "customer",
+      "json":   "{\"id\":\"1\",\"name\":\"CA-Customer\",\"email\":\"name@domain.com\",\"address\":{\"state\":\"CA\"},\"lastOrderDate\":\"2022-07-16\"}",
+      "result": {
+        "hasAccess": false,
+        "json":      null,
+        "errors":    [ ]
+      }
+    },
+    {
+      "name":   "DENY user1 to read schema=employee, field=id",
+      "user":   "user1", "accessType": "read",
+      "schema": "employee",
+      "json":   "{\"id\":\"1\"}",
+      "result": { "hasAccess":  false, "json": null, "errors": [ ] }
+    }
+  ]
+}
diff --git a/pom.xml b/pom.xml
index 0945f4b1d..8698e67e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -283,6 +283,7 @@
                 <module>security-admin</module>
                 <module>plugin-kafka</module>
                 <module>plugin-solr</module>
+                <module>plugin-nestedstructure</module>
                 <module>plugin-nifi</module>
                 <module>plugin-nifi-registry</module>
                 <module>plugin-presto</module>
@@ -351,6 +352,7 @@
                 <module>security-admin</module>
                 <module>plugin-kafka</module>
                 <module>plugin-solr</module>
+                <module>plugin-nestedstructure</module>
                 <module>plugin-nifi</module>
                 <module>plugin-nifi-registry</module>
                 <module>plugin-presto</module>
@@ -638,6 +640,18 @@
                 <module>ranger-trino-plugin-shim</module>
             </modules>
         </profile>
+        <profile>
+            <id>ranger-nestedstructure-plugin</id>
+            <modules>
+                <module>agents-audit</module>
+                <module>agents-common</module>
+                <module>agents-cred</module>
+                <module>agents-installer</module>
+                <module>credentialbuilder</module>
+                <module>ranger-util</module>
+                <module>plugin-nestedstructure</module>
+            </modules>
+        </profile>
         <profile>
             <id>linux</id>
             <activation>
diff --git a/tagsync/src/main/java/org/apache/ranger/tagsync/nestedstructureplugin/AtlasNestedStructureResourceMapper.java b/tagsync/src/main/java/org/apache/ranger/tagsync/nestedstructureplugin/AtlasNestedStructureResourceMapper.java
new file mode 100644
index 000000000..71dd70b7f
--- /dev/null
+++ b/tagsync/src/main/java/org/apache/ranger/tagsync/nestedstructureplugin/AtlasNestedStructureResourceMapper.java
@@ -0,0 +1,95 @@
+/**
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+*/
+package org.apache.ranger.tagsync.nestedstructureplugin;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.ranger.plugin.model.RangerPolicy.RangerPolicyResource;
+import org.apache.ranger.plugin.model.RangerServiceResource;
+import org.apache.ranger.tagsync.source.atlas.AtlasResourceMapper;
+import org.apache.ranger.tagsync.source.atlasrest.RangerAtlasEntity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Syncing tag-metadata (resource) relationships from Apache Atlas to Ranger
+ **/
+
+public class AtlasNestedStructureResourceMapper extends AtlasResourceMapper {
+    public static final String RANGER_SERVICETYPE                 = "nestedstructure";
+    public static final String ENTITY_TYPE_NESTEDSTRUCTURE_SCHEMA = "json_object";
+    public static final String ENTITY_TYPE_NESTEDSTRUCTURE_FIELD  = "json_field";
+    public static final String RANGER_TYPE_NESTEDSTRUCTURE_SCHEMA = "schema";
+    public static final String RANGER_TYPE_NESTEDSTRUCTURE_FIELD  = "field";
+    public static final String QUALIFIED_NAME_DELIMITER           = "#";
+
+    public static final String[] SUPPORTED_ENTITY_TYPES = { ENTITY_TYPE_NESTEDSTRUCTURE_SCHEMA, ENTITY_TYPE_NESTEDSTRUCTURE_FIELD };
+
+    public AtlasNestedStructureResourceMapper() {
+        super(RANGER_SERVICETYPE, SUPPORTED_ENTITY_TYPES);
+    }
+
+    @Override
+    public RangerServiceResource buildResource(final RangerAtlasEntity entity) throws Exception {
+        String qualifiedName = (String)entity.getAttributes().get(AtlasResourceMapper.ENTITY_ATTRIBUTE_QUALIFIED_NAME);
+
+        if (StringUtils.isEmpty(qualifiedName)) {
+            throw new Exception("attribute '" +  ENTITY_ATTRIBUTE_QUALIFIED_NAME + "' not found in entity");
+        }
+
+        String resourceStr = getResourceNameFromQualifiedName(qualifiedName);
+
+        if (StringUtils.isEmpty(resourceStr)) {
+            throwExceptionWithMessage("resource not found in attribute '" +  ENTITY_ATTRIBUTE_QUALIFIED_NAME + "': " + qualifiedName);
+        }
+
+        String clusterName = getClusterNameFromQualifiedName(qualifiedName);
+        if (StringUtils.isEmpty(clusterName)) {
+            clusterName = defaultClusterName;
+        }
+
+        String   entityType  = entity.getTypeName();
+        String   entityGuid  = entity.getGuid();
+        String   serviceName = getRangerServiceName(clusterName);
+        String[] resources   = resourceStr.split(QUALIFIED_NAME_DELIMITER);
+        String   schemaName  = resources.length > 0 ? resources[0] : null;
+        String   fieldName   = resources.length > 1 ? resources[1] : null;
+
+        Map<String, RangerPolicyResource> elements = new HashMap<>();
+
+        if (StringUtils.equals(entityType, ENTITY_TYPE_NESTEDSTRUCTURE_SCHEMA)) {
+            if (StringUtils.isNotEmpty(schemaName)) {
+                elements.put(RANGER_TYPE_NESTEDSTRUCTURE_SCHEMA , new RangerPolicyResource(schemaName));
+            }
+        } else if (StringUtils.equals(entityType, ENTITY_TYPE_NESTEDSTRUCTURE_FIELD)) {
+            if (StringUtils.isNotEmpty(schemaName) && StringUtils.isNotEmpty(fieldName)) {
+                elements.put(RANGER_TYPE_NESTEDSTRUCTURE_SCHEMA, new RangerPolicyResource(schemaName));
+                elements.put(RANGER_TYPE_NESTEDSTRUCTURE_FIELD, new RangerPolicyResource(fieldName));
+            }
+        } else {
+            throwExceptionWithMessage("unrecognized entity-type: " + entityType);
+        }
+
+        if (elements.isEmpty()) {
+            throwExceptionWithMessage("invalid qualifiedName for entity-type '" + entityType + "': " + qualifiedName);
+        }
+
+        RangerServiceResource ret = new RangerServiceResource(entityGuid, serviceName, elements);
+
+        return ret;
+    }
+}
diff --git a/tagsync/src/test/java/org/apache/ranger/tagsync/nestedstructureplugin/ResourceTests.java b/tagsync/src/test/java/org/apache/ranger/tagsync/nestedstructureplugin/ResourceTests.java
new file mode 100644
index 000000000..05ca49e82
--- /dev/null
+++ b/tagsync/src/test/java/org/apache/ranger/tagsync/nestedstructureplugin/ResourceTests.java
@@ -0,0 +1,139 @@
+/*
+ * 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.tagsync.nestedstructureplugin;
+
+import junit.framework.TestCase;
+import org.apache.ranger.plugin.model.RangerServiceResource;
+import org.apache.ranger.tagsync.source.atlasrest.RangerAtlasEntity;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.apache.ranger.tagsync.nestedstructureplugin.AtlasNestedStructureResourceMapper.QUALIFIED_NAME_DELIMITER;
+
+public class ResourceTests extends TestCase {
+    private AtlasNestedStructureResourceMapper mapper = new AtlasNestedStructureResourceMapper();
+
+    @Test
+    public void test_ResourceParseFieldName() {
+        String   resourceStr = "json_object.foo.v1#partner";
+        String[] resources   = resourceStr.split(QUALIFIED_NAME_DELIMITER);
+        String   schemaName  = resources.length > 0 ? resources[0] : null;
+        String   fieldName   = resources.length > 1 ? resources[1] : null;
+
+
+        assertEquals("schemaName does not match expected value", "json_object.foo.v1", schemaName);
+        assertEquals("fieldName does not match expected value", "partner", fieldName);
+        System.out.println(schemaName);
+        System.out.println(fieldName);
+    }
+
+    @Test
+    public void test_ResourceParseSchemaName() {
+        String   resourceStr = "json_object.foo.v1";
+        String[] resources   = resourceStr.split(QUALIFIED_NAME_DELIMITER);
+        String   schemaName  = resources.length > 0 ? resources[0] : null;
+        String   fieldName   = resources.length > 1 ? resources[1] : null;
+
+        assertEquals("schemaName does not match expected value", resourceStr, schemaName);
+        assertNull("fieldName does not match expected value", fieldName);
+        System.out.println(schemaName);
+        System.out.println(fieldName);
+    }
+
+    @Test
+    public void test_RangerEntityJsonField() {
+        String typeName = "json_field";
+        String guid     = "0265354542434ff-aewra7297dc";
+
+        try {
+            Map<String, Object>   attributes = Collections.singletonMap("qualifiedName", "json_object.foo.v1#channel");
+            RangerAtlasEntity     entity     = new RangerAtlasEntity(typeName, guid, attributes);
+            RangerServiceResource resource   = mapper.buildResource(entity);
+
+            assertTrue("Resource elements list is empty", resource.getResourceElements().size() > 0);
+            assertEquals("Resource elements list size does not match expected", 2, resource.getResourceElements().size());
+            assertNotNull("Resource element missing value for schema", resource.getResourceElements().get("schema"));
+            assertEquals("Resource element schema value does not match", Collections.singletonList("json_object.foo.v1"), resource.getResourceElements().get("schema").getValues());
+            assertNotNull("Resource element missing value for field", resource.getResourceElements().get("field"));
+            assertEquals("Resource element field value does not match", Collections.singletonList("channel"), resource.getResourceElements().get("field").getValues());
+            assertEquals("serviceName does not match expected value", "null_nestedstructure", resource.getServiceName());
+        } catch(Exception e) {
+            e.printStackTrace();
+            fail("An error occurred while processing resource");
+        }
+
+        // qualifiedName containing clusterName
+        try {
+            Map<String, Object>   attributes = Collections.singletonMap("qualifiedName", "json_object.foo.v1#channel@dev");
+            RangerAtlasEntity     entity     = new RangerAtlasEntity(typeName, guid, attributes);
+            RangerServiceResource resource   = mapper.buildResource(entity);
+
+            assertTrue("Resource elements list is empty", resource.getResourceElements().size() > 0);
+            assertEquals("Resource elements list size does not match expected", 2, resource.getResourceElements().size());
+            assertNotNull("Resource element missing value for schema", resource.getResourceElements().get("schema"));
+            assertEquals("Resource element schema value does not match", Collections.singletonList("json_object.foo.v1"), resource.getResourceElements().get("schema").getValues());
+            assertNotNull("Resource element missing value for field", resource.getResourceElements().get("field"));
+            assertEquals("Resource element field value does not match", Collections.singletonList("channel"), resource.getResourceElements().get("field").getValues());
+            assertEquals("serviceName does not match expected value", "dev_nestedstructure", resource.getServiceName());
+        } catch(Exception e) {
+            e.printStackTrace();
+            fail("An error occurred while processing resource");
+        }
+    }
+
+    @Test
+    public void test_RangerEntityJsonObject() {
+        String typeName = "json_object";
+        String guid     = "9fsdd-sfsrsag-dasd-3fa97";
+
+        try {
+            Map<String, Object>   attributes = Collections.singletonMap("qualifiedName", "json_object.foo.v1");
+            RangerAtlasEntity     entity     = new RangerAtlasEntity(typeName, guid, attributes);
+            RangerServiceResource resource   = mapper.buildResource(entity);
+
+            assertTrue("Resource elements list is empty", resource.getResourceElements().size() > 0);
+            assertEquals("Resource elements list size does not match expected", 1, resource.getResourceElements().size());
+            assertNotNull("Resource element missing value for schema", resource.getResourceElements().get("schema"));
+            assertEquals("Resource element schema value does not match", Collections.singletonList("json_object.foo.v1"), resource.getResourceElements().get("schema").getValues());
+            assertEquals("serviceName does not match expected value", "null_nestedstructure", resource.getServiceName());
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail("An error occurred while processing resource");
+        }
+
+        // qualifiedName containing clusterName
+        try {
+            Map<String, Object>   attributes = Collections.singletonMap("qualifiedName", "json_object.foo.v1@dev");
+            RangerAtlasEntity     entity     = new RangerAtlasEntity(typeName, guid, attributes);
+            RangerServiceResource resource   = mapper.buildResource(entity);
+
+            assertTrue("Resource elements list is empty", resource.getResourceElements().size() > 0);
+            assertEquals("Resource elements list size does not match expected", 1, resource.getResourceElements().size());
+            assertNotNull("Resource element missing value for schema", resource.getResourceElements().get("schema"));
+            assertEquals("Resource element schema value does not match", Collections.singletonList("json_object.foo.v1"), resource.getResourceElements().get("schema").getValues());
+            assertEquals("serviceName does not match expected value", "dev_nestedstructure", resource.getServiceName());
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail("An error occurred while processing resource");
+        }
+    }
+}