LDAP Persistence Backend for Extbase

Now that you successfully authenticated your Backend and/or Frontend users against your LDAP or Active Directory (see my other article if needed), it’s time to go a step forward and let your authenticated users show, edit and review their own data. You don’t want to get emails each time someone wants to change her email address, do you?

Goal

  • We would like to use Extbase and Fluid to create a basic form allowing Frontend authenticated users to modify their information such as email address or telephone.
  • These pieces of information are stored and retrieved using an LDAP backend.
  • Some of this information may already be mapped to the corresponding fe_user within TYPO3 but we won't use it even when reading.
  • We will describe the process when using the TYPO3 extension  ig_ldap_sso_auth but any other LDAP extension should do the trick as long as the user DN is stored somewhere within the corresponding fe_user record.

Extbase Extension

We won’t describe the whole process here as it should be known. Please note that we used the trunk version of ig_ldap_sso_auth for the searchLdap method but you may of course use the various ldap_* methods right away. The interesting part is that we created our own Person class extending the standard fe_user Extbase class to let us map the mobile phone number as well. The corresponding repository will only need to extend the base repository, that’s all:

class Tx_MyExt_Domain_Model_Person extends Tx_Extbase_Domain_Model_FrontendUser {
    // additional attributes and getter/setter
}
 
class Tx_MyExt_Domain_Repository_PersonRepository extends Tx_Extbase_Persistence_Repository {
    // intentionally left empty
}

Here comes the interesting part. Extbase allows us to change the persistence storage to use using TypoScript configuration. I did it globally with an extension template put on the page containing my LDAP edit form plugin but feel free to narrow it down to your extension solely:

config.tx_extbase {
    features.rewrittenPropertyMapper = 1
    objects.Tx_Extbase_Persistence_Storage_BackendInterface.className
                   = Tx_MyExt_Persistence_Storage_LdapBackend
}

Of course, attributes in LDAP need to be mapped with class fields in Extbase. The power of Extbase allows us to do that using a simple configuration, meaning we won’t have to bother about it when writing our LDAP backend :-)

plugin.tx_myext {
    persistence {
        classes {
            Tx_MyExt_Domain_Model_Person {
                mapping {
                    columns {
                        givenName.mapOnProperty       = firstName
                        sn.mapOnProperty              = lastName
                        cn.mapOnProperty              = name
                        username.mapOnProperty        = username
                        userPassword.mapOnProperty    = password
                        title.mapOnProperty           = title
                        mobile.mapOnProperty          = mobilePhone
                        mail.mapOnProperty            = email
                        o.mapOnProperty               = company
                        street.mapOnProperty          = street
                        telephoneNumber.mapOnProperty = telephone
                        postalCode.mapOnProperty      = zip
                        l.mapOnProperty               = city
                    }
                }
            }
        }
    }
}

You may need to adapt this mapping configuration which is based on OpenLDAP. Attributes returned (or aliases for them) may slightly differ when using other LDAP server implementations. For the sake of completeness, we mapped the attribute “password” in order to let the user change it and thus become it again in the update array when about to store the modified data back to LDAP.

Beware: You should never ever store the user password as-this, as plain text. You will need to check if password is set and hash it accordingly prior to saving the modified information back to LDAP, typically as salted SHA1 (“{SSHA}” as prefix).

So finally, here is the LDAP persistence storage backend for Extbase:

