/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2010-2015 ForgeRock AS
*/
package org.opends.server.authorization.dseecompat;
import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.util.StaticUtils.*;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteSequence;
import org.opends.server.types.DN;
/**
* The Aci class represents ACI strings.
*/
public class Aci implements Comparable<Aci>
{
/**
* The body of the ACI is the version, name and permission-bind rule
* pairs.
*/
private AciBody body;
/**
* The ACI targets.
*/
private AciTargets targets;
/**
* Version that we support.
*/
public static final String supportedVersion="3.0";
/**
* String representation of the ACI used.
*/
private String aciString;
/**
* The DN of the entry containing this ACI.
*/
private final DN dn;
/**
* Regular expression matching a word group.
*/
public static final String WORD_GROUP="(\\w+)";
/**
* Regular expression matching a word group at the start of a
* pattern.
*/
public static final String WORD_GROUP_START_PATTERN = "^" + WORD_GROUP;
/**
* Regular expression matching a white space.
*/
public static final String ZERO_OR_MORE_WHITESPACE="\\s*";
/**
* Regular expression matching a white space at the start of a pattern.
*/
public static final String ZERO_OR_MORE_WHITESPACE_START_PATTERN =
"^" + ZERO_OR_MORE_WHITESPACE ;
/**
* Regular expression matching a white space at the end of a pattern.
*/
private static final String ZERO_OR_MORE_WHITESPACE_END_PATTERN =
ZERO_OR_MORE_WHITESPACE + "$";
/**
* Regular expression matching a ACL statement separator.
*/
public static final String ACI_STATEMENT_SEPARATOR =
ZERO_OR_MORE_WHITESPACE + ";" + ZERO_OR_MORE_WHITESPACE;
/**
* This regular expression is used to do a quick syntax check
* when an ACI is being decoded.
*/
private static final String aciRegex =
ZERO_OR_MORE_WHITESPACE_START_PATTERN + AciTargets.targetsRegex +
ZERO_OR_MORE_WHITESPACE + AciBody.bodyRegx +
ZERO_OR_MORE_WHITESPACE_END_PATTERN;
/**
* Regular expression that graciously matches an attribute type name. Must
* begin with an ASCII letter or digit, and contain only ASCII letters,
* digit characters, hyphens, semi-colons and underscores. It also allows
* the special shorthand characters "*" for all user attributes and "+" for
* all operational attributes.
*/
public static final String ATTR_NAME =
"((?i)[a-z\\d]{1}[[a-z]\\d-_.]*(?-i)|\\*{1}|\\+{1})";
/**
* Regular expression matching a LDAP URL.
*/
public static final String LDAP_URL = ZERO_OR_MORE_WHITESPACE +
"(ldap:///[^\\|]+)";
/**
* String used to check for NULL ldap URL.
*/
public static final String NULL_LDAP_URL = "ldap:///";
/**
* Regular expression used to match token that joins expressions (||).
*/
public static final String LOGICAL_OR = "\\|\\|";
/**
* Regular expression used to match an open parenthesis.
*/
public static final String OPEN_PAREN = "\\(";
/**
* Regular expression used to match a closed parenthesis.
*/
public static final String CLOSED_PAREN = "\\)";
/**
* Regular expression used to match a single equal sign.
*/
public static final String EQUAL_SIGN = "={1}";
/**
* Regular expression the matches "*".
*/
public static final String ALL_USER_ATTRS_WILD_CARD =
ZERO_OR_MORE_WHITESPACE +
"\\*" + ZERO_OR_MORE_WHITESPACE;
/**
* Regular expression the matches "+".
*/
public static final String ALL_OP_ATTRS_WILD_CARD =
ZERO_OR_MORE_WHITESPACE +
"\\+" + ZERO_OR_MORE_WHITESPACE;
/**
* Regular expression used to do quick check of OID string.
*/
private static final String OID_NAME = "[\\d.\\*]*";
/**
* Regular expression that matches one or more OID_NAME's separated by
* the "||" token.
*/
private static final String oidListRegex = ZERO_OR_MORE_WHITESPACE +
OID_NAME + ZERO_OR_MORE_WHITESPACE + "(" +
LOGICAL_OR + ZERO_OR_MORE_WHITESPACE + OID_NAME +
ZERO_OR_MORE_WHITESPACE + ")*";
/**
* ACI_ADD is used to set the container rights for a LDAP add operation.
*/
public static final int ACI_ADD = 0x0020;
/**
* ACI_DELETE is used to set the container rights for a LDAP
* delete operation.
*/
public static final int ACI_DELETE = 0x0010;
/**
* ACI_READ is used to set the container rights for a LDAP
* search operation.
*/
public static final int ACI_READ = 0x0004;
/**
* ACI_WRITE is used to set the container rights for a LDAP
* modify operation.
*/
public static final int ACI_WRITE = 0x0008;
/**
* ACI_COMPARE is used to set the container rights for a LDAP
* compare operation.
*/
public static final int ACI_COMPARE = 0x0001;
/**
* ACI_SEARCH is used to set the container rights a LDAP search operation.
*/
public static final int ACI_SEARCH = 0x0002;
/**
* ACI_SELF is used for the SELFWRITE right.
*/
public static final int ACI_SELF = 0x0040;
/**
* ACI_ALL is used to as a mask for all of the above. These
* six below are not masked by the ACI_ALL.
*/
public static final int ACI_ALL = 0x007F;
/**
* ACI_PROXY is used for the PROXY right.
*/
public static final int ACI_PROXY = 0x0080;
/**
* ACI_IMPORT is used to set the container rights for a LDAP
* modify dn operation.
*/
public static final int ACI_IMPORT = 0x0100;
/**
* ACI_EXPORT is used to set the container rights for a LDAP
* modify dn operation.
*/
public static final int ACI_EXPORT = 0x0200;
/**
* ACI_WRITE_ADD is used by the LDAP modify operation.
*/
public static final int ACI_WRITE_ADD = 0x800;
/**
* ACI_WRITE_DELETE is used by the LDAP modify operation.
*/
public static final int ACI_WRITE_DELETE = 0x400;
/**
* ACI_SKIP_PROXY_CHECK is used to bypass the proxy access check.
*/
public static final int ACI_SKIP_PROXY_CHECK = 0x400000;
/**
* TARGATTRFILTER_ADD is used to specify that a
* targattrfilters ADD operation was seen in the ACI. For example,
* given an ACI with:
*
* (targattrfilters="add=mail:(mail=*@example.com)")
*
* The TARGATTRFILTERS_ADD flag would be set during ACI parsing in the
* TargAttrFilters class.
*/
public static final int TARGATTRFILTERS_ADD = 0x1000;
/**
* TARGATTRFILTER_DELETE is used to specify that a
* targattrfilters DELETE operation was seen in the ACI. For example,
* given an ACI with:
*
* (targattrfilters="del=mail:(mail=*@example.com)")
*
* The TARGATTRFILTERS_DELETE flag would be set during ACI parsing in the
* TargAttrFilters class.
*/
public static final int TARGATTRFILTERS_DELETE = 0x2000;
/**
* Used by the control evaluation access check.
*/
public static final int ACI_CONTROL = 0x4000;
/**
* Used by the extended operation access check.
*/
public static final int ACI_EXT_OP = 0x8000;
/**
* ACI_ATTR_STAR_MATCHED is the flag set when the evaluation reason of a
* AciHandler.maysend ACI_READ access evaluation was the result of an
* ACI targetattr all attributes expression (targetattr="*") target match.
* For this flag to be set, there must be only one ACI matching.
*
* This flag and ACI_FOUND_ATTR_RULE are used in the
* AciHandler.filterEntry.accessAllowedAttrs method to skip access
* evaluation if the flag is ACI_ATTR_STAR_MATCHED (all attributes match)
* and the attribute type is not operational.
*/
public static final int ACI_USER_ATTR_STAR_MATCHED = 0x0008;
/**
* ACI_FOUND_USER_ATTR_RULE is the flag set when the evaluation reason of a
* AciHandler.maysend ACI_READ access evaluation was the result of an
* ACI targetattr specific user attribute expression
* (targetattr="some user attribute type") target match.
*/
public static final int ACI_FOUND_USER_ATTR_RULE = 0x0010;
/**
* ACI_OP_ATTR_PLUS_MATCHED is the flag set when the evaluation reason of a
* AciHandler.maysend ACI_READ access evaluation was the result of an
* ACI targetattr all operational attributes expression (targetattr="+")
* target match. For this flag to be set, there must be only one
* ACI matching.
*
* This flag and ACI_FOUND_OP_ATTR_RULE are used in the
* AciHandler.filterEntry.accessAllowedAttrs method to skip access
* evaluation if the flag is ACI_OP_ATTR_PLUS_MATCHED (all operational
* attributes match) and the attribute type is operational.
*/
public static final int ACI_OP_ATTR_PLUS_MATCHED = 0x0004;
/**
* ACI_FOUND_OP_ATTR_RULE is the flag set when the evaluation reason of a
* AciHandler.maysend ACI_READ access evaluation was the result of an
* ACI targetattr specific operational attribute expression
* (targetattr="some operational attribute type") target match.
*/
public static final int ACI_FOUND_OP_ATTR_RULE = 0x0020;
/**
* ACI_NULL is used to set the container rights to all zeros. Used
* by LDAP modify.
*/
public static final int ACI_NULL = 0x0000;
/**
* Construct a new Aci from the provided arguments.
* @param input The string representation of the ACI.
* @param dn The DN of entry containing the ACI.
* @param body The body of the ACI.
* @param targets The targets of the ACI.
*/
private Aci(String input, DN dn, AciBody body, AciTargets targets) {
this.aciString = input;
this.dn=dn;
this.body=body;
this.targets=targets;
}
/**
* Decode an ACI byte string.
* @param byteString The ByteString containing the ACI string.
* @param dn DN of the ACI entry.
* @return Returns a decoded ACI representing the string argument.
* @throws AciException If the parsing of the ACI string fails.
*/
public static Aci decode (ByteSequence byteString, DN dn)
throws AciException {
String input=byteString.toString();
//Perform a quick pattern check against the string to catch any
//obvious syntax errors.
if (!Pattern.matches(aciRegex, input)) {
LocalizableMessage message = WARN_ACI_SYNTAX_GENERAL_PARSE_FAILED.get(input);
throw new AciException(message);
}
//Decode the body first.
AciBody body=AciBody.decode(input);
//Create a substring from the start of the string to start of
//the body. That should be the target.
String targetStr = input.substring(0, body.getMatcherStartPos());
//Decode that target string using the substring.
AciTargets targets=AciTargets.decode(targetStr, dn);
return new Aci(input, dn, body, targets);
}
/**
* Return the string representation of the ACI. This was the string that
* was used to create the Aci class.
* @return A string representation of the ACI.
*/
@Override
public String toString() {
return aciString;
}
/**
* Returns the targets of the ACI.
* @return Any AciTargets of the ACI. There may be no targets
* so this might be null.
*/
public AciTargets getTargets() {
return targets;
}
/**
* Return the DN of the entry containing the ACI.
* @return The DN of the entry containing the ACI.
*/
public DN getDN() {
return dn;
}
/**
* Test if the given ACI is applicable using the target match information
* provided. The ACI target can have seven keywords at this time:
*
* These two base decision on the resource entry DN:
*
* 1. target - checked in isTargetApplicable.
* 2. targetscope - checked in isTargetApplicable.
*
* These three base decision on resource entry attributes:
*
* 3. targetfilter - checked in isTargetFilterApplicable.
* 4. targetattr - checked in isTargetAttrApplicable.
* 5. targattrfilters - checked in isTargAttrFiltersApplicable.
*
* These two base decisions on a resource entry built by the ACI handler
* that only contains a DN:
* 6. targetcontrol - check in isTargetControlApplicable.
* 7. extop - check in isExtOpApplicable.
*
* Six and seven are specific to the check being done: targetcontrol when a
* control is being evaluated and extop when an extended operation is
* evaluated. None of the attribute based keywords should be checked
* when a control or extended op is being evaluated, because one
* of those attribute keywords rule might incorrectly make an ACI
* applicable that shouldn't be. This can happen by erroneously basing
* their decision on the ACI handler generated stub resource entry. For
* example, a "(targetattr != userpassword)" rule would match the generated
* stub resource entry, even though a control or extended op might be
* denied.
*
* What is allowed is the target and targetscope keywords, since the DN is
* known, so they are checked along with the correct method for the access
* check (isTargetControlApplicable for control and
* isTExtOpApplicable for extended operations). See comments in code
* where these checks are done.
*
* @param aci The ACI to test.
* @param matchCtx The target matching context containing all the info
* needed to match ACI targets.
* @return True if this ACI targets are applicable or match.
*/
public static boolean isApplicable(Aci aci, AciTargetMatchContext matchCtx) {
if(matchCtx.hasRights(ACI_EXT_OP)) {
//Extended operation is being evaluated.
return AciTargets.isTargetApplicable(aci, matchCtx) &&
AciTargets.isExtOpApplicable(aci, matchCtx);
} else if(matchCtx.hasRights(ACI_CONTROL)) {
//Control is being evaluated.
return AciTargets.isTargetApplicable(aci, matchCtx) &&
AciTargets.isTargetControlApplicable(aci, matchCtx);
} else {
//If an ACI has extOp or targetControl targets skip it because the
//matchCtx right does not contain either ACI_EXT_OP or ACI_CONTROL at
//this point.
return hasNoExtOpOrTargetControl(aci.getTargets())
&& haveSimilarRights(aci, matchCtx)
&& AciTargets.isTargetApplicable(aci, matchCtx)
&& AciTargets.isTargetFilterApplicable(aci, matchCtx)
&& AciTargets.isTargAttrFiltersApplicable(aci, matchCtx)
&& AciTargets.isTargetAttrApplicable(aci, matchCtx);
}
}
private static boolean hasNoExtOpOrTargetControl(AciTargets aciTargets)
{
return aciTargets.getExtOp() == null
&& aciTargets.getTargetControl() == null;
}
private static boolean haveSimilarRights(Aci aci,
AciTargetMatchContext matchCtx)
{
return aci.hasRights(matchCtx.getRights())
|| (aci.hasRights(ACI_SEARCH| ACI_READ)
&& matchCtx.hasRights(ACI_SEARCH | ACI_READ));
}
/**
* Check if the body of the ACI matches the rights specified.
* @param rights Bit mask representing the rights to match.
* @return True if the body's rights match one of the rights specified.
*/
public boolean hasRights(int rights) {
return body.hasRights(rights);
}
/**
* Re-direct has access type to the body's hasAccessType method.
* @param accessType The access type to match.
* @return True if the body's hasAccessType determines a permission
* contains this access type (allow or deny are valid types).
*/
public boolean hasAccessType(EnumAccessType accessType) {
return body.hasAccessType(accessType);
}
/**
* Evaluate this ACI using the evaluation context provided. Re-direct
* that calls the body's evaluate method.
* @param evalCtx The evaluation context to evaluate with.
* @return EnumEvalResult that contains the evaluation result of this
* aci evaluation.
*/
private EnumEvalResult evaluate(AciEvalContext evalCtx) {
return body.evaluate(evalCtx);
}
/**
* Static class used to evaluate an ACI and evaluation context.
* @param evalCtx The context to evaluate with.
* @param aci The ACI to evaluate.
* @return EnumEvalResult that contains the evaluation result of the aci
* evaluation.
*/
public static EnumEvalResult evaluate(AciEvalContext evalCtx, Aci aci) {
return aci.evaluate(evalCtx);
}
/**
* Returns the name string of this ACI.
* @return The name string.
*/
public String getName() {
return this.body.getName();
}
/**
* Decode an OIDs expression string.
*
* @param expr A string representing the OID expression.
* @param msg A message to be used if there is an exception.
*
* @return Return a hash set of verified OID strings parsed from the OID
* expression.
*
* @throws AciException If the specified expression string is invalid.
*/
public static Set<String> decodeOID(String expr, LocalizableMessage msg)
throws AciException {
Set<String> OIDs = new HashSet<>();
//Quick check to see if the expression is valid.
if (Pattern.matches(oidListRegex, expr)) {
// Remove the spaces in the oid string and
// split the list.
Pattern separatorPattern =
Pattern.compile(LOGICAL_OR);
String oidString =
expr.replaceAll(ZERO_OR_MORE_WHITESPACE, "");
String[] oidArray=
separatorPattern.split(oidString);
//More careful analysis of each OID string.
for(String oid : oidArray) {
verifyOid(oid);
OIDs.add(oid);
}
} else {
throw new AciException(msg);
}
return OIDs;
}
/**
* Verify the specified OID string.
*
* @param oidStr The string representing an OID.
*
* @throws AciException If the specified string is invalid.
*/
private static void verifyOid(String oidStr) throws AciException {
int pos=0, length=oidStr.length();
char c;
if("*".equals(oidStr))
{
return;
}
boolean lastWasPeriod = false;
while (pos < length && ((c = oidStr.charAt(pos++)) != ' ')) {
if (c == '.') {
if (lastWasPeriod) {
LocalizableMessage message = WARN_ACI_SYNTAX_DOUBLE_PERIOD_IN_NUMERIC_OID.get(
oidStr, pos-1);
throw new AciException(message);
}
lastWasPeriod = true;
} else if (! isDigit(c)) {
LocalizableMessage message =
WARN_ACI_SYNTAX_ILLEGAL_CHAR_IN_NUMERIC_OID.get(oidStr, c, pos-1);
throw new AciException(message);
} else {
lastWasPeriod = false;
}
}
}
/**
* Compares this Aci with the provided Aci based on a natural order.
* This order will be first hierarchical (ancestors will come before
* descendants) and then alphabetical by attribute name(s) and
* value(s).
*
* @param aci The Aci against which to compare this Aci.
*
* @return A negative integer if this Aci should come before the
* provided Aci, a positive integer if this Aci should come
* after the provided Aci, or zero if there is no difference
* with regard to ordering.
*/
@Override
public int compareTo(Aci aci)
{
return this.aciString.compareTo(aci.toString());
}
}