blob: 57030f277fc8941c2f56403382485c25a10bec9f [file] [log] [blame]
/**
* 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);
}
}