/*
* This file is part of Cockpit.
*
* Copyright (C) 2014 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 "cockpitdbusinternal.h"
#include "common/cockpitpipe.h"
#include
#include
#include
#include
#include
#include
#include
const gchar *cockpit_bridge_path_passwd = "/etc/passwd";
const gchar *cockpit_bridge_path_group = "/etc/group";
const gchar *cockpit_bridge_path_shadow = "/etc/shadow";
const gchar *cockpit_bridge_path_newusers = PATH_NEWUSERS;
const gchar *cockpit_bridge_path_chpasswd = PATH_CHPASSWD;
const gchar *cockpit_bridge_path_usermod = PATH_USERMOD;
#ifdef HAVE_NEWUSERS_CRYPT_METHOD
gboolean cockpit_bridge_have_newusers_crypt_method = TRUE;
#else
gboolean cockpit_bridge_have_newusers_crypt_method = FALSE;
#endif
static GVariant *
setup_get_property (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *property_name,
GError **error,
gpointer user_data)
{
const gchar *mechanisms[] = { "passwd1", NULL, };
g_return_val_if_fail (g_str_equal (property_name, "Mechanisms"), NULL);
return g_variant_new_strv (mechanisms, -1);
}
static GDBusPropertyInfo setup_mechanisms_property = {
-1, "Mechanisms", "as", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
};
static GDBusPropertyInfo *setup_properties[] = {
&setup_mechanisms_property,
NULL
};
static gboolean
fgetpwent_callback (void (* callback) (struct passwd *, gpointer),
gpointer user_data)
{
struct passwd pwb;
struct passwd *pw;
gchar *buffer;
gsize buflen;
FILE *fp;
int ret;
fp = fopen (cockpit_bridge_path_passwd, "r");
if (fp == NULL)
{
g_message ("unable to open %s: %s", cockpit_bridge_path_passwd, g_strerror (errno));
return FALSE;
}
buflen = 16 * 1024;
buffer = g_malloc (buflen);
for (;;)
{
ret = fgetpwent_r (fp, &pwb, buffer, buflen, &pw);
if (ret == 0)
{
callback (pw, user_data);
}
else
{
g_free (buffer);
fclose (fp);
return (ret == ENOENT);
}
}
}
static gboolean
fgetspent_callback (void (* callback) (struct spwd *, gpointer),
gpointer user_data)
{
struct spwd spb;
struct spwd *sp;
gchar *buffer;
gsize buflen;
FILE *fp;
int ret;
fp = fopen (cockpit_bridge_path_shadow, "r");
if (fp == NULL)
{
g_message ("unable to open %s: %s", cockpit_bridge_path_shadow, g_strerror (errno));
return FALSE;
}
buflen = 16 * 1024;
buffer = g_malloc (buflen);
for (;;)
{
ret = fgetspent_r (fp, &spb, buffer, buflen, &sp);
if (ret == 0)
{
callback (sp, user_data);
}
else
{
g_free (buffer);
fclose (fp);
return (ret == ENOENT);
}
}
}
static gboolean
fgetgrent_callback (void (* callback) (struct group *, gpointer),
gpointer user_data)
{
struct group grb;
struct group *gr;
gchar *buffer;
gsize buflen;
FILE *fp;
int ret;
fp = fopen (cockpit_bridge_path_group, "r");
if (fp == NULL)
{
g_message ("unable to open %s: %s", cockpit_bridge_path_group, g_strerror (errno));
return FALSE;
}
buflen = 16 * 1024;
buffer = g_malloc (buflen);
for (;;)
{
ret = fgetgrent_r (fp, &grb, buffer, buflen, &gr);
if (ret == 0)
{
callback (gr, user_data);
}
else
{
g_free (buffer);
fclose (fp);
return (ret == ENOENT);
}
}
}
static gboolean
is_system_uid (int uid)
{
/* We could make this read from login.defs */
return (uid != 0 && uid < 1000);
}
static void
add_name_to_array (struct passwd *pw,
gpointer user_data)
{
if (!is_system_uid (pw->pw_uid))
g_ptr_array_add (user_data, g_strdup (pw->pw_name));
}
static void
add_group_to_array (struct group *gr,
gpointer user_data)
{
g_ptr_array_add (user_data, g_strdup (gr->gr_name));
}
static void
setup_prepare_passwd1 (GVariant *parameters,
GDBusMethodInvocation *invocation)
{
const gchar *mechanism;
GPtrArray *names;
GPtrArray *groups;
GVariant *result = NULL;
g_variant_get (parameters, "(&s)", &mechanism);
names = g_ptr_array_new_with_free_func (g_free);
groups = g_ptr_array_new_with_free_func (g_free);
if (!g_str_equal (mechanism, "passwd1"))
{
g_message ("unsupported setup mechanism: %s", mechanism);
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
N_("Unsupported setup mechanism"));
}
else if (fgetpwent_callback (add_name_to_array, names) &&
fgetgrent_callback (add_group_to_array, groups))
{
/* We don't need to prepare anything */
result = g_variant_new ("(@as@as)",
g_variant_new_strv ((const gchar * const *)names->pdata, names->len),
g_variant_new_strv ((const gchar * const *)groups->pdata, groups->len));
g_dbus_method_invocation_return_value (invocation, g_variant_new ("(v)", result));
}
else
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
N_("Couldn't list users"));
}
g_ptr_array_free (groups, TRUE);
g_ptr_array_free (names, TRUE);
}
typedef struct {
GHashTable *gids; /* Group IDs to name mapping */
GHashTable *members; /* Information about membership */
GHashTable *users; /* Information about loaded users */
GPtrArray *pwdata; /* Results */
} TransferAdmin1;
static void
filter_and_load_group (struct group *gr,
gpointer user_data)
{
TransferAdmin1 *context = user_data;
GHashTable *table;
gint j;
g_hash_table_insert (context->gids, GINT_TO_POINTER (gr->gr_gid), g_strdup (gr->gr_name));
table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
for (j = 0; gr->gr_mem[j] != NULL; j++)
g_hash_table_add (table, g_strdup (gr->gr_mem[j]));
g_hash_table_insert (context->members, g_strdup (gr->gr_name), table);
}
static void
filter_and_load_user (struct passwd *pw,
gpointer user_data)
{
TransferAdmin1 *context = user_data;
GHashTable *table;
gchar *group;
g_return_if_fail (pw->pw_name != NULL);
if (!is_system_uid (pw->pw_uid))
{
g_hash_table_insert (context->users, g_strdup (pw->pw_name),
g_strdup_printf ("%s:%s:%s", pw->pw_gecos, pw->pw_dir, pw->pw_shell));
group = g_hash_table_lookup (context->gids, GINT_TO_POINTER (pw->pw_gid));
if (group)
{
table = g_hash_table_lookup (context->members, group);
if (table)
g_hash_table_add (table, g_strdup (pw->pw_name));
}
}
}
static void
filter_and_add_passwd (struct spwd *sp,
gpointer user_data)
{
TransferAdmin1 *context = user_data;
gchar *gecos_dir_shell;
gecos_dir_shell = g_hash_table_lookup (context->users, sp->sp_namp);
if (gecos_dir_shell && sp->sp_pwdp && strlen (sp->sp_pwdp) >= 4)
{
g_ptr_array_add (context->pwdata,
g_strdup_printf ("%s:%s:::%s", sp->sp_namp, sp->sp_pwdp, gecos_dir_shell));
/* Remove this one, so we can track which ones we didn't transfer */
g_hash_table_remove (context->users, sp->sp_namp);
}
}
static void
build_group_lines (GHashTable *membership,
GHashTable *exclude,
GPtrArray *lines)
{
GHashTableIter giter, miter;
gpointer name;
gpointer user;
gpointer table;
GString *memlist;
gboolean first;
/* Build the group listing */
g_hash_table_iter_init (&giter, membership);
while (g_hash_table_iter_next (&giter, &name, &table))
{
memlist = g_string_new ("");
g_string_append_printf (memlist, "%s:::", (gchar *)name);
first = TRUE;
g_hash_table_iter_init (&miter, table);
while (g_hash_table_iter_next (&miter, &user, NULL))
{
if (g_hash_table_lookup (exclude, user))
continue;
if (!first)
g_string_append_c (memlist, ',');
first = FALSE;
g_string_append (memlist, user);
}
g_ptr_array_add (lines, g_string_free (memlist, FALSE));
}
}
static void
setup_transfer_passwd1 (GVariant *parameters,
GDBusMethodInvocation *invocation)
{
TransferAdmin1 *context = NULL;
const gchar *mechanism;
GVariant *prepared;
GVariant *result;
GPtrArray *grdata;
g_variant_get (parameters, "(&sv)", &mechanism, &prepared);
if (!g_str_equal (mechanism, "passwd1"))
{
g_message ("unsupported setup mechanism: %s", mechanism);
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
N_("Unsupported setup mechanism"));
goto out;
}
if (!g_variant_is_of_type (prepared, G_VARIANT_TYPE ("(asas)")))
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
N_("Bad data passed for passwd1 mechanism"));
goto out;
}
context = g_new0 (TransferAdmin1, 1);
context->members = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_unref);
context->gids = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
context->users = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
context->pwdata = g_ptr_array_new_with_free_func (g_free);
if (!fgetgrent_callback (filter_and_load_group, context) ||
!fgetpwent_callback (filter_and_load_user, context) ||
!fgetspent_callback (filter_and_add_passwd, context))
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
N_("Couldn't load user data"));
}
else
{
/* Build the group listing, excluding any remaining users */
grdata = g_ptr_array_new_with_free_func (g_free);
build_group_lines (context->members, context->users, grdata);
result = g_variant_new ("(@as@as)",
g_variant_new_strv ((const gchar *const *)context->pwdata->pdata, context->pwdata->len),
g_variant_new_strv ((const gchar *const *)grdata->pdata, grdata->len));
g_ptr_array_free (grdata, TRUE);
g_dbus_method_invocation_return_value (invocation, g_variant_new ("(v)", result));
}
out:
if (context)
{
g_hash_table_unref (context->members);
g_hash_table_unref (context->gids);
g_hash_table_unref (context->users);
g_ptr_array_free (context->pwdata, TRUE);
g_free (context);
}
g_variant_unref (prepared);
}
typedef struct {
GDBusMethodInvocation *invocation;
GBytes *chpasswd;
GHashTable *usermod;
} CommitAdmin1;
static void
commit_passwd1_free (gpointer data)
{
CommitAdmin1 *context = data;
g_bytes_unref (context->chpasswd);
g_object_unref (context->invocation);
g_hash_table_unref (context->usermod);
g_free (context);
}
static gboolean
check_pipe_exit_status (CockpitPipe *pipe,
const gchar *problem,
const gchar *prefix)
{
GError *error = NULL;
gint status;
if (problem == NULL ||
g_str_equal (problem, "internal-error"))
{
status = cockpit_pipe_exit_status (pipe);
if (!g_spawn_check_exit_status (status, &error))
{
g_message ("%s: %s", prefix, error->message);
g_error_free (error);
return FALSE;
}
}
else if (problem)
{
g_message ("%s: %s", prefix, problem);
return FALSE;
}
return TRUE;
}
static void
perform_usermod (CommitAdmin1 *context);
static void
on_usermod_close (CockpitPipe *pipe,
const gchar *problem,
gpointer user_data)
{
CommitAdmin1 *context = user_data;
if (!check_pipe_exit_status (pipe, problem, "couldn't run usermod command"))
{
g_dbus_method_invocation_return_error (context->invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
N_("Couldn't change user groups"));
commit_passwd1_free (context);
}
else
{
perform_usermod (context);
}
g_object_unref (pipe);
}
static void
perform_usermod (CommitAdmin1 *context)
{
CockpitPipe *pipe;
GHashTableIter iter;
gpointer name;
gpointer grouplist;
const gchar *argv[] = { cockpit_bridge_path_usermod, "xx", "--append", "--group", "yy", NULL };
g_hash_table_iter_init (&iter, context->usermod);
if (g_hash_table_iter_next (&iter, &name, &grouplist))
{
argv[1] = (gchar *)name;
argv[4] = ((GString *)grouplist)->str;
g_debug ("adding user '%s' to groups: %s", argv[1], argv[4]);
pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
g_hash_table_iter_remove (&iter);
g_signal_connect (pipe, "close", G_CALLBACK (on_usermod_close), context);
cockpit_pipe_close (pipe, NULL);
}
else
{
/* All done, success */
g_dbus_method_invocation_return_value (context->invocation, NULL);
commit_passwd1_free (context);
}
}
static void
on_chpasswd_close (CockpitPipe *pipe,
const gchar *problem,
gpointer user_data)
{
CommitAdmin1 *context = user_data;
if (!check_pipe_exit_status (pipe, problem, "couldn't run chpasswd command"))
{
g_dbus_method_invocation_return_error (context->invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
N_("Couldn't change user password"));
commit_passwd1_free (context);
}
else
{
perform_usermod (context);
}
g_object_unref (pipe);
}
static void
on_newusers_close (CockpitPipe *pipe,
const gchar *problem,
gpointer user_data)
{
CommitAdmin1 *context = user_data;
CockpitPipe *next;
const gchar *argv[] = { cockpit_bridge_path_chpasswd, "--encrypted", NULL };
if (!check_pipe_exit_status (pipe, problem, "couldn't run newusers command"))
{
g_dbus_method_invocation_return_error (context->invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
N_("Couldn't create new users"));
commit_passwd1_free (context);
}
else
{
g_debug ("batch changing user passwords");
next = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
g_signal_connect (next, "close", G_CALLBACK (on_chpasswd_close), context);
cockpit_pipe_write (next, context->chpasswd);
cockpit_pipe_close (next, NULL);
}
g_object_unref (pipe);
}
static void
add_name_to_hashtable (struct passwd *pw,
gpointer user_data)
{
g_hash_table_add (user_data, g_strdup (pw->pw_name));
}
static void
add_group_to_hashtable (struct group *gr,
gpointer user_data)
{
g_hash_table_add (user_data, g_strdup (gr->gr_name));
}
static void
string_free (gpointer data)
{
g_string_free (data, TRUE);
}
static void
setup_commit_passwd1 (GVariant *parameters,
GDBusMethodInvocation *invocation)
{
const gchar *mechanism;
GVariant *transferred = NULL;
const gchar **lines;
GHashTable *users = NULL;
GHashTable *groups = NULL;
GString *chpasswd = NULL;
GString *newusers = NULL;
CommitAdmin1 *context;
CockpitPipe *pipe;
gsize length, i, j;
gchar **parts;
GBytes *bytes;
GVariant *pwdata = NULL;
GVariant *grdata = NULL;
GHashTable *usermod;
gchar **memlist;
GString *string;
gboolean user_exists;
/* We are getting crypted passwords so we need to use
* --crypt-method=NONE with newusers and chpasswd so that the string
* is installed unchanged. Unfortunately, newusers might or might
* not support the --crypt-method option, depending on whether it
* was compiled with or without PAM. When the option is missing, we
* fix up the password afterwards via chpasswd.
*
* However, newusers needs some valid password to create new users.
* Thus, we need a good random string that passes all password
* quality criteria, and we just use the crpyted password for that.
*/
const gchar *argv[] = { cockpit_bridge_path_newusers, "--crypt-method=NONE", NULL };
if (!cockpit_bridge_have_newusers_crypt_method)
argv[1] = NULL;
g_variant_get (parameters, "(&sv)", &mechanism, &transferred);
if (!g_str_equal (mechanism, "passwd1"))
{
g_message ("unsupported setup mechanism: %s", mechanism);
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
N_("Unsupported setup mechanism"));
goto out;
}
if (!g_variant_is_of_type (transferred, G_VARIANT_TYPE ("(asas)")))
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
N_("Bad data passed for passwd1 mechanism"));
goto out;
}
users = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
groups = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
if (!fgetpwent_callback (add_name_to_hashtable, users) ||
!fgetgrent_callback (add_group_to_hashtable, groups))
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
N_("Couldn't list local users"));
goto out;
}
g_debug ("starting setup synchronization");
g_variant_get (transferred, "(@as@as)", &pwdata, &grdata);
chpasswd = g_string_new ("");
newusers = g_string_new ("");
usermod = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, string_free);
lines = g_variant_get_strv (pwdata, &length);
for (i = 0; i < length; i++)
{
parts = g_strsplit(lines[i], ":", 3);
user_exists = (g_hash_table_lookup (users, parts[0]) != NULL);
if (!user_exists)
{
g_string_append (newusers, lines[i]);
g_string_append_c (newusers, '\n');
}
if (user_exists || !cockpit_bridge_have_newusers_crypt_method)
{
g_string_append_printf (chpasswd, "%s:%s\n", parts[0], parts[1]);
}
g_strfreev (parts);
}
g_free (lines);
lines = g_variant_get_strv (grdata, &length);
for (i = 0; i < length; i++)
{
parts = g_strsplit(lines[i], ":", 4);
if (g_hash_table_lookup (groups, parts[0]))
{
memlist = g_strsplit (parts[3], ",", -1);
for (j = 0; memlist[j] != NULL; j++)
{
string = g_hash_table_lookup (usermod, memlist[j]);
if (!string)
{
string = g_string_new ("");
g_hash_table_insert (usermod, g_strdup (memlist[j]), string);
}
if (string->len > 0)
g_string_append_c (string, ',');
g_string_append (string, parts[0]);
}
g_strfreev (memlist);
}
g_strfreev (parts);
}
g_free (lines);
context = g_new0 (CommitAdmin1, 1);
context->invocation = g_object_ref (invocation);
context->chpasswd = g_string_free_to_bytes (chpasswd);
context->usermod = usermod;
g_debug ("batch creating new users");
bytes = g_string_free_to_bytes (newusers);
pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
g_signal_connect (pipe, "close", G_CALLBACK (on_newusers_close), context);
cockpit_pipe_write (pipe, bytes);
cockpit_pipe_close (pipe, NULL);
g_bytes_unref (bytes);
out:
if (users)
g_hash_table_unref (users);
if (groups)
g_hash_table_unref (groups);
if (pwdata)
g_variant_unref (pwdata);
if (grdata)
g_variant_unref (grdata);
g_variant_unref (transferred);
}
static void
setup_method_call (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
if (g_str_equal (method_name, "Prepare"))
setup_prepare_passwd1 (parameters, invocation);
else if (g_str_equal (method_name, "Commit"))
setup_commit_passwd1 (parameters, invocation);
else if (g_str_equal (method_name, "Transfer"))
setup_transfer_passwd1 (parameters, invocation);
else
g_return_if_reached ();
}
static GDBusArgInfo setup_mechanism_arg = {
-1, "mechanism", "s", NULL
};
static GDBusArgInfo setup_data_arg = {
-1, "data", "v", NULL
};
static GDBusArgInfo *setup_mechanism_args[] = {
&setup_mechanism_arg,
NULL
};
static GDBusArgInfo *setup_data_args[] = {
&setup_data_arg,
NULL
};
static GDBusArgInfo *setup_mechanism_data_args[] = {
&setup_mechanism_arg,
&setup_data_arg,
NULL
};
static GDBusMethodInfo setup_prepare_method = {
-1, "Prepare", setup_mechanism_args, setup_data_args, NULL
};
static GDBusMethodInfo setup_transfer_method = {
-1, "Transfer", setup_mechanism_data_args, setup_data_args, NULL
};
static GDBusMethodInfo setup_commit_method = {
-1, "Commit", setup_mechanism_data_args, NULL, NULL
};
static GDBusMethodInfo *setup_methods[] = {
&setup_prepare_method,
&setup_transfer_method,
&setup_commit_method,
NULL,
};
static GDBusInterfaceInfo setup_interface = {
-1, "cockpit.Setup", setup_methods, NULL, setup_properties, NULL
};
static GDBusInterfaceVTable setup_vtable = {
.method_call = setup_method_call,
.get_property = setup_get_property,
};
void
cockpit_dbus_setup_startup (void)
{
GDBusConnection *connection;
GError *error = NULL;
connection = cockpit_dbus_internal_server ();
g_return_if_fail (connection != NULL);
g_dbus_connection_register_object (connection, "/setup", &setup_interface,
&setup_vtable, NULL, NULL, &error);
if (error != NULL)
{
g_critical ("couldn't register setup object: %s", error->message);
g_error_free (error);
}
g_object_unref (connection);
}