Tuesday, August 6, 2013

Configure LDAP to use ReportServer with ActiveDirectory authentication

In this post we will show 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.

The instructions and examples below all were written with ReportServer 2.1 in mind, as there were some improvements in this version, that make things easier. But although some modifications are necessary the principal approach is also valid for older versions.

Synchronizing Users

The complete example is available here: ldapimport.groovy

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.

So let's 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 you 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 object get imported by modifying the ldapFilter property. Keeping the default value will import all users, groups and organizational unit 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, or reports send by email. So generally there is no reason not just to 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 was created its writer-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. The guid and origin properties are also the only part that is really specific to ReportServer 2.1, as these were not present in earlier versions. To use the script with older versions of ReportServer you would need to store this information elsewhere.

After all objects got created in the target structure a postprocessing 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 a map created in the previous step.

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

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. 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 is available here: hookldappam.groovy

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

Let's first look at the LdapPAM class. PAM is short for plugable 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 probably enable SSL/TLS when using this module.
The second method authenticate performs the actual authentication. It's called with a set of tokens as collected by the specified client module and return an 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 whith 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 you 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 datamodel made by the script would be reverted after execution.

If you now change over to the usermanager 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 scripts location and execute it.
cd /fileserver/bin
exec -g hookldappam.groovy

Note, that this time the -g (global context) parameter was given. By default callbacks submitted to the callback registry are only active in the current user's session. The global flag marks all callbacks registered in the script as to be registered for all users and all sessions.

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.

Possible improvements / TODO

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 startup, 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.
scheduleScript execute myScript.rs „ „ every hour at 23
A detailed explanation of the scheduleScript command is available in the ReportServer Administrator Guide. 

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 archived by modifying the hookldappam script and adding the required functionality. 

6 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Nice post, Thorsten. Have you done something similar (LDAP authentication) for Novell's eDirectory running on SuSE Linux or Open Enterprise Server? (Reposted with correct name. ;-) )

    ReplyDelete
  3. Hi Kevin, I just tried it with OES 11 SP1 and it works fine. You have to adjust the attribute names where they don't match (sAMAccountName -> uid, objectGUID -> GUID, ...) and if you have configured mandatory encryption you need to set the relevant options. But thats it. Hope that helps - Thorsten

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Why doesn't System.out.println output to the ReportServer terminal? And how do you get a list of all the ReportServer commands available in the terminal?

    ReplyDelete
  6. System.out prints to the server console/logfile. If you want your script output to be displayed in the terminal window in the browser instead, you can use tout.println().
    A list of available terminal commands is displayed, when you press the tab-key (which also works as auto-complete and for sub-commands). Most commands offer additional instructions, when passing -?.

    ReplyDelete