view src/lib-storage/mail-storage-service.c @ 9106:fce3926fe910 HEAD

mail_storage_service_init*() can now take multiple set_roots.
author Timo Sirainen <tss@iki.fi>
date Wed, 15 Apr 2009 12:40:16 -0400
parents a06b6fa612ad
children 14ebbf71ef3e
line wrap: on
line source

/* Copyright (c) 2009 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "hostpid.h"
#include "module-dir.h"
#include "restrict-access.h"
#include "str.h"
#include "var-expand.h"
#include "dict.h"
#include "settings-parser.h"
#include "auth-master.h"
#include "master-service-private.h"
#include "master-service-settings.h"
#include "mail-user.h"
#include "mail-namespace.h"
#include "mail-storage.h"
#include "mail-storage-service.h"

#include <stdlib.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>

struct mail_storage_service_multi_ctx {
	struct master_service *service;
	enum mail_storage_service_flags flags;

	unsigned int modules_initialized:1;
};

static struct module *modules = NULL;

static void set_keyval(struct master_service *service,
		       const char *key, const char *value)
{
	const char *str;

	str = t_strconcat(key, "=", value, NULL);
	if (master_service_set(service, str) < 0) {
		i_fatal("Invalid userdb input '%s': %s", str,
			settings_parser_get_error(service->set_parser));
	}
}

static bool validate_chroot(const struct mail_user_settings *user_set,
			    const char *dir)
{
	const char *const *chroot_dirs;

	if (*dir == '\0')
		return FALSE;

	if (*user_set->valid_chroot_dirs == '\0')
		return FALSE;

	chroot_dirs = t_strsplit(user_set->valid_chroot_dirs, ":");
	while (*chroot_dirs != NULL) {
		if (**chroot_dirs != '\0' &&
		    strncmp(dir, *chroot_dirs, strlen(*chroot_dirs)) == 0)
			return TRUE;
		chroot_dirs++;
	}
	return FALSE;
}

static int
user_reply_handle(struct master_service *service,
		  const struct mail_user_settings *user_set,
		  const struct auth_user_reply *reply,
		  const char **system_groups_user_r, const char **error_r)
{
	const char *const *str, *p, *line, *key;
	unsigned int i, count;
	int ret = 0;

	*system_groups_user_r = NULL;

	if (reply->uid != (uid_t)-1) {
		if (reply->uid == 0) {
			*error_r = "userdb returned 0 as uid";
			return -1;
		}
		set_keyval(service, "mail_uid", dec2str(reply->uid));
	}
	if (reply->gid != (uid_t)-1)
		set_keyval(service, "mail_gid", dec2str(reply->gid));

	if (reply->home != NULL)
		set_keyval(service, "mail_home", reply->home);

	if (reply->chroot != NULL) {
		if (!validate_chroot(user_set, reply->chroot)) {
			*error_r = t_strdup_printf(
				"userdb returned invalid chroot directory: %s "
				"(see valid_chroot_dirs setting)",
				reply->chroot);
			return -1;
		}
		set_keyval(service, "mail_chroot", reply->chroot);
	}

	str = array_get(&reply->extra_fields, &count);
	for (i = 0; i < count && ret == 0; i++) T_BEGIN {
		line = str[i];
		if (strncmp(line, "system_groups_user=", 19) == 0) {
			*system_groups_user_r = line + 19;
			continue;
		}
		if (strncmp(line, "mail=", 5) == 0)
			line = t_strconcat("mail_location=", line + 5, NULL);
		else if ((p = strchr(str[i], '=')) == NULL)
			line = t_strconcat(str[i], "=yes", NULL);
		else
			line = str[i];

		key = t_strcut(line, '=');
		if (!settings_parse_is_valid_key(service->set_parser, key)) {
			/* assume it's a plugin setting */
			line = t_strconcat("plugin/", line, NULL);
		}

		ret = settings_parse_line(service->set_parser, line);
	} T_END;

	if (ret < 0) {
		*error_r = t_strdup_printf("Invalid userdb input '%s': %s",
			str[i], settings_parser_get_error(service->set_parser));
	}
	return ret;
}

