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