/* * This file is part of Cockpit. * * Copyright (C) 2013 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ #include "config.h" #include "cockpitauth.h" #include "cockpitws.h" #include "common/cockpittest.h" #include "common/cockpitwebserver.h" #include #include #include #include #include #include #include #include static gboolean mock_kdc_available; static void mock_kdc_up (void); static void mock_kdc_down (void); typedef struct { CockpitAuth *auth; krb5_context krb; krb5_ccache ccache; char *ccache_name; } TestCase; static void setup (TestCase *test, gconstpointer data) { krb5_get_init_creds_opt *opt; krb5_principal principal; krb5_error_code code; krb5_creds creds; OM_uint32 status; OM_uint32 minor; gchar *name; if (!mock_kdc_available) return; test->auth = cockpit_auth_new (FALSE); mock_kdc_up (); code = krb5_init_context (&test->krb); g_assert (code != ENOMEM); if (code != 0) { g_critical ("couldn't create krb context: %s", krb5_get_error_message (NULL, code)); return; } /* Initialize the client credential cache */ if (krb5_cc_new_unique (test->krb, "MEMORY", NULL, &test->ccache) != 0) g_assert_not_reached (); name = g_strdup_printf ("%s@COCKPIT.MOCK", g_get_user_name ()); /* Do a kerberos authentication */ if (krb5_parse_name (test->krb, name, &principal)) g_assert_not_reached (); if (krb5_get_init_creds_opt_alloc (test->krb, &opt)) g_assert_not_reached (); if (krb5_get_init_creds_opt_set_out_ccache (test->krb, opt, test->ccache)) g_assert_not_reached (); code = krb5_get_init_creds_password (test->krb, &creds, principal, "marmalade", NULL, NULL, 0, NULL, opt); krb5_free_principal (test->krb, principal); krb5_get_init_creds_opt_free (test->krb, opt); g_assert (code != ENOMEM); if (code != 0) { g_critical ("couldn't kinit for %s: %s", name, krb5_get_error_message (test->krb, code)); return; } krb5_free_cred_contents (test->krb, &creds); g_free (name); if (krb5_cc_get_full_name (test->krb, test->ccache, &test->ccache_name) != 0) g_assert_not_reached (); /* Sets the credential cache GSSAPI to use (for this thread) */ status = gss_krb5_ccache_name (&minor, test->ccache_name, NULL); g_assert_cmpint (status, ==, 0); } static void teardown (TestCase *test, gconstpointer data) { if (!mock_kdc_available) return; if (test->ccache) krb5_cc_close (test->krb, test->ccache); if (test->ccache_name) krb5_free_string (test->krb, test->ccache_name); if (test->krb) krb5_free_context (test->krb); mock_kdc_down (); g_clear_object (&test->auth); } static void on_ready_get_result (GObject *source, GAsyncResult *result, gpointer user_data) { GAsyncResult **retval = user_data; g_assert (retval != NULL); g_assert (*retval == NULL); *retval = g_object_ref (result); } static void assert_gss_status_msg (const gchar *domain, const gchar *file, gint line, const gchar *func, const gchar *expr, const gchar *cmp, OM_uint32 expected, OM_uint32 major_status, OM_uint32 minor_status) { OM_uint32 major, minor; OM_uint32 ctx = 0; gss_buffer_desc status; gboolean had_minor; GString *result; result = g_string_new (""); g_string_printf (result, "assertion failed (%s): (%u %s %u)", expr, (guint)expected, cmp, (guint)major_status); for (;;) { major = gss_display_status (&minor, major_status, GSS_C_GSS_CODE, GSS_C_NO_OID, &ctx, &status); if (GSS_ERROR (major)) break; if (result->len > 0) g_string_append (result, ": "); g_string_append_len (result, status.value, status.length); gss_release_buffer (&minor, &status); if (!ctx) break; } ctx = 0; had_minor = FALSE; for (;;) { major = gss_display_status (&minor, minor_status, GSS_C_MECH_CODE, GSS_C_NULL_OID, &ctx, &status); if (GSS_ERROR (major)) break; if (status.length) { if (!had_minor) g_string_append (result, " ("); else g_string_append (result, ", "); had_minor = TRUE; g_string_append_len (result, status.value, status.length); } gss_release_buffer (&minor, &status); if (!ctx) break; } if (had_minor) g_string_append (result, ")"); g_assertion_message (domain, file ,line, func, result->str); g_string_free (result, TRUE); } #define assert_gss_status(status, cmp, expect, minor) \ do { OM_uint32 __st = (status); OM_uint32 __ex = (expect); \ if (__st cmp __ex) ; else \ assert_gss_status_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ #expect " " #cmp " " #status, #cmp, (__ex), (__st), (minor)); \ } while (0); static void build_authorization_header (GHashTable *headers, gss_buffer_desc *buffer) { gchar *encoded; gchar *value; if (buffer->length) { encoded = g_base64_encode (buffer->value, buffer->length); value = g_strdup_printf ("Negotiate %s", encoded); g_free (encoded); } else { value = g_strdup ("Negotiate"); } g_hash_table_replace (headers, g_strdup ("Authorization"), value); } static void include_cookie_as_if_client (GHashTable *resp_headers, GHashTable *req_headers) { gchar *cookie; gchar *end; cookie = g_strdup (g_hash_table_lookup (resp_headers, "Set-Cookie")); g_assert (cookie != NULL); end = strchr (cookie, ';'); g_assert (end != NULL); end[0] = '\0'; g_hash_table_insert (req_headers, g_strdup ("Cookie"), cookie); } static void test_authenticate (TestCase *test, gconstpointer data) { GHashTable *in_headers; GHashTable *out_headers; GAsyncResult *result = NULL; gss_ctx_id_t ctx = GSS_C_NO_CONTEXT; OM_uint32 status; OM_uint32 minor; gss_buffer_desc input = GSS_C_EMPTY_BUFFER; gss_buffer_desc output = GSS_C_EMPTY_BUFFER; gss_name_t name = GSS_C_NO_NAME; OM_uint32 flags = 0; CockpitCreds *creds; CockpitWebService *service; JsonObject *response; GError *error = NULL; g_setenv ("COCKPIT_TEST_KEEP_PATH", "1", TRUE); if (!mock_kdc_available) { cockpit_test_skip ("mock kdc not available to test against"); return; } in_headers = cockpit_web_server_new_table (); out_headers = cockpit_web_server_new_table (); input.value = "host@localhost"; input.length = strlen (input.value) + 1; status = gss_import_name (&minor, &input, GSS_C_NT_HOSTBASED_SERVICE, &name); assert_gss_status (status, ==, GSS_S_COMPLETE, minor); input.length = 0; status = gss_init_sec_context (&minor, GSS_C_NO_CREDENTIAL, &ctx, name, GSS_C_NO_OID, GSS_C_MUTUAL_FLAG, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS, &input, NULL, &output, &flags, NULL); assert_gss_status (status, ==, GSS_S_CONTINUE_NEEDED, minor); build_authorization_header (in_headers, &output); gss_release_buffer (&minor, &output); cockpit_auth_login_async (test->auth, "/cockpit+test", NULL, in_headers, on_ready_get_result, &result); g_hash_table_unref (in_headers); while (result == NULL) g_main_context_iteration (NULL, TRUE); response = cockpit_auth_login_finish (test->auth, result, NULL, out_headers, &error); g_object_unref (result); g_assert_no_error (error); g_assert (response != NULL); json_object_unref (response); gss_release_name (&minor, &name); gss_delete_sec_context (&minor, &ctx, &output); include_cookie_as_if_client (out_headers, out_headers); service = cockpit_auth_check_cookie (test->auth, "/cockpit+test", out_headers); g_assert (service != NULL); creds = cockpit_web_service_get_creds (service); g_assert_cmpstr ("cockpit+test", ==, cockpit_creds_get_application (creds)); g_assert (NULL == cockpit_creds_get_password (creds)); g_unsetenv ("COCKPIT_TEST_KEEP_PATH"); g_hash_table_unref (out_headers); g_object_unref (service); } struct { GHashTable *environ; gboolean stopped; GPid pid; } mock_kdc; static void on_kdc_child (GPid pid, gint status, gpointer user_data) { GError *error = NULL; mock_kdc_available = FALSE; if (!mock_kdc.stopped) { g_spawn_check_exit_status (status, &error); g_assert_no_error (error); g_critical ("mock-kdc exited prematurely"); } } static void on_kdc_setup (gpointer user_data) { /* Kill all sub processes when this process exits */ if (prctl (PR_SET_PDEATHSIG, SIGTERM) < 0) g_critical ("prctl failed: %s", g_strerror (errno)); /* Start a new session for this process */ setsid (); } static void mock_kdc_start (void) { GString *input; GError *error = NULL; gint out_fd; gsize len; gssize ret; gchar **vars; gchar *pos; gint i; const gchar *argv[] = { SRCDIR "/src/ws/mock-kdc", NULL }; mock_kdc_available = FALSE; g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, on_kdc_setup, NULL, &mock_kdc.pid, NULL, &out_fd, NULL, &error); g_assert_no_error (error); g_child_watch_add (mock_kdc.pid, on_kdc_child, NULL); /* * mock-kdc prints environment vars on stdout, and then closes stdout * This also lets us know when it has initialized. */ input = g_string_new (""); for (;;) { len = input->len; g_string_set_size (input, len + 256); ret = read (out_fd, input->str + len, 256); if (ret < 0) { if (errno != EAGAIN && errno != EINTR) g_error ("couldn't read from mock-kdc: %s", g_strerror (errno)); break; } else { g_string_set_size (input, len + ret); if (strstr (input->str, "starting...")) { mock_kdc_available = TRUE; break; } else if (ret == 0) { break; } } } /* Parse into a table of environment variables */ mock_kdc.environ = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); vars = g_strsplit (input->str, "\n", -1); for (i = 0; vars[i] != NULL; i++) { pos = strchr (vars[i], '='); if (pos) { *pos = '\0'; g_hash_table_replace (mock_kdc.environ, vars[i], pos + 1); } else { g_free (vars[i]); } } g_string_free (input, TRUE); g_free (vars); } static void mock_kdc_up (void) { GHashTableIter iter; const gchar *name; const gchar *value; g_hash_table_iter_init (&iter, mock_kdc.environ); while (g_hash_table_iter_next (&iter, (gpointer *)&name, (gpointer *)&value)) g_setenv (name, value, TRUE); /* Explicitly tell server side of GSSAPI about our keytab */ value = g_hash_table_lookup (mock_kdc.environ, "KRB5_KTNAME"); if (value) g_setenv ("KRB5_KTNAME", value, TRUE); } static void mock_kdc_down (void) { GHashTableIter iter; const gchar *name; g_hash_table_iter_init (&iter, mock_kdc.environ); while (g_hash_table_iter_next (&iter, (gpointer *)&name, NULL)) g_unsetenv (name); } static void mock_kdc_stop (void) { mock_kdc.stopped = TRUE; if (mock_kdc_available) { if (kill (-mock_kdc.pid, SIGTERM) < 0) g_error ("couldn't kill mock-kdc: %s", g_strerror (errno)); } if (mock_kdc.environ) g_hash_table_destroy (mock_kdc.environ); } int main (int argc, char *argv[]) { int ret; cockpit_ws_session_program = BUILDDIR "/cockpit-session"; cockpit_test_init (&argc, &argv); /* Try to debug crashing during tests */ signal (SIGABRT, cockpit_test_signal_backtrace); if (g_strcmp0 (g_get_user_name (), "root") != 0) mock_kdc_start (); g_test_add ("/kerberos/authenticate", TestCase, NULL, setup, test_authenticate, teardown); ret = g_test_run (); mock_kdc_stop (); return ret; }