/*
* @(#)BasicEvaluationCtx.java
*
* Copyright 2004-2006 Sun Microsystems, Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistribution of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistribution in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of Sun Microsystems, Inc. or the names of contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
* ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
* AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
* AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
* DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
* REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
* INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
* OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
* EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*
* You acknowledge that this software is not designed or intended for use in
* the design, construction, operation or maintenance of any nuclear facility.
*/
package com.sun.xacml;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.w3c.dom.Node;
import com.sun.xacml.attr.AttributeDesignator;
import com.sun.xacml.attr.AttributeValue;
import com.sun.xacml.attr.BagAttribute;
import com.sun.xacml.attr.DateAttribute;
import com.sun.xacml.attr.DateTimeAttribute;
import com.sun.xacml.attr.StringAttribute;
import com.sun.xacml.attr.TimeAttribute;
import com.sun.xacml.cond.EvaluationResult;
import com.sun.xacml.ctx.Attribute;
import com.sun.xacml.ctx.RequestCtx;
import com.sun.xacml.ctx.Subject;
import com.sun.xacml.finder.AttributeFinder;
/**
* A basic implementation of <code>EvaluationCtx</code> that is created from an XACML Request and
* falls back on an AttributeFinder if a requested value isn't available in the Request.
* <p>
* Note that this class can do some optional caching for current date, time, and dateTime values
* (defined by a boolean flag to the constructors). The XACML specification requires that these
* values always be available, but it does not specify whether or not they must remain constant over
* the course of an evaluation if the values are being generated by the PDP (if the values are
* provided in the Request, then obviously they will remain constant). The default behavior is for
* these environment values to be cached, so that (for example) the current time remains constant
* over the course of an evaluation.
*
* @since 1.2
* @author Seth Proctor
*
* Adding generic type support by Christian Mueller (geotools)
*/
public class BasicEvaluationCtx implements EvaluationCtx {
// the finder to use if a value isn't in the request
private AttributeFinder finder;
// the DOM root the original RequestContext document
private Node requestRoot;
// the 4 maps that contain the attribute data
private HashMap<URI, Map<String, Set<Attribute>>> subjectMap;
private HashMap<String, Set<Attribute>> resourceMap;
private HashMap<String, Set<Attribute>> actionMap;
private HashMap<String, Set<Attribute>> environmentMap;
// the resource and its scope
private AttributeValue resourceId;
private int scope;
// the cached current date, time, and datetime, which we may or may
// not be using depending on how this object was constructed
private DateAttribute currentDate;
private TimeAttribute currentTime;
private DateTimeAttribute currentDateTime;
private boolean useCachedEnvValues;
// the logger we'll use for all messages
private static final Logger logger = Logger.getLogger(BasicEvaluationCtx.class.getName());
/**
* Constructs a new <code>BasicEvaluationCtx</code> based on the given request. The resulting
* context will cache current date, time, and dateTime values so they remain constant for this
* evaluation.
*
* @param request
* the request
*
* @throws ParsingException
* if a required attribute is missing, or if there are any problems dealing with the
* request data
*/
public BasicEvaluationCtx(RequestCtx request) throws ParsingException {
this(request, null, true);
}
/**
* Constructs a new <code>BasicEvaluationCtx</code> based on the given request.
*
* @param request
* the request
* @param cacheEnvValues
* whether or not to cache the current time, date, and dateTime so they are constant
* for the scope of this evaluation
*
* @throws ParsingException
* if a required attribute is missing, or if there are any problems dealing with the
* request data
*/
public BasicEvaluationCtx(RequestCtx request, boolean cacheEnvValues) throws ParsingException {
this(request, null, cacheEnvValues);
}
/**
* Constructs a new <code>BasicEvaluationCtx</code> based on the given request, and supports
* looking outside the original request for attribute values using the
* <code>AttributeFinder</code>. The resulting context will cache current date, time, and
* dateTime values so they remain constant for this evaluation.
*
* @param request
* the request
* @param finder
* an <code>AttributeFinder</code> to use in looking for attributes that aren't in
* the request
*
* @throws ParsingException
* if a required attribute is missing, or if there are any problems dealing with the
* request data
*/
public BasicEvaluationCtx(RequestCtx request, AttributeFinder finder) throws ParsingException {
this(request, finder, true);
}
/**
* Constructs a new <code>BasicEvaluationCtx</code> based on the given request, and supports
* looking outside the original request for attribute values using the
* <code>AttributeFinder</code>.
*
* @param request
* the request
* @param finder
* an <code>AttributeFinder</code> to use in looking for attributes that aren't in
* the request
* @param cacheEnvValues
* whether or not to cache the current time, date, and dateTime so they are constant
* for the scope of this evaluation
*
* @throws ParsingException
* if a required attribute is missing, or if there are any problems dealing with the
* request data
*/
public BasicEvaluationCtx(RequestCtx request, AttributeFinder finder, boolean cacheEnvValues)
throws ParsingException {
// keep track of the finder
this.finder = finder;
// remember the root of the DOM tree for XPath queries
requestRoot = request.getDocumentRoot();
// initialize the cached date/time values so it's clear we haven't
// retrieved them yet
this.useCachedEnvValues = cacheEnvValues;
currentDate = null;
currentTime = null;
currentDateTime = null;
// get the subjects, make sure they're correct, and setup tables
subjectMap = new HashMap<URI, Map<String, Set<Attribute>>>();
setupSubjects(request.getSubjects());
// next look at the Resource data, which needs to be handled specially
resourceMap = new HashMap<String, Set<Attribute>>();
setupResource(request.getResource());
// setup the action data, which is generic
actionMap = new HashMap<String, Set<Attribute>>();
mapAttributes(request.getAction(), actionMap);
// finally, set up the environment data, which is also generic
environmentMap = new HashMap<String, Set<Attribute>>();
mapAttributes(request.getEnvironmentAttributes(), environmentMap);
}
/**
* This is quick helper function to provide a little structure for the subject attributes so we
* can search for them (somewhat) quickly. The basic idea is to have a map indexed by
* SubjectCategory that keeps Maps that in turn are indexed by id and keep the unique
* ctx.Attribute objects.
*/
private void setupSubjects(Set<Subject> subjects) throws ParsingException {
// make sure that there is at least one Subject
if (subjects.size() == 0)
throw new ParsingException("Request must a contain subject");
// now go through the subject attributes
for (Subject subject : subjects) {
URI category = subject.getCategory();
Map<String, Set<Attribute>> categoryMap = null;
// see if we've already got a map for the category
if (subjectMap.containsKey(category)) {
categoryMap = subjectMap.get(category);
} else {
categoryMap = new HashMap<String, Set<Attribute>>();
subjectMap.put(category, categoryMap);
}
// iterate over the set of attributes
for (Attribute attr : subject.getAttributes()) {
String id = attr.getId().toString();
if (categoryMap.containsKey(id)) {
// add to the existing set of Attributes w/this id
Set<Attribute> existingIds = categoryMap.get(id);
existingIds.add(attr);
} else {
// this is the first Attr w/this id
HashSet<Attribute> newIds = new HashSet<Attribute>();
newIds.add(attr);
categoryMap.put(id, newIds);
}
}
}
}
/**
* This basically does the same thing that the other types need to do, except that we also look
* for a resource-id attribute, not because we're going to use, but only to make sure that it's
* actually there, and for the optional scope attribute, to see what the scope of the attribute
* is
*/
private void setupResource(Set<Attribute> resource) throws ParsingException {
mapAttributes(resource, resourceMap);
// make sure there resource-id attribute was included
if (!resourceMap.containsKey(RESOURCE_ID)) {
System.err.println("Resource must contain resource-id attr");
throw new ParsingException("resource missing resource-id");
} else {
// make sure there's only one value for this
Set<Attribute> set = resourceMap.get(RESOURCE_ID);
if (set.size() > 1) {
System.err.println("Resource may contain only one " + "resource-id Attribute");
throw new ParsingException("too many resource-id attrs");
} else {
// keep track of the resource-id attribute
resourceId = ((Attribute) (set.iterator().next())).getValue();
}
}
// see if a resource-scope attribute was included
if (resourceMap.containsKey(RESOURCE_SCOPE)) {
Set<Attribute> set = resourceMap.get(RESOURCE_SCOPE);
// make sure there's only one value for resource-scope
if (set.size() > 1) {
System.err.println("Resource may contain only one " + "resource-scope Attribute");
throw new ParsingException("too many resource-scope attrs");
}
Attribute attr = (Attribute) (set.iterator().next());
AttributeValue attrValue = attr.getValue();
// scope must be a string, so throw an exception otherwise
if (!attrValue.getType().toString().equals(StringAttribute.identifier))
throw new ParsingException("scope attr must be a string");
String value = ((StringAttribute) attrValue).getValue();
if (value.equals("Immediate")) {
scope = SCOPE_IMMEDIATE;
} else if (value.equals("Children")) {
scope = SCOPE_CHILDREN;
} else if (value.equals("Descendants")) {
scope = SCOPE_DESCENDANTS;
} else {
System.err.println("Unknown scope type: " + value);
throw new ParsingException("invalid scope type: " + value);
}
} else {
// by default, the scope is always Immediate
scope = SCOPE_IMMEDIATE;
}
}
/**
* Generic routine for resource, attribute and environment attributes to build the lookup map
* for each. The Form is a Map that is indexed by the String form of the attribute ids, and that
* contains Sets at each entry with all attributes that have that id
*/
private void mapAttributes(Set<Attribute> input, Map<String, Set<Attribute>> output) {
for (Attribute attr : input) {
String id = attr.getId().toString();
if (output.containsKey(id)) {
Set<Attribute> set = output.get(id);
set.add(attr);
} else {
Set<Attribute> set = new HashSet<Attribute>();
set.add(attr);
output.put(id, set);
}
}
}
/**
* Returns the DOM root of the original RequestType XML document.
*
* @return the DOM root node
*/
public Node getRequestRoot() {
return requestRoot;
}
/**
* Returns the resource scope of the request, which will be one of the three fields denoting
* Immediate, Children, or Descendants.
*
* @return the scope of the resource in the request
*/
public int getScope() {
return scope;
}
/**
* Returns the resource named in the request as resource-id.
*
* @return the resource
*/
public AttributeValue getResourceId() {
return resourceId;
}
/**
* Changes the value of the resource-id attribute in this context. This is useful when you have
* multiple resources (ie, a scope other than IMMEDIATE), and you need to keep changing only the
* resource-id to evaluate the different effective requests.
*
* @param resourceId
* the new resource-id value
*/
public void setResourceId(AttributeValue resourceId) {
this.resourceId = resourceId;
// there will always be exactly one value for this attribute
Set<Attribute> attrSet = resourceMap.get(RESOURCE_ID);
Attribute attr = (Attribute) (attrSet.iterator().next());
// remove the old value...
attrSet.remove(attr);
// ...and insert the new value
attrSet.add(new Attribute(attr.getId(), attr.getIssuer(), attr.getIssueInstant(),
resourceId));
}
/**
* Returns the value for the current time. The current time, current date, and current dateTime
* are consistent, so that they all represent the same moment. If this is the first time that
* one of these three values has been requested, and caching is enabled, then the three values
* will be resolved and stored.
* <p>
* Note that the value supplied here applies only to dynamically resolved values, not those
* supplied in the Request. In other words, this always returns a dynamically resolved value
* local to the PDP, even if a different value was supplied in the Request. This is handled
* correctly when the value is requested by its identifier.
*
* @return the current time
*/
public synchronized TimeAttribute getCurrentTime() {
long millis = dateTimeHelper();
if (useCachedEnvValues)
return currentTime;
else
return new TimeAttribute(new Date(millis));
}
/**
* Returns the value for the current date. The current time, current date, and current dateTime
* are consistent, so that they all represent the same moment. If this is the first time that
* one of these three values has been requested, and caching is enabled, then the three values
* will be resolved and stored.
* <p>
* Note that the value supplied here applies only to dynamically resolved values, not those
* supplied in the Request. In other words, this always returns a dynamically resolved value
* local to the PDP, even if a different value was supplied in the Request. This is handled
* correctly when the value is requested by its identifier.
*
* @return the current date
*/
public synchronized DateAttribute getCurrentDate() {
long millis = dateTimeHelper();
if (useCachedEnvValues)
return currentDate;
else
return new DateAttribute(new Date(millis));
}
/**
* Returns the value for the current dateTime. The current time, current date, and current
* dateTime are consistent, so that they all represent the same moment. If this is the first
* time that one of these three values has been requested, and caching is enabled, then the
* three values will be resolved and stored.
* <p>
* Note that the value supplied here applies only to dynamically resolved values, not those
* supplied in the Request. In other words, this always returns a dynamically resolved value
* local to the PDP, even if a different value was supplied in the Request. This is handled
* correctly when the value is requested by its identifier.
*
* @return the current dateTime
*/
public synchronized DateTimeAttribute getCurrentDateTime() {
long millis = dateTimeHelper();
if (useCachedEnvValues)
return currentDateTime;
else
return new DateTimeAttribute(new Date(millis));
}
/**
* Private helper that figures out if we need to resolve new values, and returns either the
* current moment (if we're not caching) or -1 (if we are caching)
*/
private long dateTimeHelper() {
// if we already have current values, then we can stop (note this
// always means that we're caching)
if (currentTime != null)
return -1;
// get the current moment
Date time = new Date();
long millis = time.getTime();
// if we're not caching then we just return the current moment
if (!useCachedEnvValues) {
return millis;
} else {
// we're caching, so resolve all three values, making sure
// to use clean copies of the date object since it may be
// modified when creating the attributes
currentTime = new TimeAttribute(time);
currentDate = new DateAttribute(new Date(millis));
currentDateTime = new DateTimeAttribute(new Date(millis));
}
return -1;
}
/**
* Returns attribute value(s) from the subject section of the request that have no issuer.
*
* @param type
* the type of the attribute value(s) to find
* @param id
* the id of the attribute value(s) to find
* @param category
* the category the attribute value(s) must be in
*
* @return a result containing a bag either empty because no values were found or containing at
* least one value, or status associated with an Indeterminate result
*/
public EvaluationResult getSubjectAttribute(URI type, URI id, URI category) {
return getSubjectAttribute(type, id, null, category);
}
/**
* Returns attribute value(s) from the subject section of the request.
*
* @param type
* the type of the attribute value(s) to find
* @param id
* the id of the attribute value(s) to find
* @param issuer
* the issuer of the attribute value(s) to find or null
* @param category
* the category the attribute value(s) must be in
*
* @return a result containing a bag either empty because no values were found or containing at
* least one value, or status associated with an Indeterminate result
*/
public EvaluationResult getSubjectAttribute(URI type, URI id, URI issuer, URI category) {
// This is the same as the other three lookups except that this
// has an extra level of indirection that needs to be handled first
Map<String, Set<Attribute>> map = subjectMap.get(category);
if (map == null) {
// the request didn't have that category, so we should try asking
// the attribute finder
return callHelper(type, id, issuer, category, AttributeDesignator.SUBJECT_TARGET);
}
return getGenericAttributes(type, id, issuer, map, category,
AttributeDesignator.SUBJECT_TARGET);
}
/**
* Returns attribute value(s) from the resource section of the request.
*
* @param type
* the type of the attribute value(s) to find
* @param id
* the id of the attribute value(s) to find
* @param issuer
* the issuer of the attribute value(s) to find or null
*
* @return a result containing a bag either empty because no values were found or containing at
* least one value, or status associated with an Indeterminate result
*/
public EvaluationResult getResourceAttribute(URI type, URI id, URI issuer) {
return getGenericAttributes(type, id, issuer, resourceMap, null,
AttributeDesignator.RESOURCE_TARGET);
}
/**
* Returns attribute value(s) from the action section of the request.
*
* @param type
* the type of the attribute value(s) to find
* @param id
* the id of the attribute value(s) to find
* @param issuer
* the issuer of the attribute value(s) to find or null
*
* @return a result containing a bag either empty because no values were found or containing at
* least one value, or status associated with an Indeterminate result
*/
public EvaluationResult getActionAttribute(URI type, URI id, URI issuer) {
return getGenericAttributes(type, id, issuer, actionMap, null,
AttributeDesignator.ACTION_TARGET);
}
/**
* Returns attribute value(s) from the environment section of the request.
*
* @param type
* the type of the attribute value(s) to find
* @param id
* the id of the attribute value(s) to find
* @param issuer
* the issuer of the attribute value(s) to find or null
*
* @return a result containing a bag either empty because no values were found or containing at
* least one value, or status associated with an Indeterminate result
*/
public EvaluationResult getEnvironmentAttribute(URI type, URI id, URI issuer) {
return getGenericAttributes(type, id, issuer, environmentMap, null,
AttributeDesignator.ENVIRONMENT_TARGET);
}
/**
* Helper function for the resource, action and environment methods to get an attribute.
*/
private EvaluationResult getGenericAttributes(URI type, URI id, URI issuer,
Map<String, Set<Attribute>> map, URI category, int designatorType) {
// try to find the id
Set<Attribute> attrSet = map.get(id.toString());
if (attrSet == null) {
// the request didn't have an attribute with that id, so we should
// try asking the attribute finder
return callHelper(type, id, issuer, category, designatorType);
}
// now go through each, considering each Attribute object
List<AttributeValue> attributes = new ArrayList<AttributeValue>();
for (Attribute attr : attrSet) {
// make sure the type and issuer are correct
if ((attr.getType().equals(type))
&& ((issuer == null) || ((attr.getIssuer() != null) && (attr.getIssuer()
.equals(issuer.toString()))))) {
// if we got here, then we found a match, so we want to pull
// out the values and put them in out list
attributes.add(attr.getValue());
}
}
// see if we found any acceptable attributes
if (attributes.size() == 0) {
// we failed to find any that matched the type/issuer, or all the
// Attribute types were empty...so ask the finder
if (logger.isLoggable(Level.FINE))
logger.fine("Attribute not in request: " + id.toString()
+ " ... querying AttributeFinder");
return callHelper(type, id, issuer, category, designatorType);
}
// if we got here, then we found at least one useful AttributeValue
return new EvaluationResult(new BagAttribute(type, attributes));
}
/**
* Private helper that calls the finder if it's non-null, or else returns an empty bag
*/
private EvaluationResult callHelper(URI type, URI id, URI issuer, URI category, int adType) {
if (finder != null) {
return finder.findAttribute(type, id, issuer, category, this, adType);
} else {
logger.warning("Context tried to invoke AttributeFinder but was "
+ "not configured with one");
return new EvaluationResult(BagAttribute.createEmptyBag(type));
}
}
/**
* Returns the attribute value(s) retrieved using the given XPath expression.
*
* @param contextPath
* the XPath expression to search
* @param namespaceNode
* the DOM node defining namespace mappings to use, or null if mappings come from the
* context root
* @param type
* the type of the attribute value(s) to find
* @param xpathVersion
* the version of XPath to use
*
* @return a result containing a bag either empty because no values were found or containing at
* least one value, or status associated with an Indeterminate result
*/
public EvaluationResult getAttribute(String contextPath, Node namespaceNode, URI type,
String xpathVersion) {
if (finder != null) {
return finder.findAttribute(contextPath, namespaceNode, type, this, xpathVersion);
} else {
logger.warning("Context tried to invoke AttributeFinder but was "
+ "not configured with one");
return new EvaluationResult(BagAttribute.createEmptyBag(type));
}
}
}