static int
service_auth_userdb_lookup(struct master_service *service, bool debug,
			   const struct mail_user_settings *user_set,
			   const char **user, const char **system_groups_user_r,
			   const char **error_r)
{
        struct auth_master_connection *conn;
	struct auth_user_reply reply;
	const char *system_groups_user, *orig_user = *user;
	unsigned int len;
	pool_t pool;
	int ret;

	pool = pool_alloconly_create("userdb lookup", 1024);
	conn = auth_master_init(user_set->auth_socket_path, debug);
	ret = auth_master_user_lookup(conn, *user, service->name,
				      pool, &reply);
	if (ret > 0) {
		len = reply.chroot == NULL ? 0 : strlen(reply.chroot);

		*user = t_strdup(reply.user);
		if (user_reply_handle(service, user_set, &reply,
				      &system_groups_user, error_r) < 0)
			ret = -1;
		*system_groups_user_r = t_strdup(system_groups_user);
	} else {
		if (ret == 0)
			*error_r = "unknown user";
		else
			*error_r = "userdb lookup failed";
		*system_groups_user_r = NULL;
	}

	if (ret > 0 && strcmp(*user, orig_user) != 0) {
		if (mail_user_set_get_storage_set(user_set)->mail_debug)
			i_info("changed username to %s", *user);
		i_set_failure_prefix(t_strdup_printf("%s(%s): ", service->name,
						     *user));
	}

	auth_master_deinit(&conn);
	pool_unref(&pool);
	return ret;
}

static bool parse_uid(const char *str, uid_t *uid_r)
{
	struct passwd *pw;
	char *p;

	if (*str >= '0' && *str <= '9') {
		*uid_r = (uid_t)strtoul(str, &p, 10);
		if (*p == '\0')
			return TRUE;
	}

	pw = getpwnam(str);
	if (pw == NULL)
		return FALSE;

	*uid_r = pw->pw_uid;
	return TRUE;
}

static bool parse_gid(const char *str, gid_t *gid_r)
{
	struct group *gr;
	char *p;

	if (*str >= '0' && *str <= '9') {
		*gid_r = (gid_t)strtoul(str, &p, 10);
		if (*p == '\0')
			return TRUE;
	}

	gr = getgrnam(str);
	if (gr == NULL)
		return FALSE;

	*gid_r = gr->gr_gid;
	return TRUE;
}

static void
service_drop_privileges(const struct mail_user_settings *set,
			const char *system_groups_user, const char *home,
			bool disallow_root, bool keep_setuid_root)
{
	struct restrict_access_settings rset;
	uid_t current_euid, setuid_uid = 0;

	current_euid = geteuid();
	restrict_access_init(&rset);
	if (*set->mail_uid != '\0') {
		if (!parse_uid(set->mail_uid, &rset.uid))
			i_fatal("Unknown mail_uid user: %s", set->mail_uid);
		if (rset.uid < (uid_t)set->first_valid_uid ||
		    (set->last_valid_uid != 0 &&
		     rset.uid > (uid_t)set->last_valid_uid)) {
			i_fatal("Mail access for users with UID %s "
				"not permitted (see first_valid_uid in config file).",
				dec2str(rset.uid));
		}
	}
	if (*set->mail_gid != '\0') {
		if (!parse_gid(set->mail_gid, &rset.gid))
			i_fatal("Unknown mail_gid group: %s", set->mail_gid);
		if (rset.gid < (gid_t)set->first_valid_gid ||
		    (set->last_valid_gid != 0 &&
		     rset.gid > (gid_t)set->last_valid_gid)) {
			i_fatal("Mail access for users with GID %s "
				"not permitted (see first_valid_gid in config file).",
				dec2str(rset.gid));
		}
	}
	if (*set->mail_privileged_group != '\0') {
		if (!parse_uid(set->mail_privileged_group, &rset.privileged_gid))
			i_fatal("Unknown mail_gid group: %s", set->mail_gid);
	}
	if (*set->mail_access_groups != '\0')
		rset.extra_groups = set->mail_access_groups;

	rset.first_valid_gid = set->first_valid_gid;
	rset.last_valid_gid = set->last_valid_gid;
	/* we can't chroot if we want to switch between users. there's not
	   much point either (from security point of view) */
	rset.chroot_dir = *set->mail_chroot == '\0' || keep_setuid_root ?
		NULL : set->mail_chroot;
	rset.system_groups_user = system_groups_user;

	if (disallow_root &&
	    (rset.uid == 0 || (rset.uid == (uid_t)-1 && current_euid == 0)))
		i_fatal("Mail access not allowed for root");

	if (keep_setuid_root && current_euid != rset.uid) {
		if (current_euid != 0) {
			/* we're changing the UID, switch back to root first */
			if (seteuid(0) < 0)
				i_fatal("seteuid(0) failed: %m");
		}
		setuid_uid = rset.uid;
		rset.uid = (uid_t)-1;
	}
	restrict_access(&rset, *home == '\0' ? NULL : home, disallow_root);
	if (keep_setuid_root) {
		if (seteuid(setuid_uid) < 0)
			i_fatal("seteuid(%s) failed: %m", dec2str(setuid_uid));
	}
}

