How to build simple ldap server in ruby

In my test environment, our product ldap server is inaccessible due to security issues. But, we need to verify ldap login for gitlab. So, we introduce a simple ldap server setup here.

Basically, there is already a ruby ldap server available in gem here, what we need to do is to configure and bring it up. here, it will be set up with chef recipe.

ruby-ldapserver gem installation

gem_package 'ruby-ldapserver' do
  action :install
end

ldap_server.rb

So, we will simply adapt the example script called rbslapd3.rb from ruby-ldapserver and add our customization, like port, certicate, namingContexts.

....
s = LDAP::Server.new(
  port: 636,
  nodelay: true,
  listen: 10,
  ssl_key_file: 'my.key',
  ssl_cert_file: 'my.crt',
  ssl_on_connect: true,
  operation_class: DirOperation,
  operation_args: [directory],
  schema: schema,
  namingContexts: ['dc=ldap,dc=example,dc=com']
)
# s.run_prefork
s.run_tcpserver
s.join
...

As test env, a self-signed certificate will suffice and can be generated by

#=> openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out my.crt -keyout my.key

core.schema

schema is vital for ldap server to understand/recognize what consist of ldap content. Here, we will continue utilize the core.schema from ruby-ldapserver, adding a few entries neccessary to our ldap server. You can/should add more according to your need.

objectclass ( 2.5.13.2 NAME 'User'
	SUP person STRUCTURAL
	MUST ( uid $ c $ mail ) )

attributetype ( 1.2.840.113556.1.4.221
	NAME 'sAMAccountName'
	DESC 'sAMAccountName'
	EQUALITY caseIgnoreMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
	SINGLE-VALUE )

attributetype ( 1.2.840.113556.1.4.656
	NAME 'userPrincipalName'
	DESC 'userPrincipalName'
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
	SINGLE-VALUE )

attributetype ( 1.2.840.113556.1.2.102
	NAME 'memberOf'
	DESC 'memberOf'
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )

attributetype ( 2.5.4.49
    NAME ('dn' 'distinguishedName' )
    EQUALITY distinguishedNameMatch
    SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )

Add test entry to ldap server

As you can see, all the attributes for ldap server to add, need to be defined in the schema. For example, the objectclass User is self-defined in the schema previously.

ruby_block 'create one test entry for ldap server' do
  block do
    require 'net/ldap'
    # ldap client instance
    con = Net::LDAP.new(host: 'localhost',
                        port: 636,
                        base: 'ou=ldap,o=example.com',
                        encryption: :simple_tls)
    # username and password, empty for simplicity
    con.auth '', ''
    # authenticate with the server
    con.bind

    dn = "uid=123456,c=us,ou=ldap,o=example.com"

    # attributes to be added
    attr = {
      uid: "123456",
      cn: 'Roger Whatever',
      objectclass: 'User',
      sn: 'Whatever',
      mail: 'Whatever@us.example.com',
      c: 'us'
    }
    # execute the addition
    con.add(dn: dn, attributes: attr)
  end
end

query against ldap server

Query looks quite similar with add:

require 'net/ldap'
con = Net::LDAP.new(:host => 'localhost',
                      :port => 636,
                      :base => 'ou=ldap,o=example.com',
                      :encryption => :simple_tls)
con.auth '', ''
con.bind
filter = Net::LDAP::Filter.eq("uid", "123456")
con.search(filter:filter)

outputs looks like:

[#<Net::LDAP::Entry:0x00000000021595f8 @myhash={:dn=>["uid=123456,c=us,ou=ldap,o=example.com"], :uid=>["123456"], :cn=>["Roger Whatever"], :objectclass=>["User", "person", "top"], :sn=>["Whatever"], :mail=>["Whatever@us.example.com"], :c=>["us"]}>]

Verify with gitlab rake task

In order to verify it works for gitlab, gitlab-rake gitlab:ldap:check must be run to check.

#=> gitlab-rake gitlab:ldap:check --trace
** Invoke gitlab:ldap:check (first_time)
** Invoke gitlab_environment (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute gitlab_environment
** Execute gitlab:ldap:check
Checking LDAP ...

Server: ldapmain
not verifying SSL hostname of LDAPS server 'ldap.example.com:636'
LDAP authentication... Success
LDAP users with access to your GitLab server (only showing the first 100 results)
  DN: uid=123456,c=us,ou=ldap,o=example.com   mail: Whatever@us.example.com

Checking LDAP ... Finished

So, it works fine.

Please note that, gitlab:ldap:check actually need attributetype ["dn", "mail", "cn", "email", "userPrincipalName", "uid", "sAMAccountName", "userid", "memberof"] to be available in the schema. If they are undefined, it will throw error. That is why we define these attributetype in core.schema in first place.

The actual query ran by gitlab:ldap:check is as below:

options = {:attributes=>["dn", "mail", "cn", "email", "userPrincipalName", "uid", "sAMAccountName", "userid", "memberof"], :base=>"ou=ldap,o=example.com", :paged_searches_supported=>false}
ldap.search(options)

Full code

package 'ruby'

gem_package 'ruby-ldapserver' do
  action :install
end

# require 'net/ldap'
chef_gem 'net-ldap'

directory '/opt/ldap-server/bin' do
  recursive true
end

%w(crt key).each do |file_extension|
  cookbook_file "my.#{file_extension}" do
    source "ldap/my.#{file_extension}"
    path "/opt/ldap-server/my.#{file_extension}"
  end
end

cookbook_file 'ldap_server.rb' do
  path '/opt/ldap-server/bin/ldap_server.rb'
  source 'ldap/ldap_server.rb'
  action :create
end

cookbook_file 'core.schema' do
  path '/opt/ldap-server/core.schema'
  source 'ldap/core.schema'
  action :create
end

systemd_unit 'ldap-server.service' do
  content(Unit: {
            Description: 'Fake Ldap Server',
            After: 'network-online.target'
          },
          Service: {
            Type: 'simple',
            Restart: 'always',
            ExecStart: '/usr/bin/env ruby  /opt/ldap-server/bin/ldap_server.rb',
            WorkingDirectory: '/opt/ldap-server',
            TimeoutSec: 20
          },
          Install: {
            WantedBy: 'multi-user.target'
          })
  action %i(create enable start)
end

service 'ldap-server.service' do
  supports start: true, restart: true, stop: true, status: true, reload: false
end

ruby_block 'create one test entry for ldap server' do
  block do
    require 'net/ldap'
    # ldap client instance
    con = Net::LDAP.new(host: 'localhost',
                        port: 636,
                        base: 'ou=ldap,o=example.com',
                        encryption: :simple_tls)
    # username and password, empty for simplicity
    con.auth '', ''
    # authenticate with the server
    con.bind

    dn = "uid=123456,c=us,ou=ldap,o=example.com"

    # attributes to be added
    attr = {
      uid: "123456",
      cn: 'Roger Whatever',
      objectclass: 'User',
      sn: 'Whatever',
      mail: 'Whatever@us.example.com',
      c: 'us'
    }
    # execute the addition
    con.add(dn: dn, attributes: attr)
  end
end