Hi all,
 
the attached patch implements authentication against an LDAP Directory Server. It has been tested on Ubuntu 16.04 (x86_64) using libldap-2.4-2 on the client side and 389-ds-base 1.3.4.9-1 on the server side. Add USE_LDAP=1 to your make command line to compile it in.
 
What do I have to to, to get this functionality integrated within the next offcial haproxy release?
 
I'm currently trying to figure out, how to pass commas ',' and bracket '(', ')' as arguments to http_auth_ldap. Do you have any hints for me on this topic?
 
Feedback is very welcome!
 
Kind regards,
 
      Danny
>From 5e50e7bc3b619a45e0ad862eaf501538d4828c97 Mon Sep 17 00:00:00 2001
From: Daniela Sonnenschein <my.card....@web.de>
Date: Thu, 2 Nov 2017 20:41:02 +0100
Subject: [PATCH] Simple LDAP authentication

This patch adds LDAP authentication via an http_auth_ldap
configuration fetch function.

http_auth_ldap(<LDAP-URL>[,<TEMPLATE>]): boolean

  Returns a boolean indicating wether the authentication data
  received from the client match a username & password stored
  in an LDAP directory server. The credentials are verified
  using ldap_simple_bind_s(3). This fetch function is not really
  useful outside of ACLs. Currently only http basic auth is
  supported.

  The optional TEMPLATE is used to create a Distinguished Name
  (DN), that is used to bind to the LDAP directory. The string
  USERCN is replaced with the username supplied by the client.
  For example specify TEMPLATE to be something like:

    "CN=USERCN,OU=People,DC=my,DC=corp"

  If no TEMPLATE is specified, it is expected, that the user
  part supplied via http will be able to bind to the directory
  as-is.

  The LDAP-URL is used as follows: The string USERDN is replaced
  with the DN of the user (after applying the <TEMPLATE>). It
  specifies the LDAP directory server, the search scope, the
  filter, and (unused) attributes (see RFC 4516 for more detailed
  information). The following example might be used to match the
  sample data below:

    "ldap://dirsrv.my.corp/cn=haproxy,ou=groups,dc=my,dc=corp?uniqueMember?sub?(&(objectClass=groupOfUniqueNames)(uniqueMember=USERDN))"

  The authentication algorithm used:

    - Connect to ldap://dirsrv.my.corp:389 (no TLS here, but
      specifiying ldaps:// is possible, see ldap_initialize(3))
    - Bind with the credentials supplied by the client using
      ldap_simple_bind_s(3) (see TEMPLATE description above)
    - Search the directory server with ldap_search_ext_s(3):
      - "cn=haproxy,ou=groups,dc=my,dc=corp" is the base DN
        for the search within the directory
      - Retrieve the attribute(s): "uniqueMember"
      - Use the scope "sub"
      - Use the filter "(&(objectClass=groupOfUniqueNames)(uniqueMember=CN=...))"
    - If at least one result is returned (the group!) access is
      granted to the user.

Sample test data for dirsrv(8):

  User allowed to use the protected resource:

    # Haproxy Technical User, people, my.corp
    dn: cn=Haproxy Technical User,ou=people,dc=my,dc=corp
    cn: Haproxy Technical User
    givenName: Haproxy
    gidNumber: 321
    homeDirectory: /var/run/haproxy
    sn: Technical User
    loginShell: /sbin/nologin
    objectClass: inetOrgPerson
    objectClass: posixAccount
    objectClass: top
    objectClass: organizationalPerson
    objectClass: person
    uidNumber: 321
    uid: haproxy
    userPassword:: e0NSWVBUfSQxJHBaSDNJQmpuJGpZQ3AxYmt3TFkzakhuUkJCUG1VUS4=

  Static group of all resource users:

    # haproxy, groups, my.corp
    dn: cn=haproxy,ou=groups,dc=my,dc=corp
    objectClass: groupOfUniqueNames
    objectClass: top
    owner: cn=Haproxy Technical User,ou=people,dc=my,dc=corp
    uniqueMember: cn=Haproxy Technical User,ou=people,dc=my,dc=corp
    uniqueMember: cn=Another User,ou=people,dc=my,dc=corp
    cn: haproxy

  ACI to allow the users to search and read their own group:

    # haproxy, groups, my.corp
    dn: cn=haproxy,ou=groups,dc=my,dc=corp
    aci: (target="ldap:///cn=haproxy,ou=groups,dc=my,dc=corp";) (targetattr="uniqueMember || objectClass") (version 3.0; acl "HA-Proxy Administrators"; allow (search, read) groupdn = "ldap:///cn=haproxy,ou=groups,dc=my,dc=corp";;)