static void
mail_storage_service_init_settings(struct master_service *service,
				   const struct setting_parser_info *set_roots[],
				   bool preserve_home)
{
	ARRAY_DEFINE(all_set_roots, const struct setting_parser_info *);
	const struct setting_parser_info *info = &mail_user_setting_parser_info;
	const char *error;
	unsigned int i;

	(void)umask(0077);

        mail_storage_init();
	mail_storage_register_all();
	mailbox_list_register_all();

	t_array_init(&all_set_roots, 5);
	array_append(&all_set_roots, &info, 1);
	for (i = 0; set_roots[i] != NULL; i++)
		array_append(&all_set_roots, &set_roots[i], 1);
	(void)array_append_space(&all_set_roots);

	/* read settings after registering storages so they can have their
	   own setting definitions too */
	set_roots = array_idx_modifiable(&all_set_roots, 0);
	if (master_service_settings_read(service, set_roots,
					 mail_storage_get_dynamic_parsers(),
					 preserve_home, &error) < 0)
		i_fatal("Error reading configuration: %s", error);
}

static int
mail_storage_service_init_post(struct master_service *service,
			       const char *user, const char *home,
			       const struct mail_user_settings *user_set,
			       struct mail_user **mail_user_r,
			       const char **error_r)
{
	const struct mail_storage_settings *mail_set;
	struct mail_user *mail_user;

	mail_set = mail_user_set_get_storage_set(user_set);

	if (mail_set->mail_debug) {
		i_info("Effective uid=%s, gid=%s, home=%s",
		       dec2str(geteuid()), dec2str(getegid()),
		       home != NULL ? home : "(none)");
	}

	/* If possible chdir to home directory, so that core file
	   could be written in case we crash. */
	if (*home != '\0') {
		if (chdir(home) < 0) {
			if (errno != ENOENT)
				i_error("chdir(%s) failed: %m", home);
			else if (mail_set->mail_debug)
				i_info("Home dir not found: %s", home);
		}
	}

	mail_user = mail_user_alloc(user, user_set);
	mail_user_set_home(mail_user, *home == '\0' ? NULL : home);
	mail_user_set_vars(mail_user, geteuid(), service->name, NULL, NULL);
	if (mail_user_init(mail_user, error_r) < 0) {
		mail_user_unref(&mail_user);
		return -1;
	}
	if (mail_namespaces_init(mail_user, error_r) < 0) {
		mail_user_unref(&mail_user);
		return -1;
	}
	*mail_user_r = mail_user;
	return 0;
}

