Creating an Active Directory multi-forest ‘Global Catalog’ using Penrose Virtual Directory

Penrose is a neat LDAP Virtual Directory that can make almost any combination of databases and other LDAP servers appear to be a single directory, for applications that don’t support communicating with more than one LDAP server. This includes doing things like passing through bind requests and updates, in addition to just searches.
Although Kerberos works pretty seamlessly between forests (if there’s a trust in place), LDAP can be a bit more complicated. Especially if your application doesn’t support following referrals. One useful but quite simple thing you can do is make several Active Directory forests appear to be a single LDAP server (for certain types of applications) by merging their Global Catalogs together. Here’s an example for a forest ‘vc.example.com’ that has the child domain ‘uk.vc.example.com’, joined to another forest ‘xx.example.com’ that only has a single domain. For every search Penrose will query both forest GCs and return all the results in a single response. With this configuration, the following types of queries work correctly:

Looking up a user based on userPrincipalName (or any other attribute), that exists in any domain:

imac:/Users/chrisl/ 1$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(userPrincipalName=harryp@vc.example.com)' cn
dn: CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com
cn: Harry Potter

imac:/Users/chrisl/ 2$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(userPrincipalName=tomr@uk.vc.example.com)' cn
dn: CN=Tom Riddle,OU=People,DC=uk,DC=vc,DC=example,DC=com
cn: Tom Riddle

imac:/Users/chrisl/ 3$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(userPrincipalName=lunal@xx.example.com)' cn
dn: CN=Luna Lovegood,OU=People,DC=xx,DC=example,DC=com
cn: Luna Lovegood

Looking up multiple users across forests that match a particular attribute. Note users pulled from vc.example.com, uk.vc.example.com and xx.example.com:

imac:/Users/chrisl/ 4$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(mail=*.example.com)' cn
dn: CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com
cn: Harry Potter

dn: CN=Ron Weasley,OU=People,DC=vc,DC=example,DC=com
cn: Ron Weasley

dn: CN=Hermione Granger,OU=People,DC=vc,DC=example,DC=com
cn: Hermione Granger

dn: CN=Tom Riddle,OU=People,DC=uk,DC=vc,DC=example,DC=com
cn: Tom Riddle

dn: CN=Luna Lovegood,OU=People,DC=xx,DC=example,DC=com
cn: Luna Lovegood

Looking up a list of groups to be used by an application, based on those groups being a member of a ‘service group’:

imac:/Users/chrisl/ 5$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(&(objectclass=group)(|(memberof=CN=Penrose Groups,OU=People,DC=xx,DC=example,DC=com)(memberof=CN=Penrose Groups,OU=People,DC=vc,DC=example,DC=com)))' cn
dn: CN=Sales,OU=People,DC=vc,DC=example,DC=com
cn: Sales

dn: CN=Engineering,OU=People,DC=vc,DC=example,DC=com
cn: Engineering

dn: CN=Marketing,OU=People,DC=vc,DC=example,DC=com
cn: Marketing

dn: CN=Engineering XX,OU=People,DC=xx,DC=example,DC=com
cn: Engineering XX

Performing LDAP authentication from an email address and password, using the LDAP search+bind method. Penrose automatically passes through the credentials:

imac:/Users/chrisl/ 6$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(mail=harryp@vc.example.com)' dn
dn: CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com

imac:/Users/chrisl/ 7$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D 'CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com' -w hpotter -b 'CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com' cn
dn: CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com
cn: Harry Potter

Special queries with bitwise filters work too, for example recursively finding all groups a user is a member of:

imac:/Users/chrisl/ 8$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(member:1.2.840.113556.1.4.1941:=CN=Hermione Granger,OU=People,DC=vc,DC=example,DC=com)' cn
dn: CN=Terminal Services Users,CN=Users,DC=vc,DC=example,DC=com
cn: Terminal Services Users

dn: CN=Sales,OU=People,DC=vc,DC=example,DC=com
cn: Sales

dn: CN=Marketing,OU=People,DC=vc,DC=example,DC=com
cn: Marketing

dn: CN=Penrose Groups,OU=People,DC=vc,DC=example,DC=com
cn: Penrose Groups

And finally, ambiguous name resolution searches:

imac:/Users/chrisl/ 9$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(anr=Har)' cn
dn: CN=Harry Potter,OU=People,DC=vc,DC=example,DC=com
cn: Harry Potter

imac:/Users/chrisl/ 10$ ldapsearch -LLL -x -h dev5.example.com -p 10389 -D cn=penrose,dc=example,dc=com -w penrose -b dc=example,dc=com '(anr=Lun)' cn
dn: CN=Luna Lovegood,OU=People,DC=xx,DC=example,DC=com
cn: Luna Lovegood

Here are the three relevant configuration files, with comments.

connections.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE connections PUBLIC "-//Penrose/DTD Connections 2.0//EN" "http://penrose.safehaus.org/dtd/connections.dtd">
<connections>

<!-- Connection definition for vc.example.com forest -->
  <connection name="vc">
    <adapter-name>LDAP</adapter-name>
<!-- Using the Global Catalog server for the 'vc.example.com' domain -->
    <parameter>
      <param-name>java.naming.provider.url</param-name>
      <param-value>ldap://dc1.vc.example.com:3268/</param-value>
    </parameter>