<?php
class Tx_MyExt_Persistence_Storage_LdapBackend
        implements Tx_Extbase_Persistence_Storage_BackendInterface, t3lib_Singleton {
 
    /**
     * Returns the object data matching the $query.
     *
     * @param Tx_Extbase_Persistence_QueryInterface $query
     * @return array
     * @api
     */
    public function getObjectDataByQuery(Tx_Extbase_Persistence_QueryInterface $_query) {
        /** @var $query Tx_Extbase_Persistence_Query */
        $query = $_query;
        $objects = array();
 
        if ($query->getType() === 'Tx_MyExt_Domain_Model_Person') {
            /** @var $constraint Tx_Extbase_Persistence_QOM_Comparison */
            $constraint = $query->getConstraint();
            /** @var $operand1 Tx_Extbase_Persistence_QOM_PropertyValue */
            $operand1 = $constraint->getOperand1();
 
            if ($operand1->getPropertyName() === 'uid' && $constraint->getOperator() == 1) {
                $uid = $constraint->getOperand2();
                $row = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
                    'tx_igldapsoauth_dn',
                    'fe_users',
                    'uid=' . intval($uid) . ' AND hidden=0 AND deleted=0'
                );
                if ($row) {
                        // Get the LDAP DN of the request user
                    $dn = $row['tx_igldapssoauth_dn'];
                    $ldapObject = $this->searchLdap($dn);
                    $object = array('uid' => $uid);
                    foreach ($ldapObject as $key => $values) {
                        if (is_numeric($key)) continue;
                        if ($key === 'uid') $key = 'username';
                        $value = '';
                        if ($values['count'] > 0) {
                            $value = $values[0];
                        }
                        $object[$key] = $value;
                    }
                    $objects[] = $object;
                }
            } else {
                throw new Exception('Cannot deal with property "' . $operand1->getPropertyName()
                    . '"', 1326130220);
            }
        } else {
            throw new Exception('Invalid query type "' . $query->getType() . '"', 1326130250);
        }
 
        return $objects;
    }
 
    /**
     * Adds a row to the storage
     *
     * @param string $tableName The database table name
     * @param array $row The row to insert
     * @param boolean $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
     * @return void
     */
    public function addRow($tableName, array $row, $isRelation = FALSE) {
        throw new Exception('TODO: Implement addRow() method.');
    }
 
    /**
     * Updates a row in the storage
     *
     * @param string $tableName The database table name
     * @param array $row The row to update
     * @param boolean $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
     * @return void
     */
    public function updateRow($tableName, array $row, $isRelation = FALSE) {
        if ($tableName !== 'tx_myext_domain_model_person') {
            throw new Exception('Invalid table name "' . $tableName . '"', 1326130577);
        }
 
        $temp = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
            'tx_igldapsoauth_dn',
            'fe_users',
            'uid=' . intval($row['uid']) . ' AND hidden=0 AND deleted=0'
        );
        $dn = $temp['tx_igldapssoauth_dn'];
 
            // uid is TYPO3-specific, remove it from LDAP update
        unset($row['uid']);
 
            // Retrieve current data
        $userData = $this->searchLdap($dn);
 
        $updateAttributes = array();
        $newAttributes = array();
        $removeAttributes = array();
        foreach ($row as $key => $value) {
            if (isset($userData[$key])) {
                if ($userData[$key][0] !== $row[$key] && $value) {
                    $updateAttributes[$key] = array($value);
                } elseif (!$value) {
                    $removeAttributes[$key] = array($userData[$key][0]);
                }
            } elseif ($value) {
                $newAttributes[$key] = array($value);
            }
        }
 
            // TODO: connect and bind to LDAP using a DN allowed to modify data
        $cnx = ldap_connect(...);
        ldap_bind($cnx, ...);
 
            // Add new attributes
        ldap_modify($cnx, $dn, $newAttributes);
 
            // Update attributes that were modified
        ldap_mod_replace($cnx, $dn, $updateAttributes);
 
            // Remove attributes that were emptied/deleted
        ldap_mod_del($cnx, $dn, $removeAttributes);
 
            // Close connection
        ldap_close($cnx);
    }
 
    /**
     * Deletes a row in the storage
     *
     * @param string $tableName The database table name
     * @param array $identifier An array of identifier array('fieldname' => value).
     * @param boolean $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
     * @return void
     */
    public function removeRow($tableName, array $identifier, $isRelation = FALSE) {
        throw new Exception('TODO: Implement removeRow() method.');
    }
 
    /**
     * Returns the number of items matching the query.
     *
     * @param Tx_Extbase_Persistence_QueryInterface $query
     * @return integer
     * @api
     */
    public function getObjectCountByQuery(Tx_Extbase_Persistence_QueryInterface $query) {
        throw new Exception('TODO: Implement getObjectCountByQuery() method.');
    }
 
    /**
     * Searches an entry in configured LDAP directory.
     *
     * @param string $dn
     * @return array
     */
    protected function searchLdap($dn) {
        $entry = array();
        $ldapConfig = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
            '*',
            'tx_igldapssoauth_config',
            'hidden=0 AND deleted=0'
        );
 
        if (!tx_igldapssoauth_utility_Ldap::connect(
            $ldapConfig['ldap_host'],
            $ldapConfig['ldap_port'],
            $ldapConfig['ldap_protocol'],
            $ldapConfig['ldap_charset'],
            $ldapConfig['ldap_server']
        )
        ) {
            return $entry;
        }
        if (!tx_igldapssoauth_utility_Ldap::bind(
            $ldapConfig['ldap_binddn'],
            $ldapConfig['ldap_password']
        )
        ) {
            return $entry;
        }
 
        list($filter, $basedn) = explode(',', $dn, 2);
        if (tx_igldapssoauth_utility_Ldap::search(
            $basedn,
            $filter,
            array(
                'cn', 'givenName', 'sn', 'title', 'mail', 'o', 'street', 'telephoneNumber',
                'postalCode', 'l', 'mobile', 'uid', 'userPassword',
            )
        )
        ) {
            $entry = tx_igldapssoauth_utility_Ldap::get_first_entry();
        }
 
        tx_igldapssoauth_utility_Ldap::disconnect();
        return $entry;
    }
}
?>

Conclusion

Creating a dedicated storage backend for Extbase is really a piece of cake. We did not implement support for multi-valued attributes as allowed in LDAP (multiple email addresses, …) but the concept should now be clear and we suggest you refer to the  PHP LDAP documentation for further information.

Concerning the Person Controller and the Fluid templates to actually edit the data, please refer to the numerous documentation on that topic, it’s really straightforward and 100% the same as when using the TYPO3 database.

Last point about having a LDAP user allowed to modify user information. Although we may reuse the same “service” user that we already used for reading LDAP information while authenticating users, we typically prefer granting it read-only access and ask the user to enter her own password whenever her profile should be updated. This allows us to rebind to LDAP using the user credentials which typically have the authorization to modify her own record.

Happy coding!

Flattr