Testing Ldap With Dotnet Core

Tags: core dotnet integration ldap testing
Categories: development software testing

This blog was written by Gert-Jan Admiraal, has 1809 words and takes about 9 minutes to read.

It was published on June 2, 2020 at 17:32 and last updated on: June 10, 2020 at 13:14

A while ago during development of an Identity and Access Management (IAM for short) service, there was a dependency on a LDAP service that was hosted by another department. And although there was access to add, or change, users through a web interface, it was less then optimal for automated (integration) testing. It also required that I had to leave open the VPN connection to the corporate LAN, which in the time of Docker and VM’s should no longer be the case.
At the time I did look into solutions for a test LDAP, but was unable at the time. And recently I found a solution that allows for testing and integrating without external dependencies.

Previous attempt

I did try a solution at the time that didn’t involve setting up my own ADFS or OpenLDAP service. I stumlbed upon this Github project: ldap-server-mock. Although it was easy to get it up and running, it did require tinkering to make sure it would fit our integration needs. And I needed it to be able to run in Docker. I managed to get the basics working, but when the requirements changed to include more details from the LDAP entries, the solution was quickly abondend.

New Attempt

In the past weeks I went looking for another solution, as I found a task my Trello board that I imported in Jira (which is super easy, but unrelated). I decided to take another look on the Docker hub and Github, and decided I might go into the rabbithole called setting up your own OpenLDAP service. I found some pages with a clear documentation on how to do it, such as How to install and configure OpenLDAP and phpldapadmin on Ubuntu and How to install and configure OpenLDAP and phpldapadmin on Centos. But then I hit this container: rroemhild/test-openldap backed by this Github repository: docker-test-openldap.

It looked promising, but I also wanted a UI for easy access, which I then found in this Docker container: osixia/openldap. Which is the phpldapadmin mentioned in the earlier blogpost, ready to go in a Docker container.

Enough components to get started and see if I can easily set it up, maintain it and utilize it from code.

Setting up the LDAP service

As the source repository included a structure I would probably wanted to change, I decided to fork the original repository.
The first thing: building the container. This is simply done by issues the following command: docker build -f Dockerfile -t open-ldap-mock.

This was the result I got after a bunch of seconds:

Configure admin config password...
sed: -e expression #1, char 30: unknown option to `s'
The command '/bin/sh -c /bin/bash /bootstrap/slapd-init.sh' returned a non-zero code: 1

Hmmm, seems something is wrong there. To see what went wrong, I decided to add the following line in /bootstrap/data/slaps-init.sh:


73
74
75
76
77
78
79
80
81
  
configure_admin_config_pw(){
  echo "Configure admin config password..."
  adminpw=$(slappasswd -h {SSHA} -s "${LDAP_SECRET}")
  echo "Password: ${adminpw}" # Check on the value
  sed -i s/{ADMINPW}/${adminpw}/g ${CONFIG_DIR}/configadminpw.ldif
  ldapmodify -Y EXTERNAL -H ldapi:/// -f ${CONFIG_DIR}/configadminpw.ldif -Q
}
  


Normally this would be a really bad idea, but since we’re only using this container for testing purposes, we can do it.

After running the Docker build again, the container could successfully be created. Either something is changed in the base container, a library or another mistake was made.
When the Docker layers are cached and I change the script back to its original state the build succeeds until the underlying cache is cleared or purged. This might be something to look into at a later moment.

Once the image was created, it was time to run the container:

docker run -it --rm \
  --privileged \
  --name open-ldap-mock \
  --hostname open-ldap-mock \
  -p 389:389 \
  -p 389:389 \
  open-ldap-mock

As I regularly test containers, I mostly run them interactively in a seperate terminal window, with -it --rm. This way I see what is going on. When the container is stopped, I remove it automatically. This wipes out any volume data, but makes sure my local Docker setup stays maintainable.

Sidenote: when I did a prune at the moment of writing, I had 11GB of unused docker layers.


The --priviliged command is needed to run on any port lower than 1024.
The --name and --hostname parameters are to name the docker container, so they can be easily referenced from the CLI or from other containers. Something we’re likely going to need.
The ports that are mapped are used for communicating with the LDAP service. Where port 389 is the normal port and 636 is used for the TLS connection.

Sidenote: I had some trouble with opening the TLS port from code, as the image uses a snake-oil a.k.a. self-signed certificate. More on that later.


And voila!, the LDAP service is working. I guess. Let’s try and see if we can communicate with it.

Starting the PHP LDAP Admin

As this interface is a nice piece of COTS software, there is no need to fork the repo or anything. We just use the available Docker container by running the following command:


docker run -it --rm \
  --name phpldapadmin-service \
  --hostname phpldapadmin-service \
  --link open-ldap-mock:ldap-host \
  --env PHPLDAPADMIN_LDAP_HOSTS=ldap-host \
  -p 6443:443 \
  osixia/phpldapadmin:0.9.0


