/* * File: ContextUtil.java * * Copyright 2007 Macquarie E-Learning Centre Of Excellence * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package melcoe.xacml.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import melcoe.xacml.MelcoeXacmlException; import org.apache.log4j.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.sun.xacml.Indenter; import com.sun.xacml.ParsingException; import com.sun.xacml.attr.AnyURIAttribute; import com.sun.xacml.attr.AttributeValue; import com.sun.xacml.attr.StringAttribute; import com.sun.xacml.ctx.Attribute; import com.sun.xacml.ctx.RequestCtx; import com.sun.xacml.ctx.ResponseCtx; import com.sun.xacml.ctx.Result; import com.sun.xacml.ctx.Subject; import fedora.common.Constants; /** * Utility class that provides various methods for creating/converting contexts. * This class can convert requests and responses from their string * representations to their object representations and vice versa as well as a * few utility methods for getting information from the contexts. It also * contains methods for constructing requests. * * @author nishen@melcoe.mq.edu.au */ public class ContextUtil { private static Logger log = Logger.getLogger(ContextUtil.class.getName()); private static final URI XACML_RESOURCE_ID = URI.create("urn:oasis:names:tc:xacml:1.0:resource:resource-id"); private static final Map<URI, URI> actionMap = new ConcurrentHashMap<URI, URI>(); private static final Map<String, String> actionValueMap = new ConcurrentHashMap<String, String>(); private static RelationshipResolver DEFAULT_RELATIONSHIP_RESOLVER; private final RelationshipResolver relationshipResolver; /** * We only read and parse the config files once. */ static { initMappings(); initRelationshipResolver(); } public ContextUtil() { this(DEFAULT_RELATIONSHIP_RESOLVER); } public ContextUtil(RelationshipResolver relationshipResolver) { if (relationshipResolver == null) { relationshipResolver = DEFAULT_RELATIONSHIP_RESOLVER; } this.relationshipResolver = relationshipResolver; } public static ContextUtil getInstance() { return new ContextUtil(); } private static void initMappings() { // get the mapping information // get the PEP configuration File configPEPFile = new File(Constants.FEDORA_HOME, "server/config/config-melcoe-pep-mapping.xml"); InputStream is = null; try { is = new FileInputStream(configPEPFile); } catch (FileNotFoundException e) { log.info("Mapping file, config-melcoe-pep-mapping.xml, not found."); } if (is != null) { log .info("Mapping file found (config-melcoe-pep-mapping.xml). Loading maps"); try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); Document doc = docBuilder.parse(is); NodeList nodes = null; nodes = doc.getElementsByTagName("actionAttribute"); if (nodes != null && nodes.getLength() > 0) { for (int x = 0; x < nodes.getLength(); x++) { if (nodes.item(x).getNodeType() == Node.ELEMENT_NODE) { String from = nodes.item(x).getAttributes() .getNamedItem("from") .getNodeValue(); String to = nodes.item(x).getAttributes() .getNamedItem("to").getNodeValue(); try { URI key = new URI(from); URI value = new URI(to); actionMap.put(key, value); } catch (URISyntaxException mue) { log.warn("Mapping contained invalid URI: [" + from + "] / [" + to + "]"); } } } } nodes = doc.getElementsByTagName("actionAttributeValue"); if (nodes != null && nodes.getLength() > 0) { for (int x = 0; x < nodes.getLength(); x++) { if (nodes.item(x).getNodeType() == Node.ELEMENT_NODE) { String from = nodes.item(x).getAttributes() .getNamedItem("from") .getNodeValue(); String to = nodes.item(x).getAttributes() .getNamedItem("to").getNodeValue(); actionValueMap.put(from, to); } } } } catch (Exception e) { log.warn("Error occurred loading the mapping file. " + "Mappings will not be used.", e); } } } private static void initRelationshipResolver() { RelationshipResolver rr; try { // get the PEP configuration File configPEPFile = new File(Constants.FEDORA_HOME, "server/config/config-melcoe-pep.xml"); InputStream is = new FileInputStream(configPEPFile); if (is == null) { throw new MelcoeXacmlException("Could not locate config file: config-melcoe-pep.xml"); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; Document doc; docBuilder = factory.newDocumentBuilder(); doc = docBuilder.parse(is); NodeList nodes = null; Map<String, String> options = new HashMap<String, String>(); String className = null; Constructor<?> c = null; nodes = doc.getElementsByTagName("relationship-resolver"); if (nodes.getLength() != 1) { throw new MelcoeXacmlException("Config file needs to contain exactly 1 'relationship-resolver' section."); } Element relationshipResolverElement = (Element) nodes.item(0); className = relationshipResolverElement.getAttributes() .getNamedItem("class").getNodeValue(); NodeList optionList = relationshipResolverElement.getElementsByTagName("option"); if (optionList == null || optionList.getLength() == 0) { if (log.isDebugEnabled()) { log.debug("creating relationship resolver WITHOUT options"); } rr = (RelationshipResolver) Class.forName(className) .newInstance(); } else { if (log.isDebugEnabled()) { log.debug("creating relationship resolver WITH options"); } options = new HashMap<String, String>(); for (int x = 0; x < optionList.getLength(); x++) { Node n = optionList.item(x); String key = n.getAttributes().getNamedItem("name") .getNodeValue(); String value = n.getFirstChild().getNodeValue(); options.put(key, value); if (log.isDebugEnabled()) { log.debug("Node [name]: " + key + ": " + value); } } c = Class.forName(className) .getConstructor(new Class[] {Map.class}); rr = (RelationshipResolver) c .newInstance(new Object[] {options}); } } catch (Exception e) { log.info("Failed to get configured RelationshipResolver, will try " + "fallback."); log.debug(e.getMessage()); rr = new RelationshipResolverImpl(); } DEFAULT_RELATIONSHIP_RESOLVER = rr; } public RelationshipResolver getRelationshipResolver() { return relationshipResolver; } /** * Sets up the Subject section of the request. Takes a list of Maps of * URI/AttributeValue pairs. Each list element represents one subject which * contains a map of URI/AttributeValues. * * @return a Set of Subject instances for inclusion in a Request */ public Set<Subject> setupSubjects(List<Map<URI, List<AttributeValue>>> subjs) { Set<Subject> subjects = new HashSet<Subject>(); if (subjs == null || subjs.size() == 0) { subjects.add(new Subject(new HashSet<Attribute>())); return subjects; } // Go through each of the subjects for (Map<URI, List<AttributeValue>> s : subjs) { Set<Attribute> attributes = new HashSet<Attribute>(); // Extract and create the attributes for this subject and add them // to the set for (URI uri : s.keySet()) { List<AttributeValue> attributeValues = s.get(uri); for (AttributeValue attributeValue : attributeValues) { attributes.add(new Attribute(uri, null, null, attributeValue)); } } // Create a new subject and add the attributes for this subject subjects.add(new Subject(attributes)); } return subjects; } /** * Creates a Resource specifying the resource-id, a required attribute. * * @return a Set of Attributes for inclusion in a Request */ public Set<Attribute> setupResources(Map<URI, AttributeValue> res) throws MelcoeXacmlException { Set<Attribute> attributes = new HashSet<Attribute>(); if (res == null || res.size() == 0) { return attributes; } try { String pid = null; AttributeValue pidAttr = res.get(XACML_RESOURCE_ID); if (pidAttr != null) { pid = pidAttr.encode(); pid = relationshipResolver.buildRESTParentHierarchy(pid); String dsid = null; AttributeValue dsidAttr = res.get(Constants.DATASTREAM.ID.getURI()); if (dsidAttr != null) { dsid = dsidAttr.encode(); if (!dsid.equals("")) { pid += "/" + dsid; } } res.put(XACML_RESOURCE_ID, new AnyURIAttribute(new URI(pid))); } } catch (Exception e) { log.error("Error finding parents.", e); throw new MelcoeXacmlException("Error finding parents.", e); } for (URI uri : res.keySet()) { attributes.add(new Attribute(uri, null, null, res.get(uri))); } return attributes; } /** * Creates an Action specifying the action-id, an optional attribute. * * @return a Set of Attributes for inclusion in a Request */ public Set<Attribute> setupAction(Map<URI, AttributeValue> a) { Set<Attribute> actions = new HashSet<Attribute>(); if (a == null || a.size() == 0) { return actions; } Map<URI, AttributeValue> newActions = new HashMap<URI, AttributeValue>(); for (URI uri : a.keySet()) { URI newUri = null; AttributeValue newValue = null; if (actionMap != null && actionMap.size() > 0) { newUri = actionMap.get(uri); } if (actionValueMap != null && actionValueMap.size() > 0) { String tmpValue = actionValueMap.get(a.get(uri).encode()); if (tmpValue != null) { newValue = new StringAttribute(tmpValue); } } newUri = newUri == null ? uri : newUri; newValue = newValue == null ? a.get(uri) : newValue; newActions.put(newUri, newValue); } for (URI uri : newActions.keySet()) { actions.add(new Attribute(uri, null, null, newActions.get(uri))); } return actions; } /** * Creates the Environment attributes. * * @return a Set of Attributes for inclusion in a Request */ public Set<Attribute> setupEnvironment(Map<URI, AttributeValue> e) { Set<Attribute> environment = new HashSet<Attribute>(); if (e == null || e.size() == 0) { return environment; } for (URI uri : e.keySet()) { environment.add(new Attribute(uri, null, null, e.get(uri))); } return environment; } /** * Constructs a RequestCtx object. * * @param subjects * list of Subjects * @param actions * list of Action attributes * @param resources * list of resource Attributes * @param environment * list of environment Attributes * @return the RequestCtx object * @throws MelcoeXacmlException */ public RequestCtx buildRequest(List<Map<URI, List<AttributeValue>>> subjects, Map<URI, AttributeValue> actions, Map<URI, AttributeValue> resources, Map<URI, AttributeValue> environment) throws MelcoeXacmlException { if (log.isDebugEnabled()) { log.debug("Building request!"); } if (relationshipResolver == null) { throw new MelcoeXacmlException("Valid relationship resolver not found."); } RequestCtx request = null; // Create the new Request. // Note that the Environment must be specified using a valid Set, even // if that Set is empty try { request = new RequestCtx(setupSubjects(subjects), setupResources(resources), setupAction(actions), setupEnvironment(environment)); } catch (Exception e) { log.error("Error creating request.", e); throw new MelcoeXacmlException("Error creating request", e); } return request; } /** * Converts a string based response to a ResponseCtx obejct. * * @param response * the string response * @return the ResponseCtx object * @throws MelcoeXacmlException */ public ResponseCtx makeResponseCtx(String response) throws MelcoeXacmlException { ResponseCtx resCtx = null; try { // sunxacml 1.2 bug. ResponseCtx.getInstance looks for // ResourceId and creates ResourceID String newResponse = response.replaceAll("ResourceID", "ResourceId"); ByteArrayInputStream is = new ByteArrayInputStream(newResponse.getBytes()); resCtx = ResponseCtx.getInstance(is); } catch (ParsingException pe) { throw new MelcoeXacmlException("Error parsing response.", pe); } return resCtx; } /** * Converts a string based request to a RequestCtx obejct. * * @param request * the string request * @return the RequestCtx object * @throws MelcoeXacmlException */ public RequestCtx makeRequestCtx(String request) throws MelcoeXacmlException { RequestCtx reqCtx = null; try { ByteArrayInputStream is = new ByteArrayInputStream(request.getBytes()); reqCtx = RequestCtx.getInstance(is); } catch (ParsingException pe) { throw new MelcoeXacmlException("Error parsing response.", pe); } return reqCtx; } /** * Converts a RequestCtx object to its string representation. * * @param reqCtx * the RequestCtx object * @return the String representation of the RequestCtx object */ public String makeRequestCtx(RequestCtx reqCtx) { ByteArrayOutputStream request = new ByteArrayOutputStream(); reqCtx.encode(request, new Indenter()); return new String(request.toByteArray()); } /** * Converst a ResponseCtx object to its string representation. * * @param resCtx * the ResponseCtx object * @return the String representation of the ResponseCtx object */ public String makeResponseCtx(ResponseCtx resCtx) { ByteArrayOutputStream response = new ByteArrayOutputStream(); resCtx.encode(response, new Indenter()); return new String(response.toByteArray()); } /** * Returns a map of resource-id, result based on an XACML response. * * @param resCtx * the XACML response * @return the Map of resource-id and result */ public Map<String, Result> makeResultMap(ResponseCtx resCtx) { @SuppressWarnings("unchecked") Iterator<Result> i = resCtx.getResults().iterator(); Map<String, Result> resultMap = new HashMap<String, Result>(); while (i.hasNext()) { Result r = i.next(); resultMap.put(r.getResource(), r); } return resultMap; } }