/*
* Copyright (c) 2005-2010, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.wso2.carbon.identity.entitlement.policy;
import com.sun.xacml.AbstractPolicy;
import com.sun.xacml.EvaluationCtx;
import com.sun.xacml.MatchResult;
import com.sun.xacml.Policy;
import com.sun.xacml.PolicyMetaData;
import com.sun.xacml.PolicyReference;
import com.sun.xacml.PolicySet;
import com.sun.xacml.Target;
import com.sun.xacml.TargetMatch;
import com.sun.xacml.TargetSection;
import com.sun.xacml.VersionConstraints;
import com.sun.xacml.combine.PolicyCombiningAlgorithm;
import com.sun.xacml.ctx.Status;
import java.net.URI;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.StringTokenizer;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.entitlement.EntitlementException;
public class PolicyCollection {
// the actual collection of policies
private LinkedHashMap<String, TreeSet<AbstractPolicy>> policies;
// the single instance of the comparator we'll use for managing versions
private VersionComparator versionComparator = new VersionComparator();
// the optional combining algorithm used when wrapping multiple policies
private PolicyCombiningAlgorithm combiningAlg;
// the optional policy id used when wrapping multiple policies
private URI parentId;
// default target that matches anything, used in wrapping policies
private static final Target target;
private static Log log = LogFactory.getLog(PolicyCollection.class);
/**
* This static initializer just sets up the default target, which is used by all wrapping policy
* sets.
*/
static {
target = new Target(new TargetSection(null, TargetMatch.SUBJECT,
PolicyMetaData.XACML_VERSION_2_0), new TargetSection(null, TargetMatch.RESOURCE,
PolicyMetaData.XACML_VERSION_2_0), new TargetSection(null, TargetMatch.ACTION,
PolicyMetaData.XACML_VERSION_2_0), new TargetSection(null, TargetMatch.ENVIRONMENT,
PolicyMetaData.XACML_VERSION_2_0));
};
/**
* Creates a new <code>PolicyCollection</code> that will return errors when multiple policies
* match for a given request.
*/
public PolicyCollection(PolicyCombiningAlgorithm combiningAlg) {
policies = new LinkedHashMap<String, TreeSet<AbstractPolicy>>();
this.combiningAlg = combiningAlg;
}
/**
* Creates a new <code>PolicyCollection</code> that will create a new top-level PolicySet when
* multiple policies match for a given request.
*
* @param combiningAlg
* the algorithm to use in a new PolicySet when more than one policy applies
* @param parentPolicyId
* the identifier to use for the new PolicySet
*/
public PolicyCollection(PolicyCombiningAlgorithm combiningAlg, URI parentPolicyId) {
policies = new LinkedHashMap<String, TreeSet<AbstractPolicy>>();
this.combiningAlg = combiningAlg;
this.parentId = parentPolicyId;
}
/**
* Adds a new policy to the collection, and uses the policy's identifier as the reference
* identifier. If this identifier already exists in the collection, and this policy does not
* represent a new version of the policy, then the policy is not added.
*
* @param policy
* the policy to add
*
* @return true if the policy was added, false otherwise
*/
public boolean addPolicy(AbstractPolicy policy) {
return addPolicy(policy, policy.getId().toString());
}
/**
* *Adds a new policy to the collection using the given identifier as the reference identifier.
* If this identifier already exists in the collection, and this policy does not represent a new
* version of the policy, then the policy is not added.
*
* @param policy
* @param identifier
* @return
*/
public boolean addPolicy(AbstractPolicy policy, String identifier) {
if (policies.containsKey(identifier)) {
// this identifier is already is use, so see if this version is
// already in the set
TreeSet<AbstractPolicy> set = policies.get(identifier);
return set.add(policy);
} else {
// this identifier isn't already being used, so create a new
// set in the map for it, and add the policy
TreeSet<AbstractPolicy> set = new TreeSet<AbstractPolicy>(versionComparator);
policies.put(identifier, set);
return set.add(policy);
}
}
/**
* Attempts to retrieve a policy based on the given context. If multiple policies match then
* this will either throw an exception or wrap the policies under a new PolicySet (depending on
* how this instance was constructed). If no policies match, then this will return null. See the
* comment in the class header about how this behaves when multiple versions of the same policy
* exist.
*
* @param context
* @return
* @throws EntitlementException
*/
public AbstractPolicy getPolicy(EvaluationCtx context) throws EntitlementException {
// setup a list of matching policies
ArrayList<AbstractPolicy> list = new ArrayList<AbstractPolicy>();
// get an iterator over all the identifiers
Iterator<TreeSet<AbstractPolicy>> it = policies.values().iterator();
while (it.hasNext()) {
// for each identifier, get only the most recent policy
AbstractPolicy policy = it.next().first();
// see if we match
MatchResult match = policy.match(context);
int result = match.getResult();
// if there was an error, we stop right away
if (result == MatchResult.INDETERMINATE) {
log.error("Error occured while processing the XACML policy "
+ policy.getId().toString());
throw new EntitlementException(match.getStatus());
}
// if we matched, we keep track of the matching policy...
if (result == MatchResult.MATCH) {
// ...first checking if this is the first match and if
// we automatically nest policies
log.info("Matching XACML policy found " + policy.getId().toString());
if ((combiningAlg == null) && (list.size() > 0)) {
ArrayList<String> code = new ArrayList<String>();
code.add(Status.STATUS_PROCESSING_ERROR);
Status status = new Status(code, "too many applicable top-level policies");
throw new EntitlementException(status);
}
list.add(policy);
}
}
// no errors happened during the search, so now take the right
// action based on how many policies we found
switch (list.size()) {
case 0:
log.info("No matching XACML policy found");
return null;
case 1:
return ((AbstractPolicy) (list.get(0)));
default:
return new PolicySet(parentId, combiningAlg, target, list);
}
}
/**
* Attempts to retrieve a policy based on the given identifier and other constraints. If there
* are multiple versions of the identified policy that meet the version constraints, then the
* most recent version is returned.
*
* @param identifier
* @param type
* @param constraints
* @return
*/
public AbstractPolicy getPolicy(String identifier, int type, VersionConstraints constraints) {
TreeSet<AbstractPolicy> set = policies.get(identifier);
// if we don't know about this identifier then there's nothing to do
if (set == null)
return null;
// walk through the set starting with the most recent version, looking
// for a match until we exhaust all known versions
Iterator<AbstractPolicy> it = set.iterator();
while (it.hasNext()) {
AbstractPolicy policy = (AbstractPolicy) (it.next());
if (constraints.meetsConstraint(policy.getVersion())) {
// we found a valid version, so see if it's the right kind,
// and if it is then we return it
if (type == PolicyReference.POLICY_REFERENCE) {
if (policy instanceof Policy)
return policy;
} else {
if (policy instanceof PolicySet)
return policy;
}
}
}
// we didn't find a match
return null;
}
/**
* A Comparator that is used within this class to maintain ordering amongst different versions
* of the same policy. Note that it actually maintains reverse-ordering, since we want to
* traverse the sets in decreasing, not increasing order.
*
* Note that this comparator is only used when there are multiple versions of the same policy,
* which in practice will probably happen far less (from this class' point of view) than
* additions or fetches.
*/
class VersionComparator implements Comparator<AbstractPolicy> {
public int compare(AbstractPolicy o1, AbstractPolicy o2) {
// we swap the parameters so that sorting goes largest to smallest
String v1 = ((AbstractPolicy) o2).getVersion();
String v2 = ((AbstractPolicy) o1).getVersion();
// do a quick check to see if the strings are equal (note that
// even if the strings aren't equal, the versions can still
// be equal)
if (v1.equals(v2))
return 0;
// setup tokenizers, and walk through both strings one set of
// numeric values at a time
StringTokenizer tok1 = new StringTokenizer(v1, ".");
StringTokenizer tok2 = new StringTokenizer(v2, ".");
while (tok1.hasMoreTokens()) {
// if there's nothing left in tok2, then v1 is bigger
if (!tok2.hasMoreTokens())
return 1;
// get the next elements in the version, convert to numbers,
// and compare them (continuing with the loop only if the
// two values were equal)
int num1 = Integer.parseInt(tok1.nextToken());
int num2 = Integer.parseInt(tok2.nextToken());
if (num1 > num2)
return 1;
if (num1 < num2)
return -1;
}
// if there's still something left in tok2, then it's bigger
if (tok2.hasMoreTokens())
return -1;
// if we got here it means both versions had the same number of
// elements and all the elements were equal, so the versions
// are in fact equal
return 0;
}
}
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + (null == this.policies ? 0 : this.policies.hashCode());
hash = 31 * hash + (null == this.combiningAlg ? 0 : this.combiningAlg.hashCode());
return hash;
}
}