---
 Makefile             |   9 ++
 include/proto/ldap.h |  28 ++++++
 src/ldap.c           | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/proto_http.c     |  28 ++++++
 tests/test-ldap.cfg  |  67 +++++++++++++++
 5 files changed, 369 insertions(+)
 create mode 100644 include/proto/ldap.h
 create mode 100644 src/ldap.c
 create mode 100644 tests/test-ldap.cfg

diff --git a/Makefile b/Makefile
index f066f31..7045b95 100644
--- a/Makefile
+++ b/Makefile
@@ -498,6 +498,15 @@ BUILD_OPTIONS   += $(call ignore_implicit,USE_ZLIB)
 OPTIONS_LDFLAGS += $(if $(ZLIB_LIB),-L$(ZLIB_LIB)) -lz
 endif
 
+ifneq ($(USE_LDAP),)
+LDAP_INC =
+LDAP_LIB =
+OPTIONS_OBJS    += src/ldap.o
+OPTIONS_CFLAGS  += -DUSE_LDAP $(if $(LDAP_INC),-I$(LDAP_INC))
+BUILD_OPTIONS   += $(call ignore_implicit,USE_LDAP)
+OPTIONS_LDFLAGS += $(if $(LDAP_LIB),-L$(LDAP_LIB)) -lldap -llber
+endif
+
 ifneq ($(USE_POLL),)
 OPTIONS_CFLAGS += -DENABLE_POLL
 OPTIONS_OBJS   += src/ev_poll.o
