Chapter 10. Integrating ReportServer with an Active Directory using LDAP

10. Integrating ReportServer with an Active Directory using LDAP

In the following we will outline the necessary steps to connect ReportServer to an Active Directory using LDAP. As there are many valid ways to organize a company's directory (may it be AD or another vendors product) ReportServer does not come with a predefined LDAP connector. This on one hand means, that the configuration might seem rather complex, but on the other hand it provides you with a maximum of flexibility.

To connect ReportServer to the Active Directory Service we will use ReportServer's integrated groovy script engine. The whole process can be divided into two, mostly separate parts. One part is the synchronization of the user objects: we will automatically copy Users, Organizational Units and Groups from the directory to ReportServer and keep them updated. The second part is a mechanism that authenticates the previously imported users when they log into ReportServer.

10.1. Synchronizing Users

The complete example is available in the appendix (ldapimport.groovy) and also for download in the support portal.

As for the length of the script we will not go through it line by line, but rather outline the general process and highlight some important spots that are likely starting-points for customization.

We will start with a general overview of the synchronization process. Besides the account data used to log-in (bind) to the Active Directory Server the script requires two additional configuration values. The first is the ldapBase value, which specifies the common root node of all the directory entries that should be imported. This could simply be your organisation's User-root, as in the example or just a subtree, if you don't want to import all directory users into ReportServer. You can further tweak which user objects get imported by modifying the ldapFilter property. Keeping the default value will import all users, groups and organizational units as present in your directory. If you decide to modify this, keep in mind, that importing a user does not grant him any privileges. On the other hand a user not present in ReportServer will not even be available as a recipient for scheduled reports (that is, scheduled reports sent by email). So generally there is no reason to not just import all your users.

The second configuration is the target node in ReportServer's user tree. This is the node below which all imported objects will be placed. The script as published above will create an organizational unit "external" to which it will add all ldap objects. We suggest to keep imported objects separate from native ReportServer user tree objects, as this makes the synchronization much easier. The script also write protects all imported objects, to ensure that no objects are accidentally added to the automatically synchronized subtree.

After all configuration data is prepared the script starts by creating a map of all objects currently present below the targetNode. This map will later be used to make sure that objects that already existed are not recreated. This is important as creating the objects anew (and deleting the old one) will assign a new ID to the objects and we would have to update the id, wherever it was referenced. It's much more reliable to just reuse the existing object.

Afterwards the script connects to the directory and issues a search for all objects below the ldapBase node that match the ldapFilter. The search results are sorted by their path, so we don't have to take care of creating objects in the right order (parents before their children). If you change the filter expression you will have to add some code here that will find/create a suitable container for the node we are about to import.

In the next step the script iterates over the search results and creates the appropriate ReportServer object for each result. Execution is handed over to a create method for the specific object type. These methods all follow the same structure: they first either retrieve the node from the map created in the first step, or create a new node. Then the node is placed at the correct position in the target tree. Finally the search result's properties are copied over to the ReportServer object.

When the object is created its write-protection flag is set and the guid and origin properties are modified to indicate that the object was retrieved from the directory. The guid set here was used in the first step to uniquely identify and match existing nodes to the ones retrieved from the directory.

After all objects are created in the target structure post processing is performed that correctly sets the members of all groups. The process is to iterate over all objects found in the directory and for each group first clear the list of members and then retrieve the members from the map created in the previous step.

As a final step all users no longer present in the directory are removed from the target subtree.

10.2. Authenticating Users

There are many different ways to check a user's credentials, like using Client Certificates, authenticating with Kerberos or using some Single-Sign-On mechanism with e.g. spnego or CAS. We will present a very simple script that authenticates a user who provided a username/password-combination by trying to use this information to bind against the directory.

The complete script (hookldappam.groovy) is also in the appendix as well as available for download in the support portal.

The script consist of three separate parts: Two classes and a short script snippet, that registers an instance of these classes with ReportServer.

Let us first look at the LdapPAM class. PAM is short for pluggable authenticator module, authenticator modules in ReportServer are made up out of two parts: a client-side part, that handles the interaction with a user and a server-side part, that checks the credentials the client module retrieved from the user.

The implemented interface ReportServerPAM is shared by all of ReportServer authenticator mechanisms. It requires the implementation of two methods, the first is getClientModule, which provides the appropriate client component. Our LdapPAM reuses the UserPasswordClientPAM, that makes the user enter a combination of username and password and transfers the cleartext of both to the server. You should ensure SSL/TLS is enabled when using this module.

The second method authenticate performs the actual authentication. It is called with a set of tokens as collected by the specified client module and returns an object of type AuthenticationResult. The AuthenticationResult has three components, a boolean value indicating if authentication was successful, the resolved user, if any, and a second boolean value that indicates if the result is authoritative. This third value is only relevant with negative results - in this case it controls whether other modules (if any are activated) are queried or if the request is denied immediately.

The actual authentication is handed off to an instance of the LdapAuthenticator class. The code is basically the same as in the import script. A connection to the directory is established using the supplied password, and the information stored during import in the users origin field.

Depending on the outcome of this connection attempt an AuthenticationResult object is created and returned. In case of a negative authentication attempt the authoritative property is set based on the users origin property.

Lastly the script uses ReportServer's callback registry to hook into the authentication process.

Putting it all together

Now that you should have a basic understanding how the two scripts work, let's give it a try. Download the two files ldapimport.groovy and hookldappam.groovy to your computer.

Open the file with a text editor and change the following lines to match your configuration:

lul.setProviderUrl("ldap://directory.example.com:389");
lul.setSecurityPrincipal("CN=ldaptest,CN=Users,DC=directory,DC=example,DC=com");
lul.setSecurityCredentials("ldaptest");

lul.setLdapBase("OU=EXAMPLE,DC=directory,DC=example,DC=com");

The provider URL is the URL of your directory server, security principal and credentials are used to authenticate with the LDAP server. The ldapBase property specifies the parent nodes in your directory, where the import starts.

After you modified the script, open ReportServer in your browser and go to the fileserver section in the admin module.

Upload both files to a location below the bin directory. Open the terminal by pressing CTRL+ALT+T. Change your current directory to the location where you put the script files using the cd command and execute the import script.

cd /fileserver/bin
exec -c ldapimport.groovy

The -c (commit) flag is important because otherwise changes to the data model made by the script would be reverted after execution.

If you now change over to the user manager section you can view the results of the import. Also some statistics were written to the server's logfile/console.

After you have verified that the import was successful, it's time to load the authenticator module. Again, open the terminal by pressing Ctrl+Alt+T, cd to the script's location and execute it.

cd /fileserver/bin
exec hookldappam.groovy

Now you are all set to give it a try: Log out, or better yet use a second browser in case something is wrong and try to log in again.

10.3. Possible Improvements
  • Autoload authenticator module on startup One important thing, to keep in mind is that hooks attached by a script will be lost when you restart ReportServer. To make sure the ldapPamHook gets automatically reattached place the script in the onstartup.d/ directory. Scripts in this directory automatically get executed on start-up, so your authenticator module is always available.
  • Using the scheduler to refresh users periodically To keep ReportServer's user database in sync with your company directory you would probably like to run the script automatically from time to time. To do this, you can use the scheduleScript terminal command.
  • Automatically fetch/refresh a user's corresponding user object on login Additionally to periodic updates you might want to refresh a user's object whenever s/he tries to log in. This can easily be achieved by modifying the hookldappam script and adding the required functionality.