[Dovecot] [Patch] Samba's proposed "ntlm_auth + winbind" support for dovecot-auth
The Samba team recommends to use their "ntlm_auth" command line helper
for "NTLM" and "GSS-SPNEGO" authentication. This helper interacts with
the Samba's winbind daemon, and this way can authenticate users against
NT or Active Directory windows domain.
Currently Dovecot can do "NTLM" authentication too, but just "locally" (against a local or sql database etc.).
I've made a patch (attached), which adds "ntlm_auth" (or "winbind") support for Dovecot.
The idea is to add two new authentication mechanisms: "mech_winbind_ntlm" and "mech_winbind_spnego". Both are coded in one additional file, "mech-winbind.c". An option "auth_ntlm_use_winbind" specifies whether to use the current implementation of ntlm, or do it by the "ntlm_auth" helper. "GSS-SPNEGO" always performed by the helper. Normally, "ntlm_auth" is invoked once, for all further requests.
Such a way, "ntlm_auth helper from the Samba package, interacting with
the Samba's winbind daemon", is used now by Squid, Apache and AFAIK some
other applications. It is "strongly recommended" by the Samba team, and
was already proposed even in this maillist 3 year ago (see f.e.
http://www.dovecot.org/list/dovecot/2004-September/004775.html ).
I hope there are no any serious performance issues for such a "complex way" -- f.e. with our web proxy, using this way, ~200 users do not feel any actual delays etc.
I've successfully tested this patch with NTLM against AD domain.
This patch can considerably improve the situation of "Email client on Windows desktop under Windows domain, but imap/pop at UNIX server".
Currently, windows users have to specify their "login/password" for email accounts manually. There is an "SPA" (Secure Password Authentication) alternative for them, where just the desktop's login is used transparently, but it cannot be used now, because Dovecot cannot perform NTLM against, say, Active Directory domain.
Since "dovecot-auth" daemon can be utilized by MTA as well (Postfix and other), the support of "ntlm_auth" in Dovecot can satisfy both SMTP and IMAP servers at UNIX side, and solve the issue completely.
Questions and requests: the helper's cmdline exactly ?
- I try to code things most close to used style, i.e. using Dovecot's memory-management and io-pipe routines etc., but could someone look at it and check whether I've missed something or not?
- Perhaps some names (of routines, modules) could be chosen better?
- Maybe some other options should be implemented, i.e. "auth_winbind_helper_ntlm" and "auth_winbind_helper_spnego" to specify
- Currently I strip domain part of the username returned, i.e. from "DOMAIN\user" just to "user". Maybe better add some option "auth_winbind_strip_domain" for this?
Certainly, it will be fine if someone else check it more, especially for "GSS-SPNEGO" which I cannot test for a while.
Regards, Dmitry Butskoy http://www.fedoraproject.org/wiki/DmitryButskoy
On Mon, 2007-07-02 at 16:19 +0400, Dmitry Butskoy wrote:
- I try to code things most close to used style, i.e. using Dovecot's memory-management and io-pipe routines etc., but could someone look at it and check whether I've missed something or not?
close() and dup2() errors should be reported, although they're unlikely.
ostream is a bit pointless there since you're doing only a single write. You could just use write_full().
- Maybe some other options should be implemented, i.e. "auth_winbind_helper_ntlm" and "auth_winbind_helper_spnego" to specify the helper's cmdline exactly ?
Yes.
- Currently I strip domain part of the username returned, i.e. from "DOMAIN\user" just to "user". Maybe better add some option "auth_winbind_strip_domain" for this?
What if you changed it to user@domain? Then you could use Dovecot's standard %n or %u variables.
There's one thing I'd want changed: make it non-blocking. Both input and output are currently blocking, so dovecot-auth is stuck while waiting for ntlm_auth to reply. I want to avoid this whenever possible (I don't ever want to see "authentication just gets stuck, why??" mails. "ntlm_auth timed out" message in log is much nicer).
I guess ntlm_auth can handle only a single session at a time? So this would pretty much require that you either implement some kind of a queue or execute multiple ntlm_auths. Or maybe both. Using auth worker processes would probably be best. Unfortunately that currently works only for passdbs and userdbs, not for mechanisms. I guess I could try changing this for v1.1, unless you want to try? :)
Timo Sirainen wrote:
- Currently I strip domain part of the username returned, i.e. from "DOMAIN\user" just to "user". Maybe better add some option "auth_winbind_strip_domain" for this?
What if you changed it to user@domain? Then you could use Dovecot's standard %n or %u variables.
AFAIK "user@domain" should have an actual form of "user@example.com", but "ntlm_auth" returns "EXAMPLE\user" in such a case, not "EXAMPLE.COM\user". At least for NTLM (against AD). OTOH the spnego could retrurn the actual "user@example.com", but it is still unknown to me :)
There's one thing I'd want changed: make it non-blocking. Both input and output are currently blocking, so dovecot-auth is stuck while waiting for ntlm_auth to reply. I want to avoid this whenever possible (I don't ever want to see "authentication just gets stuck, why??" mails. "ntlm_auth timed out" message in log is much nicer).
I guess ntlm_auth can handle only a single session at a time?
No.
Normally ntlm_auth invoked at once for ALL authentication sessions. It is a child process which lives all the time while the dovecot-auth is alive.
There's one thing I'd want changed: make it non-blocking
Still actual after the clarification above?..
So this would pretty much require that you either implement some kind of a queue
Currently I prefer to use blocking io, which provides such "a queue" de-facto. At least for initial implementation. (I hope "ntlm_auth" is fast enough).
But some timeout surely could be useful. How can it be implemented?
or execute multiple ntlm_auths.
Does the use of worker for, say, PAM userdb affects us here too?
Or maybe both. Using auth worker processes would probably be best. Unfortunately that currently works only for passdbs and userdbs, not for mechanisms. I guess I could try changing this for v1.1, unless you want to try? :)
It seems that I prefer you try it... :)
Does "blocking io" + "worker" look like the best way?
Regards Dmitry Butskoy
On Mon, 2007-07-02 at 20:19 +0400, Dmitry Butskoy wrote:
Timo Sirainen wrote:
- Currently I strip domain part of the username returned, i.e. from "DOMAIN\user" just to "user". Maybe better add some option "auth_winbind_strip_domain" for this?
What if you changed it to user@domain? Then you could use Dovecot's standard %n or %u variables.
AFAIK "user@domain" should have an actual form of "user@example.com", but "ntlm_auth" returns "EXAMPLE\user" in such a case, not "EXAMPLE.COM\user". At least for NTLM (against AD). OTOH the spnego could retrurn the actual "user@example.com", but it is still unknown to me :)
I don't think it's a problem even if it returned only user@example. %d then just expands to example. At least I think it's better than not having the domain at all.
There's one thing I'd want changed: make it non-blocking. Both input and output are currently blocking, so dovecot-auth is stuck while waiting for ntlm_auth to reply. I want to avoid this whenever possible (I don't ever want to see "authentication just gets stuck, why??" mails. "ntlm_auth timed out" message in log is much nicer).
I guess ntlm_auth can handle only a single session at a time?
No.
I mean simultaneously. Like you can't send multiple "begin/continue authentication" commands to it and then just wait until one of them finishes?
There's one thing I'd want changed: make it non-blocking
Still actual after the clarification above?..
Yep.
So this would pretty much require that you either implement some kind of a queue
Currently I prefer to use blocking io, which provides such "a queue" de-facto. At least for initial implementation. (I hope "ntlm_auth" is fast enough).
I think the problem isn't ntlm_auth itself, but that it also has to talk to AD. So any network problems there could leave it hanging.
But some timeout surely could be useful. How can it be implemented?
By using non-blocking I/O :)
or execute multiple ntlm_auths.
Does the use of worker for, say, PAM userdb affects us here too?
PAM is passdb. But anyway it shouldn't affect.
Or maybe both. Using auth worker processes would probably be best. Unfortunately that currently works only for passdbs and userdbs, not for mechanisms. I guess I could try changing this for v1.1, unless you want to try? :)
It seems that I prefer you try it... :)
Does "blocking io" + "worker" look like the best way?
I think so. Or some kind of a generic queue maybe.. Hmm. Doesn't GSSAPI have the exact same problem? I think it does. Maybe I could figure out something for them both.
Timo Sirainen wrote:
On Mon, 2007-07-02 at 20:19 +0400, Dmitry Butskoy wrote:
Timo Sirainen wrote:
- Currently I strip domain part of the username returned, i.e. from "DOMAIN\user" just to "user". Maybe better add some option "auth_winbind_strip_domain" for this?
What if you changed it to user@domain? Then you could use Dovecot's standard %n or %u variables.
AFAIK "user@domain" should have an actual form of "user@example.com", but "ntlm_auth" returns "EXAMPLE\user" in such a case, not "EXAMPLE.COM\user". At least for NTLM (against AD). OTOH the spnego could retrurn the actual "user@example.com", but it is still unknown to me :)
I don't think it's a problem even if it returned only user@example. %d then just expands to example. At least I think it's better than not having the domain at all.
I propose to have an option. For example, winbind already has "winbind use default domain" option, which cause (sometimes :) ) it to return usernames without domain part. It is useful at UNIX machines, where the actual UNIX username is just "user", not "DOMAIN\user" .
What happens when dovecot receives "DOMAIN\user" or "user@exmaple.com" form? How it determine then the local UNIX username?
There's one thing I'd want changed: make it non-blocking. Both input and output are currently blocking, so dovecot-auth is stuck while waiting for ntlm_auth to reply. I want to avoid this whenever possible (I don't ever want to see "authentication just gets stuck, why??" mails. "ntlm_auth timed out" message in log is much nicer).
I guess ntlm_auth can handle only a single session at a time?
No.
I mean simultaneously. Like you can't send multiple "begin/continue authentication" commands to it and then just wait until one of them finishes?
Oh, yep. You're right, only one at a time.
So this would pretty much require that you either implement some kind of a queue
Currently I prefer to use blocking io, which provides such "a queue" de-facto. At least for initial implementation. (I hope "ntlm_auth" is fast enough).
I think the problem isn't ntlm_auth itself, but that it also has to talk to AD. So any network problems there could leave it hanging.
OTOH winbind daemon can "cache" something now...
Hmm.. Apache seems to use blocking-io. I'll look later for Squid.
or execute multiple ntlm_auths.
Does the use of worker for, say, PAM userdb affects us here too?
PAM is passdb. But anyway it shouldn't affect.
I mean that when we use worker for some userdb, we actually have several dovecot-auth processes, hence several ntlm_auth as well. Am I right?
~buc
Version 2 of the patch. Options "auth_winbind_helper_ntlm" and "auth_winbind_helper_spnego" added. (The default values for them are actually placed in mech-winbind.c . Hmm... ) dup2() and fork() errors reported. Don't want to report close() errors here :) Usernames in form "DOMAIN\user" are transformed to "user@DOMAIN" (call to auth_request_set_username() is used). Still don't want to simplify output by "write_full()" -- for more clean code it is better to do input/output in a similar style. Does an extra "ostream" grab a significant amount of resources? I've not found an easy way how the io can be re-coded "non-blocking". I hope in the case of the network failures (where Dovecot is reachable, but domain controller not) the winbind daemon itself can do timeouts (which will be "transferred" then to the pending ntlm_auth etc.) Maybe to leave things as is (i.e. blocking), and later some new common code (a worker for mech) could help for all such mechanisms (including gssapi) ? Regards, Dmitry Butskoy diff -Nrbu dovecot-1.0.1/src/auth/Makefile.am dovecot-1.0.1-OK/src/auth/Makefile.am --- dovecot-1.0.1/src/auth/Makefile.am 2007-05-19 15:14:04.000000000 +0400 +++ dovecot-1.0.1-OK/src/auth/Makefile.am 2007-07-03 17:53:14.000000000 +0400 @@ -56,6 +56,7 @@ mech-cram-md5.c \ mech-digest-md5.c \ mech-ntlm.c \ + mech-winbind.c \ mech-gssapi.c \ mech-rpa.c \ mech-apop.c \ diff -Nrbu dovecot-1.0.1/src/auth/Makefile.in dovecot-1.0.1-OK/src/auth/Makefile.in --- dovecot-1.0.1/src/auth/Makefile.in 2007-06-14 16:02:13.000000000 +0400 +++ dovecot-1.0.1-OK/src/auth/Makefile.in 2007-07-03 17:53:41.000000000 +0400 @@ -78,6 +78,7 @@ mech.$(OBJEXT) mech-anonymous.$(OBJEXT) mech-plain.$(OBJEXT) \ mech-login.$(OBJEXT) mech-cram-md5.$(OBJEXT) \ mech-digest-md5.$(OBJEXT) mech-ntlm.$(OBJEXT) \ + mech-winbind.$(OBJEXT) \ mech-gssapi.$(OBJEXT) mech-rpa.$(OBJEXT) mech-apop.$(OBJEXT) \ passdb.$(OBJEXT) passdb-blocking.$(OBJEXT) \ passdb-bsdauth.$(OBJEXT) passdb-cache.$(OBJEXT) \ @@ -325,6 +326,7 @@ mech-cram-md5.c \ mech-digest-md5.c \ mech-ntlm.c \ + mech-winbind.c \ mech-gssapi.c \ mech-rpa.c \ mech-apop.c \ @@ -494,6 +496,7 @@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-ntlm.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-plain.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-rpa.Po@am__quote@ +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-winbind.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mycrypt.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-blocking.Po@am__quote@ diff -Nrbu dovecot-1.0.1/src/auth/mech-winbind.c dovecot-1.0.1-OK/src/auth/mech-winbind.c --- dovecot-1.0.1/src/auth/mech-winbind.c 1970-01-01 03:00:00.000000000 +0300 +++ dovecot-1.0.1-OK/src/auth/mech-winbind.c 2007-07-03 17:53:14.000000000 +0400 @@ -0,0 +1,298 @@ +/* + * NTLM and Negotiate authentication mechanisms, + * using Samba winbind daemon + * + * Copyright (c) 2007 Dmitry Butskoy <dmitry@butskoy.name> + * + * This software is released under the MIT license. + */ + +#include "common.h" +#include "mech.h" +#include "str.h" +#include "buffer.h" +#include "safe-memset.h" +#include "base64.h" +#include "istream.h" +#include "ostream.h" + +#include <stdlib.h> +#include <unistd.h> + + +#define WINBIND_HELPER_NTLM \ + "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp" +#define WINBIND_HELPER_SPNEGO \ + "/usr/bin/ntlm_auth --helper-protocol=gss-spnego" + + +struct winbind_auth_request { + struct auth_request auth_request; + + pool_t pool; + + int continued; + + int negotiate; /* 1 -- GSS-SPNEGO, 0 -- NTLM */ + +}; + + +static struct istream *in_pipe = NULL; +static struct ostream *out_pipe = NULL; + + +static void init_helper (int negotiate) { + int infd[2], outfd[2]; + pid_t pid; + + if (in_pipe) i_stream_destroy (&in_pipe); + if (out_pipe) o_stream_destroy (&out_pipe); + + + if (pipe (infd) < 0 || pipe (outfd) < 0) + i_fatal ("pipe creation failed"); + + pid = fork (); + if (pid < 0) i_fatal ("fork() failed"); + + if (pid == 0) { /* child */ + const char *helper; + const char **args; + + close (infd[0]); + close (outfd[1]); + + if (dup2 (outfd[0], 0) < 0 || + dup2 (infd[1], 1) < 0 + ) i_fatal ("dup2 failed after fork()"); + + if (negotiate) { + helper = getenv ("WINBIND_HELPER_SPNEGO"); + if (!helper) helper = WINBIND_HELPER_SPNEGO; + } else { + helper = getenv ("WINBIND_HELPER_NTLM"); + if (!helper) helper = WINBIND_HELPER_NTLM; + } + + args = t_strsplit_spaces (helper, " "); + if (!args || !args[0]) + i_fatal ("bad helper `%s' specified", helper); + + execv (args[0], (char **) args); + i_fatal ("execv of %s failed", args[0]); + _exit (127); /* paranoia */ + } + + /* parent */ + + close (infd[1]); + close (outfd[0]); + + in_pipe = i_stream_create_file (infd[0], default_pool, + AUTH_CLIENT_MAX_LINE_LENGTH, FALSE); + out_pipe = o_stream_create_file (outfd[1], default_pool, + (size_t) -1, FALSE); + + return; +} + + +static void mech_winbind_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) { + struct winbind_auth_request *request = + (struct winbind_auth_request *) auth_request; + string_t *str; + char *answer; + const char **token; + + str = t_str_new (MAX_BASE64_ENCODED_SIZE (data_size + 1) + 4); + + str_printfa (str, "%s ", request->continued ? "KK" : "YR"); + base64_encode (data, data_size, str); + str_append_c (str, '\n'); + + if (o_stream_send_str (out_pipe, str_c (str)) < 0 || + o_stream_flush (out_pipe) < 0 + ) { + auth_request_log_info (auth_request, "winbind", + "cannot write to helper pipe"); + goto restart; + } + + request->continued = 0; + + + answer = i_stream_read_next_line (in_pipe); + if (!answer) { + auth_request_log_info (auth_request, "winbind", + "cannot read from helper pipe"); + goto restart; + } + + token = t_strsplit_spaces (answer, " \n"); + if (!token || !token[0] || + (!token[1] && strcmp (token[0], "BH") != 0) || + (request->negotiate && !token[2]) + ) { + auth_request_log_info (auth_request, "winbind", + "could not parse `%s' helper callback", answer); + goto restart; + } + + /* + * NTLM: + * The child's reply contains 2 parts: + * - The code: TT, AF or NA + * - The argument: + * For TT it's the blob to send to the client, coded in base64 + * For AF it's user or DOMAIN\user + * For NA it's the NT error code + * + * GSS-SPNEGO: + * The child's reply contains 3 parts: + * - The code: TT, AF or NA + * - The blob to send to the client, coded in base64 + * - The argument: + * For TT it's a dummy '*' + * For AF it's DOMAIN\user + * For NA it's the NT error code + */ + + if (!strcmp (token[0], "TT")) { + buffer_t *buf; + size_t len = strlen (token[1]); + + buf = buffer_create_dynamic (pool_datastack_create(), + MAX_BASE64_DECODED_SIZE (len)); + base64_decode (token[1], len, NULL, buf); + + auth_request->callback (auth_request, + AUTH_CLIENT_RESULT_CONTINUE, + buf->data, buf->used); + request->continued = 1; + return; + } + else if (!strcmp (token[0], "NA")) { + const char *error = request->negotiate ? token[2] : token[1]; + + auth_request_log_info (auth_request, "winbind", + "user not authenticated: %s", error); + + auth_request_fail (auth_request); + return; + } + else if (!strcmp (token[0], "AF")) { + const char *user, *p, *error; + + user = request->negotiate ? token[2] : token[1]; + + p = strchr (user, '\\'); + if (p) { + /* change "DOMAIN\user" to uniform style "user@DOMAIN" */ + user = t_strconcat (p+1, "@", t_strdup_until (user, p), NULL); + } + + if (!auth_request_set_username (auth_request, user, &error)) { + auth_request_log_info (auth_request, "winbind", "%s", error); + + auth_request_fail (auth_request); + return; + } + + if (request->negotiate && strcmp (token[1], "*") != 0) { + buffer_t *buf; + size_t len = strlen (token[1]); + + buf = buffer_create_dynamic (pool_datastack_create(), + MAX_BASE64_DECODED_SIZE (len)); + base64_decode (token[1], len, NULL, buf); + + auth_request_success (&request->auth_request, + buf->data, buf->used); + } else + auth_request_success (&request->auth_request, NULL, 0); + + return; + } + else if (!strcmp (token[0], "BH")) { + auth_request_log_info (auth_request, "winbind", + "ntlm_auth reports broken helper: %s", + token[1] ? token[1] : ""); + auth_request_fail (auth_request); + return; + } + else { + auth_request_log_info (auth_request, "winbind", + "could not parse `%s' helper callback", answer); + auth_request_fail (auth_request); + return; + } + + +restart: + init_helper (request->negotiate); /* try to restart */ + + auth_request_fail (auth_request); + return; +} + + +static struct auth_request *do_auth_new (int negotiate) { + struct winbind_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create("winbind_auth_request", 1024); + request = p_new(pool, struct winbind_auth_request, 1); + request->pool = pool; + + request->negotiate = negotiate; + + request->continued = 0; + + if (!in_pipe || !out_pipe) + init_helper (negotiate); + + request->auth_request.pool = pool; + return &request->auth_request; +} + +static struct auth_request *mech_winbind_ntlm_auth_new (void) { + + return do_auth_new (0); +} + +static struct auth_request *mech_winbind_spnego_auth_new (void) { + + return do_auth_new (1); +} + + +const struct mech_module mech_winbind_ntlm = { + "NTLM", + + MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE, + + MEMBER(passdb_need_plain) FALSE, + MEMBER(passdb_need_credentials) FALSE, + + mech_winbind_ntlm_auth_new, + mech_generic_auth_initial, + mech_winbind_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_winbind_spnego = { + "GSS-SPNEGO", + + MEMBER(flags) 0, + + MEMBER(passdb_need_plain) FALSE, + MEMBER(passdb_need_credentials) FALSE, + + mech_winbind_spnego_auth_new, + mech_generic_auth_initial, + mech_winbind_auth_continue, + mech_generic_auth_free +}; + diff -Nrbu dovecot-1.0.1/src/auth/mech.c dovecot-1.0.1-OK/src/auth/mech.c --- dovecot-1.0.1/src/auth/mech.c 2007-05-19 15:14:04.000000000 +0400 +++ dovecot-1.0.1-OK/src/auth/mech.c 2007-07-03 17:53:14.000000000 +0400 @@ -73,6 +73,8 @@ #ifdef HAVE_GSSAPI extern struct mech_module mech_gssapi; #endif +extern struct mech_module mech_winbind_ntlm; +extern struct mech_module mech_winbind_spnego; void mech_init(void) { @@ -81,12 +83,16 @@ mech_register_module(&mech_apop); mech_register_module(&mech_cram_md5); mech_register_module(&mech_digest_md5); + if (getenv("NTLM_USE_WINBIND") != NULL) + mech_register_module(&mech_winbind_ntlm); + else mech_register_module(&mech_ntlm); mech_register_module(&mech_rpa); mech_register_module(&mech_anonymous); #ifdef HAVE_GSSAPI mech_register_module(&mech_gssapi); #endif + mech_register_module(&mech_winbind_spnego); } void mech_deinit(void) @@ -96,10 +102,14 @@ mech_unregister_module(&mech_apop); mech_unregister_module(&mech_cram_md5); mech_unregister_module(&mech_digest_md5); + if (getenv("NTLM_USE_WINBIND") != NULL) + mech_unregister_module(&mech_winbind_ntlm); + else mech_unregister_module(&mech_ntlm); mech_unregister_module(&mech_rpa); mech_unregister_module(&mech_anonymous); #ifdef HAVE_GSSAPI mech_unregister_module(&mech_gssapi); #endif + mech_unregister_module(&mech_winbind_spnego); } diff -Nrbu dovecot-1.0.1/src/master/auth-process.c dovecot-1.0.1-OK/src/master/auth-process.c --- dovecot-1.0.1/src/master/auth-process.c 2007-06-12 20:43:46.000000000 +0400 +++ dovecot-1.0.1-OK/src/master/auth-process.c 2007-07-03 17:53:14.000000000 +0400 @@ -474,6 +474,8 @@ env_put("SSL_REQUIRE_CLIENT_CERT=1"); if (set->ssl_username_from_cert) env_put("SSL_USERNAME_FROM_CERT=1"); + if (set->ntlm_use_winbind) + env_put("NTLM_USE_WINBIND=1"); if (*set->krb5_keytab != '\0') { /* Environment used by Kerberos 5 library directly */ env_put(t_strconcat("KRB5_KTNAME=", set->krb5_keytab, NULL)); @@ -482,6 +484,14 @@ env_put(t_strconcat("GSSAPI_HOSTNAME=", set->gssapi_hostname, NULL)); } + if (*set->winbind_helper_ntlm != '\0') { + env_put(t_strconcat("WINBIND_HELPER_NTLM=", + set->winbind_helper_ntlm, NULL)); + } + if (*set->winbind_helper_spnego != '\0') { + env_put(t_strconcat("WINBIND_HELPER_SPNEGO=", + set->winbind_helper_spnego, NULL)); + } restrict_process_size(set->process_size, (unsigned int)-1); } diff -Nrbu dovecot-1.0.1/src/master/master-settings.c dovecot-1.0.1-OK/src/master/master-settings.c --- dovecot-1.0.1/src/master/master-settings.c 2007-06-12 20:43:06.000000000 +0400 +++ dovecot-1.0.1-OK/src/master/master-settings.c 2007-07-03 17:53:14.000000000 +0400 @@ -72,12 +72,15 @@ DEF(SET_STR, anonymous_username), DEF(SET_STR, krb5_keytab), DEF(SET_STR, gssapi_hostname), + DEF(SET_STR, winbind_helper_ntlm), + DEF(SET_STR, winbind_helper_spnego), DEF(SET_BOOL, verbose), DEF(SET_BOOL, debug), DEF(SET_BOOL, debug_passwords), DEF(SET_BOOL, ssl_require_client_cert), DEF(SET_BOOL, ssl_username_from_cert), + DEF(SET_BOOL, ntlm_use_winbind), DEF(SET_INT, count), DEF(SET_INT, worker_max_count), @@ -291,12 +294,15 @@ MEMBER(anonymous_username) "anonymous", MEMBER(krb5_keytab) "", MEMBER(gssapi_hostname) "", + MEMBER(winbind_helper_ntlm) "", + MEMBER(winbind_helper_spnego) "", MEMBER(verbose) FALSE, MEMBER(debug) FALSE, MEMBER(debug_passwords) FALSE, MEMBER(ssl_require_client_cert) FALSE, MEMBER(ssl_username_from_cert) FALSE, + MEMBER(ntlm_use_winbind) FALSE, MEMBER(count) 1, MEMBER(worker_max_count) 30, diff -Nrbu dovecot-1.0.1/src/master/master-settings.h dovecot-1.0.1-OK/src/master/master-settings.h --- dovecot-1.0.1/src/master/master-settings.h 2007-06-12 20:42:52.000000000 +0400 +++ dovecot-1.0.1-OK/src/master/master-settings.h 2007-07-03 17:53:14.000000000 +0400 @@ -191,10 +191,13 @@ const char *anonymous_username; const char *krb5_keytab; const char *gssapi_hostname; + const char *winbind_helper_ntlm; + const char *winbind_helper_spnego; bool verbose, debug, debug_passwords; bool ssl_require_client_cert; bool ssl_username_from_cert; + bool ntlm_use_winbind; unsigned int count; unsigned int worker_max_count;
Version 3 of the patch. This version sent also to Samba team, in hope they have more chances to test it properly. Dmitry Butskoy diff -Nrbu dovecot-1.0.1/dovecot-example.conf dovecot-1.0.1-OK/dovecot-example.conf --- dovecot-1.0.1/dovecot-example.conf 2007-06-13 04:47:39.000000000 +0400 +++ dovecot-1.0.1-OK/dovecot-example.conf 2007-07-06 18:40:35.000000000 +0400 @@ -748,6 +748,27 @@ # default (usually /etc/krb5.keytab) if not specified. #auth_krb5_keytab = +# If you have "ntlm_auth" helper and running winbind daemon +# (both from Samba package, see http://www.samba.org), +# you can use it for "ntlm" and "gss-spnego" authentication mechanisms. +# It is useful when you need to authenticate users against a Windows domain +# (either AD or NT). +# +# The usernames, returned by winbind, can contain some domain part +# (either "DOMAIN\user" or "user@example.com"). Such usernames +# are always transformed to the form of "user@domain". To strip domain part +# (to obtain correspond local username, for example), specify +# auth_username_format = %n + +# Use "ntlm-auth + winbind" way for "ntlm" mechanism, +# instead of own Dovecot's ntlm support ("gss-spnego" always requires winbind) +#auth_ntlm_use_winbind = no + +# Specify an alternate path and command line for the helper for "ntlm" +#auth_winbind_helper_ntlm = /usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp" +# The same for "gss-spnego" +#auth_winbind_helper_spnego = /usr/bin/ntlm_auth --helper-protocol=gss-spnego + auth default { # Space separated list of wanted authentication mechanisms: # plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi diff -Nrbu dovecot-1.0.1/src/auth/Makefile.am dovecot-1.0.1-OK/src/auth/Makefile.am --- dovecot-1.0.1/src/auth/Makefile.am 2007-05-19 15:14:04.000000000 +0400 +++ dovecot-1.0.1-OK/src/auth/Makefile.am 2007-07-03 17:53:14.000000000 +0400 @@ -56,6 +56,7 @@ mech-cram-md5.c \ mech-digest-md5.c \ mech-ntlm.c \ + mech-winbind.c \ mech-gssapi.c \ mech-rpa.c \ mech-apop.c \ diff -Nrbu dovecot-1.0.1/src/auth/Makefile.in dovecot-1.0.1-OK/src/auth/Makefile.in --- dovecot-1.0.1/src/auth/Makefile.in 2007-06-14 16:02:13.000000000 +0400 +++ dovecot-1.0.1-OK/src/auth/Makefile.in 2007-07-03 17:53:41.000000000 +0400 @@ -78,6 +78,7 @@ mech.$(OBJEXT) mech-anonymous.$(OBJEXT) mech-plain.$(OBJEXT) \ mech-login.$(OBJEXT) mech-cram-md5.$(OBJEXT) \ mech-digest-md5.$(OBJEXT) mech-ntlm.$(OBJEXT) \ + mech-winbind.$(OBJEXT) \ mech-gssapi.$(OBJEXT) mech-rpa.$(OBJEXT) mech-apop.$(OBJEXT) \ passdb.$(OBJEXT) passdb-blocking.$(OBJEXT) \ passdb-bsdauth.$(OBJEXT) passdb-cache.$(OBJEXT) \ @@ -325,6 +326,7 @@ mech-cram-md5.c \ mech-digest-md5.c \ mech-ntlm.c \ + mech-winbind.c \ mech-gssapi.c \ mech-rpa.c \ mech-apop.c \ @@ -494,6 +496,7 @@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-ntlm.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-plain.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-rpa.Po@am__quote@ +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech-winbind.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mech.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mycrypt.Po@am__quote@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/passdb-blocking.Po@am__quote@ diff -Nrbu dovecot-1.0.1/src/auth/mech-winbind.c dovecot-1.0.1-OK/src/auth/mech-winbind.c --- dovecot-1.0.1/src/auth/mech-winbind.c 1970-01-01 03:00:00.000000000 +0300 +++ dovecot-1.0.1-OK/src/auth/mech-winbind.c 2007-07-06 17:53:00.000000000 +0400 @@ -0,0 +1,310 @@ +/* + * NTLM and Negotiate authentication mechanisms, + * using Samba winbind daemon + * + * Copyright (c) 2007 Dmitry Butskoy <dmitry@butskoy.name> + * + * This software is released under the MIT license. + */ + +#include "common.h" +#include "mech.h" +#include "str.h" +#include "buffer.h" +#include "safe-memset.h" +#include "base64.h" +#include "istream.h" +#include "ostream.h" + +#include <stdlib.h> +#include <unistd.h> + + +#define WINBIND_HELPER_NTLM \ + "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp" +#define WINBIND_HELPER_SPNEGO \ + "/usr/bin/ntlm_auth --helper-protocol=gss-spnego" + + +struct winbind_auth_request { + struct auth_request auth_request; + + pool_t pool; + + bool continued; + + bool negotiate; /* TRUE for GSS-SPNEGO case, else NTLM */ + +}; + +enum helper_result { + HR_OK = 0, /* OK or continue */ + HR_FAIL = -1, /* authentication failed */ + HR_RESTART = -2 /* FAIL + try to restart helper */ +}; + + +static struct istream *in_pipe = NULL; +static struct ostream *out_pipe = NULL; + + +static void init_helper (bool negotiate) { + int infd[2], outfd[2]; + pid_t pid; + + if (in_pipe) i_stream_destroy (&in_pipe); + if (out_pipe) o_stream_destroy (&out_pipe); + + + if (pipe (infd) < 0 || pipe (outfd) < 0) + i_fatal ("pipe creation failed: %m"); + + pid = fork (); + if (pid < 0) i_fatal ("fork() failed: %m"); + + if (pid == 0) { /* child */ + const char *helper; + const char **args; + + close (infd[0]); + close (outfd[1]); + + if (dup2 (outfd[0], 0) < 0 || + dup2 (infd[1], 1) < 0 + ) i_fatal ("dup2 failed after fork(): %m"); + + if (negotiate) { + helper = getenv ("WINBIND_HELPER_SPNEGO"); + if (!helper) helper = WINBIND_HELPER_SPNEGO; + } else { + helper = getenv ("WINBIND_HELPER_NTLM"); + if (!helper) helper = WINBIND_HELPER_NTLM; + } + + args = t_strsplit_spaces (helper, " "); + if (!args || !args[0]) + i_fatal ("bad helper `%s' specified", helper); + + execv (args[0], (char **) args); + i_fatal ("execv of %s failed: %m", args[0]); + _exit (127); /* paranoia */ + } + + /* parent */ + + close (infd[1]); + close (outfd[0]); + + in_pipe = i_stream_create_file (infd[0], default_pool, + AUTH_CLIENT_MAX_LINE_LENGTH, FALSE); + out_pipe = o_stream_create_file (outfd[1], default_pool, + (size_t) -1, FALSE); + + return; +} + + +static enum helper_result do_auth_continue (struct auth_request *auth_request, + const unsigned char *data, size_t data_size) { + struct winbind_auth_request *request = + (struct winbind_auth_request *) auth_request; + string_t *str; + char *answer; + const char **token; + + str = t_str_new (MAX_BASE64_ENCODED_SIZE (data_size + 1) + 4); + + str_printfa (str, "%s ", request->continued ? "KK" : "YR"); + base64_encode (data, data_size, str); + str_append_c (str, '\n'); + + if (o_stream_send_str (out_pipe, str_c (str)) < 0 || + o_stream_flush (out_pipe) < 0 + ) { + auth_request_log_info (auth_request, "winbind", + "cannot write to helper pipe"); + return HR_RESTART; + } + + request->continued = FALSE; + + + answer = i_stream_read_next_line (in_pipe); + if (!answer) { + auth_request_log_info (auth_request, "winbind", + "cannot read from helper pipe"); + return HR_RESTART; + } + + token = t_strsplit_spaces (answer, " \n"); + if (!token || !token[0] || + (!token[1] && strcmp (token[0], "BH") != 0) || + (request->negotiate && !token[2]) + ) { + auth_request_log_info (auth_request, "winbind", + "could not parse `%s' helper callback", answer); + return HR_RESTART; + } + + /* + * NTLM: + * The child's reply contains 2 parts: + * - The code: TT, AF or NA + * - The argument: + * For TT it's the blob to send to the client, coded in base64 + * For AF it's user or DOMAIN\user + * For NA it's the NT error code + * + * GSS-SPNEGO: + * The child's reply contains 3 parts: + * - The code: TT, AF or NA + * - The blob to send to the client, coded in base64 + * - The argument: + * For TT it's a dummy '*' + * For AF it's DOMAIN\user + * For NA it's the NT error code + */ + + if (!strcmp (token[0], "TT")) { + buffer_t *buf; + size_t len = strlen (token[1]); + + buf = buffer_create_dynamic (pool_datastack_create(), + MAX_BASE64_DECODED_SIZE (len)); + base64_decode (token[1], len, NULL, buf); + + auth_request->callback (auth_request, + AUTH_CLIENT_RESULT_CONTINUE, + buf->data, buf->used); + request->continued = TRUE; + return HR_OK; + } + else if (!strcmp (token[0], "NA")) { + const char *error = request->negotiate ? token[2] : token[1]; + + auth_request_log_info (auth_request, "winbind", + "user not authenticated: %s", error); + + return HR_FAIL; + } + else if (!strcmp (token[0], "AF")) { + const char *user, *p, *error; + + user = request->negotiate ? token[2] : token[1]; + + p = strchr (user, '\\'); + if (p) { + /* change "DOMAIN\user" to uniform style "user@DOMAIN" */ + user = t_strconcat (p+1, "@", t_strdup_until (user, p), NULL); + } + + if (!auth_request_set_username (auth_request, user, &error)) { + auth_request_log_info (auth_request, "winbind", "%s", error); + + return HR_FAIL; + } + + if (request->negotiate && strcmp (token[1], "*") != 0) { + buffer_t *buf; + size_t len = strlen (token[1]); + + buf = buffer_create_dynamic (pool_datastack_create(), + MAX_BASE64_DECODED_SIZE (len)); + base64_decode (token[1], len, NULL, buf); + + auth_request_success (&request->auth_request, + buf->data, buf->used); + } else + auth_request_success (&request->auth_request, NULL, 0); + + return HR_OK; + } + else if (!strcmp (token[0], "BH")) { + auth_request_log_info (auth_request, "winbind", + "ntlm_auth reports broken helper: %s", + token[1] ? token[1] : ""); + return HR_FAIL; + } + else { + auth_request_log_info (auth_request, "winbind", + "could not parse `%s' helper callback", answer); + return HR_FAIL; + } + +} + +static void mech_winbind_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) { + struct winbind_auth_request *request = + (struct winbind_auth_request *) auth_request; + enum helper_result res; + + res = do_auth_continue (auth_request, data, data_size); + + if (res == HR_OK) return; + else if (res == HR_RESTART) + init_helper (request->negotiate); /* try to restart */ + + auth_request_fail (auth_request); + return; +} + + +static struct auth_request *do_auth_new (bool negotiate) { + struct winbind_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create("winbind_auth_request", 1024); + request = p_new(pool, struct winbind_auth_request, 1); + request->pool = pool; + + request->negotiate = negotiate; + + request->continued = FALSE; + + if (!in_pipe || !out_pipe) + init_helper (negotiate); + + request->auth_request.pool = pool; + return &request->auth_request; +} + +static struct auth_request *mech_winbind_ntlm_auth_new (void) { + + return do_auth_new (FALSE); +} + +static struct auth_request *mech_winbind_spnego_auth_new (void) { + + return do_auth_new (TRUE); +} + + +const struct mech_module mech_winbind_ntlm = { + "NTLM", + + MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE, + + MEMBER(passdb_need_plain) FALSE, + MEMBER(passdb_need_credentials) FALSE, + + mech_winbind_ntlm_auth_new, + mech_generic_auth_initial, + mech_winbind_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_winbind_spnego = { + "GSS-SPNEGO", + + MEMBER(flags) 0, + + MEMBER(passdb_need_plain) FALSE, + MEMBER(passdb_need_credentials) FALSE, + + mech_winbind_spnego_auth_new, + mech_generic_auth_initial, + mech_winbind_auth_continue, + mech_generic_auth_free +}; + diff -Nrbu dovecot-1.0.1/src/auth/mech.c dovecot-1.0.1-OK/src/auth/mech.c --- dovecot-1.0.1/src/auth/mech.c 2007-05-19 15:14:04.000000000 +0400 +++ dovecot-1.0.1-OK/src/auth/mech.c 2007-07-03 17:53:14.000000000 +0400 @@ -73,6 +73,8 @@ #ifdef HAVE_GSSAPI extern struct mech_module mech_gssapi; #endif +extern struct mech_module mech_winbind_ntlm; +extern struct mech_module mech_winbind_spnego; void mech_init(void) { @@ -81,12 +83,16 @@ mech_register_module(&mech_apop); mech_register_module(&mech_cram_md5); mech_register_module(&mech_digest_md5); + if (getenv("NTLM_USE_WINBIND") != NULL) + mech_register_module(&mech_winbind_ntlm); + else mech_register_module(&mech_ntlm); mech_register_module(&mech_rpa); mech_register_module(&mech_anonymous); #ifdef HAVE_GSSAPI mech_register_module(&mech_gssapi); #endif + mech_register_module(&mech_winbind_spnego); } void mech_deinit(void) @@ -96,10 +102,14 @@ mech_unregister_module(&mech_apop); mech_unregister_module(&mech_cram_md5); mech_unregister_module(&mech_digest_md5); + if (getenv("NTLM_USE_WINBIND") != NULL) + mech_unregister_module(&mech_winbind_ntlm); + else mech_unregister_module(&mech_ntlm); mech_unregister_module(&mech_rpa); mech_unregister_module(&mech_anonymous); #ifdef HAVE_GSSAPI mech_unregister_module(&mech_gssapi); #endif + mech_unregister_module(&mech_winbind_spnego); } diff -Nrbu dovecot-1.0.1/src/master/auth-process.c dovecot-1.0.1-OK/src/master/auth-process.c --- dovecot-1.0.1/src/master/auth-process.c 2007-06-12 20:43:46.000000000 +0400 +++ dovecot-1.0.1-OK/src/master/auth-process.c 2007-07-03 17:53:14.000000000 +0400 @@ -474,6 +474,8 @@ env_put("SSL_REQUIRE_CLIENT_CERT=1"); if (set->ssl_username_from_cert) env_put("SSL_USERNAME_FROM_CERT=1"); + if (set->ntlm_use_winbind) + env_put("NTLM_USE_WINBIND=1"); if (*set->krb5_keytab != '\0') { /* Environment used by Kerberos 5 library directly */ env_put(t_strconcat("KRB5_KTNAME=", set->krb5_keytab, NULL)); @@ -482,6 +484,14 @@ env_put(t_strconcat("GSSAPI_HOSTNAME=", set->gssapi_hostname, NULL)); } + if (*set->winbind_helper_ntlm != '\0') { + env_put(t_strconcat("WINBIND_HELPER_NTLM=", + set->winbind_helper_ntlm, NULL)); + } + if (*set->winbind_helper_spnego != '\0') { + env_put(t_strconcat("WINBIND_HELPER_SPNEGO=", + set->winbind_helper_spnego, NULL)); + } restrict_process_size(set->process_size, (unsigned int)-1); } diff -Nrbu dovecot-1.0.1/src/master/master-settings.c dovecot-1.0.1-OK/src/master/master-settings.c --- dovecot-1.0.1/src/master/master-settings.c 2007-06-12 20:43:06.000000000 +0400 +++ dovecot-1.0.1-OK/src/master/master-settings.c 2007-07-03 17:53:14.000000000 +0400 @@ -72,12 +72,15 @@ DEF(SET_STR, anonymous_username), DEF(SET_STR, krb5_keytab), DEF(SET_STR, gssapi_hostname), + DEF(SET_STR, winbind_helper_ntlm), + DEF(SET_STR, winbind_helper_spnego), DEF(SET_BOOL, verbose), DEF(SET_BOOL, debug), DEF(SET_BOOL, debug_passwords), DEF(SET_BOOL, ssl_require_client_cert), DEF(SET_BOOL, ssl_username_from_cert), + DEF(SET_BOOL, ntlm_use_winbind), DEF(SET_INT, count), DEF(SET_INT, worker_max_count), @@ -291,12 +294,15 @@ MEMBER(anonymous_username) "anonymous", MEMBER(krb5_keytab) "", MEMBER(gssapi_hostname) "", + MEMBER(winbind_helper_ntlm) "", + MEMBER(winbind_helper_spnego) "", MEMBER(verbose) FALSE, MEMBER(debug) FALSE, MEMBER(debug_passwords) FALSE, MEMBER(ssl_require_client_cert) FALSE, MEMBER(ssl_username_from_cert) FALSE, + MEMBER(ntlm_use_winbind) FALSE, MEMBER(count) 1, MEMBER(worker_max_count) 30, diff -Nrbu dovecot-1.0.1/src/master/master-settings.h dovecot-1.0.1-OK/src/master/master-settings.h --- dovecot-1.0.1/src/master/master-settings.h 2007-06-12 20:42:52.000000000 +0400 +++ dovecot-1.0.1-OK/src/master/master-settings.h 2007-07-03 17:53:14.000000000 +0400 @@ -191,10 +191,13 @@ const char *anonymous_username; const char *krb5_keytab; const char *gssapi_hostname; + const char *winbind_helper_ntlm; + const char *winbind_helper_spnego; bool verbose, debug, debug_passwords; bool ssl_require_client_cert; bool ssl_username_from_cert; + bool ntlm_use_winbind; unsigned int count; unsigned int worker_max_count;
participants (2)
-
Dmitry Butskoy
-
Timo Sirainen