diff --git a/include/proto/ldap.h b/include/proto/ldap.h
new file mode 100644
index 0000000..6c542cc
--- /dev/null
+++ b/include/proto/ldap.h
@@ -0,0 +1,28 @@
+/*
+ * LDAP user authentication & authorization.
+ *
+ * Copyright 2017 Daniela Sonnenschein <my.card....@web.de>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ *
+ */
+
+#ifndef _PROTO_LDAP_H
+#define _PROTO_LDAP_H
+
+#include <common/config.h>
+
+int check_ldap(const char *url, const char *templ, const char *user, const char *pass);
+
+#endif /* _PROTO_LDAP_H */
+
+/*
+ * Local variables:
+ *  c-indent-level: 8
+ *  c-basic-offset: 8
+ * End:
+ */
+
diff --git a/src/ldap.c b/src/ldap.c
new file mode 100644
index 0000000..a7ec132
--- /dev/null
+++ b/src/ldap.c
@@ -0,0 +1,237 @@
+/*
+ * LDAP authentication & authorization
+ *
+ * Copyright 2010 Daniela Sonnenschein <my.card....@web.de>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ *
+ */
+#include <stdlib.h>
+#include <stdio.h>
+#include <assert.h>
+
+#include <common/config.h>
+#include <common/errors.h>
+
+#include <proto/acl.h>
+#include <proto/log.h>
+
+#include <proto/ldap.h>
+
+#define LDAP_DEPRECATED 1
+#include <ldap.h>
+
+#ifdef DEBUG
+#include <stdarg.h>
+static void local_debug(const char *fmt, ...)
+{
+	va_list ap;
+	FILE *fp;
+
+	va_start(ap, fmt);
+	if ((fp = fopen("ldap.log", "a")))
+	{
+		vfprintf(fp, fmt, ap);
+		fprintf(fp, "\n");
+		fclose(fp);
+	}
+	va_end(ap);
+}
+#else
+static void local_debug(const char *fmt, ...) {}
+#endif
+
+#define ldap_log_error(fmt, ...) local_debug(fmt " (%s)", ## __VA_ARGS__, ldap_err2string(result))
+#define ldap_log_fatal(fmt, ...) local_debug(fmt " (%s)", ## __VA_ARGS__, ldap_err2string(result))
+
+int check_ldap(const char *url, const char *templ, const char *who, const char *password)
+{
+	int result;
+	int retval = 0;
+	LDAPURLDesc *ludp = NULL;
+	char *binddn = NULL;
+
+	local_debug("got url=\"%s\" templ=\"%s\" who=\"%s\", password=\"%s\"", url, templ, who, password);
+
+	/* XXX: Workaround cfgparser comma issue */
+	{
+		char *cp;
+
+		if ((cp = url))
+		{
+			size_t url_len = strlen(url);
+			while ((cp = strstr(cp, "%2c")))
+			{
+				*cp++ = ',';
+				memmove(cp, cp + 2, url_len - (cp - url) - 1);
+			}
+		}
+		if ((cp = templ))
+		{
+			size_t templ_len = strlen(templ);
+			while ((cp = strstr(cp, "%2c")))
+			{
+				*cp++ = ',';
+				memmove(cp, cp + 2, templ_len - (cp - templ) - 1);
+			}
+		}
+	}
+	local_debug("replaced \%2e url=\"%s\" templ=\"%s\" who=\"%s\", password=\"%s\"", url, templ, who, password);
+	/* XXX: End of workaround */
+	
+	if ((result = ldap_url_parse(url, &ludp)) != LDAP_SUCCESS)
+	{
+		ldap_log_fatal("ldap_url_parse(\"%s\", ...) failed", url);
+	}
+	else
+	{
+		LDAPDN dn = NULL;
+		char buf[512];		
+
+		if (templ)
+		{
+			int who_len;
+			int used;
+			templ = "CN=USERCN,OU=People,DC=my,DC=corp";
+
+			used = snprintf(buf, sizeof(buf)-1, "%s", templ);
+			if (used >= sizeof(buf)-1)
+				 local_debug("Input has been truncated (need %i bytes)", used);
+
+			who_len = strlen(who);
+			/* Do we have enough space to store the username? */
+			if (sizeof(buf) - used - 1 > who_len)
+			{
+				char *usercn;
+				if ((usercn = strstr(buf, "USERCN")))
+				{
+					memmove(usercn + who_len, usercn + 6, used - (usercn - buf) - 5);
+					memcpy(usercn, who, who_len);
+				}
+			}
+			who = buf;
+		}
+
+		local_debug("who=\"%s\"", who);
+
+		if ((result = ldap_str2dn(who, &dn, LDAP_DN_FORMAT_LDAPV3)) != LDAP_SUCCESS)
+		{
+			ldap_log_error("ldap_str2dn(...) failed");
+		}
+		else
+		{
+			if ((result = ldap_dn2str(dn, &binddn, LDAP_DN_FORMAT_LDAPV3)) != LDAP_SUCCESS)
+				ldap_log_error("ldap_dn2str(...) failed");
+			
+			if (dn)
+			{
+				ldap_dnfree(dn);
+				dn = NULL;
+			}
+		}
+	}
+
+	if (binddn)
+	{
+		LDAP *ldap = NULL;
+		char server[512];
+
+		snprintf(server, sizeof(server), "%s://%s:%i", ludp->lud_scheme, ludp->lud_host, ludp->lud_port);
+		local_debug("Connecting to %s ...", server);
+
+		if ((result = ldap_initialize(&ldap, server)) != LDAP_SUCCESS)
+		{
+			ldap_log_fatal("ldap_initialize(..., \"%s\") failed", server);
+		}
+		else
+		{
+			int ldap_version = LDAP_VERSION3;
+
+			local_debug("Setting LDAP version %d", ldap_version);
+			if ((result = ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &ldap_version) != LDAP_OPT_SUCCESS))
+				ldap_log_error("ldap_set_option(...) failed");
+
+			local_debug("Binding \"%s\"", binddn);
+
+			if ((result = ldap_simple_bind_s(ldap, binddn, password)) != LDAP_SUCCESS)
+			{
+				ldap_log_error("ldap_simple_bind_s(...) failed");
+			}
+			else
+			{
+				char filter[1024];
+				LDAPMessage *response = NULL;
+				struct timeval tv;
+
+				tv.tv_sec = 2;
+				tv.tv_usec = 0;
+
+				{
+					size_t binddn_len = strlen(binddn);
+					int used;
+
+					used = snprintf(filter, sizeof(filter)-1, "%s", ludp->lud_filter);
+					if (used >= sizeof(filter)-1)
+						local_debug("Input filter has been truncated (need %i bytes)", used);
+					
+					/* Do we have enough space to store the DN? */
+					if (sizeof(filter) - used - 1 > binddn_len)
+					{
+						char *userdn;
+						if ((userdn = strstr(filter, "USERDN")))
+						{
+							memmove(userdn + binddn_len, userdn + 6, used - (userdn - filter) - 5);
+							memcpy(userdn, binddn, binddn_len);
+						}
+					}
+
+					local_debug("Searching: %s?%i?%s", ludp->lud_dn, ludp->lud_scope, filter);
+					if ((result = ldap_search_ext_s(ldap, ludp->lud_dn, /* base */ ludp->lud_scope,
+										filter, ludp->lud_attrs, 0, /* attrsonly = both */
+										NULL, /* clientctrls */ NULL, /* serverctrls */
+										&tv, LDAP_NO_LIMIT, &response)) != LDAP_SUCCESS)
+					{
+						ldap_log_error("ldap_search_ext_s(...) failed");
+					}
+					else
+					{
+						int count;
+	
+						count = ldap_count_entries(ldap, response);
+						local_debug("Found %i entries", count);
+	
+						retval = (count > 0);
+
+						if (response)
+						{
+							ldap_msgfree(response);
+							response = NULL;
+						}
+					}
+				}
+
+			}
+			local_debug("Unbinding ... ");
+		}
+		if ((result = ldap_unbind_s(ldap)) != LDAP_SUCCESS)
+			ldap_log_error("ldap_unbind_s(...) failed");
+		ldap = NULL;
+
+		ldap_memfree(binddn);
+		binddn = NULL;
+	}
+
+	if (ludp)
+	{
+		ldap_free_urldesc(ludp);
+		ludp = NULL;
+	}
+
+	local_debug("done - retval=%i", retval);
+
+	return retval;
+}
+
diff --git a/src/proto_http.c b/src/proto_http.c
index 8d813d3..3cdeb2d 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -48,6 +48,7 @@
 #include <proto/action.h>
 #include <proto/arg.h>
 #include <proto/auth.h>