static const struct var_expand_table *
get_var_expand_table(struct master_service *service, const char *user)
{
	static struct var_expand_table static_tab[] = {
		{ 'u', NULL, "user" },
		{ 'n', NULL, "username" },
		{ 'd', NULL, "domain" },
		{ 's', NULL, "service" },
		{ 'p', NULL, "pid" },
		{ 'i', NULL, "uid" },
		{ '\0', NULL, NULL }
	};
	struct var_expand_table *tab;

	tab = t_malloc(sizeof(static_tab));
	memcpy(tab, static_tab, sizeof(static_tab));

	tab[0].value = user;
	tab[1].value = t_strcut(user, '@');
	tab[2].value = strchr(user, '@');
	if (tab[2].value != NULL) tab[2].value++;
	tab[3].value = service->name;
	tab[4].value = my_pid;
	tab[5].value = dec2str(geteuid());
	return tab;
}

static const char *
user_expand_varstr(struct master_service *service, const char *user,
		   const char *str)
{
	string_t *ret;

	if (*str == SETTING_STRVAR_EXPANDED[0])
		return str + 1;

	i_assert(*str == SETTING_STRVAR_UNEXPANDED[0]);

	ret = t_str_new(256);
	var_expand(ret, str + 1, get_var_expand_table(service, user));
	return str_c(ret);
}

struct mail_user *
mail_storage_service_init_user(struct master_service *service, const char *user,
			       const struct setting_parser_info *set_roots[],
			       enum mail_storage_service_flags flags)
{
	const struct master_service_settings *set;
	const struct mail_user_settings *user_set;
	const struct mail_storage_settings *mail_set;
	struct mail_user *mail_user;
	void **sets;
	const char *orig_user, *home, *system_groups_user, *error;
	unsigned int len;
	bool userdb_lookup;

	userdb_lookup = (flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0;
	mail_storage_service_init_settings(service, set_roots, !userdb_lookup);

	if ((flags & MAIL_STORAGE_SERVICE_FLAG_DEBUG) != 0)
		set_keyval(service, "mail_debug", "yes");

	/* now that we've read settings, we can set up logging */
	master_service_init_log(service,
		t_strdup_printf("%s(%s): ", service->name, user));

	set = master_service_settings_get(service);
	sets = master_service_settings_get_others(service);
	user_set = sets[0];
	mail_set = mail_user_set_get_storage_set(user_set);

	if (userdb_lookup) {
		/* userdb lookup may change settings, do it as soon as
		   possible. */
		orig_user = user;
		if (service_auth_userdb_lookup(service, mail_set->mail_debug,
					       user_set, &user,
					       &system_groups_user,
					       &error) <= 0)
			i_fatal("%s", error);
	}

	/* variable strings are expanded in mail_user_init(),
	   but we need the home sooner so do it separately here. */
	home = user_expand_varstr(service, user, user_set->mail_home);

	if (!userdb_lookup) {
		system_groups_user = NULL;
		if (*home == '\0' && getenv("HOME") != NULL) {
			home = getenv("HOME");
			set_keyval(service, "mail_home", home);
		}
	}

	len = strlen(user_set->mail_chroot);
	if (len > 2 && strcmp(user_set->mail_chroot + len - 2, "/.") == 0 &&
	    strncmp(home, user_set->mail_chroot, len - 2) == 0) {
		/* If chroot ends with "/.", strip chroot dir from home dir */
		home += len - 2;
		set_keyval(service, "mail_home", home);
	}

	modules = *user_set->mail_plugins == '\0' ? NULL :
		module_dir_load(user_set->mail_plugin_dir,
				user_set->mail_plugins, TRUE,
				master_service_get_version_string(service));

	if ((flags & MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS) == 0) {
		service_drop_privileges(user_set, system_groups_user, home,
			(flags & MAIL_STORAGE_SERVICE_FLAG_DISALLOW_ROOT) != 0,
			FALSE);
	}
	/* privileges are now dropped */

	dict_drivers_register_builtin();
	module_dir_init(modules);
	mail_users_init(user_set->auth_socket_path, mail_set->mail_debug);
	if (mail_storage_service_init_post(service, user, home, user_set,
					   &mail_user, &error) < 0)
		i_fatal("%s", error);
	return mail_user;
}

void mail_storage_service_deinit_user(void)
{
	module_dir_unload(&modules);
	mail_storage_deinit();
	mail_users_deinit();
	dict_drivers_unregister_builtin();
}

struct mail_storage_service_multi_ctx *
mail_storage_service_multi_init(struct master_service *service,
				const struct setting_parser_info *set_roots[],
				enum mail_storage_service_flags flags)
{
	struct mail_storage_service_multi_ctx *ctx;
	const struct master_service_settings *set;
	const struct mail_user_settings *user_set;
	const struct mail_storage_settings *mail_set;
	void **sets;

