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 java.util.Iterator;
020import java.util.LinkedList;
021import java.util.List;
022
023import org.forgerock.i18n.LocalizedIllegalArgumentException;
024import org.forgerock.opendj.ldap.ByteString;
025import org.forgerock.opendj.ldap.DN;
026import org.forgerock.opendj.ldap.SearchScope;
027import org.forgerock.opendj.ldap.schema.AttributeType;
028import org.opends.server.core.DirectoryServer;
029import org.opends.server.types.Attribute;
030import org.opends.server.types.DirectoryException;
031import org.opends.server.types.Entry;
032import org.opends.server.types.LDAPURL;
033import org.opends.server.types.SearchFilter;
034
035import static org.opends.messages.AccessControlMessages.*;
036
037/** This class represents the userdn keyword in a bind rule. */
038public class UserDN implements KeywordBindRule {
039    /** A dummy URL for invalid URLs such as: all, parent, anyone, self. */
040    private static final String URL_STR = "ldap:///";
041
042    /** This list holds a list of objects representing a EnumUserDNType URL mapping. */
043    private final List<UserDNTypeURL> urlList;
044    /** Enumeration of the userdn operation type. */
045    private final EnumBindRuleType type;
046
047    /**
048     * Constructor that creates the userdn class. It also sets up an attribute
049     * type ("userdn") needed  for wild-card matching.
050     * @param type The type of  operation.
051     * @param urlList  A list of enumerations containing the URL type and URL
052     * object that can be retrieved at evaluation time.
053     */
054    private UserDN(EnumBindRuleType type, List<UserDNTypeURL> urlList) {
055       this.type=type;
056       this.urlList=urlList;
057    }
058
059    /**
060     * Decodes an expression string representing a userdn bind rule.
061     * @param expression The string representation of the userdn bind rule
062     * expression.
063     * @param type An enumeration of the type of the bind rule.
064     * @return A KeywordBindRule class that represents the bind rule.
065     * @throws AciException If the expression failed to LDAP URL decode.
066     */
067    public static KeywordBindRule decode(String expression,
068            EnumBindRuleType type) throws AciException {
069        String[] vals=expression.split("[|][|]");
070        List<UserDNTypeURL> urlList = new LinkedList<>();
071        for (String val : vals)
072        {
073            StringBuilder value = new StringBuilder(val.trim());
074           /*
075            * TODO Evaluate using a wild-card in the dn portion of LDAP url.
076            * The current implementation (DS6) does not treat a "*"
077            * as a wild-card.
078            *
079            * Is it allowed to have a full LDAP URL (i.e., including a base,
080            * scope, and filter) in which the base DN contains asterisks to
081            * make it a wildcard?  If so, then I don't think that the current
082            * implementation handles that correctly.  It will probably fail
083            * when attempting to create the LDAP URL because the base DN isn't a
084            * valid DN.
085            */
086            EnumUserDNType userDNType = UserDN.getType(value);
087            LDAPURL url;
088            try {
089               url=LDAPURL.decode(value.toString(), true);
090            } catch (LocalizedIllegalArgumentException | DirectoryException e) {
091                throw new AciException(WARN_ACI_SYNTAX_INVALID_USERDN_URL.get(e.getMessageObject()));
092            }
093            urlList.add(new UserDNTypeURL(userDNType, url));
094        }
095        return new UserDN(type, urlList);
096      }
097
098    /**
099     * This method determines the type of the DN (suffix in URL terms)
100     * part of a URL, by examining the full URL itself for known strings
101     * such as (corresponding type shown in parenthesis)
102     *
103     *      "ldap:///anyone"    (EnumUserDNType.ANYONE)
104     *      "ldap:///parent"    (EnumUserDNType.PARENT)
105     *      "ldap:///all"       (EnumUserDNType.ALL)
106     *      "ldap:///self"      (EnumUserDNType.SELF)
107     *
108     * If one of the four above are found, the URL is replaced with a dummy
109     * pattern "ldap:///". This is done because the above four are invalid
110     * URLs; but the syntax is valid for an userdn keyword expression. The
111     * dummy URLs are never used.
112     *
113     * If none of the above are found, it determine if the URL DN is a
114     * substring pattern, such as:
115     *
116     *      "ldap:///uid=*, dc=example, dc=com" (EnumUserDNType.PATTERN)
117     *
118     * If none of the above are determined, it checks if the URL
119     * is a complete URL with scope and filter defined:
120     *
121     *  "ldap:///uid=test,dc=example,dc=com??sub?(cn=j*)"  (EnumUserDNType.URL)
122     *
123     * If none of these those types can be identified, it defaults to
124     * EnumUserDNType.DN.
125     *
126     * @param bldr A string representation of the URL that can be modified.
127     * @return  The user DN type of the URL.
128     */
129    private static EnumUserDNType getType(StringBuilder bldr) {
130        String str=bldr.toString();
131        if (str.contains("?")) {
132            return EnumUserDNType.URL;
133        } else  if(str.equalsIgnoreCase("ldap:///self")) {
134            bldr.replace(0, bldr.length(), URL_STR);
135            return EnumUserDNType.SELF;
136        } else if(str.equalsIgnoreCase("ldap:///anyone")) {
137            bldr.replace(0, bldr.length(), URL_STR);
138            return EnumUserDNType.ANYONE;
139        } else if(str.equalsIgnoreCase("ldap:///parent")) {
140            bldr.replace(0, bldr.length(), URL_STR);
141            return EnumUserDNType.PARENT;
142        } else if(str.equalsIgnoreCase("ldap:///all")) {
143            bldr.replace(0, bldr.length(), URL_STR);
144            return EnumUserDNType.ALL;
145        } else if (str.contains("*")) {
146            return EnumUserDNType.DNPATTERN;
147        } else {
148            return EnumUserDNType.DN;
149        }
150    }
151
152    /**
153     * Performs the evaluation of a userdn bind rule based on the
154     * evaluation context passed to it. The evaluation stops when there
155     * are no more UserDNTypeURLs to evaluate or if an UserDNTypeURL
156     * evaluates to true.
157     * @param evalCtx The evaluation context to evaluate with.
158     * @return  An evaluation result enumeration containing the result
159     * of the evaluation.
160     */
161    @Override
162    public EnumEvalResult evaluate(AciEvalContext evalCtx) {
163        EnumEvalResult matched = EnumEvalResult.FALSE;
164        boolean undefined=false;
165
166        boolean isAnonUser=evalCtx.isAnonymousUser();
167        Iterator<UserDNTypeURL> it=urlList.iterator();
168        for(; it.hasNext() && matched != EnumEvalResult.TRUE &&
169                matched != EnumEvalResult.ERR;) {
170            UserDNTypeURL dnTypeURL=it.next();
171            //Handle anonymous checks here
172            if(isAnonUser) {
173                if(dnTypeURL.getUserDNType() == EnumUserDNType.ANYONE)
174                {
175                  matched = EnumEvalResult.TRUE;
176                }
177            }
178            else
179            {
180              matched=evalNonAnonymous(evalCtx, dnTypeURL);
181            }
182        }
183        return matched.getRet(type, undefined);
184    }
185
186    /**
187     * Performs an evaluation of a single UserDNTypeURL of a userdn bind
188     * rule using the evaluation context provided. This method is called
189     * for the non-anonymous user case.
190     * @param evalCtx  The evaluation context to evaluate with.
191     * @param dnTypeURL The URL dn type mapping to evaluate.
192     * @return An evaluation result enumeration containing the result
193     * of the evaluation.
194     */
195    private EnumEvalResult evalNonAnonymous(AciEvalContext evalCtx,
196                                            UserDNTypeURL dnTypeURL) {
197        return evalNonAnonymous0(evalCtx, dnTypeURL) ? EnumEvalResult.TRUE : EnumEvalResult.FALSE;
198    }
199
200    private boolean evalNonAnonymous0(AciEvalContext evalCtx,
201                                            UserDNTypeURL dnTypeURL) {
202        DN clientDN=evalCtx.getClientDN();
203        DN resDN=evalCtx.getResourceDN();
204        EnumUserDNType type=dnTypeURL.getUserDNType();
205        LDAPURL url=dnTypeURL.getURL();
206        switch (type) {
207            case URL:
208                return evalURL0(evalCtx, url);
209            case ANYONE:
210            case ALL:
211                return true;
212            case SELF:
213                return clientDN.equals(resDN);
214            case PARENT:
215                DN parentDN = resDN.parent();
216                return parentDN != null && parentDN.equals(clientDN);
217            case DNPATTERN:
218                return evalDNPattern(evalCtx, url);
219            case DN:
220                return evalDN(clientDN, url);
221            default:
222                return false;
223        }
224    }
225
226    private boolean evalDN(DN clientDN, LDAPURL url)
227    {
228      try
229      {
230          DN dn = url.getBaseDN();
231          if (clientDN.equals(dn))
232          {
233            return true;
234          }
235
236          // This code handles the case where a root dn entry does
237          // not have bypass-acl privilege and the ACI bind rule
238          // userdn DN possible is an alternate root DN.
239          DN actualDN = DirectoryServer.getActualRootBindDN(dn);
240          DN clientActualDN = DirectoryServer.getActualRootBindDN(clientDN);
241          if (actualDN != null)
242          {
243            dn = actualDN;
244          }
245          if (clientActualDN != null)
246          {
247            clientDN = clientActualDN;
248          }
249          return clientDN.equals(dn);
250      } catch (DirectoryException ex) {
251          //TODO add message
252          return false;
253      }
254    }
255
256    /**
257     * This method evaluates a DN pattern userdn expression.
258     * @param evalCtx  The evaluation context to use.
259     * @param url The LDAP URL containing the pattern.
260     * @return An enumeration evaluation result.
261     */
262    private boolean evalDNPattern(AciEvalContext evalCtx, LDAPURL url) {
263        PatternDN pattern;
264        try {
265          pattern = PatternDN.decode(url.getRawBaseDN());
266        } catch (DirectoryException ex) {
267          return false;
268        }
269
270        return pattern.matchesDN(evalCtx.getClientDN());
271    }
272
273
274    /**
275     * This method evaluates an URL userdn expression. Something like:
276     * ldap:///suffix??sub?(filter). It also searches for the client DN
277     * entry and saves it in the evaluation context for repeat evaluations
278     * that might come later in processing.
279     *
280     * @param evalCtx  The evaluation context to use.
281     * @param url URL containing the URL to use in the evaluation.
282     * @return An enumeration of the evaluation result.
283     */
284    public static EnumEvalResult evalURL(AciEvalContext evalCtx, LDAPURL url) {
285        return evalURL0(evalCtx, url) ? EnumEvalResult.TRUE : EnumEvalResult.FALSE;
286    }
287
288    private static boolean evalURL0(AciEvalContext evalCtx, LDAPURL url) {
289        DN urlDN;
290        SearchFilter filter;
291        try {
292            urlDN=url.getBaseDN();
293            filter=url.getFilter();
294        } catch (DirectoryException ex) {
295            return false;
296        }
297        SearchScope scope=url.getScope();
298        if(scope == SearchScope.WHOLE_SUBTREE) {
299            if(!evalCtx.getClientDN().isSubordinateOrEqualTo(urlDN))
300            {
301              return false;
302            }
303        } else if(scope == SearchScope.SINGLE_LEVEL) {
304            DN parent=evalCtx.getClientDN().parent();
305            if(parent != null && !parent.equals(urlDN))
306            {
307              return false;
308            }
309        } else if(scope == SearchScope.SUBORDINATES) {
310            DN userDN = evalCtx.getClientDN();
311            if (userDN.size() <= urlDN.size() ||
312                 !userDN.isSubordinateOrEqualTo(urlDN)) {
313              return false;
314            }
315        } else {
316            if(!evalCtx.getClientDN().equals(urlDN))
317            {
318              return false;
319            }
320        }
321        try {
322            return (filter.matchesEntry(evalCtx.getClientEntry()));
323        } catch (DirectoryException ex) {
324            return false;
325        }
326    }
327
328    /*
329     * TODO Evaluate making this method more efficient.
330     *
331     * The evalDNEntryAttr method isn't as efficient as it could be.
332     * It would probably be faster to to convert the clientDN to a ByteString
333     * and see if the entry has that value than to decode each value as a DN
334     * and see if it matches the clientDN.
335     */
336    /**
337     * This method searches an entry for an attribute value that is
338     * treated as a DN. That DN is then compared against the client
339     * DN.
340     * @param e The entry to get the attribute type from.
341     * @param clientDN The client authorization DN to check for.
342     * @param attrType The attribute type from the bind rule.
343     * @return An enumeration with the result.
344     */
345    public static boolean evaluate(Entry e, DN clientDN,
346                                           AttributeType attrType) {
347        List<Attribute> attrs =  e.getAttribute(attrType);
348        for(ByteString v : attrs.get(0)) {
349            try {
350                DN dn = DN.valueOf(v.toString());
351                if(dn.equals(clientDN)) {
352                    return true;
353                }
354            } catch (LocalizedIllegalArgumentException ignored) {
355                break;
356            }
357        }
358        return false;
359    }
360
361    @Override
362    public String toString() {
363        final StringBuilder sb = new StringBuilder();
364        toString(sb);
365        return sb.toString();
366    }
367
368    @Override
369    public final void toString(StringBuilder buffer) {
370        buffer.append("userdn");
371        buffer.append(this.type.getType());
372        for (UserDNTypeURL url : this.urlList) {
373            buffer.append("\"");
374            buffer.append(URL_STR);
375            buffer.append(url.getUserDNType().toString().toLowerCase());
376            buffer.append("\"");
377        }
378    }
379}