<?xml version="1.0" encoding="UTF-8"?>
<!--
! CCPL HEADER START
!
! This work is licensed under the Creative Commons
! Attribution-NonCommercial-NoDerivs 3.0 Unported License.
! To view a copy of this license, visit
! or send a letter to Creative Commons, 444 Castro Street,
! Suite 900, Mountain View, California, 94041, USA.
!
! You can also obtain a copy of the license at
! See the License for the specific language governing permissions
! and limitations under the License.
!
! If applicable, add the following below this CCPL HEADER, with the fields
! enclosed by brackets "[]" replaced with your own identifying information:
! Portions Copyright [yyyy] [name of copyright owner]
!
! CCPL HEADER END
!
! Copyright 2011-2014 ForgeRock AS
!
-->
<chapter xml:id='chap-groups'
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xsi:schemaLocation='http://docbook.org/ns/docbook
xmlns:xlink='http://www.w3.org/1999/xlink'>
<title>Working With Groups of Entries</title>
<para>OpenDJ supports several methods of grouping entries in the directory.
Static groups list their members, whereas dynamic groups look up their
membership based on an LDAP filter. OpenDJ also supports virtual static
groups, which uses a dynamic group style definition, but allows applications
to list group members as if the group were static.</para>
<para>When listing entries in static groups, you must also have a mechanism
for removing entries from the list when they are deleted or modified in ways
that end their membership. OpenDJ makes that possible with
<emphasis>referential integrity</emphasis> functionality.</para>
<para>This chapter demonstrates how to work with groups.</para>
<tip>
<para>The examples in this chapter assume that an
<literal>ou=Groups,dc=example,dc=com</literal> entry already exists. If you
generated data during setup and did not create an organizational unit for
groups yet, create the entry before you try the examples.</para>
<screen>
$ <userinput>ldapmodify \
--defaultAdd \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password
dn: ou=Groups,dc=example,dc=com
objectClass: organizationalunit
objectClass: top
ou: Groups
</userinput>
<computeroutput>Processing ADD request for ou=Groups,dc=example,dc=com
ADD operation successful for DN ou=Groups,dc=example,dc=com</computeroutput>
</screen>
</tip>
<section xml:id="static-groups">
<title>Creating Static Groups</title>
<indexterm>
<primary>Groups</primary>
<secondary>Static</secondary>
</indexterm>
<para>A <firstterm>static group</firstterm> is expressed as an entry
that enumerates all the entries that belong to the group. Static group
entries grow as their membership increases.</para>
<para>Static group entries can take the standard object class
<literal>groupOfNames</literal> where each <literal>member</literal>
attribute value is a distinguished name of an entry, or
<literal>groupOfUniqueNames</literal> where each
<literal>uniqueMember</literal> attribute value has Name and Optional UID
syntax.<footnote><para>Name and Optional UID syntax values are a DN optionally
followed by <literal>#<replaceable>BitString</replaceable></literal>. The
<replaceable>BitString</replaceable>, such as <literal>'0101111101'B</literal>,
serves to distinguish the entry from another entry having the same DN, which
can occur when the original entry was deleted and a new entry created with the
same DN.</para></footnote> Like other LDAP attributes,
<literal>member</literal> and <literal>uniqueMember</literal> attributes take
sets of unique values.</para>
<para>Static group entries can also have the object class
<literal>groupOfEntries</literal>, which is like
<literal>groupOfNames</literal> except that it is designed to allow
groups not to have members.</para>
<para>When creating a group entry, use <literal>groupOfNames</literal> or
<literal>groupOfEntries</literal> where possible.</para>
<para>To create a static group, add a group entry such as the following
to the directory.</para>
<screen>
<computeroutput>dn: cn=My Static Group,ou=Groups,dc=example,dc=com
cn: My Static Group
objectClass: groupOfNames
objectClass: top
ou: Groups
member: uid=ahunter,ou=People,dc=example,dc=com
member: uid=bjensen,ou=People,dc=example,dc=com
member: uid=tmorris,ou=People,dc=example,dc=com
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--defaultAdd \
--filename static.ldif</userinput>
<computeroutput>Processing ADD request for cn=My Static Group,ou=Groups,dc=example,dc=com
ADD operation successful for DN cn=My Static Group,ou=Groups,dc=example,dc=com</computeroutput>
</screen>
<para>To change group membership, modify the values of the membership
attribute.</para>
<screen>
<computeroutput>dn: cn=My Static Group,ou=Groups,dc=example,dc=com
changetype: modify
add: member
member: uid=scarter,ou=People,dc=example,dc=com
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--filename add2grp.ldif</userinput>
<computeroutput>Processing MODIFY request for cn=My Static Group,ou=Groups,dc=example,dc=com
MODIFY operation successful for DN
cn=My Static Group,ou=Groups,dc=example,dc=com
</computeroutput>
$ <userinput>ldapsearch \
--port 1389 \
--baseDN dc=example,dc=com \
"(cn=My Static Group)"</userinput>
<computeroutput>dn: cn=My Static Group,ou=Groups,dc=example,dc=com
ou: Groups
objectClass: groupOfNames
objectClass: top
member: uid=ahunter,ou=People,dc=example,dc=com
member: uid=bjensen,ou=People,dc=example,dc=com
member: uid=tmorris,ou=People,dc=example,dc=com
member: uid=scarter,ou=People,dc=example,dc=com
cn: My Static Group</computeroutput>
</screen>
<para>RFC 4519 says a <literal>groupOfNames</literal> entry must have
at least one member. Although OpenDJ allows you to create a
<literal>groupOfNames</literal> without members, strictly speaking that
behavior is not standard. Alternatively, you can use the
<literal>groupOfEntries</literal> object class as shown in the following
example.</para>
<screen>
<computeroutput>dn: cn=Initially Empty Static Group,ou=Groups,dc=example,dc=com
cn: Initially Empty Static Group
objectClass: groupOfEntries
objectClass: top
ou: Groups
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--defaultAdd \
--filename group-of-entries.ldif</userinput>
<computeroutput>Processing ADD request for
cn=Initially Empty Static Group,ou=Groups,dc=example,dc=com
ADD operation successful for DN
cn=Initially Empty Static Group,ou=Groups,dc=example,dc=com
</computeroutput>
<computeroutput># Now add some members to the group.
dn: cn=Initially Empty Static Group,ou=Groups,dc=example,dc=com
changetype: modify
add: member
member: uid=ahunter,ou=People,dc=example,dc=com
member: uid=bjensen,ou=People,dc=example,dc=com
member: uid=tmorris,ou=People,dc=example,dc=com
member: uid=scarter,ou=People,dc=example,dc=com
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--filename add-members.ldif</userinput>
<computeroutput>Processing MODIFY request for
cn=Initially Empty Static Group,ou=Groups,dc=example,dc=com
MODIFY operation successful for DN
cn=Initially Empty Static Group,ou=Groups,dc=example,dc=com</computeroutput>
</screen>
</section>
<section xml:id="dynamic-groups">
<title>Creating Dynamic Groups</title>
<indexterm>
<primary>Groups</primary>
<secondary>Dynamic</secondary>
</indexterm>
<para>A <firstterm>dynamic group</firstterm> specifies members using
LDAP URLs. Dynamic groups entries can stay small even as their
membership increases.</para>
<para>Dynamic group entries take the <literal>groupOfURLs</literal>
object class, with one or more <literal>memberURL</literal> values
specifying LDAP URLs to identify group members.</para>
<para>To create a dynamic group, add a group entry such as the following to
the directory.</para>
<para>The following example builds a dynamic group of entries effectively
matching the filter <literal>"(l=Cupertino)"</literal> (users whose location
is Cupertino). Change the filter if your data is different, and so no
entries have <literal>l: Cupertino</literal>.</para>
<screen>
<computeroutput>dn: cn=My Dynamic Group,ou=Groups,dc=example,dc=com
cn: My Dynamic Group
objectClass: top
objectClass: groupOfURLs
ou: Groups
memberURL: ldap:///ou=People,dc=example,dc=com??sub?l=Cupertino
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--defaultAdd \
--filename dynamic.ldif</userinput>
<computeroutput>Processing ADD request for cn=My Dynamic Group,ou=Groups,dc=example,dc=com
ADD operation successful for DN cn=My Dynamic Group,ou=Groups,dc=example,dc=com</computeroutput>
</screen>
<para>Group membership changes dynamically as entries change to match the
<literal>memberURL</literal> values.</para>
<screen width="81">
$ <userinput>ldapsearch \
--port 1389 \
--baseDN dc=example,dc=com \
"(&(uid=*jensen)(isMemberOf=cn=My Dynamic Group,ou=Groups,dc=example,dc=com))" \
mail</userinput>
<computeroutput>dn: uid=bjensen,ou=People,dc=example,dc=com
mail: bjensen@example.com
dn: uid=rjensen,ou=People,dc=example,dc=com
mail: rjensen@example.com
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password</userinput>
<computeroutput>dn: uid=ajensen,ou=People,dc=example,dc=com
changetype: modify
replace: l
l: Cupertino
Processing MODIFY request for uid=ajensen,ou=People,dc=example,dc=com
MODIFY operation successful for DN uid=ajensen,ou=People,dc=example,dc=com</computeroutput>
<userinput>^D</userinput>
$ <userinput>ldapsearch \
--port 1389 \
--baseDN dc=example,dc=com \
"(&(uid=*jensen)(isMemberOf=cn=My Dynamic Group,ou=Groups,dc=example,dc=com))" \
mail</userinput>
<computeroutput>dn: uid=ajensen,ou=People,dc=example,dc=com
mail: ajensen@example.com
dn: uid=bjensen,ou=People,dc=example,dc=com
mail: bjensen@example.com
dn: uid=rjensen,ou=People,dc=example,dc=com
mail: rjensen@example.com</computeroutput>
</screen>
</section>
<section xml:id="virtual-static-groups">
<title>Creating Virtual Static Groups</title>
<indexterm>
<primary>Groups</primary>
<secondary>Virtual static</secondary>
</indexterm>
<para>OpenDJ lets you create <firstterm>virtual static groups</firstterm>,
which let applications see dynamic groups as what appear to be static
groups.</para>
<para>The virtual static group takes auxiliary object class
<literal>ds-virtual-static-group</literal>. Virtual static groups also take
either the object class <literal>groupOfNames</literal>, or
<literal>groupOfUniqueNames</literal>, but instead of having
<literal>member</literal> or <literal>uniqueMember</literal> attributes,
have <literal>ds-target-group-dn</literal> attributes pointing to other
groups.</para>
<para>Generating the list of members can be resource intensive for large
groups, so by default you cannot retrieve the list of members. You can
change this with the <command>dsconfig</command> command by setting the
<literal>Virtual Static member</literal> or
<literal>Virtual Static uniqueMember</literal> property.</para>
<screen>
$ <userinput>dsconfig \
set-virtual-attribute-prop \
--port 4444 \
--hostname opendj.example.com \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--name "Virtual Static member" \
--set allow-retrieving-membership:true \
--trustAll \
--no-prompt</userinput>
</screen>
<para>The following example creates a virtual static group, and reads the
group entry with all members.</para>
<screen>
<computeroutput>dn: cn=Virtual Static,ou=Groups,dc=example,dc=com
cn: Virtual Static
objectclass: top
objectclass: groupOfNames
objectclass: ds-virtual-static-group
ds-target-group-dn: cn=My Dynamic Group,ou=Groups,dc=example,dc=com
</computeroutput>
$ <userinput>ldapmodify \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--defaultAdd \
--filename virtual.ldif</userinput>
<computeroutput>Processing ADD request for cn=Virtual Static,ou=Groups,dc=example,dc=com
ADD operation successful for DN cn=Virtual Static,ou=Groups,dc=example,dc=com</computeroutput>
$ <userinput>ldapsearch --port 1389 --baseDN dc=example,dc=com "(cn=Virtual Static)"</userinput>
<computeroutput>dn: cn=Virtual Static,ou=Groups,dc=example,dc=com
objectClass: groupOfNames
objectClass: ds-virtual-static-group
objectClass: top
member: uid=jwalker,ou=People,dc=example,dc=com
member: uid=jmuffly,ou=People,dc=example,dc=com
member: uid=tlabonte,ou=People,dc=example,dc=com
member: uid=dakers,ou=People,dc=example,dc=com
member: uid=jreuter,ou=People,dc=example,dc=com
member: uid=rfisher,ou=People,dc=example,dc=com
member: uid=pshelton,ou=People,dc=example,dc=com
member: uid=rjensen,ou=People,dc=example,dc=com
member: uid=jcampaig,ou=People,dc=example,dc=com
member: uid=mjablons,ou=People,dc=example,dc=com
member: uid=mlangdon,ou=People,dc=example,dc=com
member: uid=aknutson,ou=People,dc=example,dc=com
member: uid=bplante,ou=People,dc=example,dc=com
member: uid=awalker,ou=People,dc=example,dc=com
member: uid=smason,ou=People,dc=example,dc=com
member: uid=ewalker,ou=People,dc=example,dc=com
member: uid=dthorud,ou=People,dc=example,dc=com
member: uid=btalbot,ou=People,dc=example,dc=com
member: uid=tcruse,ou=People,dc=example,dc=com
member: uid=kcarter,ou=People,dc=example,dc=com
member: uid=aworrell,ou=People,dc=example,dc=com
member: uid=bjensen,ou=People,dc=example,dc=com
member: uid=ajensen,ou=People,dc=example,dc=com
member: uid=cwallace,ou=People,dc=example,dc=com
member: uid=mwhite,ou=People,dc=example,dc=com
member: uid=kschmith,ou=People,dc=example,dc=com
member: uid=mtalbot,ou=People,dc=example,dc=com
member: uid=tschmith,ou=People,dc=example,dc=com
member: uid=gfarmer,ou=People,dc=example,dc=com
member: uid=speterso,ou=People,dc=example,dc=com
member: uid=prose,ou=People,dc=example,dc=com
member: uid=jbourke,ou=People,dc=example,dc=com
member: uid=mtyler,ou=People,dc=example,dc=com
member: uid=abergin,ou=People,dc=example,dc=com
member: uid=mschneid,ou=People,dc=example,dc=com
cn: Virtual Static
ds-target-group-dn: cn=My Dynamic Group,ou=Groups,dc=example,dc=com</computeroutput>
</screen>
</section>
<section xml:id="group-membership">
<title>Looking Up Group Membership</title>
<indexterm>
<primary>Groups</primary>
<secondary>Membership</secondary>
</indexterm>
<para>OpenDJ lets you look up which groups a user belongs to by using the
<literal>isMemberOf</literal> attribute.</para>
<screen>
$ <userinput>ldapsearch \
--port 1389 \
--baseDN dc=example,dc=com \
uid=bjensen \
isMemberOf</userinput>
<computeroutput>dn: uid=bjensen,ou=People,dc=example,dc=com
isMemberOf: cn=My Static Group,ou=Groups,dc=example,dc=com
isMemberOf: cn=Virtual Static,ou=Groups,dc=example,dc=com
isMemberOf: cn=My Dynamic Group,ou=Groups,dc=example,dc=com</computeroutput>
</screen>
<para>You must request <literal>isMemberOf</literal> explicitly.</para>
</section>
<section xml:id="referential-integrity">
<title>Configuring Referential Integrity</title>
<indexterm>
<primary>Groups</primary>
<secondary>Referential integrity</secondary>
</indexterm>
<para>When you delete or rename an entry that belongs to static groups, that
entry's DN must be removed or changed in the list of each group to which it
belongs. You can configure OpenDJ to resolve membership on your behalf after
the change operation succeeds by enabling referential integrity.</para>
<para>Referential integrity functionality is implemented as a plugin. The
referential integrity plugin is disabled by default. To enable the plugin,
use the <command>dsconfig</command> command.</para>
<screen>
$ <userinput>dsconfig \
set-plugin-prop \
--port 4444 \
--hostname opendj.example.com \
--bindDN "cn=Directory Manager" \
--bindPassword password \
--plugin-name "Referential Integrity" \
--set enabled:true \
--trustAll \
--no-prompt</userinput>
</screen>
<para>With the plugin enabled, you can see OpenDJ referential integrity
resolving group membership automatically.</para>
<screen>
$ <userinput>ldapsearch --port 1389 --baseDN dc=example,dc=com "(cn=My Static Group)"</userinput>
<computeroutput>dn: cn=My Static Group,ou=Groups,dc=example,dc=com
ou: Groups
objectClass: groupOfNames
objectClass: top
member: uid=ahunter,ou=People,dc=example,dc=com
member: uid=bjensen,ou=People,dc=example,dc=com
member: uid=tmorris,ou=People,dc=example,dc=com
member: uid=scarter,ou=People,dc=example,dc=com
cn: My Static Group
</computeroutput>
$ <userinput>ldapdelete \
--port 1389 \
--bindDN "cn=Directory Manager" \
--bindPassword password \
uid=scarter,ou=People,dc=example,dc=com</userinput>
<computeroutput>Processing DELETE request for uid=scarter,ou=People,dc=example,dc=com
DELETE operation successful for DN uid=scarter,ou=People,dc=example,dc=com</computeroutput>
$ <userinput>ldapsearch --port 1389 --baseDN dc=example,dc=com "(cn=My Static Group)"</userinput>
<computeroutput>dn: cn=My Static Group,ou=Groups,dc=example,dc=com
ou: Groups
objectClass: groupOfNames
objectClass: top
cn: My Static Group
member: uid=ahunter,ou=People,dc=example,dc=com
member: uid=bjensen,ou=People,dc=example,dc=com
member: uid=tmorris,ou=People,dc=example,dc=com</computeroutput>
</screen>
<para>By default the referential integrity plugin is configured to manage
<literal>member</literal> and <literal>uniqueMember</literal> attributes.
These attributes take values that are DNs, and are indexed for equality by
default. Before you add an additional attribute to manage, make sure that
it has DN syntax and that it is indexed for equality. OpenDJ requires that
the attribute be indexed because an unindexed search for integrity would
potentially consume too many of the server's resources. Attribute syntax is
explained in the chapter on <link xlink:href="admin-guide#chap-schema"
>Managing Schema</citetitle></link>. For instructions on indexing attributes,
see the section on <link xlink:href="admin-guide#configure-indexes"
>Configuring & Rebuilding Indexes</citetitle></link>.</para>
<para>You can also configure the referential integrity plugin to check that
new entries added to groups actually exist in the directory by setting the
<literal>check-references</literal> property to <literal>true</literal>. You
can specify additional criteria once you have activated the check. To ensure
that entries added must match a filter, set the
<literal>check-references-filter-criteria</literal> to identify the attribute
and the filter. For example, you can specify that group members must be person
entries by setting <literal>check-references-filter-criteria</literal> to
<literal>member:(objectclass=person)</literal>. To ensure that entries must be
located in the same naming context, set
<literal>check-references-scope-criteria</literal> to
<literal>naming-context</literal>.</para>
</section>
</chapter>