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