<!-- Service user for vc.example.com -->
    <parameter>
      <param-name>java.naming.security.principal</param-name>
      <param-value>cn=penrose service user,ou=service users,dc=vc,dc=example,dc=com</param-value>
    </parameter>
<!-- Service user's password -->
    <parameter>
      <param-name>java.naming.security.credentials</param-name>
      <param-value>penrose</param-value>
    </parameter>
<!-- Connection pool details -->
    <parameter>
      <param-name>minEvictableIdleTimeMillis</param-name>
      <param-value>300000</param-value>
    </parameter>
    <parameter>
      <param-name>timeBetweenEvictionRunsMillis</param-name>
      <param-value>60000</param-value>
    </parameter>
    <parameter>
      <param-name>minIdle</param-name>
      <param-value>0</param-value>
    </parameter>
<!-- 'Grow' works best for the pool -->
    <parameter>
      <param-name>whenExhaustedAction</param-name>
      <param-value>grow</param-value>
    </parameter>
<!-- Set the LDAP page size. Penrose doesn't support paging on queries, -->
<!-- but it does support querying other directories using paging -->
    <parameter>
      <param-name>pageSize</param-name>
      <param-value>500</param-value>
    </parameter>
  </connection>

<!-- Connection definition for xx.example.com forest -->
  <connection name="xx">
    <adapter-name>LDAP</adapter-name>
    <parameter>
      <param-name>java.naming.provider.url</param-name>
      <param-value>ldap://dc4.xx.example.com:3268/</param-value>
    </parameter>
    <parameter>
      <param-name>java.naming.security.principal</param-name>
      <param-value>cn=penrose service user,ou=service users,dc=xx,dc=example,dc=com</param-value>
    </parameter>
    <parameter>
      <param-name>java.naming.security.credentials</param-name>
      <param-value>penrose</param-value>
    </parameter>
    <parameter>
      <param-name>minEvictableIdleTimeMillis</param-name>
      <param-value>300000</param-value>
    </parameter>
    <parameter>
      <param-name>timeBetweenEvictionRunsMillis</param-name>
      <param-value>60000</param-value>
    </parameter>
    <parameter>
      <param-name>minIdle</param-name>
      <param-value>0</param-value>
    </parameter>
    <parameter>
      <param-name>whenExhaustedAction</param-name>
      <param-value>grow</param-value>
    </parameter>
    <parameter>
      <param-name>pageSize</param-name>
      <param-value>500</param-value>
    </parameter>
  </connection>

</connections>

sources.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sources PUBLIC "-//Penrose/DTD Sources 2.0//EN" "http://penrose.safehaus.org/dtd/sources.dtd">
<sources>

<!-- Source definition for vc.example.com forest -->
  <source name="vc">
    <connection-name>vc</connection-name>
<!-- Use 'example.com' as the baseDn, this is the root of all the featured domains -->
    <parameter>
      <param-name>baseDn</param-name>
      <param-value>dc=example,dc=com</param-value>
    </parameter>
  </source>

<!-- Source definition for xx.example.com forest -->
  <source name="xx">
    <connection-name>xx</connection-name>
    <parameter>
      <param-name>baseDn</param-name>
      <param-value>dc=example,dc=com</param-value>
    </parameter>
  </source>

</sources>

directory.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE directory PUBLIC "-//Penrose/DTD Directory 2.0//EN" "http://penrose.safehaus.org/dtd/directory.dtd">
<directory>

<!-- Entry to be used for authenticating to Penrose to query the directory -->
  <entry dn="cn=penrose,dc=example,dc=com">
    <oc>person</oc>
    <at name="cn" rdn="true"> <constant>penrose</constant> </at>
    <at name="userPassword"> <constant>penrose</constant> </at>
  </entry>

<!-- Dummy root entry for the example.com domain that encloses the forest domains -->
  <entry dn="dc=example,dc=com">
    <oc>dcObject</oc>
    <oc>organization</oc>
    <at name="dc" rdn="true"> <constant>example</constant> </at>
    <at name="o"> <constant>example</constant> </at>
    <aci> <permission>rs</permission> </aci>
  </entry>

<!-- Proxy entry for the 'vc.example.com' forest root (and its child domains) -->
  <entry dn="dc=vc,dc=example,dc=com">
    <entry-class>org.safehaus.penrose.directory.ProxyEntry</entry-class>
    <source> <source-name>vc</source-name> </source>
    <aci> <permission>rs</permission> </aci>
  </entry>

<!-- Proxy entry for the 'xx.example.com' forest root -->
  <entry dn="dc=xx,dc=example,dc=com">
    <entry-class>org.safehaus.penrose.directory.ProxyEntry</entry-class>
    <source> <source-name>xx</source-name> </source>
    <aci> <permission>rs</permission> </aci>
  </entry>

</directory>

Comments

  • This is a great example, but driving me nuts as I can’t get it to work in my environment.
    I have multiple forests in AD. The primary is dc=companya,dc=local The secondary is dc=companyb,dc=local

    I tried to modify your example so I’d end up with ou=companya,dc=companies,dc=local and ou=companyb,dc=companies,dc=local – but I’m not getting anywhere. Is that possible with your config?

  • The config does merge two forests, but it might not do it 100% into the structure that you have there. I’m confident that Penrose *can* do what you want, but its configuration mechanism can be a bit obscure.

  • Leave a comment