/* * 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-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2015 ForgeRock AS */ package org.opends.server.authorization.dseecompat; import static org.opends.messages.AccessControlMessages.*; import static org.opends.server.authorization.dseecompat.Aci.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.forgerock.i18n.LocalizableMessage; import org.opends.server.types.AttributeType; import org.opends.server.types.DN; import org.forgerock.opendj.ldap.SearchScope; /** * This class represents target part of an ACI's syntax. This is the part * of an ACI before the ACI body and specifies the entry, attributes, or set * of entries and attributes which the ACI controls access. * * The supported ACI target keywords are: target, targetattr, * targetscope, targetfilter, targattrfilters, targetcontrol and extop. */ public class AciTargets { /** * ACI syntax has a target keyword. */ private Target target; /** * ACI syntax has a targetscope keyword. */ private SearchScope targetScope = SearchScope.WHOLE_SUBTREE; /** * ACI syntax has a targetattr keyword. */ private TargetAttr targetAttr; /** * ACI syntax has a targetfilter keyword. */ private TargetFilter targetFilter; /** * ACI syntax has a targattrtfilters keyword. */ private TargAttrFilters targAttrFilters; /** * The ACI syntax has a targetcontrol keyword. */ private TargetControl targetControl; /** * The ACI syntax has a extop keyword. */ private ExtOp extOp; /** * The number of regular expression group positions in a valid ACI target * expression. */ private static final int targetElementCount = 3; /** * Regular expression group position of a target keyword. */ private static final int targetKeywordPos = 1; /** * Regular expression group position of a target operator enumeration. */ private static final int targetOperatorPos = 2; /** * Regular expression group position of a target expression statement. */ private static final int targetExpressionPos = 3; /** * Regular expression used to match a single target rule. */ private static final String targetRegex = OPEN_PAREN + ZERO_OR_MORE_WHITESPACE + WORD_GROUP + ZERO_OR_MORE_WHITESPACE + "(!?=)" + ZERO_OR_MORE_WHITESPACE + "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE + CLOSED_PAREN + ZERO_OR_MORE_WHITESPACE; /** * Regular expression used to match one or more target rules. The pattern is * part of a general ACI verification. */ public static final String targetsRegex = "(" + targetRegex + ")*"; /** * Rights that are skipped for certain target evaluations. * The test is use the skipRights array is: * * Either the ACI has a targetattr's rule and the current * attribute type is null or the current attribute type has * a type specified and the targetattr's rule is null. * * The actual check against the skipRights array is: * * 1. Is the ACI's rights in this array? For example, * allow(all) or deny(add) * * AND * * 2. Is the rights from the LDAP operation in this array? For * example, an LDAP add would have rights of add and all. * * If both are true, than the target match test returns true * for this ACI. */ private static final int skipRights = ACI_ADD | ACI_DELETE | ACI_PROXY; /** * Creates an ACI target from the specified arguments. All of these * may be null. If the ACI has no targets defaults will be used. * * @param targetEntry The ACI target keyword class. * @param targetAttr The ACI targetattr keyword class. * @param targetFilter The ACI targetfilter keyword class. * @param targetScope The ACI targetscope keyword class. * @param targAttrFilters The ACI targAttrFilters keyword class. * @param targetControl The ACI targetControl keyword class. * @param extOp The ACI extop keyword class. */ private AciTargets(Target targetEntry, TargetAttr targetAttr, TargetFilter targetFilter, SearchScope targetScope, TargAttrFilters targAttrFilters, TargetControl targetControl, ExtOp extOp) { this.target=targetEntry; this.targetAttr=targetAttr; this.targetScope=targetScope; this.targetFilter=targetFilter; this.targAttrFilters=targAttrFilters; this.targetControl=targetControl; this.extOp=extOp; } /** * Return class representing the ACI target keyword. May be * null. The default is the use the DN of the entry containing * the ACI and check if the resource entry is a descendant of that. * @return The ACI target class. */ private Target getTarget() { return target; } /** * Return class representing the ACI targetattr keyword. May be null. * The default is to not match any attribute types in an entry. * @return The ACI targetattr class. */ public TargetAttr getTargetAttr() { return targetAttr; } /** * Return the ACI targetscope keyword. Default is WHOLE_SUBTREE. * @return The ACI targetscope information. */ public SearchScope getTargetScope() { return targetScope; } /** * Return class representing the ACI targetfilter keyword. May be null. * @return The targetscope information. */ public TargetFilter getTargetFilter() { return targetFilter; } /** * Return the class representing the ACI targattrfilters keyword. May be * null. * @return The targattrfilters information. */ public TargAttrFilters getTargAttrFilters() { return targAttrFilters; } /** * Return the class representing the ACI targetcontrol keyword. May be * null. * @return The targetcontrol information. */ public TargetControl getTargetControl() { return targetControl; } /** * Return the class representing the ACI extop keyword. May be * null. * @return The extop information. */ public ExtOp getExtOp() { return extOp; } /** * Decode an ACI's target part of the syntax from the string provided. * @param input String representing an ACI target part of syntax. * @param dn The DN of the entry containing the ACI. * @return An AciTargets class representing the decoded ACI target string. * @throws AciException If the provided string contains errors. */ public static AciTargets decode(String input, DN dn) throws AciException { Target target=null; TargetAttr targetAttr=null; TargetFilter targetFilter=null; TargAttrFilters targAttrFilters=null; TargetControl targetControl=null; ExtOp extOp=null; SearchScope targetScope=SearchScope.WHOLE_SUBTREE; Pattern targetPattern = Pattern.compile(targetRegex); Matcher targetMatcher = targetPattern.matcher(input); while (targetMatcher.find()) { if (targetMatcher.groupCount() != targetElementCount) { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_SYNTAX.get(input); throw new AciException(message); } String keyword = targetMatcher.group(targetKeywordPos); EnumTargetKeyword targetKeyword = EnumTargetKeyword.createKeyword(keyword); if (targetKeyword == null) { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_KEYWORD.get(keyword); throw new AciException(message); } String operator = targetMatcher.group(targetOperatorPos); EnumTargetOperator targetOperator = EnumTargetOperator.createOperator(operator); if (targetOperator == null) { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGETS_OPERATOR.get(operator); throw new AciException(message); } String expression = targetMatcher.group(targetExpressionPos); switch(targetKeyword) { case KEYWORD_TARGET: { if (target == null){ target = Target.decode(targetOperator, expression, dn); } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS. get("target", input); throw new AciException(message); } break; } case KEYWORD_TARGETCONTROL: { if (targetControl == null){ targetControl = TargetControl.decode(targetOperator, expression); } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS. get("targetcontrol", input); throw new AciException(message); } break; } case KEYWORD_EXTOP: { if (extOp == null){ extOp = ExtOp.decode(targetOperator, expression); } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS. get("extop", input); throw new AciException(message); } break; } case KEYWORD_TARGETATTR: { if (targetAttr == null){ targetAttr = TargetAttr.decode(targetOperator, expression); } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS. get("targetattr", input); throw new AciException(message); } break; } case KEYWORD_TARGETSCOPE: { // Check the operator for the targetscope is EQUALITY if (targetOperator == EnumTargetOperator.NOT_EQUALITY) { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR. get(operator, targetKeyword.name()); throw new AciException(message); } targetScope=createScope(expression); break; } case KEYWORD_TARGETFILTER: { if (targetFilter == null){ targetFilter = TargetFilter.decode(targetOperator, expression); } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS. get("targetfilter", input); throw new AciException(message); } break; } case KEYWORD_TARGATTRFILTERS: { if (targAttrFilters == null){ // Check the operator for the targattrfilters is EQUALITY if (targetOperator == EnumTargetOperator.NOT_EQUALITY) { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR. get(operator, targetKeyword.name()); throw new AciException(message); } targAttrFilters = TargAttrFilters.decode(targetOperator, expression); } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS. get("targattrfilters", input); throw new AciException(message); } break; } } } return new AciTargets(target, targetAttr, targetFilter, targetScope, targAttrFilters, targetControl, extOp); } /** * Evaluates a provided scope string and returns an appropriate * SearchScope enumeration. * @param expression The expression string. * @return An search scope enumeration matching the string. * @throws AciException If the expression is an invalid targetscope * string. */ private static SearchScope createScope(String expression) throws AciException { if(expression.equalsIgnoreCase("base")) { return SearchScope.BASE_OBJECT; } else if(expression.equalsIgnoreCase("onelevel")) { return SearchScope.SINGLE_LEVEL; } else if(expression.equalsIgnoreCase("subtree")) { return SearchScope.WHOLE_SUBTREE; } else if(expression.equalsIgnoreCase("subordinate")) { return SearchScope.SUBORDINATES; } else { LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGETSCOPE_EXPRESSION.get(expression); throw new AciException(message); } } /** * Checks an ACI's targetfilter rule information against a target match * context. * @param aci The ACI to try an match the targetfilter of. * @param matchCtx The target match context containing information needed * to perform a target match. * @return True if the targetfilter rule matched the target context. */ public static boolean isTargetFilterApplicable(Aci aci, AciTargetMatchContext matchCtx) { TargetFilter targetFilter=aci.getTargets().getTargetFilter(); return targetFilter == null || targetFilter.isApplicable(matchCtx); } /** * Check an ACI's targetcontrol rule against a target match context. * * @param aci The ACI to match the targetcontrol against. * @param matchCtx The target match context containing the information * needed to perform the target match. * @return True if the targetcontrol rule matched the target context. */ public static boolean isTargetControlApplicable(Aci aci, AciTargetMatchContext matchCtx) { TargetControl targetControl=aci.getTargets().getTargetControl(); return targetControl != null && targetControl.isApplicable(matchCtx); } /** * Check an ACI's extop rule against a target match context. * * @param aci The ACI to match the extop rule against. * @param matchCtx The target match context containing the information * needed to perform the target match. * @return True if the extop rule matched the target context. */ public static boolean isExtOpApplicable(Aci aci, AciTargetMatchContext matchCtx) { ExtOp extOp=aci.getTargets().getExtOp(); return extOp != null && extOp.isApplicable(matchCtx); } /** * Check an ACI's targattrfilters rule against a target match context. * * @param aci The ACI to match the targattrfilters against. * @param matchCtx The target match context containing the information * needed to perform the target match. * @return True if the targattrfilters rule matched the target context. */ public static boolean isTargAttrFiltersApplicable(Aci aci, AciTargetMatchContext matchCtx) { boolean ret=true; TargAttrFilters targAttrFilters=aci.getTargets().getTargAttrFilters(); if(targAttrFilters != null) { if((matchCtx.hasRights(ACI_ADD) && targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) || (matchCtx.hasRights(ACI_DELETE) && targAttrFilters.hasMask(TARGATTRFILTERS_DELETE))) { ret=targAttrFilters.isApplicableAddDel(matchCtx); } else if((matchCtx.hasRights(ACI_WRITE_ADD) && targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) || (matchCtx.hasRights(ACI_WRITE_DELETE) && targAttrFilters.hasMask(TARGATTRFILTERS_DELETE))) { ret=targAttrFilters.isApplicableMod(matchCtx, aci); } } return ret; } /* * TODO Evaluate making this method more efficient. * The isTargetAttrApplicable method looks a lot less efficient than it * could be with regard to the logic that it employs and the repeated use * of method calls over local variables. */ /** * Checks an provided ACI's targetattr rule against a target match * context. * * @param aci The ACI to evaluate. * @param targetMatchCtx The target match context to check the ACI against. * @return True if the targetattr matched the target context. */ public static boolean isTargetAttrApplicable(Aci aci, AciTargetMatchContext targetMatchCtx) { boolean ret=true; if(!targetMatchCtx.getTargAttrFiltersMatch()) { TargetAttr targetAttr = aci.getTargets().getTargetAttr(); AttributeType attrType = targetMatchCtx.getCurrentAttributeType(); boolean isFirstAttr=targetMatchCtx.isFirstAttribute(); if (attrType != null && targetAttr != null) { ret=TargetAttr.isApplicable(attrType,targetAttr); setEvalAttributes(targetMatchCtx,targetAttr,ret); } else if (attrType != null || targetAttr != null) { if (aci.hasRights(skipRights) && skipRightsHasRights(targetMatchCtx.getRights())) { ret = true; } else { ret = attrType == null && targetAttr != null && aci.hasRights(ACI_WRITE); } } if (isFirstAttr && targetAttr == null && aci.getTargets().getTargAttrFilters() == null) { targetMatchCtx.setEntryTestRule(true); } } return ret; } /** * Try and match a one or more of the specified rights in the skiprights * mask. * @param rights The rights to check for. * @return True if the one or more of the specified rights are in the * skiprights rights mask. */ public static boolean skipRightsHasRights(int rights) { //geteffectiverights sets this flag, turn it off before evaluating. int tmpRights=rights & ~ACI_SKIP_PROXY_CHECK; return (skipRights & tmpRights) == tmpRights; } /** * Wrapper class that passes an ACI, an ACI's targets and the specified * target match context's resource entry DN to the main isTargetApplicable * method. * @param aci The ACI currently be matched. * @param matchCtx The target match context to match against. * @return True if the target matched the ACI. */ public static boolean isTargetApplicable(Aci aci, AciTargetMatchContext matchCtx) { return isTargetApplicable(aci, aci.getTargets(), matchCtx.getResourceEntry().getName()); } /* * TODO Investigate supporting alternative representations of the scope. * * Should we also consider supporting alternate representations of the * scope values (in particular, allow "one" in addition to "onelevel" * and "sub" in addition to "subtree") to match the very common * abbreviations in widespread use for those terms? */ /** * Main target isApplicable method. This method performs the target keyword * match functionality, which allows for directory entry "targeting" using * the specified ACI, ACI targets class and DN. * * @param aci The ACI to match the target against. * @param targets The targets to use in this evaluation. * @param entryDN The DN to use in this evaluation. * @return True if the ACI matched the target and DN. */ public static boolean isTargetApplicable(Aci aci, AciTargets targets, DN entryDN) { DN targetDN=aci.getDN(); /* * Scoping of the ACI uses either the DN of the entry * containing the ACI (aci.getDN above), or if the ACI item * contains a simple target DN and a equality operator, that * simple target DN is used as the target DN. */ if(targets.getTarget() != null && !targets.getTarget().isPattern()) { EnumTargetOperator op=targets.getTarget().getOperator(); if(op != EnumTargetOperator.NOT_EQUALITY) { targetDN=targets.getTarget().getDN(); } } //Check if the scope is correct. switch(targets.getTargetScope().asEnum()) { case BASE_OBJECT: if(!targetDN.equals(entryDN)) { return false; } break; case SINGLE_LEVEL: /* * We use the standard definition of single level to mean the * immediate children only -- not the target entry itself. * Sun CR 6535035 has been raised on DSEE: * Non-standard interpretation of onelevel in ACI targetScope. */ if(!targetDN.equals(entryDN.parent())) { return false; } break; case WHOLE_SUBTREE: if(!entryDN.isDescendantOf(targetDN)) { return false; } break; case SUBORDINATES: if (entryDN.size() <= targetDN.size() || !entryDN.isDescendantOf(targetDN)) { return false; } break; default: return false; } /* * The entry is in scope. For inequality checks, scope was tested * against the entry containing the ACI. If operator is inequality, * check that it doesn't match the target DN. */ if(targets.getTarget() != null && !targets.getTarget().isPattern()) { EnumTargetOperator op=targets.getTarget().getOperator(); if(op == EnumTargetOperator.NOT_EQUALITY) { DN tmpDN=targets.getTarget().getDN(); if(entryDN.isDescendantOf(tmpDN)) { return false; } } } /* * There is a pattern, need to match the substring filter * created when the ACI was decoded. If inequality flip the * result. */ if(targets.getTarget() != null && targets.getTarget().isPattern()) { final boolean ret = targets.getTarget().matchesPattern(entryDN); EnumTargetOperator op=targets.getTarget().getOperator(); if(op == EnumTargetOperator.NOT_EQUALITY) { return !ret; } return ret; } return true; } /** * The method is used to try and determine if a targetAttr expression that * is applicable has a '*' (or '+' operational attributes) token or if it * was applicable because of a specific attribute type declared in the * targetattrs expression (i.e., targetattrs=cn). * * * @param ctx The ctx to check against. * @param targetAttr The targetattrs part of the ACI. * @param ret The is true if the ACI has already been evaluated to be * applicable. */ private static void setEvalAttributes(AciTargetMatchContext ctx, TargetAttr targetAttr, boolean ret) { ctx.clearEvalAttributes(ACI_USER_ATTR_STAR_MATCHED); ctx.clearEvalAttributes(ACI_OP_ATTR_PLUS_MATCHED); /* If an applicable targetattr's match rule has not been seen (~ACI_FOUND_OP_ATTR_RULE or ~ACI_FOUND_USER_ATTR_RULE) and the current attribute type is applicable because of a targetattr all user (or operational) attributes rule match, set a flag to indicate this situation (ACI_USER_ATTR_STAR_MATCHED or ACI_OP_ATTR_PLUS_MATCHED). This check also catches the following case where the match was by a specific attribute type (either user or operational) and the other attribute type has an all attribute token. For example, the expression is: (targetattrs="cn || +) and the current attribute type is cn. */ if(ret && targetAttr.isAllUserAttributes() && !ctx.hasEvalUserAttributes()) { ctx.setEvalUserAttributes(ACI_USER_ATTR_STAR_MATCHED); } else { ctx.setEvalUserAttributes(ACI_FOUND_USER_ATTR_RULE); } if(ret && targetAttr.isAllOpAttributes() && !ctx.hasEvalOpAttributes()) { ctx.setEvalOpAttributes(ACI_OP_ATTR_PLUS_MATCHED); } else { ctx.setEvalOpAttributes(ACI_FOUND_OP_ATTR_RULE); } } }