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