Again I named the container for easy access.
There is also a --link statement, which maps another docker container to a DNS name within the new container. Although the link command is a legacy feature 1 and a network bridge is the future-proof solution, it also requires more commands to set it up. So for now I went the lazy route. Otherwise, create a brdige network and use the --network flag on all containers.
With the --env variable we set the hostname of the LDAP server we already setup in the previous step.
Another thing I like to do is use hardcoded version of Docker images. This way the result is more stable when the latest tag moves on to a version that has a breaking change.

phpLDAPadmin interface

Figure: phpLDAPadmin interface

Running this command succeeded and the opening the admin portal worked like a charm. Also logging in using the DN of the admin, as mentioned in the Readme file of the OpenLDAP test server worked. So all systems go!!!

Integrating with the existing environment

The LDAP is a plain and simple database that contains organisational details. And depending on the organisation the structure might change or the DN uses a different structure. The implementation that I was looking at already used string substitution for the DN, so changing the code was a matter of configuration. However, the code expected the DN to contain the uid and not a logical name.

In the LDAP test server all DN entries start with sn=, so in order to make the LDAP work with the code, I had to change the bootstrap data to have different DN’s that did contain the uid property. This also needs to be applied to the group data, as they use the DN to create a list of members.

Besides that change in the bootstrap data, the setup was good to go and I could login using the original implementation without the need of changing a single line of code.

My changes can be found in the fork I created on GitHub.

Test usage

Although one of the goals was to run the LDAP service in docker, to eliminate the need for the VPN connection, I was still looking for a way to use the LDAP for testing purposes by being able to change data.

For manual and automated acceptance testing the UI of PHP LDAP Admin can be used, but for unit, integration and system/component testing this would be cumbersome. There should be an easier way.

Since we run our own LDAP server, and therefor we know the credentials for the admin account, we can connect to the LDAP with these credentials:

var connection = new LdapConnection();
connection.Connect("localhost", 389);
var adminDn = "cn=admin,dc=planetexpress,dc=com";
var adminPassword = "GoodNewsEveryone";
connection.Bind( adminDn, adminPassword);

var adminEntry = connection.Read(adminDn);

connection.Disconnect();     

Running this code, by using the Novell.Directory.Ldap.NETStandard/3.2.0 NuGet package2, should run without throwing any exceptions. You can debug the code and see the adminEntry being populated with the LDAP data for the admin account.

With this code we connect to normal LDAP port and bind with the Admin account. The next step: search for an entry:

string searchBase = "ou=people,dc=planetexpress,dc=com";
string searchFilter = "(CN=*a*)";

var results = connection.Search (searchBase, 
                                 LdapConnection.ScopeOne, 
                                 searchFilter, 
                                 attrs: new []{"cn", "dn", "uid", "memberOf"}, 
                                 typesOnly: false, 
                                 cons: null);

do
{
    var userEntry = results.Next();
    Console.WriteLine($"User: {userEntry.Dn}");
}
} while( results.HasMore() );

(This code should be executed before the disconnect function is called).

In this example we look for people that have a Canonical Name (CN) that contains an ‘a’. And we decide to only return the CN, DN, UID and memberOf paramters of the entries. Another option would be to return all the attributes by sending a null as attrs:, but then the results would also include images.

The next step would be to change an attribute for an entry, so we can change the data while testing and validate our code. The following code can be used to change the Display Name:

connection.Modify(
    "uid=fry,ou=people,dc=planetexpress,dc=com", 
    new LdapModification(
        LdapModification.Replace, 
        new LdapAttribute("displayName", "The Fry")
    )
);


In case where an entry does not have a display name, this will throw an error. In that case, use the LdapModification.Add operator. And use LdapModification.Delete when you want to remove an attribute. Be aware that when you do a replace on a list of items, you’ll override the entire array of values. So if you want to update a list, use the Delete and Add commands.

In case where you want to add a user to a group, do the modification on the group. The LDAP will change the memberOf-attribute for you.


connection.Modify(
    "cn=admin_staff,ou=people,dc=planetexpress,dc=com", 
    new LdapModification(
        LdapModification.Add, 
        new LdapAttribute("member", "uid=fry,ou=people,dc=planetexpress,dc=com")
    )
);

These LDAP modifications do require the right access level, hence our usage of the admin account during the Bind request.

Using TLS

It is also to use the TLS ability of OpenLDAP and the provided Docker image. But since it uses a self-signed certificate, we need to add code:

//--- Connect 
var connection = new LdapConnection();
connection.SecureSocketLayer = true;
connection.UserDefinedServerCertValidationDelegate += (sender, certificate, chain, errors) => true;
connection.Connect("localhost", 636);


We need to set SecureSocketLayer to true, and we need to use the UserDefinedServerCertValidationDelegate to accept all certificates. However, for Production code I would put this code between DEBUG pragma’s to make sure the actual certificate validation is done.

Reuse connections

It is also possible to re-use the connection during setup, execution and tear-down. As long as the code under test is issuing a bind request, you can rebind using different credentials without disconnecting and connecting again.

Footnotes


  1. Docker --links legacy documentation. ↩︎

  2. The Novell LDAP SDK pages can be found here. ↩︎

© 2020 Houthakker Engineering