/* * 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()); } }