blob: 203b2ed6c48d63b57f08924d42e1f0b891a1658a [file] [log] [blame]
/*
* 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.
*
*/
/*
* AT&T - PROPRIETARY
* THIS FILE CONTAINS PROPRIETARY INFORMATION OF
* AT&T AND IS NOT TO BE DISCLOSED OR USED EXCEPT IN
* ACCORDANCE WITH APPLICABLE AGREEMENTS.
*
* Copyright (c) 2013 AT&T Knowledge Ventures
* Unpublished and Not for Publication
* All Rights Reserved
*/
package org.apache.openaz.xacml.std.json;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.security.auth.x500.X500Principal;
import javax.xml.XMLConstants;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.openaz.xacml.api.Attribute;
import org.apache.openaz.xacml.api.AttributeValue;
import org.apache.openaz.xacml.api.DataType;
import org.apache.openaz.xacml.api.DataTypeFactory;
import org.apache.openaz.xacml.api.Identifier;
import org.apache.openaz.xacml.api.Request;
import org.apache.openaz.xacml.api.RequestAttributes;
import org.apache.openaz.xacml.api.RequestAttributesReference;
import org.apache.openaz.xacml.api.RequestReference;
import org.apache.openaz.xacml.api.SemanticString;
import org.apache.openaz.xacml.api.XACML3;
import org.apache.openaz.xacml.std.IdentifierImpl;
import org.apache.openaz.xacml.std.StdAttribute;
import org.apache.openaz.xacml.std.StdAttributeValue;
import org.apache.openaz.xacml.std.StdMutableRequest;
import org.apache.openaz.xacml.std.StdMutableRequestAttributes;
import org.apache.openaz.xacml.std.StdMutableRequestReference;
import org.apache.openaz.xacml.std.StdRequest;
import org.apache.openaz.xacml.std.StdRequestAttributesReference;
import org.apache.openaz.xacml.std.StdRequestDefaults;
import org.apache.openaz.xacml.std.datatypes.DataTypes;
import org.apache.openaz.xacml.std.datatypes.ExtendedNamespaceContext;
import org.apache.openaz.xacml.std.datatypes.StringNamespaceContext;
import org.apache.openaz.xacml.std.datatypes.XPathExpressionWrapper;
import org.apache.openaz.xacml.std.dom.DOMUtil;
import org.apache.openaz.xacml.util.FactoryException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* JSONRequest is used to convert JSON into {@link org.apache.openaz.xacml.api.Request} objects. Instances are
* only generated by loading a file, string, or InputStream representing the Request.
*/
public class JSONRequest {
private static final Log logger = LogFactory.getLog(JSONRequest.class);
/*
* Map of Data Type Identifiers used to map shorthand notation for DataTypes into the full Identifer. This
* is loaded the first time a Request is processed. Loading is done using Reflection. The map contains
* keys for both the short form and the long form of each DataType. For example both of the following are
* in the table: http://www.w3.org/2001/XMLSchema#base64Binary =
* http://www.w3.org/2001/XMLSchema#base64Binary base64Binary =
* http://www.w3.org/2001/XMLSchema#base64Binary (Note difference in structure and usage from
* JSONResponse.)
*/
private static Map<String, Identifier> shorthandMap = null;
/*
* To check the individual data attributes for being the correct type, we need an instance of the
* DataTypeFactory
*/
private static DataTypeFactory dataTypeFactory = null;
/*
* Prevent creation of instances - this class contains only static methods that return other object types.
*/
protected JSONRequest() {
}
//
// HELPER METHODS used in Parsing
//
/**
* Allow both JSON boolean and Strings containing JSON booleans. If value is null, assume it is optional
* and just return null. If Boolean, return same value. If String, note in log that it was string and
* return converted value. Otherwise throw exception.
*
* @param value
* @param location
* @return
* @throws JSONStructureException
*/
private static Boolean makeBoolean(Object value, String location) throws JSONStructureException {
if (value == null || value instanceof Boolean) {
return (Boolean)value;
}
try {
Boolean b = DataTypes.DT_BOOLEAN.convert(value);
logger.warn(location + " has string containing boolean, should be unquoted boolean value");
return b;
} catch (Exception e) {
throw new JSONStructureException(location + " must be Boolean");
}
}
/**
* Check the given map for all components having been removed (i.e. everything in the map was known and
* used). If anything remains, throw an exception based on the component and the keys left in the map
*/
private static void checkUnknown(String component, Map<?, ?> map) throws JSONStructureException {
if (map.size() == 0) {
return;
}
String keys = null;
Iterator<?> it = map.keySet().iterator();
while (it.hasNext()) {
if (keys == null) {
keys = "'" + it.next().toString() + "'";
} else {
keys += ", '" + it.next().toString() + "'";
}
}
String message = component + " contains unknown element" + ((map.size() == 1) ? " " : "s ") + keys;
throw new JSONStructureException(message);
}
/**
* Convert a JSON representation of an XPathExpression into the internal objects. XPathExpression is the
* only DataType that has a complex multi-part description. It includes - XPathCategory - required - XPath
* - required - Namespaces - optional; a list of complex structures
*
* @param valueMap
* @return
* @throws JSONStructureException
*/
private static AttributeValue<Object> convertXPathExpressionMapToAttributeValue(Map<?, ?> valueMap)
throws JSONStructureException {
// get required elements
Object xpathCategoryObject = valueMap.remove("XPathCategory");
Object xpathObject = valueMap.remove("XPath");
if (!(xpathCategoryObject instanceof String) || !(xpathObject instanceof String)) {
throw new JSONStructureException("XpathCategory and XPath must both be strings");
}
String xpathCategoryString = (String)xpathCategoryObject;
String xpathString = (String)xpathObject;
if (xpathCategoryString == null || xpathCategoryString.length() == 0 || xpathString == null
|| xpathString.length() == 0) {
throw new JSONStructureException("XPathCategory or XPath missing or 0-length");
}
Identifier xpathCategoryId = new IdentifierImpl(xpathCategoryString);
// get the Namespaces, if any.
// Use StringNamespaceContext because we need to use the add functions to incrementally add the
// namespaces
StringNamespaceContext namespaceContext = null;
Object namespacesObject = valueMap.remove("Namespaces");
if (namespacesObject != null) {
if (!(namespacesObject instanceof List)) {
throw new JSONStructureException("Namespaces must be an array");
}
List<?> namespacesList = (List<?>)namespacesObject;
if (namespacesList.size() > 0) {
// create a NamespaceContext object to hold the namespaces
namespaceContext = new StringNamespaceContext();
for (Object n : namespacesList) {
if (!(n instanceof Map)) {
throw new JSONStructureException("Namespace within Namespaces array must be object");
}
Map<?, ?> namespaceMap = (Map<?, ?>)n;
Object namespaceObject = namespaceMap.remove("Namespace");
if (namespaceObject == null || !(namespaceObject instanceof String)) {
throw new JSONStructureException(
"Namespace object within Namespaces array must contain Namespace string member");
}
Object prefixObject = namespaceMap.remove("Prefix");
if (prefixObject != null && !(prefixObject instanceof String)) {
throw new JSONStructureException(
"Namespace object within Namespaces array Prefix must be string");
}
checkUnknown("Namespaces item", namespaceMap);
// add this namespace to the NamespaceContext
try {
namespaceContext.add((String)prefixObject, (String)namespaceObject);
} catch (Exception e) {
throw new JSONStructureException("Unable to add namespace prefix='" + prefixObject
+ "' URI='" + namespaceObject + "'");
}
}
}
}
// create the XPathExpressionWrapper to contain this value
XPathExpressionWrapper xpathExpressionWrapper = new XPathExpressionWrapper(namespaceContext,
xpathString);
AttributeValue<Object> attributeValue = new StdAttributeValue<Object>(
DataTypes.DT_XPATHEXPRESSION
.getId(),
xpathExpressionWrapper,
xpathCategoryId);
checkUnknown("XPathExpression", valueMap);
return attributeValue;
}
/**
* Use reflection to load the map with all the names of all DataTypes, both the long name and the
* shorthand, and point each name to the appropriate Identifier. The shorthand map is used differently in
* JSONRequest than in JSONResponse, so there are similarities and differences in the implementation. This
* is done once the first time a Request is processed.
*/
private static void initShorthandMap() throws JSONStructureException {
Field[] declaredFields = XACML3.class.getDeclaredFields();
shorthandMap = new HashMap<String, Identifier>();
for (Field field : declaredFields) {
if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("ID_DATATYPE")
&& Modifier.isPublic(field.getModifiers())) {
try {
Identifier id = (Identifier)field.get(null);
String longName = id.stringValue();
// most names start with 'http://www.w3.org/2001/XMLSchema#'
int sharpIndex = longName.lastIndexOf("#");
if (sharpIndex <= 0) {
// some names start with 'urn:oasis:names:tc:xacml:1.0:data-type:'
// or urn:oasis:names:tc:xacml:2.0:data-type:
if (longName.contains(":data-type:")) {
sharpIndex = longName.lastIndexOf(":");
} else {
continue;
}
}
String shortName = longName.substring(sharpIndex + 1);
// put both the full name and the short name in the table
shorthandMap.put(longName, id);
shorthandMap.put(shortName, id);
} catch (Exception e) {
throw new JSONStructureException("Error loading ID Table, e=" + e);
}
}
}
}
//
// MAIN PARSING CODE
//
/**
* Handle a List of Attributes for a Category
*
* @param categoryID
* @param attributes
* @param stdMutableRequest
* @throws JSONStructureException
*/
private static List<Attribute> parseAttribute(Identifier categoryID, ArrayList<?> attributes)
throws JSONStructureException {
Iterator<?> iterAttributes = attributes.iterator();
List<Attribute> collectedAttributes = new ArrayList<Attribute>();
while (iterAttributes.hasNext()) {
Map<?, ?> attributeMap = (Map<?, ?>)iterAttributes.next();
if (!(attributeMap instanceof Map)) {
throw new JSONStructureException("Expect Attribute content to be Map got "
+ attributeMap.getClass());
}
Attribute attribute = parseAttribute(categoryID, attributeMap);
collectedAttributes.add(attribute);
}
// return list of all attributes for this Category
return collectedAttributes;
}
/**
* Given the map of the parsed JSON representation of an Attribute, create the Attribute from the map.
*
* @param categoryID
* @param attributeMap
* @param stdMutableRequest
* @return
* @throws JSONStructureException
*/
private static Attribute parseAttribute(Identifier categoryID, Map<?, ?> attributeMap)
throws JSONStructureException {
// TODO - ASSUME that the spec will remove the requirement that we MUST "handle" JavaScript special
// values NaN, INF, -INF, none of which make sense on this interface.
// TODO - ASSUME that the spec will fix inconsistency between AttributeId and Id (both are mentioned),
// but we have code using both so allow both on input.
Object idString = attributeMap.remove("AttributeId");
if (idString == null) {
//
// This is an annoying message, and since we have PEP's that already use it
// make this a debugging message. Otherwise it will clog our log file.
//
if (logger.isDebugEnabled()) {
logger.debug("Attribute missing AttributeId, looking for Id");
}
idString = attributeMap.remove("Id");
if (idString == null) {
throw new JSONStructureException("Attribute missing AttributeId (and Id)");
}
} else {
// we have the AttributeId - should not also have Id
if (attributeMap.remove("Id") != null) {
throw new JSONStructureException("Found both AttributeId '" + idString
+ "' and Id field. Please use only AttributeId.");
}
}
if (!(idString instanceof String)) {
throw new JSONStructureException("AttributeId must be String, got " + idString.getClass());
}
Identifier id = new IdentifierImpl(idString.toString());
Object Value = attributeMap.remove("Value");
if (Value == null) {
throw new JSONStructureException("Attribute missing Value");
}
String Issuer = (String)attributeMap.remove("Issuer");
Object includeInResultObject = attributeMap.remove("IncludeInResult");
Boolean includeInResult = makeBoolean(includeInResultObject, "IncludeInResult");
if (includeInResult == null) {
includeInResult = Boolean.FALSE;
}
//
// Data Type is complicated because:
// - it may use shorthand (e.g. "integer" instead of full Id)
// - it may be missing and have to be inferred
// - inference on Arrays of values is tricky
// - arrays must all use the same DataType
// - we are limited in the data types that the Jackson parser is able to infer
//
Object DataType = attributeMap.remove("DataType");
if (DataType != null && !(DataType instanceof String)) {
throw new JSONStructureException("DataType must be String, got " + DataType.getClass());
}
// get Identifier for either long-form or shorthand notation of DataType
String dataTypeString = null;
if (DataType != null) {
dataTypeString = DataType.toString();
}
Identifier dataTypeId = shorthandMap.get(dataTypeString);
// check for unknown DataType
if (DataType != null && dataTypeId == null) {
// attribute contained a DataType but it was not known
throw new JSONStructureException("Unknown DataType '" + dataTypeString + "'");
}
// At this point the dataTypeId may be null if no explicit DataType was given.
// In that case we need to infer the data type from the value object.
// The best we can do is infer based on the JSON data type, so we recognize boolean, integer, and
// double, and everything else is handled as a string.
// Unfortunately the value may be an array of values, so we need to look at all of them before making
// a decision.
// The algorithm for arrays is:
// - take the dataType of the first element in the array as the array's data type
// - if the array is of type Integer and we see a Double, make the array be Double.
// Try to determine the over-all type for the list based on the contents.
// The only mixing that is allowed is Integers and Doubles, in which case the list is Double
// Values are converted to the current DataType wherever possible.
// This includes converting doubles, integers and booleans into Strings if the DataType is String.
// Auto-conversions generate warning messages to the logger.
// TODO - ASSUME that we need to infer data type for array if not given. Spec is inconsistent on this
// point, but author seems to want to do it.
// TODO - Also ASSUME that
// - everything other than JSON integer, double and boolean is handled as a string, and
// - the only mixture of data types allowed within the same array is integer and double, yielding the
// DataType for the array = Double, and
// - an array of the same JSON data type has the same data type; for strings this means type=string
// irrespective of what the strings represent (e.g. Date, URI, etc).
if (Value instanceof List) {
List<?> valueList = (List<?>)Value;
// if nothing in the list then we don't care about the type
if (valueList.size() > 0) {
Identifier inferredDataTypeId = null;
if (dataTypeId == null) {
// DataType was not given in Attribute - must infer it
for (Object item : (List<?>)Value) {
// figure out what data type to use for this array
if (inferredDataTypeId == null) {
// first item, need to set provisional inferred data type
if (item instanceof Boolean) {
inferredDataTypeId = DataTypes.DT_BOOLEAN.getId();
} else if (item instanceof Integer) {
inferredDataTypeId = DataTypes.DT_INTEGER.getId();
} else if (item instanceof Double) {
inferredDataTypeId = DataTypes.DT_DOUBLE.getId();
} else {
inferredDataTypeId = DataTypes.DT_STRING.getId();
}
} else if (inferredDataTypeId.equals(DataTypes.DT_INTEGER.getId())
&& item instanceof Double) {
// special case - Double seen in Integer list means whole list is really
// Double
inferredDataTypeId = DataTypes.DT_DOUBLE.getId();
}
}
// we have inferred a data type for the whole array
dataTypeId = inferredDataTypeId;
}
}
} else {
// single-value attribute
if (dataTypeId == null) {
// single value with no DataType defined - Infer the XACML DataType for JSON data types
if (Value instanceof Integer) {
dataTypeId = DataTypes.DT_INTEGER.getId();
} else if (Value instanceof Double) {
dataTypeId = DataTypes.DT_DOUBLE.getId();
} else if (Value instanceof Boolean) {
dataTypeId = DataTypes.DT_BOOLEAN.getId();
} else {
// the Default DataType if none is given is String
dataTypeId = DataTypes.DT_STRING.getId();
}
}
// all other data types are not explicitly checked for compatibility
}
// we now have the DataType to convert the values into.
// create a single Attribute to return (it may contain multiple AttributeValues)
Attribute attribute = null;
DataType<?> dataType = dataTypeFactory.getDataType(dataTypeId);
// Variable to use for reporting errors
Object incomingValue = null;
try {
if (Value instanceof List) {
// this attribute has a list of values
List<AttributeValue<?>> attributeValueList = new ArrayList<AttributeValue<?>>();
for (Object o : (List<?>)Value) {
// for error reporting we make a copy visible to the Catch clause
incomingValue = o;
AttributeValue<Object> attributeValue;
if (dataType.getId().equals(DataTypes.DT_XPATHEXPRESSION.getId())) {
if (!(o instanceof Map)) {
throw new JSONStructureException(
"XPathExpression must contain object, not simple value");
}
attributeValue = convertXPathExpressionMapToAttributeValue((Map<?, ?>)o);
} else {
Object convertedValue = dataType.convert(o);
attributeValue = new StdAttributeValue<Object>(dataTypeId, convertedValue);
if ((convertedValue instanceof Integer || convertedValue instanceof Boolean || convertedValue instanceof Double) && o instanceof String
|| convertedValue instanceof Double && o instanceof Integer
|| convertedValue instanceof String && (o instanceof Integer
|| o instanceof Boolean || o instanceof Double)) {
// we converted a String to something else
logger.warn("Attribute Id '" + id.stringValue() + "' Value '" + incomingValue
+ "' in Array auto-converted from '" + o.getClass().getName()
+ "' to type '" + dataType.getId().stringValue());
}
}
attributeValueList.add(attributeValue);
}
attribute = new StdAttribute(categoryID, id, attributeValueList, Issuer, includeInResult);
} else {
// for error reporting we make a copy visible to the Catch clause
incomingValue = Value;
// this attribute has a single value
AttributeValue<Object> attributeValue;
if (dataType.getId().equals(DataTypes.DT_XPATHEXPRESSION.getId())) {
if (!(Value instanceof Map)) {
throw new JSONStructureException(
"XPathExpression must contain object, not simple value");
}
attributeValue = convertXPathExpressionMapToAttributeValue((Map<?, ?>)Value);
} else {
Object convertedValue = dataType.convert(Value);
attributeValue = new StdAttributeValue<Object>(dataTypeId, convertedValue);
// some auto-conversions should be logged because they shouldn't be necessary
if ((convertedValue instanceof BigInteger || convertedValue instanceof Boolean || convertedValue instanceof Double) && Value instanceof String
|| convertedValue instanceof Double && Value instanceof Integer
|| convertedValue instanceof String && (Value instanceof Integer
|| Value instanceof Boolean || Value instanceof Double)) {
// we converted a String to something else
logger.warn("Attribute Id '" + id.stringValue() + "' Value '" + incomingValue
+ "' auto-converted from '" + Value.getClass().getName() + "' to type '"
+ dataType.getId().stringValue());
}
}
attribute = new StdAttribute(categoryID, id, attributeValue, Issuer, includeInResult);
}
} catch (Exception e) {
throw new JSONStructureException("In Id='" + id.stringValue()
+ "' Unable to convert Attribute Value '" + incomingValue
+ "' to type '" + dataTypeId.stringValue() + "'");
}
checkUnknown(id.stringValue() + "Attribute '" + idString.toString() + "'", attributeMap);
return attribute;
}
/**
* Convert the contents of a Content element from XML into XML Node
*
* @param xmlContent
* @return Node
* @throws Exception
*/
public static Node parseXML(String xmlContent) throws JSONStructureException {
if (xmlContent == null || xmlContent.length() == 0) {
return null;
}
//
// First of all, the String is possible escaped.
//
// The meaning of "escaped" is defined in section 4.2.3.1 in the JSON spec
//
String unescapedContent = xmlContent.replace("\\\"", "\"");
unescapedContent = unescapedContent.replace("\\\\", "\\");
// logger.info("Escaped content: \n" + unescapedContent);
try (InputStream is = new ByteArrayInputStream(unescapedContent.getBytes("UTF-8"))) {
Document doc = DOMUtil.loadDocument(is);
if (doc != null) {
return doc.getDocumentElement();
}
return null;
} catch (Exception ex) {
throw new JSONStructureException("Unable to parse Content '" + xmlContent + "'");
}
}
/**
* Helper to parse all components of one Category or default Category
*
* @param categoryMap
* @param request
*/
private static void parseCategory(Map<?, ?> categoryMap, String categoryName,
Identifier defaultCategoryId, StdMutableRequest stdMutableRequest)
throws JSONStructureException {
Identifier categoryId = defaultCategoryId;
Object categoryIDString = ((Map<?, ?>)categoryMap).remove("CategoryId");
if (categoryIDString == null && defaultCategoryId == null) {
throw new JSONStructureException("Category is missing CategoryId");
}
if (categoryIDString != null) {
if (!(categoryIDString instanceof String)) {
throw new JSONStructureException("Expect '" + categoryName + "' CategoryId to be String got "
+ categoryIDString.getClass());
} else {
// TODO Spec says CategoryId may be shorthand, but none have been specified
categoryId = new IdentifierImpl(categoryIDString.toString());
}
}
// if we know the category, make sure user gave correct Id
if (defaultCategoryId != null && !defaultCategoryId.equals(categoryId)) {
throw new JSONStructureException(categoryName + " given CategoryId '" + categoryId
+ "' which does not match default id '" + defaultCategoryId
+ "'");
}
// get the Id, a.k.a xmlId
String xmlId = (String)((Map<?, ?>)categoryMap).remove("Id");
// get the Attributes for this Category, if any
List<Attribute> attributeList = new ArrayList<Attribute>();
Object attributesMap = ((Map<?, ?>)categoryMap).remove("Attribute");
if (attributesMap != null) {
if (attributesMap instanceof ArrayList) {
attributeList = parseAttribute(categoryId, (ArrayList<?>)attributesMap);
} else if (attributesMap instanceof Map) {
// underlying code expects only collections of Attributes, so create a collection of one to
// pass this single value
ArrayList<Map<?, ?>> listForOne = new ArrayList<Map<?, ?>>();
listForOne.add((Map<?, ?>)attributesMap);
attributeList = parseAttribute(categoryId, listForOne);
} else {
throw new JSONStructureException("Category '" + categoryName
+ "' saw unexpected Attribute class "
+ attributesMap.getClass());
}
}
// Get the Content node for this Category, if any
Node contentRootNode = null;
Object content = categoryMap.remove("Content");
if (content != null) {
if (content instanceof String) {
//
// Is it Base64 Encoded?
//
if (Base64.isBase64(((String)content).getBytes())) {
//
// Attempt to decode it
//
byte[] realContent = Base64.decodeBase64((String)content);
//
// Now what is it? JSON or XML? Should be XML.
//
try {
contentRootNode = parseXML(new String(realContent, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new JSONStructureException("Category '" + categoryName
+ "' Unsupported encoding in Content");
}
} else {
//
// No, so what is it? Should be XML escaped
//
contentRootNode = parseXML((String)content);
}
} else if (content instanceof byte[]) {
//
// Should be Base64
//
if (Base64.isBase64(((String)content).getBytes())) {
//
// Attempt to decode it
//
byte[] realContent = Base64.decodeBase64((String)content);
//
// Now what is it? JSON or XML? Should be XML.
//
try {
contentRootNode = parseXML(new String(realContent, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new JSONStructureException("Category '" + categoryName
+ "' Unsupported encoding in Content");
}
} else {
throw new JSONStructureException("Category '" + categoryName
+ "' Content expected Base64 value");
}
} else {
throw new JSONStructureException("Category '" + categoryName
+ "' Unable to determine what Content is "
+ content.getClass());
}
}
checkUnknown(categoryName, categoryMap);
StdMutableRequestAttributes attributeCategory = new StdMutableRequestAttributes(categoryId,
attributeList,
contentRootNode,
xmlId);
stdMutableRequest.add(attributeCategory);
}
/**
* Load the "Default Category" objects, if any. This is used for the special cases of AccessSubject,
* Action, Resource, and Environment
*
* @param jsonRequestMap
* @param categoryName
* @param categoryIdString
* @param stdMutableRequest
* @throws JSONStructureException
*/
private static void parseDefaultCategory(Map<?, ?> jsonRequestMap, String categoryName,
String categoryIdString, StdMutableRequest stdMutableRequest)
throws JSONStructureException {
Object categoryMap = jsonRequestMap.remove(categoryName);
if (categoryMap != null) {
Identifier defaultIdentifier = new IdentifierImpl(categoryIdString);
// The contents may be either a single item (whose attributes are in a Map)
// or a list of items
if (categoryMap instanceof Map) {
// default category contains a single object
parseCategory((Map<?, ?>)categoryMap, categoryName, defaultIdentifier, stdMutableRequest);
} else if (categoryMap instanceof List) {
// Array (for Multiple Decision) of this default category - create separate element for each
// item in list using same CategoryId for all
List<?> categoryList = (List<?>)categoryMap;
for (Object subCategory : categoryList) {
if (!(subCategory instanceof Map)) {
throw new JSONStructureException(
categoryName
+ " array can only contain objects within curly braces");
}
parseCategory((Map<?, ?>)subCategory, categoryName, defaultIdentifier, stdMutableRequest);
}
} else {
// do not understand this
throw new JSONStructureException(
categoryName
+ " must have one object contained within curly braces ({}) or an array of objects ([{}{}])");
}
}
}
//
// Primary interface methods
//
/**
* Parse and JSON string into a {@link org.apache.openaz.xacml.api.Request} object.
*
* @param jsonString
* @return
* @throws JSONStructureException
*/
public static Request load(String jsonString) throws JSONStructureException {
Request request = null;
try (InputStream is = new ByteArrayInputStream(jsonString.getBytes("UTF-8"))) {
request = JSONRequest.load(is);
} catch (Exception ex) {
throw new JSONStructureException("Exception loading String Request: " + ex.getMessage(), ex);
}
return request;
}
/**
* Read a file containing the JSON description of a XACML Request and parse it into a
* {@link org.apache.openaz.xacml.api.Request} Object. This is only used for testing. In normal operation a
* Request arrives through the RESTful interface and is processed using
* <code>load(String jsonString)</code>.
*
* @param fileRequest
* @return
* @throws JSONStructureException
*/
public static Request load(File fileRequest) throws JSONStructureException {
Request request = null;
try (FileInputStream fis = new FileInputStream(fileRequest)) {
request = JSONRequest.load(fis);
} catch (Exception ex) {
throw new JSONStructureException("Exception loading File Request: " + ex.getMessage(), ex);
}
return request;
}
/**
* Read characters from the given <code>InputStream</code> and parse them into an XACML
* {@link org.apache.openaz.xacml.api.Request} object.
*
* @param is
* @return
* @throws JSONStructureException
*/
public static Request load(InputStream is) throws JSONStructureException {
// TODO - ASSUME that order of members within an object does not matter (Different from XML, in JSON
// everything is handled as Maps so order does not matter)
// ensure shorthand map is set up
if (shorthandMap == null) {
initShorthandMap();
}
// ensure that we have an instance of the DataTypeFactory for generating AttributeValues by DataType
if (dataTypeFactory == null) {
try {
dataTypeFactory = DataTypeFactory.newInstance();
if (dataTypeFactory == null) {
throw new NullPointerException("No DataTypeFactory found");
}
} catch (FactoryException e) {
throw new JSONStructureException("Unable to find DataTypeFactory, e=" + e);
}
}
// create a new Request object to be filled in
StdMutableRequest stdMutableRequest = null;
String json = null;
ObjectMapper mapper = null;
try {
// read the inputStream into a buffer (trick found online scans entire input looking for
// end-of-file)
java.util.Scanner scanner = new java.util.Scanner(is);
scanner.useDelimiter("\\A");
json = scanner.hasNext() ? scanner.next() : "";
scanner.close();
mapper = new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
// TODO - ASSUME that any duplicated component is a bad thing (probably indicating an error in the
// incoming JSON)
mapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
Map<?, ?> root = mapper.readValue(json, Map.class);
//
// Does the request exist?
//
Map<?, ?> jsonRequestMap = (Map<?, ?>)root.remove("Request");
if (jsonRequestMap == null) {
throw new JSONStructureException("No \"Request\" property found.");
}
checkUnknown("Top-level message", root);
stdMutableRequest = new StdMutableRequest();
//
// Is there a Category?
//
Object categoryList = jsonRequestMap.remove("Category");
if (categoryList != null && !(categoryList instanceof List)) {
throw new JSONStructureException("Category must contain list of objects, not '"
+ categoryList.getClass() + "'");
}
if (categoryList != null) {
//
// Iterate each Category
//
Iterator<?> iter = ((List<?>)categoryList).iterator();
while (iter.hasNext()) {
Object category = iter.next();
if (!(category instanceof Map)) {
throw new JSONStructureException(
"Category list must contain objects contained within curly braces ({})");
}
parseCategory((Map<?, ?>)category, "Category", null, stdMutableRequest);
}
}
// The following may be either a single instance or an array. This allows multiple decisions to
// work with the Default Category objects.
// Example:
// "AccessSubject" : [ {attributes group one},
// {attributes group two}
// ]
//
// Look for default Shorthand AccessSubject
//
parseDefaultCategory(jsonRequestMap, "AccessSubject",
"urn:oasis:names:tc:xacml:1.0:subject-category:access-subject",
stdMutableRequest);
//
// Provide backward compatibility for our PEP's
//
parseDefaultCategory(jsonRequestMap, "Subject",
"urn:oasis:names:tc:xacml:1.0:subject-category:access-subject",
stdMutableRequest);
//
// Look for default Shorthand Action
//
parseDefaultCategory(jsonRequestMap, "Action",
"urn:oasis:names:tc:xacml:3.0:attribute-category:action", stdMutableRequest);
//
// Look for default Shorthand Resource
//
parseDefaultCategory(jsonRequestMap, "Resource",
"urn:oasis:names:tc:xacml:3.0:attribute-category:resource",
stdMutableRequest);
//
// Look for default Shorthand Environment
//
parseDefaultCategory(jsonRequestMap, "Environment",
"urn:oasis:names:tc:xacml:3.0:attribute-category:environment",
stdMutableRequest);
//
// Look for default Shorthand RecipientSubject
//
parseDefaultCategory(jsonRequestMap, "RecipientSubject",
"urn:oasis:names:tc:xacml:1.0:subject-category:recipient-subject",
stdMutableRequest);
//
// Look for default Shorthand IntermediarySubject
//
parseDefaultCategory(jsonRequestMap, "IntermediarySubject",
"urn:oasis:names:tc:xacml:1.0:subject-category:intermediary-subject",
stdMutableRequest);
//
// Look for default Shorthand Codebase
//
parseDefaultCategory(jsonRequestMap, "Codebase",
"urn:oasis:names:tc:xacml:1.0:subject-category:codebase", stdMutableRequest);
//
// Look for default Shorthand RequestingMachine
//
parseDefaultCategory(jsonRequestMap, "RequestingMachine",
"urn:oasis:names:tc:xacml:1.0:subject-category:requesting-machine",
stdMutableRequest);
//
// MultiRequest
//
Map<?, ?> multiRequests = (Map<?, ?>)jsonRequestMap.remove("MultiRequests");
if (multiRequests != null) {
if (!(multiRequests instanceof Map)) {
throw new JSONStructureException(
"MultiRequests must be object structure, not single value");
}
List<?> requestReferenceList = (List<?>)multiRequests.remove("RequestReference");
if (requestReferenceList == null) {
throw new JSONStructureException("MultiRequest must contain a RequestReference element");
}
if (requestReferenceList.size() < 1) {
throw new JSONStructureException(
"MultiRequest must contain at least one element in the RequestReference list");
}
checkUnknown("MultiRequest", multiRequests);
for (Object requestReferenceMapObject : requestReferenceList) {
if (!(requestReferenceMapObject instanceof Map)) {
throw new JSONStructureException("MultiRequest RequestReference must be object");
}
Map<?, ?> requestReferenceMap = (Map<?, ?>)requestReferenceMapObject;
// each object within the list must contain a ReferenceId and only a ReferenceId
Object referenceIdListObject = requestReferenceMap.remove("ReferenceId");
if (referenceIdListObject == null) {
throw new JSONStructureException(
"MultiRequest RequestReference list element must contain ReferenceId");
}
List<?> referenceIdList = (List<?>)referenceIdListObject;
if (referenceIdList.size() == 0) {
// the spec does not disallow empty list RequestReference objects
continue;
}
checkUnknown("RequestReference", requestReferenceMap);
// create reference corresponding to RequestReference list element
StdMutableRequestReference requestReference = new StdMutableRequestReference();
for (Object referenceId : referenceIdList) {
// add attributes to the reference
// Since the order of the JSON is not constrained, we could process this section
// before the section containing attribute being referenced,
// so we cannot do a cross-check here to verify that the attribute reference exists.
// That will happen later when the PDP attempts to find the attribute.
StdRequestAttributesReference requestAttributesReference = new StdRequestAttributesReference(
(String)referenceId);
requestReference.add(requestAttributesReference);
}
stdMutableRequest.add(requestReference);
}
}
//
// ReturnPolicyIdList
//
// If omitted this is set to a default of false by the StdMutableRequest constructor.
//
Object returnPolicyIdList = jsonRequestMap.remove("ReturnPolicyIdList");
Boolean returnPolicyIdListBoolean = makeBoolean(returnPolicyIdList, "ReturnPolicyIdList");
if (returnPolicyIdList != null) {
stdMutableRequest.setReturnPolicyIdList(returnPolicyIdListBoolean);
}
//
// CombinedDecision
//
// If omitted this is set to a default of false by the StdMutableRequest constructor.
//
Object combinedDecision = jsonRequestMap.remove("CombinedDecision");
Boolean combinedDecisionBoolean = makeBoolean(combinedDecision, "CombinedDecision");
if (combinedDecision != null) {
stdMutableRequest.setCombinedDecision(combinedDecisionBoolean);
}
//
// XPath
//
// The JSON spec says that this has a default value, implying that if it is missing in the Request
// we should fill it in.
// However the XML (DOM) version does not do that. If the value is missing it leaves the
// requestDefaults object blank.
// We are following the XML approach and ignoring the Default value for this field in the spec.
// TODO - Assume that no value for XPathVersion means "leave as null", not "fill in the default
// value from spec. This violates the JSON spec
Object xPath = jsonRequestMap.remove("XPathVersion");
if (xPath != null) {
// XPath is given in the JSON input
if (!(xPath instanceof String)) {
throw new JSONStructureException("XPathVersion not a URI passed as a String");
}
URI xPathUri = null;
try {
xPathUri = new URI(xPath.toString());
} catch (Exception e) {
throw new JSONStructureException("XPathVersion not a valid URI: '" + xPath + "'", e);
}
StdRequestDefaults requestDefaults = new StdRequestDefaults(xPathUri);
stdMutableRequest.setRequestDefaults(requestDefaults);
}
checkUnknown("Request", jsonRequestMap);
} catch (JsonParseException e) {
// try to point to problem area in JSON input, if possible
JsonLocation location = e.getLocation();
String locationOfError = "(unavailable)";
if (location != null && location != JsonLocation.NA) {
String jsonText = json;
if (location.getLineNr() > 1) {
String[] jsonArray = jsonText.split("\\r?\\n|\\r");
jsonText = jsonArray[location.getLineNr()];
}
if (location.getCharOffset() < jsonText.length()) {
if (location.getCharOffset() > 0) {
locationOfError = jsonText.substring((int)location.getCharOffset() - 1);
}
if (locationOfError.length() > 30) {
locationOfError = locationOfError.substring(0, 30);
}
}
}
throw new JSONStructureException("Unable to parse JSON starting at text'" + locationOfError
+ "', input was '" + json + "', exception: " + e, e);
} catch (JsonMappingException e) {
throw new JSONStructureException("Unable to map JSON '" + json + "', exception: " + e, e);
} catch (IOException e) {
throw new JSONStructureException("Unable to read JSON input, exception: " + e, e);
}
// all done
return new StdRequest(stdMutableRequest);
}
//
// Generate JSON string from a Request object created by another means (e.g. XML).
//
/**
* Convert the {@link org.apache.openaz.xacml.api.Request} into an JSON string with pretty-printing.
*
* @param request
* @return
* @throws Exception
*/
public static String toString(Request request) throws Exception {
return toString(request, true);
}
/**
* Convert the {@link org.apache.openaz.xacml.api.Response} into an JSON string, pretty-printing is
* optional.
*
* @param response
* @param prettyPrint
* @return
* @throws Exception
*/
public static String toString(Request request, boolean prettyPrint) throws Exception {
String outputString = null;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
convert(request, os, prettyPrint);
outputString = new String(os.toByteArray(), "UTF-8");
} catch (Exception ex) {
throw ex;
}
return outputString;
}
/**
* Convert the {@link org.apache.openaz.xacml.api.Request} object into a string suitable for output.
* IMPORTANT: This method does NOT close the outputStream. It is the responsibility of the caller to (who
* opened the stream) to close it.
*
* @param request
* @param outputStream
* @throws java.io.IOException
* @throws JSONStructureException
*/
public static void convert(Request request, OutputStream outputStream) throws IOException,
JSONStructureException {
convert(request, outputStream, false);
}
/**
* Do the work of converting the {@link org.apache.openaz.xacml.api.Request} object to a string, allowing
* for pretty-printing if desired. IMPORTANT: This method does NOT close the outputStream. It is the
* responsibility of the caller to (who opened the stream) to close it.
*
* @param request
* @param outputStream
* @param prettyPrint
* @throws java.io.IOException #throws JSONStructureException
*/
public static void convert(Request request, OutputStream outputStream, boolean prettyPrint)
throws IOException, JSONStructureException {
if (request == null) {
throw new NullPointerException("No Request in convert");
}
Map<String, Object> requestMap = new HashMap<String, Object>();
// ReturnPolicyIdList
requestMap.put("ReturnPolicyIdList", request.getReturnPolicyIdList());
// Combined
requestMap.put("CombinedDecision", request.getCombinedDecision());
// XPath
if (request.getRequestDefaults() != null) {
requestMap.put("XPathVersion", request.getRequestDefaults().getXPathVersion());
}
// Categories
Iterator<RequestAttributes> rait = request.getRequestAttributes().iterator();
List<Map<String, Object>> generalCategoriesList = new ArrayList<Map<String, Object>>();
while (rait.hasNext()) {
RequestAttributes ra = rait.next();
// create a new map for the category
Map<String, Object> categoryMap = new HashMap<String, Object>();
// fill in the category
if (ra.getXmlId() != null) {
categoryMap.put("Id", ra.getXmlId());
}
if (ra.getContentRoot() != null) {
StringWriter writer = new StringWriter();
Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(ra.getContentRoot()), new StreamResult(writer));
} catch (Exception e) {
throw new JSONStructureException("Unable to Content node to string; e=" + e);
}
String xml = writer.toString();
categoryMap.put("Content", xml);
}
Iterator<Attribute> attrIt = ra.getAttributes().iterator();
List<Map<String, Object>> attributesList = new ArrayList<Map<String, Object>>();
while (attrIt.hasNext()) {
Attribute attr = attrIt.next();
Map<String, Object> attrMap = new HashMap<String, Object>();
attrMap.put("AttributeId", attr.getAttributeId().stringValue());
if (attr.getIssuer() != null) {
attrMap.put("Issuer", attr.getIssuer());
}
attrMap.put("IncludeInResult", attr.getIncludeInResults());
Collection<AttributeValue<?>> valuesCollection = attr.getValues();
Iterator<AttributeValue<?>> valuesIt = valuesCollection.iterator();
if (valuesCollection.size() == 1) {
// single-value
AttributeValue<?> attrValue = valuesIt.next();
attrMap.put("DataType", attrValue.getDataTypeId().stringValue());
attrMap.put("Value", jsonOutputObject(attrValue.getValue(), attrValue));
} else if (valuesCollection.size() > 1) {
// multiple values
List<Object> attrValueList = new ArrayList<Object>();
while (valuesIt.hasNext()) {
AttributeValue<?> attrValue = valuesIt.next();
// assume all have the same type, so last one in list is fine
attrMap.put("DataType", attrValue.getDataTypeId().stringValue());
attrValueList.add(jsonOutputObject(attrValue.getValue(), attrValue));
}
attrMap.put("Value", attrValueList);
}
attributesList.add(attrMap);
}
if (attributesList.size() > 0) {
categoryMap.put("Attribute", attributesList);
}
// We do not use the "Default" category objects because the XML may have multiples of the same
// Category.
// This is fine when the categories are contained in the array of Category objects,
// but if we use the Default category objects we might end up with multiples of the same Category
// name,
// and the Jackson parser does not handle that well.
// Example: This is ok because the AccessSubjects are independent items within the list:
// { "Request" : {
// "Category" : [
// { "CategoryId" : ""subject", " },
// { "CategoryId" : ""subject", " }
// ]
// }}
//
// This is NOT ok because the Subjects are seen as duplicate elements:
// { "Request" : {
// "AccessSubject" : {"},
// "AccessSubject" : {"},
// }}
categoryMap.put("CategoryId", ra.getCategory().stringValue());
generalCategoriesList.add(categoryMap);
}
if (generalCategoriesList.size() > 0) {
requestMap.put("Category", generalCategoriesList);
}
// MultiRequests
if (request.getMultiRequests() != null) {
Collection<RequestReference> referenceCollection = request.getMultiRequests();
Map<String, Object> multiRequestMap = new HashMap<String, Object>();
List<Map<String, Object>> requestReferenceList = new ArrayList<Map<String, Object>>();
Iterator<RequestReference> rrIt = referenceCollection.iterator();
while (rrIt.hasNext()) {
RequestReference rr = rrIt.next();
Map<String, Object> requestReferenceMap = new HashMap<String, Object>();
Collection<RequestAttributesReference> rarCollection = rr.getAttributesReferences();
List<Object> ridList = new ArrayList<Object>();
Iterator<RequestAttributesReference> rarIt = rarCollection.iterator();
while (rarIt.hasNext()) {
RequestAttributesReference rar = rarIt.next();
ridList.add(rar.getReferenceId());
}
if (ridList.size() > 0) {
requestReferenceMap.put("ReferenceId", ridList);
}
if (requestReferenceMap.size() > 0) {
requestReferenceList.add(requestReferenceMap);
}
if (requestReferenceList.size() > 0) {
multiRequestMap.put("RequestReference", requestReferenceList);
}
}
if (multiRequestMap.size() > 0) {
requestMap.put("MultiRequests", multiRequestMap);
}
}
//
// Create the overall Request map
//
Map<String, Object> theWholeRequest = new HashMap<String, Object>();
theWholeRequest.put("Request", requestMap);
//
// Create a string buffer
//
ObjectMapper mapper = new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
mapper.configure(SerializationFeature.INDENT_OUTPUT, prettyPrint);
try (OutputStreamWriter osw = new OutputStreamWriter(outputStream)) {
// convert the request to json string
String json = mapper.writeValueAsString(theWholeRequest);
// write it
osw.write(json);
// force output
osw.flush();
} catch (Exception e) {
logger.error("Failed to write to json string: " + e.getLocalizedMessage(), e);
}
}
/**
* Create the appropriate object for JSON output. This needs to be a Boolean, Integer or Double for those
* data types so that the ObjectMapper knows how to format the JSON text. For objects implementing
* stringValue we use that string. for XPathExpressions use the Path. Otherwise default to using toString.
*
* @param obj
* @return
*/
private static Object jsonOutputObject(Object obj, AttributeValue<?> attrValue)
throws JSONStructureException {
if (obj instanceof String || obj instanceof Boolean || obj instanceof BigInteger) {
return obj;
} else if (obj instanceof Double) {
Double d = (Double)obj;
if (d == Double.NaN) {
return "NaN";
} else if (d == Double.POSITIVE_INFINITY) {
return "INF";
} else if (d == Double.NEGATIVE_INFINITY) {
return "-INF";
}
return obj;
} else if (obj instanceof SemanticString) {
return ((SemanticString)obj).stringValue();
} else if (obj instanceof X500Principal || obj instanceof URI) {
// something is very weird with X500Principal data type. If left on its own the output is a map
// that includes encoding.
return obj.toString();
} else if (obj instanceof XPathExpressionWrapper) {
// create a map containing the complex value for the XPathExpression
Map<String, Object> xpathExpressionMap = new HashMap<String, Object>();
Identifier xpathCategoryId = attrValue.getXPathCategory();
if (xpathCategoryId == null) {
throw new JSONStructureException("XPathExpression is missing XPathCategory");
}
xpathExpressionMap.put("XPathCategory", attrValue.getXPathCategory().stringValue());
XPathExpressionWrapper xw = (XPathExpressionWrapper)obj;
xpathExpressionMap.put("XPath", xw.getPath());
ExtendedNamespaceContext namespaceContext = xw.getNamespaceContext();
if (namespaceContext != null) {
List<Object> namespaceList = new ArrayList<Object>();
// get the list of all namespace prefixes
Iterator<String> prefixIt = namespaceContext.getAllPrefixes();
while (prefixIt.hasNext()) {
String prefix = prefixIt.next();
String namespaceURI = namespaceContext.getNamespaceURI(prefix);
Map<String, Object> namespaceMap = new HashMap<String, Object>();
if (prefix != null && !prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
namespaceMap.put("Prefix", prefix);
}
namespaceMap.put("Namespace", namespaceURI);
namespaceList.add(namespaceMap);
}
xpathExpressionMap.put("Namespaces", namespaceList);
}
return xpathExpressionMap;
} else {
throw new JSONStructureException("Unhandled data type='" + obj.getClass().getName() + "'");
}
}
}
/*
* Place to put very long output strings for editing during debugging
*/