2 * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
10 * ext_ldap_group_acl: lookup group membership in LDAP
14 * (C)2002,2003 MARA Systems AB
16 * License: squid_ldap_group is free software; you can redistribute it
17 * and/or modify it under the terms of the GNU General Public License
18 * as published by the Free Software Foundation; either version 2,
19 * or (at your option) any later version.
22 * Flavio Pescuma <flavio@marasystems.com>
23 * Henrik Nordstrom <hno@marasystems.com>
24 * MARA Systems AB, Sweden <http://www.marasystems.com>
26 * With contributions from others mentioned in the ChangeLog file
28 * In part based on squid_ldap_auth by Glen Newton and Henrik Nordstrom.
30 * Latest version of this program can always be found from MARA Systems
31 * at http://marasystems.com/download/LDAP_Group/
33 * Dependencies: You need to get the OpenLDAP libraries
34 * from http://www.openldap.org or use another compatible
37 * If you want to make a TLS enabled connection you will also need the
38 * OpenSSL libraries linked into openldap. See http://www.openssl.org/
41 #include "helper/protocol_defines.h"
45 #define LDAP_DEPRECATED 1
55 #if _SQUID_WINDOWS_ && !_SQUID_CYGWIN_
57 #define snprintf _snprintf
61 #define LDAPAPI __cdecl
64 #ifndef LDAP_OPT_X_TLS
65 #define LDAP_OPT_X_TLS 0x6000
67 /* Some tricks to allow dynamic bind with ldap_start_tls_s entry point at
70 #undef ldap_start_tls_s
72 #define LDAP_START_TLS_S "ldap_start_tls_sW"
73 typedef WINLDAPAPI
ULONG(LDAPAPI
* PFldap_start_tls_s
) (IN PLDAP
, OUT PULONG
, OUT LDAPMessage
**, IN PLDAPControlW
*, IN PLDAPControlW
*);
75 #define LDAP_START_TLS_S "ldap_start_tls_sA"
76 typedef WINLDAPAPI
ULONG(LDAPAPI
* PFldap_start_tls_s
) (IN PLDAP
, OUT PULONG
, OUT LDAPMessage
**, IN PLDAPControlA
*, IN PLDAPControlA
*);
77 #endif /* LDAP_UNICODE */
78 PFldap_start_tls_s Win32_ldap_start_tls_s
;
79 #define ldap_start_tls_s(l,s,c) Win32_ldap_start_tls_s(l,NULL,NULL,s,c)
80 #endif /* LDAP_VERSION3 */
93 #define PROGRAM_NAME "ext_ldap_group_acl"
94 #define PROGRAM_VERSION "2.18"
98 static const char *basedn
= nullptr;
99 static const char *searchfilter
= nullptr;
100 static const char *userbasedn
= nullptr;
101 static const char *userdnattr
= nullptr;
102 static const char *usersearchfilter
= nullptr;
103 static const char *binddn
= nullptr;
104 static const char *bindpasswd
= nullptr;
105 static int searchscope
= LDAP_SCOPE_SUBTREE
;
106 static int persistent
= 0;
107 static int noreferrals
= 0;
108 static int aliasderef
= LDAP_DEREF_NEVER
;
109 #if defined(NETSCAPE_SSL)
110 static char *sslpath
= NULL
;
111 static int sslinit
= 0;
113 static int connect_timeout
= 0;
114 static int timelimit
= LDAP_NO_LIMIT
;
117 /* Added for TLS support and version 3 */
118 static int use_tls
= 0;
119 static int version
= -1;
122 static int searchLDAP(LDAP
* ld
, char *group
, char *user
, char *extension_dn
);
124 static int readSecret(const char *filename
);
126 /* Yuck.. we need to glue to different versions of the API */
128 #ifndef LDAP_NO_ATTRS
129 #define LDAP_NO_ATTRS "1.1"
132 #if defined(LDAP_API_VERSION) && LDAP_API_VERSION > 1823
134 squid_ldap_errno(LDAP
* ld
)
137 ldap_get_option(ld
, LDAP_OPT_ERROR_NUMBER
, &err
);
141 squid_ldap_set_aliasderef(LDAP
* ld
, int deref
)
143 ldap_set_option(ld
, LDAP_OPT_DEREF
, &deref
);
146 squid_ldap_set_referrals(LDAP
* ld
, int referrals
)
148 int *value
= static_cast<int*>(referrals
? LDAP_OPT_ON
:LDAP_OPT_OFF
);
149 ldap_set_option(ld
, LDAP_OPT_REFERRALS
, value
);
152 squid_ldap_set_timelimit(LDAP
* ld
, int aTimeLimit
)
154 ldap_set_option(ld
, LDAP_OPT_TIMELIMIT
, &aTimeLimit
);
157 squid_ldap_set_connect_timeout(LDAP
* ld
, int aTimeLimit
)
159 #if defined(LDAP_OPT_NETWORK_TIMEOUT)
161 tv
.tv_sec
= aTimeLimit
;
163 ldap_set_option(ld
, LDAP_OPT_NETWORK_TIMEOUT
, &tv
);
164 #elif defined(LDAP_X_OPT_CONNECT_TIMEOUT)
166 ldap_set_option(ld
, LDAP_X_OPT_CONNECT_TIMEOUT
, &aTimeLimit
);
170 squid_ldap_memfree(char *p
)
177 squid_ldap_errno(LDAP
* ld
)
182 squid_ldap_set_aliasderef(LDAP
* ld
, int deref
)
184 ld
->ld_deref
= deref
;
187 squid_ldap_set_referrals(LDAP
* ld
, int referrals
)
190 ld
->ld_options
|= ~LDAP_OPT_REFERRALS
;
192 ld
->ld_options
&= ~LDAP_OPT_REFERRALS
;
195 squid_ldap_set_timelimit(LDAP
* ld
, int timelimit
)
197 ld
->ld_timelimit
= timelimit
;
200 squid_ldap_set_connect_timeout(LDAP
* ld
, int timelimit
)
202 fprintf(stderr
, "WARNING: Connect timeouts not supported in your LDAP library\n");
205 squid_ldap_memfree(char *p
)
212 #ifdef LDAP_API_FEATURE_X_OPENLDAP
213 #if LDAP_VENDOR_VERSION > 194
214 #define HAS_URI_SUPPORT 1
219 main(int argc
, char **argv
)
221 char buf
[HELPER_INPUT_BUFFER
];
222 char *user
, *group
, *extension_dn
= nullptr;
223 char *ldapServer
= nullptr;
225 int tryagain
= 0, rc
;
226 int port
= LDAP_PORT
;
227 int use_extension_dn
= 0;
228 int strip_nt_domain
= 0;
229 int strip_kerberos_realm
= 0;
231 setbuf(stdout
, nullptr);
233 while (argc
> 1 && argv
[1][0] == '-') {
234 const char *value
= "";
235 char option
= argv
[1][1];
247 if (strlen(argv
[1]) > 2) {
249 } else if (argc
> 2) {
262 fprintf(stderr
, "FATAL: Your LDAP library does not have URI support\n");
265 /* Fall thru to -h */
268 int len
= strlen(ldapServer
) + 1 + strlen(value
) + 1;
269 char *newhost
= static_cast<char*>(xmalloc(len
));
270 snprintf(newhost
, len
, "%s %s", ldapServer
, value
);
272 ldapServer
= newhost
;
274 ldapServer
= xstrdup(value
);
281 searchfilter
= value
;
287 usersearchfilter
= value
;
293 if (strcmp(value
, "base") == 0)
294 searchscope
= LDAP_SCOPE_BASE
;
295 else if (strcmp(value
, "one") == 0)
296 searchscope
= LDAP_SCOPE_ONELEVEL
;
297 else if (strcmp(value
, "sub") == 0)
298 searchscope
= LDAP_SCOPE_SUBTREE
;
300 fprintf(stderr
, PROGRAM_NAME
": FATAL: Unknown search scope '%s'\n", value
);
305 #if defined(NETSCAPE_SSL)
307 if (port
== LDAP_PORT
)
310 fprintf(stderr
, PROGRAM_NAME
": FATAL: -E unsupported with this LDAP library\n");
315 connect_timeout
= atoi(value
);
318 timelimit
= atoi(value
);
321 if (strcmp(value
, "never") == 0)
322 aliasderef
= LDAP_DEREF_NEVER
;
323 else if (strcmp(value
, "always") == 0)
324 aliasderef
= LDAP_DEREF_ALWAYS
;
325 else if (strcmp(value
, "search") == 0)
326 aliasderef
= LDAP_DEREF_SEARCHING
;
327 else if (strcmp(value
, "find") == 0)
328 aliasderef
= LDAP_DEREF_FINDING
;
330 fprintf(stderr
, PROGRAM_NAME
": FATAL: Unknown alias dereference method '%s'\n", value
);
344 persistent
= !persistent
;
350 noreferrals
= !noreferrals
;
354 switch (atoi(value
)) {
356 version
= LDAP_VERSION2
;
359 version
= LDAP_VERSION3
;
362 fprintf(stderr
, "FATAL: Protocol version should be 2 or 3\n");
367 if (version
== LDAP_VERSION2
) {
368 fprintf(stderr
, "FATAL: TLS (-Z) is incompatible with version %d\n",
372 version
= LDAP_VERSION3
;
380 use_extension_dn
= 1;
386 strip_kerberos_realm
= 1;
389 fprintf(stderr
, PROGRAM_NAME
": FATAL: Unknown command line option '%c'\n", option
);
395 char *value
= argv
[1];
397 int len
= strlen(ldapServer
) + 1 + strlen(value
) + 1;
398 char *newhost
= static_cast<char*>(xmalloc(len
));
399 snprintf(newhost
, len
, "%s %s", ldapServer
, value
);
401 ldapServer
= newhost
;
403 ldapServer
= xstrdup(value
);
410 ldapServer
= (char *) "localhost";
412 if (!basedn
|| !searchfilter
) {
413 fprintf(stderr
, "\n" PROGRAM_NAME
" version " PROGRAM_VERSION
"\n\n");
414 fprintf(stderr
, "Usage: " PROGRAM_NAME
" -b basedn -f filter [options] ldap_server_name\n\n");
415 fprintf(stderr
, "\t-b basedn (REQUIRED)\tbase dn under where to search for groups\n");
416 fprintf(stderr
, "\t-f filter (REQUIRED)\tgroup search filter pattern. %%u = user,\n\t\t\t\t%%v = group\n");
417 fprintf(stderr
, "\t-B basedn (REQUIRED)\tbase dn under where to search for users\n");
418 fprintf(stderr
, "\t-F filter (REQUIRED)\tuser search filter pattern. %%s = login\n");
419 fprintf(stderr
, "\t-s base|one|sub\t\tsearch scope\n");
420 fprintf(stderr
, "\t-D binddn\t\tDN to bind as to perform searches\n");
421 fprintf(stderr
, "\t-w bindpasswd\t\tpassword for binddn\n");
422 fprintf(stderr
, "\t-W secretfile\t\tread password for binddn from file secretfile\n");
424 fprintf(stderr
, "\t-H URI\t\t\tLDAPURI (defaults to ldap://localhost)\n");
426 fprintf(stderr
, "\t-h server\t\tLDAP server (defaults to localhost)\n");
427 fprintf(stderr
, "\t-p port\t\t\tLDAP server port (defaults to %d)\n", LDAP_PORT
);
428 fprintf(stderr
, "\t-P\t\t\tpersistent LDAP connection\n");
429 #if defined(NETSCAPE_SSL)
430 fprintf(stderr
, "\t-E sslcertpath\t\tenable LDAP over SSL\n");
432 fprintf(stderr
, "\t-c timeout\t\tconnect timeout\n");
433 fprintf(stderr
, "\t-t timelimit\t\tsearch time limit\n");
434 fprintf(stderr
, "\t-R\t\t\tdo not follow referrals\n");
435 fprintf(stderr
, "\t-a never|always|search|find\n\t\t\t\twhen to dereference aliases\n");
437 fprintf(stderr
, "\t-v 2|3\t\t\tLDAP version\n");
438 fprintf(stderr
, "\t-Z\t\t\tTLS encrypt the LDAP connection, requires\n\t\t\t\tLDAP version 3\n");
440 fprintf(stderr
, "\t-g\t\t\tfirst query parameter is base DN extension\n\t\t\t\tfor this query\n");
441 fprintf(stderr
, "\t-S\t\t\tStrip NT domain from usernames\n");
442 fprintf(stderr
, "\t-K\t\t\tStrip Kerberos realm from usernames\n");
443 fprintf(stderr
, "\t-d\t\t\tenable debug mode\n");
444 fprintf(stderr
, "\n");
445 fprintf(stderr
, "\tIf you need to bind as a user to perform searches then use the\n\t-D binddn -w bindpasswd or -D binddn -W secretfile options\n\n");
448 /* On Windows ldap_start_tls_s is available starting from Windows XP,
449 * so we need to bind at run-time with the function entry point
454 HMODULE WLDAP32Handle
;
456 WLDAP32Handle
= GetModuleHandle("wldap32");
457 if ((Win32_ldap_start_tls_s
= (PFldap_start_tls_s
) GetProcAddress(WLDAP32Handle
, LDAP_START_TLS_S
)) == NULL
) {
458 fprintf(stderr
, PROGRAM_NAME
": FATAL: TLS (-Z) not supported on this platform.\n");
464 while (fgets(buf
, HELPER_INPUT_BUFFER
, stdin
) != nullptr) {
466 if (!strchr(buf
, '\n')) {
467 /* too large message received.. skip and deny */
468 fprintf(stderr
, "%s: ERROR: Input Too large: %s\n", argv
[0], buf
);
469 while (fgets(buf
, sizeof(buf
), stdin
)) {
470 fprintf(stderr
, "%s: ERROR: Input Too large..: %s\n", argv
[0], buf
);
471 if (strchr(buf
, '\n') != nullptr)
474 SEND_BH(HLP_MSG("Input too large"));
477 user
= strtok(buf
, " \n");
479 debug("%s: Invalid request: No Username given\n", argv
[0]);
480 SEND_BH(HLP_MSG("Invalid request. No Username"));
483 rfc1738_unescape(user
);
484 if (strip_nt_domain
) {
485 char *u
= strrchr(user
, '\\');
487 u
= strrchr(user
, '/');
489 u
= strrchr(user
, '+');
493 if (strip_kerberos_realm
) {
494 char *u
= strchr(user
, '@');
499 if (use_extension_dn
) {
500 extension_dn
= strtok(nullptr, " \n");
502 debug("%s: Invalid request: Extension DN configured, but none sent.\n", argv
[0]);
503 SEND_BH(HLP_MSG("Invalid Request. Extension DN required"));
506 rfc1738_unescape(extension_dn
);
508 const char *broken
= nullptr;
509 while (!found
&& user
&& (group
= strtok(nullptr, " \n")) != nullptr) {
510 rfc1738_unescape(group
);
515 if (strstr(ldapServer
, "://") != nullptr) {
516 rc
= ldap_initialize(&ld
, ldapServer
);
517 if (rc
!= LDAP_SUCCESS
) {
518 broken
= HLP_MSG("Unable to connect to LDAP server");
519 fprintf(stderr
, "%s: ERROR: Unable to connect to LDAPURI:%s\n", argv
[0], ldapServer
);
526 if (!sslinit
&& (ldapssl_client_init(sslpath
, NULL
) != LDAP_SUCCESS
)) {
527 fprintf(stderr
, "FATAL: Unable to initialise SSL with cert path %s\n", sslpath
);
532 if ((ld
= ldapssl_init(ldapServer
, port
, 1)) == NULL
) {
533 fprintf(stderr
, "FATAL: Unable to connect to SSL LDAP server: %s port:%d\n",
539 if ((ld
= ldap_init(ldapServer
, port
)) == nullptr) {
540 broken
= HLP_MSG("Unable to connect to LDAP server");
541 fprintf(stderr
, "ERROR: %s:%s port:%d\n", broken
, ldapServer
, port
);
545 squid_ldap_set_connect_timeout(ld
, connect_timeout
);
549 version
= LDAP_VERSION3
;
551 if (ldap_set_option(ld
, LDAP_OPT_PROTOCOL_VERSION
, &version
) != LDAP_SUCCESS
) {
552 broken
= HLP_MSG("Could not set LDAP_OPT_PROTOCOL_VERSION");
553 fprintf(stderr
, "ERROR: %s %d\n", broken
, version
);
559 #ifdef LDAP_OPT_X_TLS
560 if (version
!= LDAP_VERSION3
) {
561 fprintf(stderr
, "FATAL: TLS requires LDAP version 3\n");
563 } else if (ldap_start_tls_s(ld
, nullptr, nullptr) != LDAP_SUCCESS
) {
564 broken
= HLP_MSG("Could not Activate TLS connection");
565 fprintf(stderr
, "ERROR: %s\n", broken
);
571 fprintf(stderr
, "FATAL: TLS not supported with your LDAP library\n");
576 squid_ldap_set_timelimit(ld
, timelimit
);
577 squid_ldap_set_referrals(ld
, !noreferrals
);
578 squid_ldap_set_aliasderef(ld
, aliasderef
);
579 if (binddn
&& bindpasswd
&& *binddn
&& *bindpasswd
) {
580 rc
= ldap_simple_bind_s(ld
, binddn
, bindpasswd
);
581 if (rc
!= LDAP_SUCCESS
) {
582 broken
= HLP_MSG("could not bind");
583 fprintf(stderr
, PROGRAM_NAME
": WARNING: %s to binddn '%s'\n", broken
, ldap_err2string(rc
));
589 debug("Connected OK\n");
591 int searchResult
= searchLDAP(ld
, group
, user
, extension_dn
);
592 if (searchResult
== 0) {
595 } else if (searchResult
< 0) {
602 broken
= HLP_MSG("LDAP search error");
614 if (!persistent
|| (squid_ldap_errno(ld
) != LDAP_SUCCESS
&& squid_ldap_errno(ld
) != LDAP_INVALID_CREDENTIALS
)) {
628 ldap_escape_value(const std::string
&src
)
630 std::stringstream str
;
631 for (const auto &c
: src
) {
637 str
<< '\\' << std::setfill('0') << std::setw(2) << std::hex
<< static_cast<int>(c
);
647 build_filter(std::string
&filter
, const char *templ
, const char *user
, const char *group
)
649 std::stringstream str
;
658 str
<< ldap_escape_value(user
);
663 str
<< ldap_escape_value(group
);
666 fprintf(stderr
, "ERROR: Unknown filter template string %%%c\n", *templ
);
689 build_searchbase(const char *extension_dn
, const char *base_dn
)
691 std::stringstream searchBaseStream
;
692 if (extension_dn
&& *extension_dn
)
693 searchBaseStream
<< extension_dn
<< ",";
694 searchBaseStream
<< base_dn
;
695 return searchBaseStream
.str();
698 static bool ldap_search_ok(const int result
)
700 if (result
== LDAP_SUCCESS
)
702 if (noreferrals
&& result
== LDAP_PARTIAL_RESULTS
) {
703 /* Everything is fine. This is expected when referrals
708 std::cerr
<< PROGRAM_NAME
<< ": WARNING: LDAP search error '" <<
709 ldap_err2string(result
) << "'" << std::endl
;
710 #if defined(NETSCAPE_SSL)
711 if (sslpath
&& ((result
== LDAP_SERVER_DOWN
) || (result
== LDAP_CONNECT_ERROR
))) {
712 int sslerr
= PORT_GetError();
713 std::cerr
<< PROGRAM_NAME
<< ": WARNING: SSL error " << sslerr
<< " (" <<
714 ldapssl_err2string(sslerr
) << ")" << std::endl
;
720 typedef const std::unique_ptr
<LDAPMessage
, decltype(&ldap_msgfree
)> LdapResult
;
723 searchLDAPGroup(LDAP
* ld
, const char *group
, const char *member
, const char *extension_dn
)
726 LDAPMessage
*res
= nullptr;
728 char *searchattr
[] = {(char *) LDAP_NO_ATTRS
, nullptr};
730 const std::string searchbase
= build_searchbase(extension_dn
, basedn
);
731 if (!build_filter(filter
, searchfilter
, member
, group
)) {
732 std::cerr
<< PROGRAM_NAME
<< ": ERROR: Failed to construct LDAP search filter. filter=\"" <<
733 filter
.c_str() << "\", user=\"" << member
<< "\", group=\"" << group
<< "\"" << std::endl
;
736 debug("group filter '%s', searchbase '%s'\n", filter
.c_str(), searchbase
.c_str());
738 rc
= ldap_search_s(ld
, searchbase
.c_str(), searchscope
, filter
.c_str(), searchattr
, 1, &res
);
739 LdapResult
ldapRes(res
, ldap_msgfree
);
740 if (!ldap_search_ok(rc
))
743 return ldap_first_entry(ld
, ldapRes
.get()) ? 0 : 1;
747 formatWithString(std::string
&formatted
, const std::string
&value
)
749 size_t start_pos
= 0;
750 while ((start_pos
= formatted
.find("%s", start_pos
)) != std::string::npos
) {
751 formatted
.replace(start_pos
, 2, value
);
757 searchLDAP(LDAP
* ld
, char *group
, char *login
, char *extension_dn
)
760 const char *current_userdn
= userbasedn
? userbasedn
: basedn
;
761 if (usersearchfilter
) {
762 LDAPMessage
*res
= nullptr;
766 char *searchattr
[] = {(char *) LDAP_NO_ATTRS
, nullptr};
767 const std::string searchbase
= build_searchbase(extension_dn
, current_userdn
);
768 std::string
filter(usersearchfilter
);
769 const std::string escaped_login
= ldap_escape_value(login
);
770 formatWithString(filter
, escaped_login
);
772 debug("user filter '%s', searchbase '%s'\n", filter
.c_str(), searchbase
.c_str());
773 rc
= ldap_search_s(ld
, searchbase
.c_str(), searchscope
, filter
.c_str(), searchattr
, 1, &res
);
774 LdapResult
ldapRes(res
, ldap_msgfree
);
775 if (!ldap_search_ok(rc
))
777 entry
= ldap_first_entry(ld
, ldapRes
.get());
779 std::cerr
<< PROGRAM_NAME
<< ": WARNING: User '" << login
<<
780 " not found in '" << searchbase
.c_str() << "'" << std::endl
;
783 userdn
= ldap_get_dn(ld
, entry
);
784 rc
= searchLDAPGroup(ld
, group
, userdn
, extension_dn
);
785 squid_ldap_memfree(userdn
);
787 } else if (userdnattr
) {
788 std::stringstream str
;
789 str
<< userdnattr
<< "=" << login
<< ", ";
790 if (extension_dn
&& *extension_dn
)
791 str
<< extension_dn
<< ", ";
792 str
<< current_userdn
;
793 return searchLDAPGroup(ld
, group
, str
.str().c_str(), extension_dn
);
795 return searchLDAPGroup(ld
, group
, login
, extension_dn
);
800 readSecret(const char *filename
)
806 if (!(f
= fopen(filename
, "r"))) {
807 fprintf(stderr
, PROGRAM_NAME
": ERROR: Can not read secret file %s\n", filename
);
810 if (!fgets(buf
, sizeof(buf
) - 1, f
)) {
811 fprintf(stderr
, PROGRAM_NAME
": ERROR: Secret file %s is empty\n", filename
);
815 /* strip whitespaces on end */
816 if ((e
= strrchr(buf
, '\n')))
818 if ((e
= strrchr(buf
, '\r')))
821 bindpasswd
= xstrdup(buf
);
823 fprintf(stderr
, PROGRAM_NAME
": ERROR: can not allocate memory\n");