+#include <proto/ldap.h>
 #include <proto/backend.h>
 #include <proto/channel.h>
 #include <proto/checks.h>
@@ -10366,6 +10367,30 @@ smp_fetch_http_auth_grp(const struct arg *args, struct sample *smp, const char *
 	return 1;
 }
 
+#ifdef USE_LDAP
+/* Accepts 1 required argument of type string (the LDAP URL),
+       and 1 optional argument of type string (an DN template) */
+static int
+smp_fetch_http_auth_ldap(const struct arg *args, struct sample *smp, const char *kw, void *private)
+{
+   char *url;
+   char *templ = NULL;
+
+	url = args[0].data.str.str;
+	if (args[1].type == ARGT_STR)
+		templ = args[1].data.str.str;
+
+	CHECK_HTTP_MESSAGE_FIRST();
+
+	if (!get_http_auth(smp->strm))
+		return 0;
+
+	smp->data.type = SMP_T_BOOL;
+	smp->data.u.sint = check_ldap(url, templ, smp->strm->txn->auth.user, smp->strm->txn->auth.pass);
+	return 1;
+}
+#endif
+
 /* Try to find the next occurrence of a cookie name in a cookie header value.
  * The lookup begins at <hdr>. The pointer and size of the next occurrence of
  * the cookie value is returned into *value and *value_l, and the function
@@ -12618,6 +12643,9 @@ static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
 
 	{ "http_auth",       smp_fetch_http_auth,      ARG1(1,USR),      NULL,    SMP_T_BOOL, SMP_USE_HRQHV },
 	{ "http_auth_group", smp_fetch_http_auth_grp,  ARG1(1,USR),      NULL,    SMP_T_STR,  SMP_USE_HRQHV },
+#ifdef USE_LDAP
+   { "http_auth_ldap",  smp_fetch_http_auth_ldap, ARG2(1,STR,STR),  NULL,    SMP_T_BOOL, SMP_USE_HRQHV },
+#endif
 	{ "http_first_req",  smp_fetch_http_first_req, 0,                NULL,    SMP_T_BOOL, SMP_USE_HRQHP },
 	{ "method",          smp_fetch_meth,           0,                NULL,    SMP_T_METH, SMP_USE_HRQHP },
 	{ "path",            smp_fetch_path,           0,                NULL,    SMP_T_STR,  SMP_USE_HRQHV },
diff --git a/tests/test-ldap.cfg b/tests/test-ldap.cfg
new file mode 100644
index 0000000..668f2ef
--- /dev/null
+++ b/tests/test-ldap.cfg
@@ -0,0 +1,67 @@
+# This is a test configuration.
+# It is used to check that http_auth_ldap() is correctly parsed.
+
+global
+	maxconn    1000
+   stats timeout 3s
+	log /dev/log	local0 info
+	log /dev/log	local0 notice
+#   debug
+	
+defaults
+	log global
+   timeout connect 1s
+   timeout server 3s
+   timeout client 3s
+
+listen  sample1
+	bind :8888
+   mode		http
+	option	httplog
+	stats		uri /stats
+	stats		refresh 5000ms
+
+   # Create a cn=haproxy groupOfUniqueNames under ou=groups,dc=my,dc=corp
+   # Add all administrator accounts as uniqueMember
+   # dirsrv(8): Allow access aci: (target="ldap:///cn=haproxy,ou=groups,dc=my,dc=corp";) (targetattr="uniqueMember || objectClass") (version 3.0; acl "HA-Proxy Administrators"; allow (search, read) groupdn = "ldap:///cn=haproxy,ou=groups,dc=my,dc=corp";;)
+   # USERDN is replaced with the bind DN (eg. CN=Admin,OU=people,dc=my,dc=corp)
+
+
+   # The following line should be possible:
+   acl acl_ldap_ok http_auth_ldap("ldap://dirsrv.my.corp/cn=haproxy,ou=groups,dc=my,dc=corp?uniqueMember?sub?(&(objectClass=groupOfUniqueNames)(uniqueMember=USERDN))")
+
+   # The following line is a workaround (using escape sequences for ',', '(', and ')'):
+   # acl acl_ldap_ok http_auth_ldap("ldap://dirsrv.my.corp/cn=haproxy%2cou=groups%2cdc=my%2cdc=corp?uniqueMember?sub?%28&%28objectClass=groupOfUniqueNames%29%28uniqueMember=USERDN%29%29";)
+	http-request auth realm "LDAP with full DN" if !acl_ldap_ok
+
+listen  sample2
+	bind :8889
+   mode		http
+	option	httplog
+	stats		uri /stats
+	stats		refresh 5000ms
+
+   # The following line should be possible:
+   acl acl_ldap_ok http_auth_ldap("ldap://dirsrv.my.corp/cn=haproxy,ou=groups,dc=my,dc=corp?uniqueMember?sub?(&(objectClass=groupOfUniqueNames)(uniqueMember=USERDN))","USERCN")
+
+   # The following line is a workaround (using escape sequences for ',', '(', and ')'):
+   # acl acl_ldap_ok http_auth_ldap("ldap://dirsrv.my.corp/cn=haproxy%2cou=groups%2cdc=my%2cdc=corp?uniqueMember?sub?%28&%28objectClass=groupOfUniqueNames%29%28uniqueMember=USERDN%29%29","USERCN";)
+
+	http-request auth realm "LDAP with full DN" if !acl_ldap_ok
+
+listen  sample3
+	bind :8890
+   mode		http
+	option	httplog
+	stats		uri /stats
+	stats		refresh 5000ms
+
+   # USERCN is replaced with supplied username (eg. Admin)
+
+   # The following line should be possible:
+   acl acl_ldap_ok http_auth_ldap("ldap://dirsrv.my.corp/cn=haproxy,ou=groups,dc=my,dc=corp?uniqueMember?sub?(&(objectClass=groupOfUniqueNames)(uniqueMember=USERDN))","CN=USERCN,OU=People,OU=dc,OU=corp")
+
+   # The following line is a workaround (using escape sequences for ',', '(', and ')'):
+   # acl acl_ldap_ok http_auth_ldap("ldap://dirsrv.my.corp/cn=haproxy%2cou=groups%2cdc=my%2cdc=corp?uniqueMember?sub?%28&%28objectClass=groupOfUniqueNames%29%28uniqueMember=USERDN%29%29","CN=USERCN%2cOU=People%2cOU=dc%2cOU=corp";)
+	http-request auth realm "LDAP with CN only" if !acl_ldap_ok
+
-- 
2.7.4

Reply via email to