	ctx = i_new(struct mail_storage_service_multi_ctx, 1);
	ctx->service = service;
	ctx->flags = flags;

	mail_storage_service_init_settings(service, set_roots, FALSE);

	/* do all the global initialization. delay initializing plugins until
	   we drop privileges the first time. */
	master_service_init_log(service,
				t_strdup_printf("%s: ", service->name));

	set = master_service_settings_get(service);
	sets = master_service_settings_get_others(service);
	user_set = sets[0];
	mail_set = mail_user_set_get_storage_set(user_set);

	modules = *user_set->mail_plugins == '\0' ? NULL :
		module_dir_load(user_set->mail_plugin_dir,
				user_set->mail_plugins, TRUE,
				master_service_get_version_string(service));

	dict_drivers_register_builtin();
	mail_users_init(user_set->auth_socket_path, mail_set->mail_debug);
	return ctx;
}

int mail_storage_service_multi_next(struct mail_storage_service_multi_ctx *ctx,
				    const char *user,
				    struct mail_user **mail_user_r,
				    const char **error_r)
{
	const struct mail_user_settings *user_set;
	const struct mail_storage_settings *mail_set;
	const char *orig_user, *system_groups_user, *home;
	void **sets;
	unsigned int len;
	int ret;

	sets = master_service_settings_get_others(ctx->service);
	user_set = sets[0];
	mail_set = mail_user_set_get_storage_set(user_set);

	if ((ctx->flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0) {
		orig_user = user;
		ret = service_auth_userdb_lookup(ctx->service,
						 mail_set->mail_debug,
						 user_set, &user,
						 &system_groups_user,
						 error_r);
		if (ret <= 0)
			return ret;
	} else {
		system_groups_user = NULL;
	}

	/* variable strings are expanded in mail_user_init(),
	   but we need the home sooner so do it separately here. */
	home = user_expand_varstr(ctx->service, user, user_set->mail_home);

	if ((ctx->flags & MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS) == 0) {
		service_drop_privileges(user_set, system_groups_user, home,
			(ctx->flags & MAIL_STORAGE_SERVICE_FLAG_DISALLOW_ROOT) != 0,
			TRUE);
	}
	if (!ctx->modules_initialized) {
		/* privileges dropped for the first time. initialize the
		   modules now to avoid code running as root. */
		module_dir_init(modules);
		ctx->modules_initialized = TRUE;
	}

	/* we couldn't do chrooting, so if chrooting was enabled fix
	   the home directory */
	len = strlen(user_set->mail_chroot);
	if (len > 2 && strcmp(user_set->mail_chroot + len - 2, "/.") == 0 &&
	    strncmp(home, user_set->mail_chroot, len - 2) == 0) {
		/* home dir already contains the chroot dir */
	} else if (len > 0) {
		set_keyval(ctx->service, "mail_home",
			t_strconcat(user_set->mail_chroot, "/", home, NULL));
	}
	if (mail_storage_service_init_post(ctx->service, user, home, user_set,
					   mail_user_r, error_r) < 0)
		return -1;
	return 1;
}

void mail_storage_service_multi_deinit(struct mail_storage_service_multi_ctx **_ctx)
{
	struct mail_storage_service_multi_ctx *ctx = *_ctx;

	*_ctx = NULL;
	i_free(ctx);
	mail_storage_service_deinit_user();
}

void *mail_storage_service_get_settings(struct master_service *service)
{
	void **sets;

	sets = master_service_settings_get_others(service);
	return sets[1];
}