Java tutorial
/* * 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.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 */