diff options
Diffstat (limited to 'src/gs-auth-pam.c')
-rw-r--r-- | src/gs-auth-pam.c | 843 |
1 files changed, 843 insertions, 0 deletions
diff --git a/src/gs-auth-pam.c b/src/gs-auth-pam.c new file mode 100644 index 0000000..44c8aca --- /dev/null +++ b/src/gs-auth-pam.c @@ -0,0 +1,843 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2006 William Jon McCann <[email protected]> + * Copyright (C) 2006 Ray Strode <[email protected]> + * Copyright (C) 2003 Bill Nottingham <[email protected]> + * Copyright (c) 1993-2003 Jamie Zawinski <[email protected]> + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif + +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <sys/types.h> +#include <pwd.h> +#include <grp.h> +#include <security/pam_appl.h> +#include <signal.h> +#include <errno.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "gs-auth.h" + +#include "subprocs.h" + +/* Some time between Red Hat 4.2 and 7.0, the words were transposed + in the various PAM_x_CRED macro names. Yay! +*/ +#ifndef PAM_REFRESH_CRED +# define PAM_REFRESH_CRED PAM_CRED_REFRESH +#endif + +#ifdef HAVE_PAM_FAIL_DELAY +/* We handle delays ourself.*/ +/* Don't set this to 0 (Linux bug workaround.) */ +# define PAM_NO_DELAY(pamh) pam_fail_delay ((pamh), 1) +#else /* !HAVE_PAM_FAIL_DELAY */ +# define PAM_NO_DELAY(pamh) /* */ +#endif /* !HAVE_PAM_FAIL_DELAY */ + + +/* On SunOS 5.6, and on Linux with PAM 0.64, pam_strerror() takes two args. + On some other Linux systems with some other version of PAM (e.g., + whichever Debian release comes with a 2.2.5 kernel) it takes one arg. + I can't tell which is more "recent" or "correct" behavior, so configure + figures out which is in use for us. Shoot me! +*/ +#ifdef PAM_STRERROR_TWO_ARGS +# define PAM_STRERROR(pamh, status) pam_strerror((pamh), (status)) +#else /* !PAM_STRERROR_TWO_ARGS */ +# define PAM_STRERROR(pamh, status) pam_strerror((status)) +#endif /* !PAM_STRERROR_TWO_ARGS */ + +static gboolean verbose_enabled = FALSE; +static pam_handle_t *pam_handle = NULL; +static gboolean did_we_ask_for_password = FALSE; + +struct pam_closure +{ + const char *username; + GSAuthMessageFunc cb_func; + gpointer cb_data; + int signal_fd; + int result; +}; + +typedef struct +{ + struct pam_closure *closure; + GSAuthMessageStyle style; + const char *msg; + char **resp; + gboolean should_interrupt_stack; +} GsAuthMessageHandlerData; + +static GCond *message_handled_condition; +static GMutex *message_handler_mutex; + +GQuark +gs_auth_error_quark (void) +{ + static GQuark quark = 0; + if (! quark) + { + quark = g_quark_from_static_string ("gs_auth_error"); + } + + return quark; +} + +void +gs_auth_set_verbose (gboolean enabled) +{ + verbose_enabled = enabled; +} + +gboolean +gs_auth_get_verbose (void) +{ + return verbose_enabled; +} + +static GSAuthMessageStyle +pam_style_to_gs_style (int pam_style) +{ + GSAuthMessageStyle style; + + switch (pam_style) + { + case PAM_PROMPT_ECHO_ON: + style = GS_AUTH_MESSAGE_PROMPT_ECHO_ON; + break; + case PAM_PROMPT_ECHO_OFF: + style = GS_AUTH_MESSAGE_PROMPT_ECHO_OFF; + break; + case PAM_ERROR_MSG: + style = GS_AUTH_MESSAGE_ERROR_MSG; + break; + case PAM_TEXT_INFO: + style = GS_AUTH_MESSAGE_TEXT_INFO; + break; + default: + g_assert_not_reached (); + break; + } + + return style; +} + +static gboolean +auth_message_handler (GSAuthMessageStyle style, + const char *msg, + char **response, + gpointer data) +{ + gboolean ret; + + ret = TRUE; + *response = NULL; + + switch (style) + { + case GS_AUTH_MESSAGE_PROMPT_ECHO_ON: + break; + case GS_AUTH_MESSAGE_PROMPT_ECHO_OFF: + if (msg != NULL && g_str_has_prefix (msg, _("Password:"))) + { + did_we_ask_for_password = TRUE; + } + break; + case GS_AUTH_MESSAGE_ERROR_MSG: + break; + case GS_AUTH_MESSAGE_TEXT_INFO: + break; + default: + g_assert_not_reached (); + } + + return ret; +} + +static gboolean +gs_auth_queued_message_handler (GsAuthMessageHandlerData *data) +{ + gboolean res; + + if (gs_auth_get_verbose ()) + { + g_message ("Waiting for lock"); + } + + g_mutex_lock (message_handler_mutex); + + if (gs_auth_get_verbose ()) + { + g_message ("Waiting for response"); + } + + res = data->closure->cb_func (data->style, + data->msg, + data->resp, + data->closure->cb_data); + + data->should_interrupt_stack = res == FALSE; + + g_cond_signal (message_handled_condition); + g_mutex_unlock (message_handler_mutex); + + if (gs_auth_get_verbose ()) + { + g_message ("Got response"); + } + + return FALSE; +} + +static gboolean +gs_auth_run_message_handler (struct pam_closure *c, + GSAuthMessageStyle style, + const char *msg, + char **resp) +{ + GsAuthMessageHandlerData data; + + data.closure = c; + data.style = style; + data.msg = msg; + data.resp = resp; + data.should_interrupt_stack = TRUE; + + g_mutex_lock (message_handler_mutex); + + /* Queue the callback in the gui (the main) thread + */ + g_idle_add ((GSourceFunc) gs_auth_queued_message_handler, &data); + + if (gs_auth_get_verbose ()) + { + g_message ("Waiting for respose to message style %d: '%s'", style, msg); + } + + /* Wait for the response + */ + g_cond_wait (message_handled_condition, + message_handler_mutex); + g_mutex_unlock (message_handler_mutex); + + if (gs_auth_get_verbose ()) + { + g_message ("Got respose to message style %d: interrupt:%d", style, data.should_interrupt_stack); + } + + return data.should_interrupt_stack == FALSE; +} + +static int +pam_conversation (int nmsgs, + const struct pam_message **msg, + struct pam_response **resp, + void *closure) +{ + int replies = 0; + struct pam_response *reply = NULL; + struct pam_closure *c = (struct pam_closure *) closure; + gboolean res; + int ret; + + reply = (struct pam_response *) calloc (nmsgs, sizeof (*reply)); + + if (reply == NULL) + { + return PAM_CONV_ERR; + } + + res = TRUE; + ret = PAM_SUCCESS; + + for (replies = 0; replies < nmsgs && ret == PAM_SUCCESS; replies++) + { + GSAuthMessageStyle style; + char *utf8_msg; + + style = pam_style_to_gs_style (msg [replies]->msg_style); + + utf8_msg = g_locale_to_utf8 (msg [replies]->msg, + -1, + NULL, + NULL, + NULL); + + /* if we couldn't convert text from locale then + * assume utf-8 and hope for the best */ + if (utf8_msg == NULL) + { + char *p; + char *q; + + utf8_msg = g_strdup (msg [replies]->msg); + + p = utf8_msg; + while (*p != '\0' && !g_utf8_validate ((const char *)p, -1, (const char **)&q)) + { + *q = '?'; + p = q + 1; + } + } + + /* handle message locally first */ + auth_message_handler (style, + utf8_msg, + &reply [replies].resp, + NULL); + + if (c->cb_func != NULL) + { + if (gs_auth_get_verbose ()) + { + g_message ("Handling message style %d: '%s'", style, utf8_msg); + } + + /* blocks until the gui responds + */ + res = gs_auth_run_message_handler (c, + style, + utf8_msg, + &reply [replies].resp); + + if (gs_auth_get_verbose ()) + { + g_message ("Msg handler returned %d", res); + } + + /* If the handler returns FALSE - interrupt the PAM stack */ + if (res) + { + reply [replies].resp_retcode = PAM_SUCCESS; + } + else + { + int i; + for (i = 0; i <= replies; i++) + { + free (reply [i].resp); + } + free (reply); + reply = NULL; + ret = PAM_CONV_ERR; + } + } + + g_free (utf8_msg); + } + + *resp = reply; + + return ret; +} + +static gboolean +close_pam_handle (int status) +{ + + if (pam_handle != NULL) + { + int status2; + + status2 = pam_end (pam_handle, status); + pam_handle = NULL; + + if (gs_auth_get_verbose ()) + { + g_message (" pam_end (...) ==> %d (%s)", + status2, + (status2 == PAM_SUCCESS ? "Success" : "Failure")); + } + } + + if (message_handled_condition != NULL) + { + g_cond_free (message_handled_condition); + message_handled_condition = NULL; + } + + if (message_handler_mutex != NULL) + { + g_mutex_free (message_handler_mutex); + message_handler_mutex = NULL; + } + + return TRUE; +} + +static gboolean +create_pam_handle (const char *username, + const char *display, + struct pam_conv *conv, + int *status_code) +{ + int status; + const char *service = PAM_SERVICE_NAME; + char *disp; + gboolean ret; + + if (pam_handle != NULL) + { + g_warning ("create_pam_handle: Stale pam handle around, cleaning up"); + close_pam_handle (PAM_SUCCESS); + } + + /* init things */ + pam_handle = NULL; + status = -1; + disp = NULL; + ret = TRUE; + + /* Initialize a PAM session for the user */ + if ((status = pam_start (service, username, conv, &pam_handle)) != PAM_SUCCESS) + { + pam_handle = NULL; + g_warning (_("Unable to establish service %s: %s\n"), + service, + PAM_STRERROR (NULL, status)); + + if (status_code != NULL) + { + *status_code = status; + } + + ret = FALSE; + goto out; + } + + if (gs_auth_get_verbose ()) + { + g_message ("pam_start (\"%s\", \"%s\", ...) ==> %d (%s)", + service, + username, + status, + PAM_STRERROR (pam_handle, status)); + } + + disp = g_strdup (display); + if (disp == NULL) + { + disp = g_strdup (":0.0"); + } + + if ((status = pam_set_item (pam_handle, PAM_TTY, disp)) != PAM_SUCCESS) + { + g_warning (_("Can't set PAM_TTY=%s"), display); + + if (status_code != NULL) + { + *status_code = status; + } + + ret = FALSE; + goto out; + } + + ret = TRUE; + message_handled_condition = g_cond_new (); + message_handler_mutex = g_mutex_new (); + +out: + if (status_code != NULL) + { + *status_code = status; + } + + g_free (disp); + + return ret; +} + +static void +set_pam_error (GError **error, + int status) +{ + if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN) + { + char *msg; + + if (did_we_ask_for_password) + { + msg = g_strdup (_("Incorrect password.")); + } + else + { + msg = g_strdup (_("Authentication failed.")); + } + + g_set_error (error, + GS_AUTH_ERROR, + GS_AUTH_ERROR_AUTH_ERROR, + "%s", + msg); + g_free (msg); + } + else if (status == PAM_PERM_DENIED) + { + g_set_error (error, + GS_AUTH_ERROR, + GS_AUTH_ERROR_AUTH_DENIED, + "%s", + _("Not permitted to gain access at this time.")); + } + else if (status == PAM_ACCT_EXPIRED) + { + g_set_error (error, + GS_AUTH_ERROR, + GS_AUTH_ERROR_AUTH_DENIED, + "%s", + _("No longer permitted to access the system.")); + } + +} + +static int +gs_auth_thread_func (int auth_operation_fd) +{ + static const int flags = 0; + int status; + int status2; + struct timespec timeout; + sigset_t set; + const void *p; + + timeout.tv_sec = 0; + timeout.tv_nsec = 1; + + set = block_sigchld (); + + status = pam_authenticate (pam_handle, flags); + + sigtimedwait (&set, NULL, &timeout); + unblock_sigchld (); + + if (gs_auth_get_verbose ()) + { + g_message (" pam_authenticate (...) ==> %d (%s)", + status, + PAM_STRERROR (pam_handle, status)); + } + + if (status != PAM_SUCCESS) + { + goto done; + } + + if ((status = pam_get_item (pam_handle, PAM_USER, &p)) != PAM_SUCCESS) + { + /* is not really an auth problem, but it will + pretty much look as such, it shouldn't really + happen */ + goto done; + } + + /* We don't actually care if the account modules fail or succeed, + * but we need to run them anyway because certain pam modules + * depend on side effects of the account modules getting run. + */ + status2 = pam_acct_mgmt (pam_handle, 0); + + if (gs_auth_get_verbose ()) + { + g_message ("pam_acct_mgmt (...) ==> %d (%s)\n", + status2, + PAM_STRERROR (pam_handle, status2)); + } + + /* FIXME: should we handle these? */ + switch (status2) + { + case PAM_SUCCESS: + break; + case PAM_NEW_AUTHTOK_REQD: + break; + case PAM_AUTHINFO_UNAVAIL: + break; + case PAM_ACCT_EXPIRED: + break; + case PAM_PERM_DENIED: + break; + default : + break; + } + + /* Each time we successfully authenticate, refresh credentials, + for Kerberos/AFS/DCE/etc. If this fails, just ignore that + failure and blunder along; it shouldn't matter. + + Note: this used to be PAM_REFRESH_CRED instead of + PAM_REINITIALIZE_CRED, but Jason Heiss <[email protected]> + says that the Linux PAM library ignores that one, and only refreshes + credentials when using PAM_REINITIALIZE_CRED. + */ + status2 = pam_setcred (pam_handle, PAM_REINITIALIZE_CRED); + if (gs_auth_get_verbose ()) + { + g_message (" pam_setcred (...) ==> %d (%s)", + status2, + PAM_STRERROR (pam_handle, status2)); + } + +done: + /* we're done, close the fd and wake up the main + * loop + */ + close (auth_operation_fd); + + return status; +} + +static gboolean +gs_auth_loop_quit (GIOChannel *source, + GIOCondition condition, + gboolean *thread_done) +{ + *thread_done = TRUE; + gtk_main_quit (); + return FALSE; +} + +static gboolean +gs_auth_pam_verify_user (pam_handle_t *handle, + int *status) +{ + GThread *auth_thread; + GIOChannel *channel; + guint watch_id; + int auth_operation_fds[2]; + int auth_status; + gboolean thread_done; + + channel = NULL; + watch_id = 0; + auth_status = PAM_AUTH_ERR; + + /* This pipe gives us a set of fds we can hook into + * the event loop to be notified when our helper thread + * is ready to be reaped. + */ + if (pipe (auth_operation_fds) < 0) + { + goto out; + } + + if (fcntl (auth_operation_fds[0], F_SETFD, FD_CLOEXEC) < 0) + { + close (auth_operation_fds[0]); + close (auth_operation_fds[1]); + goto out; + } + + if (fcntl (auth_operation_fds[1], F_SETFD, FD_CLOEXEC) < 0) + { + close (auth_operation_fds[0]); + close (auth_operation_fds[1]); + goto out; + } + + channel = g_io_channel_unix_new (auth_operation_fds[0]); + + /* we use a recursive main loop to process ui events + * while we wait on a thread to handle the blocking parts + * of pam authentication. + */ + thread_done = FALSE; + watch_id = g_io_add_watch (channel, G_IO_ERR | G_IO_HUP, + (GIOFunc) gs_auth_loop_quit, &thread_done); + + auth_thread = g_thread_create ((GThreadFunc) gs_auth_thread_func, + GINT_TO_POINTER (auth_operation_fds[1]), + TRUE, NULL); + + if (auth_thread == NULL) + { + goto out; + } + + gtk_main (); + + /* if the event loop was quit before the thread is done then we can't + * reap the thread without blocking on it finishing. The + * thread may not ever finish though if the pam module is blocking. + * + * The only time the event loop is going to stop when the thread isn't + * done, however, is if the dialog quits early (from, e.g., "cancel"), + * so we can just exit. An alternative option would be to switch to + * using pthreads directly and calling pthread_cancel. + */ + if (!thread_done) + { + raise (SIGTERM); + } + + auth_status = GPOINTER_TO_INT (g_thread_join (auth_thread)); + +out: + if (watch_id != 0) + { + g_source_remove (watch_id); + } + + if (channel != NULL) + { + g_io_channel_unref (channel); + } + + if (status) + { + *status = auth_status; + } + + return auth_status == PAM_SUCCESS; +} + +gboolean +gs_auth_verify_user (const char *username, + const char *display, + GSAuthMessageFunc func, + gpointer data, + GError **error) +{ + int status = -1; + struct pam_conv conv; + struct pam_closure c; + struct passwd *pwent; + + pwent = getpwnam (username); + if (pwent == NULL) + { + return FALSE; + } + + c.username = username; + c.cb_func = func; + c.cb_data = data; + + conv.conv = &pam_conversation; + conv.appdata_ptr = (void *) &c; + + /* Initialize PAM. */ + create_pam_handle (username, display, &conv, &status); + if (status != PAM_SUCCESS) + { + goto done; + } + + pam_set_item (pam_handle, PAM_USER_PROMPT, _("Username:")); + + PAM_NO_DELAY(pam_handle); + + did_we_ask_for_password = FALSE; + if (! gs_auth_pam_verify_user (pam_handle, &status)) + { + goto done; + } + +done: + if (status != PAM_SUCCESS) + { + set_pam_error (error, status); + } + + close_pam_handle (status); + + return (status == PAM_SUCCESS ? TRUE : FALSE); +} + +gboolean +gs_auth_init (void) +{ + return TRUE; +} + +gboolean +gs_auth_priv_init (void) +{ + /* We have nothing to do at init-time. + However, we might as well do some error checking. + If "/etc/pam.d" exists and is a directory, but "/etc/pam.d/xlock" + does not exist, warn that PAM probably isn't going to work. + + This is a priv-init instead of a non-priv init in case the directory + is unreadable or something (don't know if that actually happens.) + */ + const char dir [] = "/etc/pam.d"; + const char file [] = "/etc/pam.d/" PAM_SERVICE_NAME; + const char file2 [] = "/etc/pam.conf"; + struct stat st; + + if (g_stat (dir, &st) == 0 && st.st_mode & S_IFDIR) + { + if (g_stat (file, &st) != 0) + { + g_warning ("%s does not exist.\n" + "Authentication via PAM is unlikely to work.", + file); + } + } + else if (g_stat (file2, &st) == 0) + { + FILE *f = g_fopen (file2, "r"); + if (f) + { + gboolean ok = FALSE; + char buf[255]; + while (fgets (buf, sizeof(buf), f)) + { + if (strstr (buf, PAM_SERVICE_NAME)) + { + ok = TRUE; + break; + } + } + + fclose (f); + if (!ok) + { + g_warning ("%s does not list the `%s' service.\n" + "Authentication via PAM is unlikely to work.", + file2, PAM_SERVICE_NAME); + } + } + /* else warn about file2 existing but being unreadable? */ + } + else + { + g_warning ("Neither %s nor %s exist.\n" + "Authentication via PAM is unlikely to work.", + file2, file); + } + + /* Return true anyway, just in case. */ + return TRUE; +} |