001/*
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
005 *
006 * The contents of this file are subject to the terms
007 * of the Common Development and Distribution License
008 * (the License). You may not use this file except in
009 * compliance with the License.
010 *
011 * You can obtain a copy of the License at
012 * https://opensso.dev.java.net/public/CDDLv1.0.html or
013 * opensso/legal/CDDLv1.0.txt
014 * See the License for the specific language governing
015 * permission and limitations under the License.
016 *
017 * When distributing Covered Code, include this CDDL
018 * Header Notice in each file and include the License file
019 * at opensso/legal/CDDLv1.0.txt.
020 * If applicable, add the following below the CDDL Header,
021 * with the fields enclosed by brackets [] replaced by
022 * your own identifying information:
023 * "Portions Copyrighted [year] [name of copyright owner]"
024 *
025 * $Id: DynamicGroup.java,v 1.6 2009/01/28 05:34:50 ww203982 Exp $
026 *
027 * Portions Copyrighted 2011-2015 ForgeRock AS.
028 */
029package com.iplanet.ums;
030
031import com.iplanet.services.ldap.Attr;
032import com.iplanet.services.ldap.AttrSet;
033import com.iplanet.services.util.I18n;
034import com.sun.identity.shared.debug.Debug;
035import com.sun.identity.shared.encode.URLEncDec;
036import java.security.Principal;
037
038import org.forgerock.i18n.LocalizedIllegalArgumentException;
039import org.forgerock.opendj.ldap.DN;
040import org.forgerock.opendj.ldap.Filter;
041import org.forgerock.opendj.ldap.LDAPUrl;
042import org.forgerock.opendj.ldap.ModificationType;
043import org.forgerock.opendj.ldap.SearchScope;
044
045/**
046 * Represents a dynamic group entry.
047 *
048 * @supported.api
049 */
050public class DynamicGroup extends PersistentObject implements
051        IDynamicMembership {
052
053    private static I18n i18n = I18n.getInstance(IUMSConstants.UMS_PKG);
054
055    private static Debug debug;
056    static {
057        debug = Debug.getInstance(IUMSConstants.UMS_DEBUG);
058    }
059
060    /**
061     * Default constructor.
062     */
063    protected DynamicGroup() {
064    }
065
066    /**
067     * Constructs a group object from an ID by reading from persistent storage.
068     * 
069     * @param session Authenticated session.
070     * @param guid globally unique identifier for the group entry.
071     * @exception UMSException if fail to instantiate from persistent storage.
072     * @deprecated
073     */
074    DynamicGroup(Principal principal, Guid guid) throws UMSException {
075        super(principal, guid);
076        verifyClass();
077    }
078
079    /**
080     * Constructs a <code>DynamicGroup</code> in memory using the default
081     * registered template for <code>DynamicGroup</code>. This is an
082     * in-memory representation of a new object and one needs to call the
083     * <code>save</code> method to save this new object to persistent storage.
084     * 
085     * @param attrSet Attribute/value set, which should contain
086     *        <code>memberUrl</code>.
087     * @exception UMSException if fail to instantiate from persistent storage.
088     */
089    DynamicGroup(AttrSet attrSet) throws UMSException {
090        this(TemplateManager.getTemplateManager().getCreationTemplate(_class,
091                null), attrSet);
092    }
093
094    /**
095     * Constructs a <code>DynamicGroup</code> in memory with a given template
096     * for <code>DynamicGroup</code>. This is an in-memory representation of a
097     * new object; the <code>save</code> method must be called to save this
098     * new object to persistent storage.
099     * 
100     * @param template Template for creating a group.
101     * @param attrSet Attribute/value set, which should contain
102     *        <code>memberUrl</code>.
103     * @exception UMSException if fail to instantiate from persistent storage.
104     *
105     * @supported.api
106     */
107    public DynamicGroup(CreationTemplate template, AttrSet attrSet)
108            throws UMSException {
109        super(template, attrSet);
110    }
111
112    /**
113     * Constructs a <code>DynamicGroup</code> in memory using the default
114     * registered template for <code>DynamicGroup</code>. This is an in memory
115     * representation of a new object and the <code>save</code> method must be
116     * called to save this new object to persistent storage.
117     * 
118     * @param attrSet Attribute/value set, which should not contain
119     *        <code>memberUrl</code>; any values of <code>memberUrl</code> will
120     *        be overwritten by the explicit search criteria arguments.
121     * @param base Search base for evaluating members of the group.
122     * @param filter Search filter for evaluating members of the group.
123     * @param scope Search scope for evaluating members of the group.
124     * @exception UMSException if fail to instantiate from persistent storage.
125     */
126    DynamicGroup(AttrSet attrSet, Guid baseGuid, String filter, int scope)
127            throws UMSException {
128        this(TemplateManager.getTemplateManager().getCreationTemplate(_class,
129                null), attrSet, baseGuid, filter, scope);
130    }
131
132    /**
133     * Constructs a <code>DynamicGroup</code> in memory given a template for
134     * <code>DynamicGroup</code>. This is an in-memory representation of a new
135     * object and the <code>save</code> method must be called to save this new
136     * object to persistent storage.
137     * 
138     * @param template Template for creating a group.
139     * @param attrSet Attribute/value set, which should not contain member Url;
140     *        any values of memberUrl will be overwritten by the explicit search
141     *        criteria arguments.
142     * @param baseGuid Search base for evaluating members of the group.
143     * @param filter Search filter for evaluating members of the group.
144     * @param scope Search scope for evaluating members of the group has to be
145     *        <code>LDAPv2.SCOPE_ONE</code> or <code>LDAPv2.SCOPE_SUB</code>.
146     * 
147     * @exception UMSException if fail to instantiate from persistent storage.
148     *
149     * @supported.api
150     */
151    public DynamicGroup(
152        CreationTemplate template,
153        AttrSet attrSet,
154        Guid baseGuid,
155        String filter,
156        int scope
157    ) throws UMSException {
158        super(template, attrSet);
159        try {
160            setUrl(baseGuid, Filter.valueOf(filter), SearchScope.valueOf(scope));
161        } catch (Exception e) {
162            // TODO - Log Exception
163            debug.error("DynamicGroup : Exception : " + e.getMessage());
164        }
165    }
166
167    /**
168     * Sets the search filter used to evaluate this dynamic group.
169     * 
170     * @param filter Search filter for evaluating members of the group.
171     *
172     * @supported.api
173     */
174    public void setSearchFilter(String filter) {
175        LDAPUrl url = getUrl();
176        SearchScope scope = url.getScope();
177
178        Guid baseGuid = new Guid(url.getName().toString());
179        try {
180            setUrl(baseGuid, Filter.valueOf(filter), scope);
181        } catch (Exception e) {
182            // TODO - Log Exception
183            debug.error("DynamicGroup.setSearchFilter : Exception : "
184                    + e.getMessage());
185        }
186    }
187
188    /**
189     * Returns the search filter used to evaluate this dynamic group.
190     * 
191     * @return Search filter for evaluating members of the group the scope in
192     *         the filter has to be <code>LDAPv2.SCOPE_ONE</code> or
193     *         <code>LDAPv2.SCOPE_SUB</code>.
194     *
195     * @supported.api
196     */
197    public String getSearchFilter() {
198        return getUrl().getFilter().toString();
199    }
200
201    /**
202     * Sets the search base used to evaluate this dynamic group.
203     * 
204     * @param baseGuid Search base for evaluating members of the group.
205     *
206     * @supported.api
207     */
208    public void setSearchBase(Guid baseGuid) {
209        LDAPUrl url = getUrl();
210        SearchScope scope = url.getScope();
211        Filter filter = url.getFilter();
212        try {
213            setUrl(baseGuid, filter, scope);
214        } catch (Exception e) {
215            // TODO - Log Exception
216            debug.error("DynamicGroup.setSearchFilter : Exception : "
217                    + e.getMessage());
218        }
219    }
220
221    /**
222     * Returns the search base used to evaluate this dynamic group.
223     * 
224     * @return Search base for evaluating members of the group.
225     *
226     * @supported.api
227     */
228    public Guid getSearchBase() {
229        return new Guid(getUrl().getName().toString());
230    }
231
232    /**
233     * Sets the search scope used to evaluate this dynamic group.
234     * 
235     * @param scope Search scope for evaluating members of the group. Use one of
236     *        the search scope <code>SCOPE_BASE</code>,
237     *        <code>SCOPE_ONE</code>, or <code>SCOPE_SUB</code>.
238     *
239     * @supported.api
240     */
241    public void setSearchScope(int scope) {
242        LDAPUrl url = getUrl();
243        Guid baseGuid = new Guid(url.getName().toString());
244        Filter filter = url.getFilter();
245        try {
246            setUrl(baseGuid, filter, SearchScope.valueOf(scope));
247        } catch (Exception e) {
248            // TODO - Log Exception
249            debug.error("DynamicGroup.setSearchFilter : Exception : "
250                    + e.getMessage());
251        }
252    }
253
254    /**
255     * Returns the search scope used to evaluate this dynamic group.
256     * 
257     * @return Search scope for evaluating members of the group.
258     *
259     * @supported.api
260     */
261    public int getSearchScope() {
262        return getUrl().getScope().intValue();
263    }
264
265    /**
266     * Convert the given parameters into an LDAP URL string. No LDAP host, port,
267     * and attribute to return are present in the LDAP URL. Only search base,
268     * filter and scope are given.
269     * 
270     * @param base Search Base DN in the LDAP URL.
271     * @param filter Search filter in LDAP URL.
272     * @param scope Search scope in LDAP URL.
273     * @return LDAP URL.
274     */
275    protected String toUrlStr(String base, Filter filter, SearchScope scope) {
276        StringBuilder urlBuf = new StringBuilder();
277        urlBuf.append("ldap:///").append(base).append("?");
278
279        if (SearchScope.BASE_OBJECT.equals(scope)) {
280            urlBuf.append("?base");
281        } else if (SearchScope.SINGLE_LEVEL.equals(scope)) {
282            urlBuf.append("?one");
283        } else {
284            urlBuf.append("?sub");
285        }
286
287        if (filter != null && !filter.toString().isEmpty()) {
288            urlBuf.append("?").append(filter);
289        } else {
290            urlBuf.append("?");
291        }
292
293        return urlBuf.toString();
294    }
295
296    /**
297     * Creates a new search definition; the change is not persistent until
298     * save() is called.
299     * 
300     * @param baseGuid Search base for evaluating members of the group.
301     * @param filter Search filter for evaluating members of the group.
302     * @param scope Search scope for evaluating members of the group.
303     */
304    protected void setUrl(Guid baseGuid, Filter filter, SearchScope scope) {
305        // Only valid scope is "sub" and "one"
306        //
307        if (!SearchScope.SINGLE_LEVEL.equals(scope) && !SearchScope.WHOLE_SUBTREE.equals(scope)) {
308            String msg = i18n.getString(IUMSConstants.ILLEGAL_GROUP_SCOPE);
309            throw new IllegalArgumentException(msg);
310        }
311
312        String urlStr = toUrlStr(baseGuid.getDn(), filter, scope);
313
314        // Sanity check on the url
315        //
316        try {
317            LDAPUrl.valueOf(urlStr);
318        } catch (LocalizedIllegalArgumentException e) {
319            throw new IllegalArgumentException(e.getMessage());
320        }
321
322        // TODO: Need to support multiple values of memberUrl? If so, do
323        // an ADD instead of a replace.
324        //
325        modify(new Attr(MEMBER_URL_NAME, urlStr), ModificationType.REPLACE);
326    }
327
328    /**
329     * Returns the native LDAP URL used to evaluate this dynamic group.
330     * 
331     * @return LDAP URL for evaluating members of the group
332     */
333    protected LDAPUrl getUrl() {
334        Attr attr = getAttribute(MEMBER_URL_NAME);
335        LDAPUrl url = null;
336        try {
337            // TODO: Need to support multiple values of memberUrl?
338            if (attr != null && attr.getStringValues().length > 0) {
339
340                // Converting the url string to
341                // application/x-www-form-urlencoded as expected by
342                // LDAPUrl constructor.
343                url = LDAPUrl.valueOf(URLEncDec.encodeLDAPUrl(attr.getStringValues()[0]));
344            }
345        } catch (LocalizedIllegalArgumentException ex) {
346            debug.error("DynamicGroup.setSearchFilter : Exception : " + ex.getMessage());
347            throw new IllegalArgumentException(ex.getMessage());
348        }
349        return url;
350    }
351
352    /**
353     * Sets the native LDAP URL used to evaluate this dynamic group.
354     * 
355     * @param url LDAP URL for evaluating members of the group search scope in
356     *        the url has to be <code>LDAPv2.SCOPE_ONE</code> or
357     *        <code>LDAPv2.SCOPE_SUB</code>.
358     */
359    protected void setUrl(LDAPUrl url) {
360        String ldapurl = url.toString();
361        if (SearchScope.SINGLE_LEVEL.equals(url.getScope()) && SearchScope.WHOLE_SUBTREE.equals(url.getScope())) {
362            String msg = i18n.getString(IUMSConstants.ILLEGAL_GROUP_SCOPE);
363            throw new IllegalArgumentException(msg);
364        }
365        // TODO: Need to support multiple values of memberUrl? If so, do
366        // an ADD instead of a replace.
367        modify(new Attr(MEMBER_URL_NAME, ldapurl), ModificationType.REPLACE);
368        // modify( new Attr( MEMBER_URL_NAME, url.toString() ), ModSet.ADD );
369    }
370
371    /**
372     * Returns the members of the group.
373     * 
374     * @param attributes Attributes to return.
375     * @return Iterator for unique identifiers for members of the group.
376     * @exception UMSException if fail to search.
377     */
378    protected SearchResults getMemberIDs(String[] attributes)
379            throws UMSException {
380        return DataLayer.getInstance().search(getPrincipal(), getSearchBase(),
381                getSearchScope(), getSearchFilter(), attributes, false, null);
382    }
383
384    /**
385     * Returns the members of the group.
386     * 
387     * @return Iterator for unique identifiers for members of the group.
388     * @exception UMSException if fail to search.
389     *
390     * @supported.api
391     */
392    public SearchResults getMemberIDs() throws UMSException {
393        String[] attributesToGet = { "objectclass" };
394        return getMemberIDs(attributesToGet);
395    }
396
397    /**
398     * Returns the member count.
399     * 
400     * @return Number of members of the group.
401     * @exception UMSException if fail to search.
402     *
403     * @supported.api
404     */
405    public int getMemberCount() throws UMSException {
406        int count = 0;
407        String[] attributesToGet = { "dn" };
408        SearchResults searchResults = getMemberIDs(attributesToGet);
409        while (searchResults.hasMoreElements()) {
410            searchResults.next().getDN();
411            count++;
412        }
413        return count;
414    }
415
416    /**
417     * Returns a member given an index (zero-based).
418     * 
419     * @param index Zero-based index into the group container.
420     * @return Unique identifier for a member.
421     * @exception UMSException if fail to search.
422     *
423     * @supported.api
424     */
425    public Guid getMemberIDAt(int index) throws UMSException {
426        if (index < 0) {
427            throw new IllegalArgumentException(Integer.toString(index));
428        }
429        String filter = getSearchFilter();
430        if (filter == null) {
431            return null;
432        }
433        String[] attributesToGet = { "dn" };
434        SearchResults searchResults = getMemberIDs(attributesToGet);
435        while (searchResults.hasMoreElements()) {
436            String s = searchResults.next().getDN();
437            if (index == 0) {
438                searchResults.abandon();
439                return new Guid(s);
440            }
441            index--;
442        }
443        throw new ArrayIndexOutOfBoundsException(Integer.toString(index));
444    }
445
446    /**
447     * Returns <code>true</code> if a given identifier is a member of the
448     * group.
449     * 
450     * @param guid Identity of member to be checked for membership.
451     * @return <code>true</code> if it is a member.
452     * @exception UMSException if fail to evaluate group
453     *
454     * @supported.api
455     */
456    public boolean hasMember(Guid guid) throws UMSException {
457        String filter = getSearchFilter();
458        if (filter == null) {
459            return false;
460        }
461        // Narrow the filter by using the RDN of the target
462        // TODO: Should not have to use a DN here
463        String dn = guid.getDn();
464        String rdn = DN.valueOf(dn).rdn().toString();
465        filter = "(&" + filter + "(" + rdn + "))";
466        String[] attributesToGet = { "dn" };
467        SearchResults searchResults = DataLayer.getInstance().search(
468                getPrincipal(), getSearchBase(), getSearchScope(), filter,
469                attributesToGet, false, null);
470        while (searchResults.hasMoreElements()) {
471            String s = searchResults.next().getDN();
472            if (Guid.equals(s, dn)) {
473                searchResults.abandon();
474                return true;
475            }
476        }
477        return false;
478    }
479
480    private static final String MEMBER_URL_NAME = "memberurl";
481
482    private static final Class _class = com.iplanet.ums.DynamicGroup.class;
483}