001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.authorization.dseecompat;
018
019import static org.opends.messages.AccessControlMessages.*;
020import static org.opends.server.authorization.dseecompat.Aci.*;
021
022import java.util.ArrayList;
023import java.util.Map;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.forgerock.i18n.LocalizableMessage;
028import org.forgerock.opendj.ldap.ByteString;
029import org.forgerock.opendj.ldap.DN;
030import org.forgerock.opendj.ldap.schema.AttributeType;
031import org.opends.server.types.Attribute;
032import org.opends.server.types.Attributes;
033import org.opends.server.types.DirectoryException;
034import org.opends.server.types.Entry;
035import org.opends.server.types.SearchFilter;
036
037/** The TargAttrFilters class represents a targattrfilters rule of an ACI. */
038public class TargAttrFilters {
039    /** A valid targattrfilters rule may have two TargFilterlist parts -- the first one is required. */
040    private final TargAttrFilterList firstFilterList;
041    private final TargAttrFilterList secondFilterList;
042
043    /** Regular expression group position for the first operation value. */
044    private static final int firstOpPos = 1;
045
046    /** Regular expression group position for the rest of an partially parsed rule. */
047    private static final int restOfExpressionPos=2;
048
049    /** Regular expression used to match the operation group (either add or del). */
050    private static final String ADD_OR_DEL_KEYWORD_GROUP = "(add|del)";
051
052    /** Regular expression used to check for valid expression separator. */
053    private static final Pattern secondOpSeparator = Pattern.compile("\\)" + ZERO_OR_MORE_WHITESPACE + ",");
054
055    /**
056     * Regular expression used to match the second operation of the filter list.
057     * If the first was "add" this must be "del", if the first was "del" this
058     * must be "add".
059     */
060    private static final Pattern secondOp = Pattern.compile(
061            "[,]{1}" + ZERO_OR_MORE_WHITESPACE + "del|add" +
062            ZERO_OR_MORE_WHITESPACE + EQUAL_SIGN + ZERO_OR_MORE_WHITESPACE);
063
064    /**
065     * Regular expression used to match the first targFilterList, it must exist
066     * or an exception is thrown.
067     */
068    private static final Pattern firstOp = Pattern.compile("^" + ADD_OR_DEL_KEYWORD_GROUP +
069            ZERO_OR_MORE_WHITESPACE + EQUAL_SIGN + ZERO_OR_MORE_WHITESPACE);
070
071    /**
072     * Regular expression used to group the remainder of a partially parsed
073     * rule.  Any character one or more times.
074     */
075    private static final String restOfExpression = "(.+)";
076
077    /** Regular expression used to match the first operation keyword and the rest of the expression. */
078    private static final String keywordFullPattern = firstOp + restOfExpression;
079
080    /** The enumeration representing the operation. */
081    private final EnumTargetOperator op;
082
083    /**
084     * A mask used to denote if the rule has add, del or both operations in the
085     * composite TargFilterList parts.
086     */
087    private int operationMask;
088
089    /**
090     * Represents an targattrfilters keyword rule.
091     * @param op The enumeration representing the operation type.
092     *
093     * @param firstFilterList  The first filter list class parsed from the rule.
094     * This one is required.
095     *
096     * @param secondFilterList The second filter list class parsed from the
097     * rule. This one is optional.
098     */
099    private TargAttrFilters(EnumTargetOperator op,
100                           TargAttrFilterList firstFilterList,
101                           TargAttrFilterList secondFilterList ) {
102        this.op=op;
103        this.firstFilterList=firstFilterList;
104        operationMask=firstFilterList.getMask();
105        if(secondFilterList != null) {
106            //Add the second filter list mask to the mask.
107            operationMask |= secondFilterList.getMask();
108            this.secondFilterList=secondFilterList;
109        } else {
110            this.secondFilterList = null;
111        }
112    }
113
114    /**
115     * Decode an targattrfilter rule.
116     * @param type The enumeration representing the type of this rule. Defaults
117     * to equality for this target.
118     *
119     * @param expression The string expression to be decoded.
120     * @return  A TargAttrFilters class representing the decode expression.
121     * @throws AciException If the expression string contains errors and
122     * cannot be decoded.
123     */
124    public static TargAttrFilters decode(EnumTargetOperator type,
125                                        String expression) throws AciException {
126        Pattern fullPattern=Pattern.compile(keywordFullPattern);
127        Matcher matcher = fullPattern.matcher(expression);
128        //First match for overall correctness and to get the first operation.
129        if(!matcher.find()) {
130            LocalizableMessage message =
131              WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_EXPRESSION.
132                  get(expression);
133            throw new AciException(message);
134        }
135        String firstOp=matcher.group(firstOpPos);
136        String subExpression=matcher.group(restOfExpressionPos);
137        //This pattern is built dynamically and is used to see if the operations
138        //in the two filter list parts (if the second exists) are equal. See
139        //comment below.
140        String opPattern=
141                "[,]{1}" + ZERO_OR_MORE_WHITESPACE  +
142                firstOp + ZERO_OR_MORE_WHITESPACE + EQUAL_SIGN +
143                ZERO_OR_MORE_WHITESPACE;
144        String[] temp=subExpression.split(opPattern);
145        /*
146         * Check that the initial list operation is not equal to the second.
147         * For example:  Matcher find
148         *
149         *  "add:cn:(cn=foo), add:cn:(cn=bar)"
150         *
151         * This is invalid.
152         */
153        if(temp.length > 1) {
154            LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_OPS_MATCH.
155                get(expression);
156            throw new AciException(message);
157        }
158        /* Check that there are not too many filter lists. There can only be either one or two. */
159        String[] filterLists = secondOp.split(subExpression, -1);
160        if(filterLists.length > 2) {
161          throw new AciException(WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_MAX_FILTER_LISTS.get(expression));
162        } else if (filterLists.length == 1) {
163          //Check if the there is something like ") , deel=". A bad token
164          //that the regular expression didn't pick up.
165          String[] filterList2 = secondOpSeparator.split(subExpression);
166          if(filterList2.length == 2) {
167              throw new AciException(WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_EXPRESSION.get(expression));
168          }
169          String rg = getReverseOp(firstOp) + "=";
170          //This check catches the case where there might not be a
171          //',' character between the first filter list and the second.
172          if (subExpression.contains(rg)) {
173            throw new AciException(WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_EXPRESSION.get(expression));
174          }
175        }
176        filterLists[0]=filterLists[0].trim();
177        //First filter list must end in an ')' character.
178        if(!filterLists[0].endsWith(")")) {
179            throw new AciException(WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_EXPRESSION.get(expression));
180        }
181        TargAttrFilterList firstFilterList =
182                TargAttrFilterList.decode(getMask(firstOp), filterLists[0]);
183        TargAttrFilterList secondFilterList=null;
184        //Handle the second filter list if there is one.
185          if(filterLists.length == 2) {
186            String filterList=filterLists[1].trim();
187            //Second filter list must start with a '='.
188            if(!filterList.startsWith("=")) {
189              throw new AciException(WARN_ACI_SYNTAX_INVALID_TARGATTRFILTERS_EXPRESSION.get(expression));
190            }
191            String temp2= filterList.substring(1,filterList.length());
192            //Assume the first op is an "add" so this has to be a "del".
193            //If the first op is a "del", the second has to be an "add".
194            String secondOp = getReverseOp(firstOp);
195            secondFilterList =
196                    TargAttrFilterList.decode(getMask(secondOp), temp2);
197        }
198        return new TargAttrFilters(type, firstFilterList, secondFilterList);
199    }
200
201  /**
202   * If the passed in op is an "add", then return "del"; Otherwise If the passed
203   * in op is an "del", then return "add".
204   */
205  private static String getReverseOp(String op)
206  {
207    if (getMask(op) == TARGATTRFILTERS_DELETE)
208    {
209      return "add";
210    }
211    return "del";
212  }
213
214    /**
215     * Return the mask corresponding to the specified string.
216     *
217     * @param op The op string.
218     * @return   The mask corresponding to the operation string.
219     */
220    private static int getMask(String op) {
221        if ("add".equals(op))
222        {
223          return TARGATTRFILTERS_ADD;
224        }
225        return TARGATTRFILTERS_DELETE;
226    }
227
228    /**
229     * Gets the TargFilterList  corresponding to the mask value.
230     * @param matchCtx The target match context containing the rights to
231     * match against.
232     * @return  A TargAttrFilterList matching both the rights of the target
233     * match context and the mask of the TargFilterAttrList. May return null.
234     */
235    private TargAttrFilterList
236    getTargAttrFilterList(AciTargetMatchContext matchCtx) {
237        int mask=ACI_NULL;
238        //Set up the wanted mask by evaluating both the target match
239        //context's rights and the mask.
240        if((matchCtx.hasRights(ACI_WRITE_ADD) || matchCtx.hasRights(ACI_ADD)) &&
241                hasMask(TARGATTRFILTERS_ADD))
242        {
243          mask=TARGATTRFILTERS_ADD;
244        }
245        else if((matchCtx.hasRights(ACI_WRITE_DELETE) ||
246                 matchCtx.hasRights(ACI_DELETE)) &&
247                hasMask(TARGATTRFILTERS_DELETE))
248        {
249          mask=TARGATTRFILTERS_DELETE;
250        }
251
252        //Check the first list first, it always has to be there. If it doesn't
253        //match then check the second if it exists.
254        if(firstFilterList.hasMask(mask))
255        {
256          return firstFilterList;
257        }
258        else if (secondFilterList != null && secondFilterList.hasMask(mask))
259        {
260          return secondFilterList;
261        }
262        return null;
263    }
264
265    /**
266     * Check if this TargAttrFilters object is applicable to the target
267     * specified match context. This check is only used for the LDAP modify
268     * operation.
269     * @param matchCtx The target match context containing the information
270     * needed to match.
271     * @param aci  The ACI currently being evaluated for a target match.
272     * @return True if this TargAttrFitlers object is applicable to this
273     * target match context.
274     */
275    public boolean isApplicableMod(AciTargetMatchContext matchCtx,
276                                   Aci aci) {
277        //Get the targFitlerList corresponding to this context's rights.
278        TargAttrFilterList attrFilterList=getTargAttrFilterList(matchCtx);
279        //If the list is empty return true and go on to the targattr check
280        //in AciTargets.isApplicable().
281        if(attrFilterList == null)
282        {
283          return true;
284        }
285        Map<AttributeType, SearchFilter> filterList  =
286                attrFilterList.getAttributeTypeFilterList();
287        boolean attrMatched=true;
288        AttributeType attrType=matchCtx.getCurrentAttributeType();
289        //If the filter list contains the current attribute type; check
290        //the attribute types value(s) against the corresponding filter.
291        // If the filter list does not contain the attribute type skip the
292        // attribute type.
293        if(attrType != null && filterList.containsKey(attrType)) {
294            ByteString value = matchCtx.getCurrentAttributeValue();
295            SearchFilter filter = filterList.get(attrType);
296            attrMatched=matchFilterAttributeValue(attrType, value, filter);
297            //This flag causes any targattr checks to be bypassed in AciTargets.
298            matchCtx.setTargAttrFiltersMatch(true);
299            //Doing a geteffectiverights eval, save the ACI and the name
300            //in the context.
301            if(matchCtx.isGetEffectiveRightsEval()) {
302              matchCtx.setTargAttrFiltersAciName(aci.getName());
303              matchCtx.addTargAttrFiltersMatchAci(aci);
304            }
305            attrMatched = revertForInequalityOperator(op, attrMatched);
306        }
307        return attrMatched;
308    }
309
310  private boolean revertForInequalityOperator(EnumTargetOperator op,
311      boolean result)
312  {
313    if (EnumTargetOperator.NOT_EQUALITY.equals(op))
314    {
315      return !result;
316    }
317    return result;
318  }
319
320    /**
321     * Check if this TargAttrFilters object is applicable to the specified
322     * target match context. This check is only used for either LDAP add or
323     * delete operations.
324     * @param matchCtx The target match context containing the information
325     * needed to match.
326     * @return True if this TargAttrFilters object is applicable to this
327     * target match context.
328     */
329    public boolean isApplicableAddDel(AciTargetMatchContext matchCtx) {
330        TargAttrFilterList attrFilterList=getTargAttrFilterList(matchCtx);
331        //List didn't match current operation return true.
332        if(attrFilterList == null)
333        {
334          return true;
335        }
336
337        Map<AttributeType, SearchFilter> filterList  =
338                attrFilterList.getAttributeTypeFilterList();
339        Entry resEntry=matchCtx.getResourceEntry();
340        //Iterate through each attribute type in the filter list checking
341        //the resource entry to see if it has that attribute type. If not
342        //go to the next attribute type. If it is found, then check the entries
343        //attribute type values against the filter.
344      for(Map.Entry<AttributeType, SearchFilter> e : filterList.entrySet()) {
345            AttributeType attrType=e.getKey();
346            SearchFilter f=e.getValue();
347            if(!matchFilterAttributeType(resEntry, attrType, f)) {
348                return revertForInequalityOperator(op, false);
349            }
350        }
351        return revertForInequalityOperator(op, true);
352    }
353
354  private boolean matchFilterAttributeType(Entry entry,
355      AttributeType attrType, SearchFilter f)
356  {
357    if (entry.hasAttribute(attrType))
358    {
359      // Found a match in the entry, iterate over each attribute
360      // type in the entry and check its values against the filter.
361      for (Attribute a : entry.getAttribute(attrType))
362      {
363        if (!matchFilterAttributeValues(a, attrType, f))
364        {
365          return false;
366        }
367      }
368    }
369    return true;
370  }
371
372    /**
373     * Iterate over each attribute type attribute and compare the values
374     * against the provided filter.
375     * @param a The attribute from the resource entry.
376     * @param attrType The attribute type currently working on.
377     * @param filter  The filter to evaluate the values against.
378     * @return  True if all of the values matched the filter.
379     */
380    private boolean matchFilterAttributeValues(Attribute a,
381                                               AttributeType attrType,
382                                               SearchFilter filter) {
383        //Iterate through each value and apply the filter against it.
384        for (ByteString value : a) {
385            if (!matchFilterAttributeValue(attrType, value, filter)) {
386                return false;
387            }
388        }
389        return true;
390    }
391
392    /**
393     * Matches an specified attribute value against a specified filter. A dummy
394     * entry is created with only a single attribute containing the value  The
395     * filter is applied against that entry.
396     *
397     * @param attrType The attribute type currently being evaluated.
398     * @param value  The value to match the filter against.
399     * @param filter  The filter to match.
400     * @return  True if the value matches the filter.
401     */
402    private boolean matchFilterAttributeValue(AttributeType attrType,
403                                              ByteString value,
404                                              SearchFilter filter) {
405        Attribute attr = Attributes.create(attrType, value);
406        Entry e = new Entry(DN.rootDN(), null, null, null);
407        e.addAttribute(attr, new ArrayList<ByteString>());
408        try {
409            return filter.matchesEntry(e);
410        } catch(DirectoryException ex) {
411            return false;
412        }
413    }
414
415    /**
416     * Return true if the TargAttrFilters mask contains the specified mask.
417     * @param mask  The mask to check for.
418     * @return  True if the mask matches.
419     */
420    public boolean hasMask(int mask) {
421        return (this.operationMask & mask) != 0;
422    }
423}