////
  The contents of this file are subject to the terms of the Common Development and
  Distribution License (the License). You may not use this file except in compliance with the
  License.
 
  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
  specific language governing permission and limitations under the License.
 
  When distributing Covered Software, include this CDDL Header Notice in each file and include
  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
  Header, with the fields enclosed by brackets [] replaced by your own identifying
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2024 3A Systems LLC.
////

:figure-caption!:
:example-caption!:
:table-caption!:


[#chap-virtual-attrs-collective-attrs]
== Working With Virtual and Collective Attributes

OpenDJ supports virtual attributes with dynamically generated values. Virtual attributes are used by the server. You can also define your own. OpenDJ also supports standard collective attributes as described in link:http://tools.ietf.org/html/rfc3671[RFC 3671, window=\_top], allowing entries to share common, read-only attribute values.

In this chapter you will learn how to define virtual and collective attributes.

[#virtual-attributes]
=== Virtual Attributes

Virtual attributes augment directory entries with attribute values that OpenDJ directory server computes or obtains dynamically. Virtual attribute values do not exist in persistent storage. They help to limit the amount of data that needs to be stored and are great for some uses, such as determining the groups a users belongs to or adding an ETag to an entry.

Do not index virtual attributes. Virtual attribute values generated by the server when they are read. They are not designed to be stored in a persistent index.

Since you do not index virtual attributes, searching on a virtual attribute can result in an unindexed search. For an unindexed search OpenDJ directory server potentially has to go through all entries to look for candidate matches. Looking through all entries is resource-intensive for large directories. By default, OpenDJ directory server allows only the Directory Manager superuser to perform unindexed searches. Generally avoid searches that use a simple filter with a virtual attribute. Instead, consider the alternatives. You can assign a password policy to a group as described in xref:../admin-guide/chap-pwd-policy.adoc#assign-pwp-to-group["To Assign a Password Policy to a Group"] in the __Administration Guide__. The procedure uses a virtual attribute only in a subtree specification filter. If you must use a virtual attribute in a search filter, use it in a complex search filter after narrowing the search by filtering on an indexed attribute. For example, the following filter first narrows the search based on the user's ID before checking group membership. Make sure that the user performing the search has access to read `isMemberOf` in the results:

[source]
----
(&(uid=user-id)(isMemberOf=group-dn))
----
Two virtual attributes, `entryDN` and `isMemberOf`, can also be used in simple equality filters. The following example shows how to add access to read `isMemberOf` and then run a search that returns the common names for members of a group:

[source, console]
----
$ ldapmodify \
 --hostname opendj.example.com \
 --port 1389 \
 --bindDN "cn=Directory Manager" \
 --bindPassword password
dn: dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="isMemberOf")(version 3.0;
  acl "See isMemberOf"; allow (read,search,compare) groupdn=
  "ldap:///cn=Directory Administrators,ou=Groups,dc=example,dc=com";)

Processing MODIFY request for dc=example,dc=com
MODIFY operation successful for DN dc=example,dc=com
$ ldapsearch \
 --hostname opendj.example.com \
 --port 1389 \
 --baseDN dc=example,dc=com \
 --bindDN uid=kvaughan,ou=people,dc=example,dc=com \
 --bindPassword bribery \
 "(isMemberOf=cn=Directory Administrators,ou=Groups,dc=example,dc=com)" \
 cn
dn: uid=hmiller,ou=People,dc=example,dc=com
cn: Harry Miller

dn: uid=kvaughan,ou=People,dc=example,dc=com
cn: Kirsten Vaughan

dn: uid=rdaugherty,ou=People,dc=example,dc=com
cn: Robert Daugherty
----
OpenDJ defines the following virtual attributes by default:
--

`entryDN`::
The value is the DN of the entry.

`entryUUID`::
Provides a universally unique identifier for the entry.

`etag`::
Entity tag as defined in link:http://tools.ietf.org/html/rfc2616#section-3.11[RFC 2616, window=\_blank], useful for checking whether an entry has changed since you last read it from the directory.

`hasSubordinates`::
Boolean. Indicates whether the entry has children.

`numSubordinates`::
Provides the number of direct child entries.

`isMemberOf`::
Identifies groups the entry belongs to.

+
By default OpenDJ generates `isMemberOf` on user entries (entries that have the object class `person`), and on group entries (entries that have the object class `groupOfNames`, `groupOfUniqueNames`, or `groupOfEntries`). You can change this by editing the filter property of the `isMemberOf` virtual attribute configuration.

`member`::
Generated for virtual static groups.

`uniqueMember`::
Generated for virtual static groups.

`pwdPolicySubentry`::
Identifies the password policy that applies to the entry.

+
By default, OpenDJ directory server assigns __root DN__ users the password policy with DN `cn=Root Password Policy,cn=Password Policies,cn=config`, and regular users the password policy with DN `cn=Default Password Policy,cn=Password Policies,cn=config`. See xref:../admin-guide/chap-pwd-policy.adoc#chap-pwd-policy["Configuring Password Policy"] in the __Administration Guide__ for information on configuring and assigning password policies.

+
The default global access control instructions prevent this operational attribute from being visible to normal users.

`subschemaSubentry`::
References the schema definitions.

`collectiveAttributeSubentries`::
References applicable collective attribute definitions.

`governingStructureRule`::
References the rule on what type of subordinates the entry can have.

`structuralObjectClass`::
References the structural object class for the entry.

--
These virtual attributes are typically operational, so you get them back from a search only when you request them:

[source, console]
----
$ ldapsearch --port 1389 --baseDN dc=example,dc=com dc=example
dn: dc=example,dc=com
dc: example
objectClass: domain
objectClass: top

$ ldapsearch --port 1389 --baseDN dc=example,dc=com dc=example numSubordinates
dn: dc=example,dc=com
numSubordinates: 12
----
You can use the existing virtual attribute types to create your own virtual attributes, and you can also use the `user-defined` type to create your own virtual attribute types. The virtual attribute is defined by the server configuration, which is not replicated:

[source, console]
----
$ dsconfig \
 create-virtual-attribute \
 --hostname opendj.example.com \
 --port 4444 \
 --bindDN "cn=Directory Manager" \
 --bindPassword password \
 --name "Served By Description" \
 --type user-defined \
 --set enabled:true \
 --set attribute-type:description \
 --set base-dn:dc=example,dc=com \
 --set value:"Served by OpenDJ.Example.com" \
 --trustAll \
 --no-prompt

$ ldapsearch --port 1389 --baseDN dc=example,dc=com uid=bjensen description
dn: uid=bjensen,ou=People,dc=example,dc=com
description: Served by OpenDJ.Example.com
----
Collective attributes cover many use cases better than virtual attributes.


[#collective-attributes]
=== Collective Attributes

Collective attributes provide a standard mechanism for defining attributes that appear on all the entries in a subtree potentially filtered by object class. Standard collective attribute type names have the prefix `c-`.

OpenDJ extends collective attributes to make them easier to use. You can define any OpenDJ attribute as collective using the `;collective` attribute option. You can use LDAP filters in your subtree specification for fine-grained control over which entries have the collective attributes.

You can have entries inherit attributes from other entries through collective attributes. You establish the relationship between entries either by indicating the attribute holding the DN of the entry from which to inherit the attributes, or by specifying how to construct the RDN of the entry from which to inherit the attributes.
xref:../admin-guide/chap-privileges-acis.adoc#change-group-privileges["To Add Privileges For a Group of Administrators"] in the __Administration Guide__ demonstrates setting administrative privileges in OpenDJ using collective attributes. The following examples demonstrate additional ways to use collective attributes:

* xref:#example-collective-attrs-cos["Class of Service With Collective Attributes"]

* xref:#example-dept-from-manager["Inheriting an Attribute From the Manager's Entry"]

* xref:#example-inherit-from-locality["Inheriting Attributes From the Locality"]


[#example-collective-attrs-cos]
.Class of Service With Collective Attributes
====
This example defines attributes that specify services available to a user depending on their service level.

[NOTE]
======
The following example depends on the `cos` object class, and the `classOfService` attribute type defined but commented out in the link:../resources/Example.ldif[Example.ldif, window=\_blank] file imported as sample data. To try this example for yourself, add the attribute type and object class definitions in comments near the top of the file, and then uncomment the `objectClass: cos` and `classOfService` attribute lines in `Example.ldif` before importing the data into OpenDJ.
======
This example positions collective attributes that depend on the `classOfService` attribute values:

* For entries with `classOfService: bronze`, `mailQuota` is set to 1 GB, and `diskQuota` is set to 10 GB.

* For entries with `classOfService: silver`, `mailQuota` is set to 5 GB, and `diskQuota` is set to 50 GB.

* For entries with `classOfService: gold`, `mailQuota` is set to 10 GB, and `diskQuota` is set to 100 GB.

You define collective attributes in the user data using a subentry. In other words, collective attributes can be replicated. Collective attributes use attributes defined in the directory schema. First, add the `mailQuote` and `diskQuota` attributes, and adjust the definition of the `cos` object class to allow the two quota attributes:

[source, console]
----
$ cat quotas.ldif
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( example-class-of-service-attribute-type NAME 'classOfService
 ' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnore
 SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE USAGE user
 Applications X-ORIGIN 'OpenDJ Documentation Examples' )
-
add: attributeTypes
attributeTypes: ( example-class-of-service-disk-quota NAME 'diskQuota
 ' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR case
 IgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE user
 Applications X-ORIGIN 'OpenDJ Documentation Examples' )
-
add: attributeTypes
attributeTypes: ( example-class-of-service-mail-quota NAME 'mailQuota
 ' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR case
 IgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE user
 Applications X-ORIGIN 'OpenDJ Documentation Examples' )
-
add: objectClasses
objectClasses: ( example-class-of-service-object-class NAME 'cos' SUP top AUX
 ILIARY MAY ( classOfService $ diskQuota $ mailQuota ) X-ORIGIN 'OpenDJ Doc
 umentation Examples' )

$ ldapmodify \
 --port 1389 \
 --bindDN "cn=Directory Manager" \
 --bindPassword password \
 --filename quotas.ldif
Processing MODIFY request for cn=schema
MODIFY operation successful for DN cn=schema
----
Use the following collective attribute definitions to set the quotas depending on class of service:

[source, ldif]
----
# cos.ldif: quotas by class of service
dn: cn=Bronze Class of Service,dc=example,dc=com
objectClass: collectiveAttributeSubentry
objectClass: extensibleObject
objectClass: subentry
objectClass: top
cn: Bronze Class of Service
diskQuota;collective: 10 GB
mailQuota;collective: 1 GB
subtreeSpecification: { base "ou=People", specificationFilter "(classOfService=
 bronze)" }

dn: cn=Silver Class of Service,dc=example,dc=com
objectClass: collectiveAttributeSubentry
objectClass: extensibleObject
objectClass: subentry
objectClass: top
cn: Silver Class of Service
diskQuota;collective: 50 GB
mailQuota;collective: 5 GB
subtreeSpecification: { base "ou=People", specificationFilter "(classOfService=
 silver)" }

dn: cn=Gold Class of Service,dc=example,dc=com
objectClass: collectiveAttributeSubentry
objectClass: extensibleObject
objectClass: subentry
objectClass: top
cn: Gold Class of Service
diskQuota;collective: 100 GB
mailQuota;collective: 10 GB
subtreeSpecification: { base "ou=People", specificationFilter "(classOfService=
 gold)" }
----
You can add the collective attribute subentries by using the `ldapmodify` command:

[source, console]
----
$ ldapmodify \
 --port 1389 \
 --bindDN "cn=Directory Manager" \
 --bindPassword password \
 --defaultAdd \
 --filename cos.ldif
Processing ADD request for cn=Bronze Class of Service,dc=example,dc=com
ADD operation successful for DN cn=Bronze Class of Service,dc=example,dc=com
Processing ADD request for cn=Silver Class of Service,dc=example,dc=com
ADD operation successful for DN cn=Silver Class of Service,dc=example,dc=com
Processing ADD request for cn=Gold Class of Service,dc=example,dc=com
ADD operation successful for DN cn=Gold Class of Service,dc=example,dc=com
----
With the collective attributes defined, you can see the results on user entries:

[source, console]
----
$ ldapsearch \
 --port 1389 \
 --baseDN dc=example,dc=com \
 uid=bjensen \
 classOfService mailQuota diskQuota
dn: uid=bjensen,ou=People,dc=example,dc=com
mailQuota: 1 GB
classOfService: bronze
diskQuota: 10 GB

$ ldapsearch \
 --port 1389 \
 --baseDN dc=example,dc=com \
 uid=kvaughan \
 classOfService mailQuota diskQuota
dn: uid=kvaughan,ou=People,dc=example,dc=com
mailQuota: 5 GB
classOfService: silver
diskQuota: 50 GB

$ ldapsearch \
 --port 1389 \
 --baseDN dc=example,dc=com \
 uid=scarter \
 classOfService mailQuota diskQuota
dn: uid=scarter,ou=People,dc=example,dc=com
mailQuota: 10 GB
classOfService: gold
diskQuota: 100 GB
----
====

[#example-dept-from-manager]
.Inheriting an Attribute From the Manager's Entry
====
This example demonstrates how to instruct OpenDJ to set an employee's department number using the manager's department number. To try the example, first import link:../resources/Example.ldif[Example.ldif, window=\_blank] into OpenDJ in order to load the appropriate sample data.

For this example, the relationship between employee entries and manager entries is based on the manager attributes on employee entries. Each `manager` attribute on an employee's entry specifies the DN of the manager's entry. OpenDJ retrieves the department number from the manager's entry to populate the attribute on the employee's entry.

The collective attribute subentry that specifies the relationship looks like this:

[source, ldif]
----
dn: cn=Inherit Department Number From Manager,dc=example,dc=com
objectClass: top
objectClass: subentry
objectClass: inheritedCollectiveAttributeSubentry
objectClass: inheritedFromDNCollectiveAttributeSubentry
cn: Inherit Department Number From Manager
subtreeSpecification: { base "ou=People" }
inheritFromDNAttribute: manager
inheritAttribute: departmentNumber
----
This entry specifies that users inherit department number from their manager.

As seen in `Example.ldif`, Babs Jensen's manager is Torrey Rigden:

[source, ldif]
----
dn: uid=bjensen,ou=People,dc=example,dc=com
manager: uid=trigden, ou=People, dc=example,dc=com
----
Torrey's department number is 3001:

[source, ldif]
----
dn: uid=trigden,ou=People,dc=example,dc=com
departmentNumber: 3001
----
Babs inherits her department number from Torrey:

[source, console]
----
$ ldapsearch --port 1389 --baseDN dc=example,dc=com uid=bjensen departmentNumber
dn: uid=bjensen,ou=People,dc=example,dc=com
departmentNumber: 3001
----
====

[#example-inherit-from-locality]
.Inheriting Attributes From the Locality
====
This example demonstrates how to instruct OpenDJ to set a user's language preferences and street address based on locality. To try the example, first import link:../resources/Example.ldif[Example.ldif, window=\_blank] into OpenDJ in order to load the appropriate sample data.

For this example, the relationship between entries is based on locality. The collective attribute subentry specifies how to construct the RDN of the object holding the attribute values to inherit:

[source, ldif]
----
dn: cn=Inherit From Locality,dc=example,dc=com
objectClass: top
objectClass: subentry
objectClass: inheritedCollectiveAttributeSubentry
objectClass: inheritedFromRDNCollectiveAttributeSubentry
cn: Inherit From Locality
subtreeSpecification: { base "ou=People" }
inheritFromBaseRDN: ou=Locations
inheritFromRDNAttribute: l
inheritFromRDNType: l
inheritAttribute: preferredLanguage
inheritAttribute: street
collectiveConflictBehavior: real-overrides-virtual
----
This specifies that the RDN of the entry to inherit attributes from is like `l=localityName,ou=Locations`, where __localityName__ is the value of the `l` (`localityName`) attribute on the user's entry.

In other words, if the user's entry has `l: Bristol`, then the RDN of the entry from which to inherit attributes starts with `l=Bristol,ou=Locations`. The actual entry looks like this:

[source, ldif]
----
dn: l=Bristol,ou=Locations,dc=example,dc=com
objectClass: top
objectClass: locality
objectClass: extensibleObject
l: Bristol
street: 60 Queen Square
preferredLanguage: en-gb
----
The subentry also specifies two attributes to inherit for preferred language and street address.

The object class `extensibleObject` is added to allow the entry to take a preferred language.footnote:d0e8763[The object class`extensibleObject`means, "Let me add whatever attributes I want." It is usually better practice to add your own auxiliary object class if you need to decorate an entry with more attributes. The shortcut is taken here as the focus of this example is not schema extension, but instead how to use collective attributes.]

Notice the last line of the collective attribute subentry:

[source]
----
collectiveConflictBehavior: real-overrides-virtual
----
This line indicates that if a collective attribute clashes with a real attribute, the real value takes precedence over the virtual, collective value. You can also set `collectiveConflictBehavior` to `virtual-overrides-real` for the opposite precedence, or to `merge-real-and-virtual` to keep both sets of values.

Here, users can set their own language preferences. When users set language preferences manually, the collective attribute subentry is configured to give the user's settings precedence over the locality-based setting, which is only a default guess.

Sam Carter is located in Bristol. Sam has specified no preferred languages:

[source, ldif]
----
dn: uid=scarter,ou=People,dc=example,dc=com
l: Bristol
----
Sam inherits both the street address and also preferred language from the Bristol locality:

[source, console]
----
$ ldapsearch --port 1389 --baseDN dc=example,dc=com uid=scarter \
 preferredLanguage street
dn: uid=scarter,ou=People,dc=example,dc=com
preferredLanguage: en-gb
street: 60 Queen Square
----
Babs's locality is San Francisco. Babs prefers English, but also knows Korean:

[source, ldif]
----
dn: uid=bjensen,ou=People,dc=example,dc=com
preferredLanguage: en, ko;q=0.8
l: San Francisco
----
Babs inherits the street address from the San Francisco locality, but keeps her language preferences:

[source, console]
----
$ ldapsearch --port 1389 --baseDN dc=example,dc=com uid=bjensen \
 preferredLanguage street
dn: uid=bjensen,ou=People,dc=example,dc=com
preferredLanguage: en, ko;q=0.8
street: 500 3rd Street
----
====


