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