Blob Blame History Raw
// SPDX-License-Identifier: GPL-2.0+
/*
 * Søren Sandmann <sandmann@daimi.au.dk>
 * Dan Williams <dcbw@redhat.com>
 * Tambet Ingo <tambet@gmail.com>
 * Copyright (C) 2007 - 2011 Red Hat, Inc.
 * Copyright (C) 2008 Novell, Inc.
 */

#include "nm-default.h"

#include "nm-settings.h"

#include <unistd.h>
#include <sys/stat.h>
#include <gmodule.h>
#include <pwd.h>

#if HAVE_SELINUX
#include <selinux/selinux.h>
#endif

#include "nm-libnm-core-intern/nm-common-macros.h"
#include "nm-glib-aux/nm-keyfile-aux.h"
#include "nm-keyfile/nm-keyfile-internal.h"
#include "nm-dbus-interface.h"
#include "nm-connection.h"
#include "nm-setting-8021x.h"
#include "nm-setting-bluetooth.h"
#include "nm-setting-cdma.h"
#include "nm-setting-connection.h"
#include "nm-setting-gsm.h"
#include "nm-setting-ip4-config.h"
#include "nm-setting-ip6-config.h"
#include "nm-setting-olpc-mesh.h"
#include "nm-setting-ppp.h"
#include "nm-setting-pppoe.h"
#include "nm-setting-serial.h"
#include "nm-setting-vpn.h"
#include "nm-setting-wired.h"
#include "nm-setting-adsl.h"
#include "nm-setting-wireless.h"
#include "nm-setting-wireless-security.h"
#include "nm-setting-proxy.h"
#include "nm-setting-bond.h"
#include "nm-utils.h"
#include "nm-core-internal.h"

#include "nm-std-aux/c-list-util.h"
#include "nm-glib-aux/nm-c-list.h"
#include "nm-dbus-object.h"
#include "devices/nm-device-ethernet.h"
#include "nm-settings-connection.h"
#include "nm-settings-plugin.h"
#include "nm-dbus-manager.h"
#include "nm-auth-utils.h"
#include "nm-libnm-core-intern/nm-auth-subject.h"
#include "nm-session-monitor.h"
#include "plugins/keyfile/nms-keyfile-plugin.h"
#include "plugins/keyfile/nms-keyfile-storage.h"
#include "nm-agent-manager.h"
#include "nm-config.h"
#include "nm-manager.h"
#include "nm-audit-manager.h"
#include "NetworkManagerUtils.h"
#include "nm-dispatcher.h"
#include "nm-hostname-manager.h"

/*****************************************************************************/

static
NM_CACHED_QUARK_FCN ("default-wired-connection", _default_wired_connection_quark)

static
NM_CACHED_QUARK_FCN ("default-wired-connection-blocked", _default_wired_connection_blocked_quark)

/*****************************************************************************/

typedef struct _StorageData {
	CList sd_lst;
	NMSettingsStorage *storage;
	NMConnection *connection;
	bool prioritize:1;
} StorageData;

static StorageData *
_storage_data_new_stale (NMSettingsStorage *storage,
                         NMConnection *connection)
{
	StorageData *sd;

	sd = g_slice_new (StorageData);
	sd->storage    = g_object_ref (storage);
	sd->connection = nm_g_object_ref (connection);
	sd->prioritize = FALSE;
	return sd;
}

static void
_storage_data_destroy (StorageData *sd)
{
	c_list_unlink_stale (&sd->sd_lst);
	g_object_unref (sd->storage);
	nm_g_object_unref (sd->connection);
	g_slice_free (StorageData, sd);
}

static StorageData *
_storage_data_find_in_lst (CList *head,
                           NMSettingsStorage *storage)
{
	StorageData *sd;

	nm_assert (head);
	nm_assert (NM_IS_SETTINGS_STORAGE (storage));

	c_list_for_each_entry (sd, head, sd_lst) {
		if (sd->storage == storage)
			return sd;
	}
	return NULL;
}

static void
nm_assert_storage_data_lst (CList *head)
{
#if NM_MORE_ASSERTS > 5
	const char *uuid = NULL;
	StorageData *sd;
	CList *iter;

	nm_assert (head);

	if (c_list_is_empty (head))
		return;

	c_list_for_each_entry (sd, head, sd_lst) {
		const char *u;

		nm_assert (NM_IS_SETTINGS_STORAGE (sd->storage));
		nm_assert (!sd->connection || NM_IS_CONNECTION (sd->connection));
		u = nm_settings_storage_get_uuid (sd->storage);
		if (!uuid) {
			uuid = u;
			nm_assert (nm_utils_is_uuid (uuid));
		} else
			nm_assert (nm_streq0 (uuid, u));
	}

	/* assert that all storages are unique. */
	c_list_for_each_entry (sd, head, sd_lst) {
		for (iter = sd->sd_lst.next; iter != head; iter = iter->next)
			nm_assert (c_list_entry (iter, StorageData, sd_lst)->storage != sd->storage);
	}
#endif
}

static gboolean
_storage_data_is_alive (StorageData *sd)
{
	/* If the storage tracks a connection, it is considered alive.
	 *
	 * Meta-data storages are special: they never track a connection.
	 * We need to check them specially to know when to drop them. */
	return    sd->connection
	       || nm_settings_storage_is_meta_data_alive (sd->storage);
}

/*****************************************************************************/

typedef struct {
	const char *uuid;
	NMSettingsConnection *sett_conn;
	NMSettingsStorage *storage;
	CList sd_lst_head;
	CList dirty_sd_lst_head;

	CList sce_dirty_lst;

	char _uuid_data[];
} SettConnEntry;

static SettConnEntry *
_sett_conn_entry_new (const char *uuid)
{
	SettConnEntry *sett_conn_entry;
	gsize l_p_1;

	nm_assert (nm_utils_is_uuid (uuid));

	l_p_1 = strlen (uuid) + 1;

	sett_conn_entry = g_malloc (sizeof (SettConnEntry) + l_p_1);
	sett_conn_entry->uuid = sett_conn_entry->_uuid_data;
	sett_conn_entry->sett_conn = NULL;
	sett_conn_entry->storage = NULL;
	c_list_init (&sett_conn_entry->sd_lst_head);
	c_list_init (&sett_conn_entry->dirty_sd_lst_head);
	c_list_init (&sett_conn_entry->sce_dirty_lst);
	memcpy (sett_conn_entry->_uuid_data, uuid, l_p_1);
	return sett_conn_entry;
}

static void
_sett_conn_entry_free (SettConnEntry *sett_conn_entry)
{
	c_list_unlink_stale (&sett_conn_entry->sce_dirty_lst);
	nm_c_list_free_all (&sett_conn_entry->sd_lst_head,       StorageData, sd_lst, _storage_data_destroy);
	nm_c_list_free_all (&sett_conn_entry->dirty_sd_lst_head, StorageData, sd_lst, _storage_data_destroy);
	nm_g_object_unref (sett_conn_entry->sett_conn);
	nm_g_object_unref (sett_conn_entry->storage);
	g_free (sett_conn_entry);
}

static NMSettingsConnection *
_sett_conn_entry_get_conn (SettConnEntry *sett_conn_entry)
{
	return sett_conn_entry ? sett_conn_entry->sett_conn : NULL;
}

/**
 * _sett_conn_entry_storage_find_conflicting_storage:
 * @sett_conn_entry: the list of settings-storages for the given UUID.
 * @target_plugin: the settings plugin to check
 * @storage_check_including: (allow-none): optionally compare against this storage.
 * @plugins: the list of plugins sorted in descending priority. This determines
 *   the priority and whether a storage conflicts.
 *
 * If we were to add the a storage to @target_plugin, then this function checks
 * whether there are already other storages that would hide the storage after we
 * add it. Those conflicting/hiding storages are a problem, because they have higher
 * priority, so we cannot add the storage.
 *
 * @storage_check_including is optional, and if given then it checks whether updating
 * the profile in this storage would result in confict. This is the check before
 * update-connection. If this parameter is omitted, then it's about what happens
 * when adding a new profile (add-connection).
 *
 * Returns: the conflicting storage or %NULL if there is none.
 */
static NMSettingsStorage *
_sett_conn_entry_storage_find_conflicting_storage (SettConnEntry *sett_conn_entry,
                                                   NMSettingsPlugin *target_plugin,
                                                   NMSettingsStorage *storage_check_including,
                                                   const GSList *plugins)
{
	StorageData *sd;

	if (!sett_conn_entry)
		return NULL;

	if (   storage_check_including
	    && nm_settings_storage_is_keyfile_run (storage_check_including)) {
		/* the storage we check against is in-memory. It always has highest
		 * priority, so there can be no other conflicting storages. */
		return NULL;
	}

	/* Finds the first (highest priority) storage that has a connection.
	 * Note that due to tombstones (that have a high priority), the connection
	 * may not actually be exposed. This is to find hidden/shadowed storages
	 * that provide a connection. */
	c_list_for_each_entry (sd, &sett_conn_entry->sd_lst_head, sd_lst) {
		nm_assert (NM_IS_SETTINGS_STORAGE (sd->storage));

		if (!sd->connection) {
			/* We only consider storages with connection. In particular,
			 * tombstones are not relevant, because we can delete them to
			 * resolve the conflict. */
			continue;
		}

		if (sd->storage == storage_check_including) {
			/* ok, the storage is the one we are about to check. All other
			 * storages are lower priority, so there is no storage that hides
			 * our storage_check_including. */
			return NULL;
		}

		if (nm_settings_plugin_cmp_by_priority (nm_settings_storage_get_plugin (sd->storage),
		                                        target_plugin,
		                                        plugins) <= 0) {
			/* the plugin of the existing storage is less important than @target_plugin.
			 * We have no conflicting/hiding storage. */
			return NULL;
		}

		/* Found. If we would add the profile to @target_plugin, then it would be hidden
		 * by existing_storage. */
		return sd->storage;
	}

	return NULL;
}

static NMSettingsStorage *
_sett_conn_entry_find_shadowed_storage (SettConnEntry *sett_conn_entry,
                                        const char *shadowed_storage_filename,
                                        NMSettingsStorage *blacklisted_storage)
{
	StorageData *sd;

	if (!shadowed_storage_filename)
		return NULL;

	c_list_for_each_entry (sd, &sett_conn_entry->sd_lst_head, sd_lst) {

		nm_assert (NM_IS_SETTINGS_STORAGE (sd->storage));

		if (!sd->connection)
			continue;

		if (blacklisted_storage == sd->storage)
			continue;

		if (!nm_streq0 (nm_settings_storage_get_filename_for_shadowed_storage (sd->storage), shadowed_storage_filename))
			continue;

		return sd->storage;
	}

	return NULL;
}

/*****************************************************************************/

NM_GOBJECT_PROPERTIES_DEFINE (NMSettings,
	PROP_MANAGER,
	PROP_UNMANAGED_SPECS,
	PROP_HOSTNAME,
	PROP_CAN_MODIFY,
	PROP_CONNECTIONS,
	PROP_STARTUP_COMPLETE,
);

enum {
	CONNECTION_ADDED,
	CONNECTION_UPDATED,
	CONNECTION_REMOVED,
	CONNECTION_FLAGS_CHANGED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

typedef struct {
	NMAgentManager *agent_mgr;

	NMConfig *config;

	NMPlatform *platform;

	NMManager *manager;

	NMHostnameManager *hostname_manager;

	NMSessionMonitor *session_monitor;

	CList auth_lst_head;

	NMSKeyfilePlugin *keyfile_plugin;

	GSList *plugins;

	NMKeyFileDB *kf_db_timestamps;
	NMKeyFileDB *kf_db_seen_bssids;

	GHashTable *sce_idx;

	CList sce_dirty_lst_head;

	CList connections_lst_head;

	NMSettingsConnection **connections_cached_list;

	GSList *unmanaged_specs;
	GSList *unrecognized_specs;

	gint64 startup_complete_start_timestamp_msec;
	GHashTable *startup_complete_idx;
	CList startup_complete_scd_lst_head;
	guint startup_complete_timeout_id;

	guint connections_len;

	guint connections_generation;

	guint kf_db_flush_idle_id_timestamps;
	guint kf_db_flush_idle_id_seen_bssids;

	bool started:1;

} NMSettingsPrivate;

struct _NMSettings {
	NMDBusObject parent;
	NMSettingsPrivate _priv;
};

struct _NMSettingsClass {
	NMDBusObjectClass parent;
};

G_DEFINE_TYPE (NMSettings, nm_settings, NM_TYPE_DBUS_OBJECT);

#define NM_SETTINGS_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMSettings, NM_IS_SETTINGS)

/*****************************************************************************/

/* FIXME: a lot of logging lines are directly connected to a profile. Set the @con_uuid
 *   argument for structured logging. */

#define _NMLOG_DOMAIN         LOGD_SETTINGS
#define _NMLOG(level, ...) __NMLOG_DEFAULT (level, _NMLOG_DOMAIN, "settings", __VA_ARGS__)

/*****************************************************************************/

static const NMDBusInterfaceInfoExtended interface_info_settings;
static const GDBusSignalInfo signal_info_new_connection;
static const GDBusSignalInfo signal_info_connection_removed;

static void default_wired_clear_tag (NMSettings *self,
                                     NMDevice *device,
                                     NMSettingsConnection *sett_conn,
                                     gboolean add_to_no_auto_default);

static void _clear_connections_cached_list (NMSettingsPrivate *priv);

static void _startup_complete_check (NMSettings *self,
                                     gint64 now_msec);

/*****************************************************************************/

static void
_emit_connection_added (NMSettings *self,
                        NMSettingsConnection *sett_conn)
{
	g_signal_emit (self, signals[CONNECTION_ADDED], 0, sett_conn);
}

static void
_emit_connection_updated (NMSettings *self,
                          NMSettingsConnection *sett_conn,
                          NMSettingsConnectionUpdateReason update_reason)
{
	_nm_settings_connection_emit_signal_updated_internal (sett_conn, update_reason);
	g_signal_emit (self, signals[CONNECTION_UPDATED], 0, sett_conn, (guint) update_reason);
}

static void
_emit_connection_removed (NMSettings *self,
                          NMSettingsConnection *sett_conn)
{
	g_signal_emit (self, signals[CONNECTION_REMOVED], 0, sett_conn);
}

static void
_emit_connection_flags_changed (NMSettings *self,
                                NMSettingsConnection *sett_conn)
{
	g_signal_emit (self, signals[CONNECTION_FLAGS_CHANGED], 0, sett_conn);
}

/*****************************************************************************/

typedef struct {
	NMSettingsConnection *sett_conn;
	CList scd_lst;
	gint64 timeout_msec;
} StartupCompleteData;

static void
_startup_complete_data_destroy (StartupCompleteData *scd)
{
	c_list_unlink_stale (&scd->scd_lst);
	g_object_unref (scd->sett_conn);
	nm_g_slice_free (scd);
}

static gboolean
_startup_complete_check_is_ready (NMSettings *self,
                                  NMSettingsConnection *sett_conn,
                                  gboolean ignore_pending_actions)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMConnection *conn;
	const CList *tmp_lst;
	NMDevice *device;

	if (!priv->manager)
		return TRUE;

	conn = nm_settings_connection_get_connection (sett_conn);

	nm_manager_for_each_device (priv->manager, device, tmp_lst) {

		if (!nm_device_is_real (device))
			continue;

		if (   nm_device_get_state (device) < NM_DEVICE_STATE_UNAVAILABLE
		    || (   !ignore_pending_actions
		        && nm_device_has_pending_action (device))) {
			/* while a device is not yet available and still has a pending
			 * action itself, it's not a suitable candidate. */
			continue;
		}

		if (!nm_device_check_connection_compatible (device, conn, NULL))
			continue;

		return TRUE;
	}

	return FALSE;
}

static gboolean
_startup_complete_timeout_cb (gpointer user_data)
{
	NMSettings *self = user_data;
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	priv->startup_complete_timeout_id = 0;
	_startup_complete_check (self, 0);
	return G_SOURCE_REMOVE;
}

static void
_startup_complete_check (NMSettings *self,
                         gint64 now_msec)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	StartupCompleteData *scd_not_ready;
	StartupCompleteData *scd_safe;
	StartupCompleteData *scd;
	gint64 elapsed_msec;
	CList ready_lst;

	if (priv->startup_complete_start_timestamp_msec == 0) {
		/* we are already done for good or didn't start yet. */
		return;
	}

	if (!priv->started) {
		/* before we are started there is no need to evaluate our list because
		 * we are anyway blocking startup-complete. */
		return;
	}

	if (c_list_is_empty (&priv->startup_complete_scd_lst_head))
		goto ready;

	nm_utils_get_monotonic_timestamp_msec_cached (&now_msec);

	elapsed_msec = now_msec - priv->startup_complete_start_timestamp_msec;

	/* We search the entire list whether they all timed-out or found a compatible device.
	 * We do that by appending elements that are ready to the end of the list, so that
	 * we hopefully keep testing the elements that are ready already (and can shortcut
	 * the test in common cases).
	 *
	 * Note that all profiles that we wait for need to have their dependencies satisfied
	 * at the same time. For example, consider connection A is waiting for device A' which is ready.
	 * Connection B waits for device B', which isn't ready. Once B'/B becomes ready, A/A' must
	 * still be ready. Otherwise, we would wait for A/A' to become ready again. */
	scd_not_ready = NULL;
	c_list_init (&ready_lst);
	c_list_for_each_entry_safe (scd, scd_safe, &priv->startup_complete_scd_lst_head, scd_lst) {

		if (scd->timeout_msec <= elapsed_msec)
			goto next_with_ready;

		if (_startup_complete_check_is_ready (self, scd->sett_conn, FALSE))
			goto next_with_ready;

		scd_not_ready = scd;
		break;

next_with_ready:
		/* this element is ready. We move it to a temporary list, so that we
		 * can reorder the list (to next time evaluate the non-ready element first). */
		nm_c_list_move_tail (&ready_lst, &scd->scd_lst);
	}
	c_list_splice (&priv->startup_complete_scd_lst_head, &ready_lst);

	nm_clear_g_source (&priv->startup_complete_timeout_id);

	if (scd_not_ready) {
		gint64 timeout_msec;

		timeout_msec = priv->startup_complete_start_timestamp_msec + scd_not_ready->timeout_msec - nm_utils_get_monotonic_timestamp_msec ();
		priv->startup_complete_timeout_id = g_timeout_add (NM_CLAMP (0, timeout_msec, 60000),
		                                                   _startup_complete_timeout_cb,
		                                                   self);
		_LOGT ("startup-complete: wait for suitable device for connection \"%s\" (%s) which has \"connection.wait-device-timeout\" set",
		       nm_settings_connection_get_id (scd_not_ready->sett_conn),
		       nm_settings_connection_get_uuid (scd_not_ready->sett_conn));
		return;
	}

	if (_LOGW_ENABLED ()) {
		c_list_for_each_entry (scd, &priv->startup_complete_scd_lst_head, scd_lst) {
			if (!_startup_complete_check_is_ready (self, scd->sett_conn, TRUE)) {
				_LOGW ("startup-complete: profile \"%s\" (%s) was waiting for non-existing device (with timeout \"connection.wait-device-timeout=%"G_GINT64_FORMAT"\")",
				       nm_settings_connection_get_id (scd->sett_conn),
				       nm_settings_connection_get_uuid (scd->sett_conn),
				       scd->timeout_msec);
			}
		}
	}

ready:
	nm_clear_pointer (&priv->startup_complete_idx, g_hash_table_destroy);
	nm_assert (c_list_is_empty (&priv->startup_complete_scd_lst_head));
	nm_assert (priv->started);
	_LOGT ("startup-complete: ready, no more profiles to wait for");
	priv->startup_complete_start_timestamp_msec = 0;
	nm_assert (!priv->startup_complete_idx);
	nm_assert (priv->startup_complete_timeout_id == 0);
	_notify (self, PROP_STARTUP_COMPLETE);
}

static void
_startup_complete_notify_connection (NMSettings *self,
                                     NMSettingsConnection *sett_conn,
                                     gboolean forget)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	StartupCompleteData *scd;
	gint64 timeout_msec;
	gint64 now_msec = 0;
	NMSettingConnection *s_con;
	gint32 v;

	nm_assert (priv->startup_complete_start_timestamp_msec != 0);

	if (forget) {
		if (!priv->startup_complete_idx)
			return;
		if (!g_hash_table_remove (priv->startup_complete_idx, &sett_conn))
			return;
		goto check;
	}

	s_con = nm_connection_get_setting_connection (nm_settings_connection_get_connection (sett_conn));
	v = nm_setting_connection_get_wait_device_timeout (s_con);
	if (v > 0)
		timeout_msec = v;
	else
		timeout_msec = 0;

	if (!priv->startup_complete_idx) {
		nm_assert (!priv->started);

		if (timeout_msec == 0)
			return;

		priv->startup_complete_idx = g_hash_table_new_full (nm_pdirect_hash,
		                                                    nm_pdirect_equal,
		                                                    NULL,
		                                                    (GDestroyNotify) _startup_complete_data_destroy);
		scd = NULL;
	} else
		scd = g_hash_table_lookup (priv->startup_complete_idx, &sett_conn);

	if (!scd) {
		if (timeout_msec == 0)
			return;
		scd = g_slice_new (StartupCompleteData);
		*scd = (StartupCompleteData) {
			.sett_conn    = g_object_ref (sett_conn),
			.timeout_msec = timeout_msec,
		};
		g_hash_table_add (priv->startup_complete_idx, scd);
		c_list_link_tail (&priv->startup_complete_scd_lst_head, &scd->scd_lst);
	} else {
		scd->timeout_msec = timeout_msec;
		nm_c_list_move_front (&priv->startup_complete_scd_lst_head, &scd->scd_lst);
	}

check:
	_startup_complete_check (self, now_msec);
}

const char *
nm_settings_get_startup_complete_blocked_reason (NMSettings *self,
                                                 gboolean force_reload)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	StartupCompleteData *scd;
	const char *uuid;

	if (priv->startup_complete_start_timestamp_msec == 0)
		goto out_done;

	if (force_reload)
		_startup_complete_check (self, 0);

	if (c_list_is_empty (&priv->startup_complete_scd_lst_head))
		goto out_done;

	scd = c_list_first_entry (&priv->startup_complete_scd_lst_head, StartupCompleteData, scd_lst);

	nm_assert (scd);
	nm_assert (NM_IS_SETTINGS_CONNECTION (scd->sett_conn));
	nm_assert (scd == nm_g_hash_table_lookup (priv->startup_complete_idx, &scd->sett_conn));

	uuid = nm_settings_connection_get_uuid (scd->sett_conn);
	if (uuid)
		return uuid;

	g_return_val_if_reached ("settings-starting");

out_done:
	if (!priv->started)
		return "settings-starting";
	return NULL;
}

/*****************************************************************************/

const GSList *
nm_settings_get_unmanaged_specs (NMSettings *self)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	return priv->unmanaged_specs;
}

static gboolean
update_specs (NMSettings *self, GSList **specs_ptr,
              GSList * (*get_specs_func) (NMSettingsPlugin *))
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	GSList *new = NULL;
	GSList *iter;

	for (iter = priv->plugins; iter; iter = g_slist_next (iter)) {
		GSList *specs;

		specs = get_specs_func (iter->data);
		while (specs) {
			GSList *s = specs;

			specs = g_slist_remove_link (specs, s);
			if (nm_utils_g_slist_find_str (new, s->data)) {
				g_free (s->data);
				g_slist_free_1 (s);
				continue;
			}
			s->next = new;
			new = s;
		}
	}

	if (nm_utils_g_slist_strlist_cmp (new, *specs_ptr) == 0) {
		g_slist_free_full (new, g_free);
		return FALSE;
	}

	g_slist_free_full (*specs_ptr, g_free);
	*specs_ptr = new;
	return TRUE;

}

static void
_plugin_unmanaged_specs_changed (NMSettingsPlugin *config,
                                 gpointer user_data)
{
	NMSettings *self = NM_SETTINGS (user_data);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	if (update_specs (self, &priv->unmanaged_specs,
	                  nm_settings_plugin_get_unmanaged_specs))
		_notify (self, PROP_UNMANAGED_SPECS);
}

static void
_plugin_unrecognized_specs_changed (NMSettingsPlugin *config,
                                    gpointer user_data)
{
	NMSettings *self = NM_SETTINGS (user_data);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	update_specs (self, &priv->unrecognized_specs,
	              nm_settings_plugin_get_unrecognized_specs);
}

/*****************************************************************************/

static void
connection_flags_changed (NMSettingsConnection *sett_conn,
                          gpointer user_data)
{
	_emit_connection_flags_changed (NM_SETTINGS (user_data), sett_conn);
}

/*****************************************************************************/

static SettConnEntry *
_sett_conn_entries_get (NMSettings *self,
                        const char *uuid)
{
	nm_assert (uuid);
	return g_hash_table_lookup (NM_SETTINGS_GET_PRIVATE (self)->sce_idx, &uuid);
}

static SettConnEntry *
_sett_conn_entries_create_and_add (NMSettings *self,
                                   const char *uuid)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	SettConnEntry *sett_conn_entry;

	sett_conn_entry = _sett_conn_entry_new (uuid);

	if (!g_hash_table_add (priv->sce_idx, sett_conn_entry))
		nm_assert_not_reached ();
	else if (g_hash_table_size (priv->sce_idx) == 1)
		g_object_ref (self);

	return sett_conn_entry;
}

static void
_sett_conn_entries_remove_and_destroy (NMSettings *self,
                                       SettConnEntry *sett_conn_entry)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	if (!g_hash_table_remove (priv->sce_idx, sett_conn_entry))
		nm_assert_not_reached ();
	else if (g_hash_table_size (priv->sce_idx) == 0)
		g_object_unref (self);
}

/*****************************************************************************/

static int
_sett_conn_entry_sds_update_cmp_ascending (const StorageData *sd_a,
                                           const StorageData *sd_b,
                                           const GSList *plugins)
{
	const NMSettingsMetaData *meta_data_a;
	const NMSettingsMetaData *meta_data_b;
	bool is_keyfile_run_a;
	bool is_keyfile_run_b;

	/* Sort storages by priority. More important storages are sorted
	 * higher (ascending sort). For example, if "sd_a" is more important than
	 * "sd_b" (sd_a>sd_b), a positive integer is returned. */

	meta_data_a = nm_settings_storage_is_meta_data (sd_a->storage);
	meta_data_b = nm_settings_storage_is_meta_data (sd_b->storage);

	/* runtime storages (both connections and meta-data) are always more
	 * important. */
	is_keyfile_run_a = nm_settings_storage_is_keyfile_run (sd_a->storage);
	is_keyfile_run_b = nm_settings_storage_is_keyfile_run (sd_b->storage);
	if (is_keyfile_run_a != is_keyfile_run_b) {

		if (   !meta_data_a
		    && !meta_data_b) {
			/* Ok, both are non-meta-data providing actual profiles. But one is in /run and one is in
			 * another storage. In this case we first honor whether one of the storages is explicitly
			 * prioritized. The prioritize flag is an in-memory hack to overwrite relative priorities
			 * contrary to what exists on-disk.
			 *
			 * This is done because when we use explicit D-Bus API (like update-connection)
			 * to update a profile, then we really want to prioritize the candidate
			 * despite having multiple other profiles.
			 *
			 * The example is if you have the same UUID twice in /run (one of them shadowed).
			 * If you move it to disk, then one of the profiles gets deleted and re-created
			 * on disk, but that on-disk profile must win against the remainging profile in
			 * /run. At least until the next reload/restart. */
			NM_CMP_FIELD_UNSAFE (sd_a, sd_b, prioritize);
		}

		/* in-memory has higher priority. That is regardless of whether any of
		 * them is meta-data/tombstone or a profile.
		 *
		 * That works, because if any of them are tombstones/metadata, then we are in full
		 * control. There can by only one meta-data file, which is fully owned (and accordingly
		 * created/deleted) by NetworkManager.
		 *
		 * The only case where this might not be right is if we have profiles
		 * in /run that are shadowed. When we move such a profile to disk, then
		 * a conflict might arise. That is handled by "prioritize" above! */
		NM_CMP_DIRECT (is_keyfile_run_a, is_keyfile_run_b);
	}

	/* After we determined that both profiles are either in /run or not,
	 * tombstones are always more important than non-tombstones. */
	NM_CMP_DIRECT (meta_data_a && meta_data_a->is_tombstone,
	               meta_data_b && meta_data_b->is_tombstone);

	/* Again, prioritized entries are sorted first (higher priority). */
	NM_CMP_FIELD_UNSAFE (sd_a, sd_b, prioritize);

	/* finally, compare the storages. This basically honors the timestamp
	 * of the profile and the relative order of the source plugin (via the
	 * @plugins list). */
	return nm_settings_storage_cmp (sd_a->storage, sd_b->storage, plugins);
}

static int
_sett_conn_entry_sds_update_cmp (const CList *ls_a,
                                 const CList *ls_b,
                                 gconstpointer user_data)
{
	/* we sort highest priority storages first (descending). Hence, the order is swapped. */
	return _sett_conn_entry_sds_update_cmp_ascending (c_list_entry (ls_b, StorageData, sd_lst),
	                                                  c_list_entry (ls_a, StorageData, sd_lst),
	                                                  user_data);
}

static void
_sett_conn_entry_sds_update (NMSettings *self,
                             SettConnEntry *sett_conn_entry)
{
	StorageData *sd;
	StorageData *sd_safe;
	StorageData *sd_dirty;
	gboolean reprioritize;

	nm_assert_storage_data_lst (&sett_conn_entry->sd_lst_head);
	nm_assert_storage_data_lst (&sett_conn_entry->dirty_sd_lst_head);

	/* we merge the dirty list with the previous list.
	 *
	 * The idea is:
	 *
	 *  - _connection_changed_track() appends events for the same UUID. Meaning:
	 *    if the storage is new, it get appended (having lower priority).
	 *    If it already exist and is an update for an event that we already
	 *    track it, it keeps the list position in @dirty_sd_lst_head unchanged.
	 *
	 *  - during merge, we want to preserve the previous order (with higher
	 *    priority first in the list).
	 */

	/* first go through all storages that we track and check whether they
	 * got an update...*/

	reprioritize = FALSE;
	c_list_for_each_entry (sd, &sett_conn_entry->dirty_sd_lst_head, sd_lst) {
		if (sd->prioritize) {
			reprioritize = TRUE;
			break;
		}
	}

	nm_assert_storage_data_lst (&sett_conn_entry->sd_lst_head);

	c_list_for_each_entry_safe (sd, sd_safe, &sett_conn_entry->sd_lst_head, sd_lst) {

		sd_dirty = _storage_data_find_in_lst (&sett_conn_entry->dirty_sd_lst_head, sd->storage);
		if (!sd_dirty) {
			/* there is no update for this storage (except maybe reprioritize). */
			if (reprioritize)
				sd->prioritize = FALSE;
			continue;
		}

		nm_g_object_ref_set (&sd->connection, sd_dirty->connection);
		sd->prioritize = sd_dirty->prioritize;

		_storage_data_destroy (sd_dirty);
	}

	nm_assert_storage_data_lst (&sett_conn_entry->sd_lst_head);

	/* all remaining (so far unseen) dirty entries are appended to the merged list.
	 * (append means lower priority). */

	c_list_splice (&sett_conn_entry->sd_lst_head, &sett_conn_entry->dirty_sd_lst_head);

	nm_assert_storage_data_lst (&sett_conn_entry->sd_lst_head);

	/* we drop the entries that are no longer "alive" (meaning, they no longer
	 * indicate a connection and are not a tombstone). */
	c_list_for_each_entry_safe (sd, sd_safe, &sett_conn_entry->sd_lst_head, sd_lst) {
		if (!_storage_data_is_alive (sd))
			_storage_data_destroy (sd);
	}

	nm_assert_storage_data_lst (&sett_conn_entry->sd_lst_head);
	nm_assert (c_list_is_empty (&sett_conn_entry->dirty_sd_lst_head));

	/* as last, we sort the entries. Note that this is a stable-sort... */
	c_list_sort (&sett_conn_entry->sd_lst_head,
	             _sett_conn_entry_sds_update_cmp,
	             NM_SETTINGS_GET_PRIVATE (self)->plugins);

	nm_assert_storage_data_lst (&sett_conn_entry->sd_lst_head);
	nm_assert (c_list_is_empty (&sett_conn_entry->dirty_sd_lst_head));
}

/*****************************************************************************/

static NMConnection *
_connection_changed_normalize_connection (NMSettingsStorage *storage,
                                          NMConnection *connection,
                                          GVariant *secrets_to_merge,
                                          NMConnection **out_connection_cloned)
{
	gs_unref_object NMConnection *connection_cloned = NULL;
	gs_free_error GError *error = NULL;
	const char *uuid;

	nm_assert (NM_IS_SETTINGS_STORAGE (storage));
	nm_assert (out_connection_cloned && !*out_connection_cloned);

	if (!connection)
		return NULL;

	nm_assert (NM_IS_CONNECTION (connection));

	uuid = nm_settings_storage_get_uuid (storage);

	if (secrets_to_merge) {
		connection_cloned = nm_simple_connection_new_clone (connection);
		connection = connection_cloned;
		nm_connection_update_secrets (connection,
		                              NULL,
		                              secrets_to_merge,
		                              NULL);
	}

	if (!_nm_connection_ensure_normalized (connection,
	                                       !!connection_cloned,
	                                       uuid,
	                                       FALSE,
	                                       connection_cloned ? NULL : &connection_cloned,
	                                       &error)) {
		/* this is most likely a bug in the plugin. It provided a connection that no longer verifies.
		 * Well, I guess it could also happen when we merge @secrets_to_merge above. In any case
		 * somewhere is a bug. */
		_LOGT ("storage[%s,"NM_SETTINGS_STORAGE_PRINT_FMT"]: plugin provided an invalid connection: %s",
		       uuid,
		       NM_SETTINGS_STORAGE_PRINT_ARG (storage),
		       error->message);
		return NULL;
	}
	if (connection_cloned)
		connection = connection_cloned;

	*out_connection_cloned = g_steal_pointer (&connection_cloned);
	return connection;
}

/*****************************************************************************/

static void
_connection_changed_update (NMSettings *self,
                            SettConnEntry *sett_conn_entry,
                            NMConnection *connection,
                            NMSettingsConnectionIntFlags sett_flags,
                            NMSettingsConnectionIntFlags sett_mask,
                            NMSettingsConnectionUpdateReason update_reason)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_unref_object NMConnection *connection_old = NULL;
	NMSettingsStorage *storage = sett_conn_entry->storage;
	gs_unref_object NMSettingsConnection *sett_conn = g_object_ref (sett_conn_entry->sett_conn);
	const char *path;
	gboolean is_new;

	nm_assert (!NM_FLAGS_ANY (sett_mask, ~_NM_SETTINGS_CONNECTION_INT_FLAGS_PERSISTENT_MASK));
	nm_assert (!NM_FLAGS_ANY (sett_flags, ~sett_mask));

	is_new = c_list_is_empty (&sett_conn->_connections_lst);

	_LOGT ("update[%s]: %s connection \"%s\" ("NM_SETTINGS_STORAGE_PRINT_FMT")",
	       nm_settings_storage_get_uuid (storage),
	       is_new ? "adding" : "updating",
	       nm_connection_get_id (connection),
	       NM_SETTINGS_STORAGE_PRINT_ARG (storage));

	_nm_settings_connection_set_storage (sett_conn, storage);

	_nm_settings_connection_set_connection (sett_conn, connection, &connection_old, update_reason);


	if (is_new) {
		_nm_settings_connection_register_kf_dbs (sett_conn,
		                                         priv->kf_db_timestamps,
		                                         priv->kf_db_seen_bssids);

		_clear_connections_cached_list (priv);
		c_list_link_tail (&priv->connections_lst_head, &sett_conn->_connections_lst);
		priv->connections_len++;
		priv->connections_generation++;

		g_signal_connect (sett_conn, NM_SETTINGS_CONNECTION_FLAGS_CHANGED, G_CALLBACK (connection_flags_changed), self);
	}

	if (NM_FLAGS_HAS (update_reason, NM_SETTINGS_CONNECTION_UPDATE_REASON_BLOCK_AUTOCONNECT)) {
		nm_settings_connection_autoconnect_blocked_reason_set (sett_conn,
		                                                       NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST,
		                                                       TRUE);
	}

	sett_mask |= NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE;
	if (nm_settings_connection_check_visibility (sett_conn, priv->session_monitor))
		sett_flags |= NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE;
	else
		nm_assert (!NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE));

	sett_mask |= NM_SETTINGS_CONNECTION_INT_FLAGS_UNSAVED;
	if (nm_settings_storage_is_keyfile_run (storage))
		sett_flags |= NM_SETTINGS_CONNECTION_INT_FLAGS_UNSAVED;
	else {
		nm_assert (!NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_UNSAVED));

		/* Profiles that don't reside in /run, are never nm-generated,
		 * volatile, and external. */
		sett_mask |= (  NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
		              | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
		              | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL);
		sett_flags &= ~(  NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
		                | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
		                | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL);
	}

	nm_settings_connection_set_flags_full (sett_conn,
	                                       sett_mask,
	                                       sett_flags);

	if (is_new) {
		/* FIXME(shutdown): The NMSettings instance can't be disposed
		 * while there is any exported connection. Ideally we should
		 * unexport all connections on NMSettings' disposal, but for now
		 * leak @self on termination when there are connections alive. */
		path = nm_dbus_object_export (NM_DBUS_OBJECT (sett_conn));
	} else
		path = nm_dbus_object_get_path (NM_DBUS_OBJECT (sett_conn));

	if (   is_new
	    || connection_old) {
		nm_utils_log_connection_diff (nm_settings_connection_get_connection (sett_conn),
		                              connection_old,
		                              LOGL_DEBUG,
		                              LOGD_CORE,
		                              is_new ? "new connection" : "update connection",
		                              "++ ",
		                              path);
	}

	if (is_new) {
		nm_dbus_object_emit_signal (NM_DBUS_OBJECT (self),
		                            &interface_info_settings,
		                            &signal_info_new_connection,
		                            "(o)",
		                            path);
		_notify (self, PROP_CONNECTIONS);
		_emit_connection_added (self, sett_conn);
	} else {
		_nm_settings_connection_emit_dbus_signal_updated (sett_conn);
		_emit_connection_updated (self, sett_conn, update_reason);
	}

	if (priv->startup_complete_start_timestamp_msec != 0) {
		if (nm_settings_has_connection (self, sett_conn))
			_startup_complete_notify_connection (self, sett_conn, FALSE);
	}
}

static void
_connection_changed_delete (NMSettings *self,
                            NMSettingsStorage *storage,
                            NMSettingsConnection *sett_conn,
                            gboolean allow_add_to_no_auto_default)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_unref_object NMConnection *connection_for_agents = NULL;
	NMDevice *device;
	const char *uuid;

	nm_assert (NM_IS_SETTINGS_CONNECTION (sett_conn));
	nm_assert (c_list_contains (&priv->connections_lst_head, &sett_conn->_connections_lst));
	nm_assert (nm_dbus_object_is_exported (NM_DBUS_OBJECT (sett_conn)));

	uuid = nm_settings_storage_get_uuid (storage);

	_LOGT ("update[%s]: delete connection \"%s\" ("NM_SETTINGS_STORAGE_PRINT_FMT")",
	       uuid,
	       nm_settings_connection_get_id (sett_conn),
	       NM_SETTINGS_STORAGE_PRINT_ARG (storage));

	/* When the default wired sett_conn is removed (either deleted or saved to
	 * a new persistent sett_conn by a plugin), write the MAC address of the
	 * wired device to the config file and don't create a new default wired
	 * sett_conn for that device again.
	 */
	device = nm_settings_connection_default_wired_get_device (sett_conn);
	if (device)
		default_wired_clear_tag (self, device, sett_conn, allow_add_to_no_auto_default);

	g_signal_handlers_disconnect_by_func (sett_conn, G_CALLBACK (connection_flags_changed), self);

	_clear_connections_cached_list (priv);
	c_list_unlink (&sett_conn->_connections_lst);
	priv->connections_len--;
	priv->connections_generation++;

	/* Tell agents to remove secrets for this connection */
	connection_for_agents = nm_simple_connection_new_clone (nm_settings_connection_get_connection (sett_conn));
	nm_connection_clear_secrets (connection_for_agents);
	nm_agent_manager_delete_secrets (priv->agent_mgr,
	                                 nm_dbus_object_get_path (NM_DBUS_OBJECT (self)),
	                                 connection_for_agents);

	_notify (self, PROP_CONNECTIONS);
	_nm_settings_connection_emit_dbus_signal_removed (sett_conn);
	nm_dbus_object_emit_signal (NM_DBUS_OBJECT (self),
	                            &interface_info_settings,
	                            &signal_info_connection_removed,
	                            "(o)",
	                            nm_dbus_object_get_path (NM_DBUS_OBJECT (sett_conn)));

	nm_dbus_object_unexport (NM_DBUS_OBJECT (sett_conn));

	nm_settings_connection_set_flags (sett_conn,
	                                    NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE
	                                  | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
	                                  | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL,
	                                  FALSE);

	_emit_connection_removed (self, sett_conn);

	_nm_settings_connection_cleanup_after_remove (sett_conn);

	nm_key_file_db_remove_key (priv->kf_db_timestamps, uuid);
	nm_key_file_db_remove_key (priv->kf_db_seen_bssids, uuid);

	if (priv->startup_complete_start_timestamp_msec != 0)
		_startup_complete_notify_connection (self, sett_conn, TRUE);
}

static void
_connection_changed_process_one (NMSettings *self,
                                 SettConnEntry *sett_conn_entry,
                                 gboolean allow_add_to_no_auto_default,
                                 NMSettingsConnectionIntFlags sett_flags,
                                 NMSettingsConnectionIntFlags sett_mask,
                                 gboolean override_sett_flags,
                                 NMSettingsConnectionUpdateReason update_reason)
{
	StorageData *sd_best;

	c_list_unlink (&sett_conn_entry->sce_dirty_lst);

	_sett_conn_entry_sds_update (self, sett_conn_entry);

	sd_best = c_list_first_entry (&sett_conn_entry->sd_lst_head, StorageData, sd_lst);;

	if (   !sd_best
	    || !sd_best->connection) {
		gs_unref_object NMSettingsConnection *sett_conn = NULL;
		gs_unref_object NMSettingsStorage *storage = NULL;

		if (!sett_conn_entry->sett_conn) {

			if (!sd_best) {
				_sett_conn_entries_remove_and_destroy (self, sett_conn_entry);
				return;
			}

			if (sett_conn_entry->storage != sd_best->storage) {
				_LOGT ("update[%s]: shadow UUID ("NM_SETTINGS_STORAGE_PRINT_FMT")",
				       sett_conn_entry->uuid,
				       NM_SETTINGS_STORAGE_PRINT_ARG (sd_best->storage));
			}

			nm_g_object_ref_set (&sett_conn_entry->storage, sd_best->storage);
			return;
		}

		sett_conn = g_steal_pointer (&sett_conn_entry->sett_conn);
		if (sd_best) {
			storage = g_object_ref (sd_best->storage);
			nm_g_object_ref_set (&sett_conn_entry->storage, storage);
			nm_assert_valid_settings_storage (NULL, storage);
		} else {
			storage = g_object_ref (sett_conn_entry->storage);
			_sett_conn_entries_remove_and_destroy (self, sett_conn_entry);
		}

		_connection_changed_delete (self, storage, sett_conn, allow_add_to_no_auto_default);
		return;
	}

	if (override_sett_flags) {
		NMSettingsConnectionIntFlags s_f, s_m;

		nm_settings_storage_load_sett_flags (sd_best->storage, &s_f, &s_m);

		nm_assert (!NM_FLAGS_ANY (s_f, ~s_m));

		sett_mask |= s_m;
		sett_flags = (sett_flags & ~s_m) | (s_f & s_m);
	}

	nm_g_object_ref_set (&sett_conn_entry->storage, sd_best->storage);

	if (!sett_conn_entry->sett_conn)
		sett_conn_entry->sett_conn = nm_settings_connection_new ();

	_connection_changed_update (self,
	                            sett_conn_entry,
	                            sd_best->connection,
	                            sett_flags,
	                            sett_mask,
	                            update_reason);
}

static void
_connection_changed_process_all_dirty (NMSettings *self,
                                       gboolean allow_add_to_no_auto_default,
                                       NMSettingsConnectionIntFlags sett_flags,
                                       NMSettingsConnectionIntFlags sett_mask,
                                       gboolean override_sett_flags,
                                       NMSettingsConnectionUpdateReason update_reason)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	SettConnEntry *sett_conn_entry;

	while ((sett_conn_entry = c_list_first_entry (&priv->sce_dirty_lst_head, SettConnEntry, sce_dirty_lst))) {
		_connection_changed_process_one (self,
		                                 sett_conn_entry,
		                                 allow_add_to_no_auto_default,
		                                 sett_flags,
		                                 sett_mask,
		                                 override_sett_flags,
		                                 update_reason);
	}
}

static SettConnEntry *
_connection_changed_track (NMSettings *self,
                           NMSettingsStorage *storage,
                           NMConnection *connection,
                           gboolean prioritize)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	SettConnEntry *sett_conn_entry;
	StorageData *sd;
	const char *uuid;

	nm_assert_valid_settings_storage (NULL, storage);

	uuid = nm_settings_storage_get_uuid (storage);

	nm_assert (!connection || NM_IS_CONNECTION (connection));
	nm_assert (!connection || (_nm_connection_verify (connection, NULL) == NM_SETTING_VERIFY_SUCCESS));
	nm_assert (!connection || nm_streq0 (uuid, nm_connection_get_uuid (connection)));

	nmtst_connection_assert_unchanging (connection);

	sett_conn_entry =    _sett_conn_entries_get (self, uuid)
	                  ?: _sett_conn_entries_create_and_add (self, uuid);

	if (_LOGT_ENABLED ()) {
		const char *filename;
		const NMSettingsMetaData *meta_data;
		const char *shadowed_storage;
		gboolean shadowed_owned;

		filename = nm_settings_storage_get_filename (storage);
		if (connection) {
			shadowed_storage = nm_settings_storage_get_shadowed_storage (storage, &shadowed_owned);
			_LOGT ("storage[%s,"NM_SETTINGS_STORAGE_PRINT_FMT"]: change event with connection \"%s\"%s%s%s%s%s%s",
			       sett_conn_entry->uuid,
			       NM_SETTINGS_STORAGE_PRINT_ARG (storage),
			       nm_connection_get_id (connection),
			       NM_PRINT_FMT_QUOTED (filename, " (file \"", filename, "\")", ""),
			       NM_PRINT_FMT_QUOTED (shadowed_storage, shadowed_owned ? " (owns \"" : " (shadows \"", shadowed_storage, "\")", ""));
		} else if ((meta_data = nm_settings_storage_is_meta_data (storage))) {
			nm_assert (meta_data->is_tombstone);
			shadowed_storage = nm_settings_storage_get_shadowed_storage (storage, &shadowed_owned);
			_LOGT ("storage[%s,"NM_SETTINGS_STORAGE_PRINT_FMT"]: change event for %shiding profile%s%s%s%s%s%s",
			       sett_conn_entry->uuid,
			       NM_SETTINGS_STORAGE_PRINT_ARG (storage),
			       nm_settings_storage_is_meta_data_alive  (storage) ? "" : "dropping ",
			       NM_PRINT_FMT_QUOTED (filename, " (file \"", filename, "\")", ""),
			       NM_PRINT_FMT_QUOTED (shadowed_storage, shadowed_owned ? " (owns \"" : " (shadows \"", shadowed_storage, "\")", ""));
		} else {
			_LOGT ("storage[%s,"NM_SETTINGS_STORAGE_PRINT_FMT"]: change event for dropping profile%s%s%s",
			       sett_conn_entry->uuid,
			       NM_SETTINGS_STORAGE_PRINT_ARG (storage),
			       NM_PRINT_FMT_QUOTED (filename, " (file \"", filename, "\")", ""));
		}
	}

	/* see _sett_conn_entry_sds_update() for why we append the new events
	 * and leave existing ones at their position. */
	sd = _storage_data_find_in_lst (&sett_conn_entry->dirty_sd_lst_head, storage);
	if (sd)
		nm_g_object_ref_set (&sd->connection, connection);
	else {
		sd = _storage_data_new_stale (storage, connection);
		c_list_link_tail (&sett_conn_entry->dirty_sd_lst_head, &sd->sd_lst);
	}

	if (prioritize) {
		StorageData *sd2;

		/* only one entry can be prioritized. */
		c_list_for_each_entry (sd2, &sett_conn_entry->dirty_sd_lst_head, sd_lst)
			sd2->prioritize = FALSE;
		sd->prioritize = TRUE;
	}

	nm_c_list_move_tail (&priv->sce_dirty_lst_head, &sett_conn_entry->sce_dirty_lst);

	return sett_conn_entry;
}

/*****************************************************************************/

static void
_plugin_connections_reload_cb (NMSettingsPlugin *plugin,
                               NMSettingsStorage *storage,
                               NMConnection *connection,
                               gpointer user_data)
{
	_connection_changed_track (user_data, storage, connection, FALSE);
}

static void
_plugin_connections_reload (NMSettings *self)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	GSList *iter;

	for (iter = priv->plugins; iter; iter = iter->next) {
		nm_settings_plugin_reload_connections (iter->data,
		                                       _plugin_connections_reload_cb,
		                                       self);
	}

	_connection_changed_process_all_dirty (self,
	                                       FALSE,
	                                       NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
	                                       NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
	                                       TRUE,
	                                         NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_SYSTEM_SECRETS
	                                       | NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_AGENT_SECRETS);

	for (iter = priv->plugins; iter; iter = iter->next)
		nm_settings_plugin_load_connections_done (iter->data);
}

/*****************************************************************************/

static gboolean
_add_connection_to_first_plugin (NMSettings *self,
                                 SettConnEntry *sett_conn_entry,
                                 NMConnection *new_connection,
                                 gboolean in_memory,
                                 NMSettingsConnectionIntFlags sett_flags,
                                 const char *shadowed_storage,
                                 gboolean shadowed_owned,
                                 NMSettingsStorage **out_new_storage,
                                 NMConnection **out_new_connection,
                                 GError **error)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_free_error GError *first_error = NULL;
	GSList *iter;
	const char *uuid;

	uuid = nm_connection_get_uuid (new_connection);

	nm_assert (nm_utils_is_uuid (uuid));

	for (iter = priv->plugins; iter; iter = iter->next) {
		NMSettingsPlugin *plugin = NM_SETTINGS_PLUGIN (iter->data);
		gs_unref_object NMSettingsStorage *storage = NULL;
		gs_unref_object NMConnection *connection_to_add = NULL;
		gs_unref_object NMConnection *connection_to_add_cloned = NULL;
		NMConnection *connection_to_add_real = NULL;
		gs_unref_variant GVariant *agent_owned_secrets = NULL;
		gs_free_error GError *add_error = NULL;
		gboolean success;
		const char *filename;

		if (!in_memory) {
			NMSettingsStorage *conflicting_storage;

			conflicting_storage = _sett_conn_entry_storage_find_conflicting_storage (sett_conn_entry, plugin, NULL, priv->plugins);
			if (conflicting_storage) {
				/* we have a connection provided by a plugin with higher priority than the one
				 * we would want to add the connection. We cannot do that, because doing so
				 * would result in adding a connection that gets hidden by the existing profile.
				 * Also, since we test the plugins in order of priority, all following plugins
				 * are unsuitable.
				 *
				 * Multiple connection plugins are so cumbersome, especially if they are unable
				 * to add the connection. I suggest to disable all plugins except keyfile. */
				_LOGT ("add-connection: failed to add %s/'%s': there is an existing storage "NM_SETTINGS_STORAGE_PRINT_FMT" with higher priority",
				       nm_connection_get_uuid (new_connection),
				       nm_connection_get_id (new_connection),
				       NM_SETTINGS_STORAGE_PRINT_ARG (conflicting_storage));
				nm_assert (first_error);
				break;
			}
		}

		if (plugin == (NMSettingsPlugin *) priv->keyfile_plugin) {
			success = nms_keyfile_plugin_add_connection (priv->keyfile_plugin,
			                                             new_connection,
			                                             in_memory,
			                                             NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED),
			                                             NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE),
			                                             NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL),
			                                             shadowed_storage,
			                                             shadowed_owned,
			                                             &storage,
			                                             &connection_to_add,
			                                             &add_error);
		} else {
			if (in_memory)
				continue;
			nm_assert (!NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED));
			nm_assert (!NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE));
			nm_assert (!NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL));
			success = nm_settings_plugin_add_connection (plugin,
			                                             new_connection,
			                                             &storage,
			                                             &connection_to_add,
			                                             &add_error);
		}

		if (!success) {
			_LOGT ("add-connection: failed to add %s/'%s': %s",
			       nm_connection_get_uuid (new_connection),
			       nm_connection_get_id (new_connection),
			       add_error->message);
			if (!first_error)
				first_error = g_steal_pointer (&add_error);
			continue;
		}

		if (!nm_streq0 (nm_settings_storage_get_uuid (storage), uuid)) {
			nm_assert_not_reached ();
			continue;
		}

		agent_owned_secrets = nm_connection_to_dbus (new_connection,
		                                               NM_CONNECTION_SERIALIZE_ONLY_SECRETS
		                                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_AGENT_OWNED);
		connection_to_add_real = _connection_changed_normalize_connection (storage,
		                                                                   connection_to_add,
		                                                                   agent_owned_secrets,
		                                                                   &connection_to_add_cloned);
		if (!connection_to_add_real) {
			nm_assert_not_reached ();
			continue;
		}

		filename = nm_settings_storage_get_filename (storage);
		_LOGT ("add-connection: successfully added connection %s,'%s' ("NM_SETTINGS_STORAGE_PRINT_FMT"%s%s%s",
		       nm_settings_storage_get_uuid (storage),
		       nm_connection_get_id (new_connection),
		       NM_SETTINGS_STORAGE_PRINT_ARG (storage),
		       NM_PRINT_FMT_QUOTED (filename, ", \"", filename, "\")", ")"));

		*out_new_storage = g_steal_pointer (&storage);
		*out_new_connection =    g_steal_pointer (&connection_to_add_cloned)
		                      ?: g_steal_pointer (&connection_to_add);
		nm_assert (NM_IS_CONNECTION (*out_new_connection));
		return TRUE;
	}

	nm_assert (first_error);
	g_propagate_error (error, g_steal_pointer (&first_error));
	return FALSE;
}

static gboolean
_update_connection_to_plugin (NMSettings *self,
                              NMSettingsStorage *storage,
                              NMConnection *connection,
                              NMSettingsConnectionIntFlags sett_flags,
                              gboolean force_rename,
                              const char *shadowed_storage,
                              gboolean shadowed_owned,
                              NMSettingsStorage **out_new_storage,
                              NMConnection **out_new_connection,
                              GError **error)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMSettingsPlugin *plugin;
	gboolean success;

	plugin = nm_settings_storage_get_plugin (storage);

	if (plugin == (NMSettingsPlugin *) priv->keyfile_plugin) {
		success = nms_keyfile_plugin_update_connection (priv->keyfile_plugin,
		                                                storage,
		                                                connection,
		                                                NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED),
		                                                NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE),
		                                                NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL),
		                                                shadowed_storage,
		                                                shadowed_owned,
		                                                force_rename,
		                                                out_new_storage,
		                                                out_new_connection,
		                                                error);
	} else {
		nm_assert (!shadowed_storage);
		nm_assert (!shadowed_owned);
		success = nm_settings_plugin_update_connection (plugin,
		                                                storage,
		                                                connection,
		                                                out_new_storage,
		                                                out_new_connection,
		                                                error);
	}

	return success;
}

static void
_set_nmmeta_tombstone (NMSettings *self,
                       const char *uuid,
                       gboolean tombstone_on_disk,
                       gboolean tombstone_in_memory,
                       const char *shadowed_storage)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_unref_object NMSettingsStorage *tombstone_1_storage = NULL;
	gs_unref_object NMSettingsStorage *tombstone_2_storage = NULL;

	if (tombstone_on_disk) {
		if (!nms_keyfile_plugin_set_nmmeta_tombstone (priv->keyfile_plugin,
		                                              FALSE,
		                                              uuid,
		                                              FALSE,
		                                              TRUE,
		                                              NULL,
		                                              &tombstone_1_storage,
		                                              NULL))
			tombstone_in_memory = TRUE;
		if (tombstone_1_storage)
			_connection_changed_track (self, tombstone_1_storage, NULL, FALSE);
	}

	if (tombstone_in_memory) {
		if (!nms_keyfile_plugin_set_nmmeta_tombstone (priv->keyfile_plugin,
		                                              FALSE,
		                                              uuid,
		                                              TRUE,
		                                              TRUE,
		                                              shadowed_storage,
		                                              &tombstone_2_storage,
		                                              NULL)) {
			nms_keyfile_plugin_set_nmmeta_tombstone (priv->keyfile_plugin,
			                                         TRUE,
			                                         uuid,
			                                         TRUE,
			                                         TRUE,
			                                         shadowed_storage,
			                                         &tombstone_2_storage,
			                                         NULL);
		}
		_connection_changed_track (self, tombstone_2_storage, NULL, FALSE);
	}
}

/**
 * nm_settings_add_connection:
 * @self: the #NMSettings object
 * @connection: the source connection to create a new #NMSettingsConnection from
 * @persist_mode: the persist-mode for this profile.
 * @add_reason: the add-reason flags.
 * @sett_flags: the settings flags to set.
 * @out_sett_conn: (allow-none) (transfer none): the added settings connection on success.
 * @error: on return, a location to store any errors that may occur
 *
 * Creates a new #NMSettingsConnection for the given source @connection.
 * The returned object is owned by @self and the caller must reference
 * the object to continue using it.
 *
 * Returns: TRUE on success.
 */
gboolean
nm_settings_add_connection (NMSettings *self,
                            NMConnection *connection,
                            NMSettingsConnectionPersistMode persist_mode,
                            NMSettingsConnectionAddReason add_reason,
                            NMSettingsConnectionIntFlags sett_flags,
                            NMSettingsConnection **out_sett_conn,
                            GError **error)
{
	NMSettingsPrivate *priv;
	gs_unref_object NMConnection *connection_cloned_1 = NULL;
	gs_unref_object NMConnection *new_connection = NULL;
	gs_unref_object NMSettingsStorage *new_storage = NULL;
	gs_unref_object NMSettingsStorage *shadowed_storage = NULL;
	NMSettingsStorage *update_storage = NULL;
	gs_free_error GError *local = NULL;
	SettConnEntry *sett_conn_entry;
	const char *uuid;
	StorageData *sd;
	gboolean new_in_memory;
	gboolean success;
	const char *shadowed_storage_filename = NULL;

	priv = NM_SETTINGS_GET_PRIVATE (self);

	nm_assert (NM_IN_SET (persist_mode, NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK,
	                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY));

	new_in_memory = (persist_mode != NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK);

	nm_assert (!NM_FLAGS_ANY (sett_flags, ~_NM_SETTINGS_CONNECTION_INT_FLAGS_PERSISTENT_MASK));

	if (NM_FLAGS_ANY (sett_flags,   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
	                              | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
	                              | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL)) {
		nm_assert (new_in_memory);
		new_in_memory = TRUE;
	}

	nm_assert (!NM_FLAGS_ANY (add_reason, ~NM_SETTINGS_CONNECTION_ADD_REASON_BLOCK_AUTOCONNECT));

	NM_SET_OUT (out_sett_conn, NULL);

	uuid = nm_connection_get_uuid (connection);

	sett_conn_entry = _sett_conn_entries_get (self, uuid);
	if (_sett_conn_entry_get_conn (sett_conn_entry)) {
		g_set_error_literal (error,
		                     NM_SETTINGS_ERROR,
		                     NM_SETTINGS_ERROR_UUID_EXISTS,
		                     "a connection with this UUID already exists");
		return FALSE;
	}

	if (!_nm_connection_ensure_normalized (connection,
	                                       FALSE,
	                                       NULL,
	                                       FALSE,
	                                       &connection_cloned_1,
	                                       &local)) {
		g_set_error (error,
		             NM_SETTINGS_ERROR,
		             NM_SETTINGS_ERROR_INVALID_CONNECTION,
		             "connection is invalid: %s",
		             local->message);
		return FALSE;
	}
	if (connection_cloned_1)
		connection = connection_cloned_1;

	if (sett_conn_entry) {
		c_list_for_each_entry (sd, &sett_conn_entry->sd_lst_head, sd_lst) {
			if (!nm_settings_storage_is_meta_data (sd->storage))
				continue;
			shadowed_storage = nm_g_object_ref (_sett_conn_entry_find_shadowed_storage (sett_conn_entry,
			                                                                            nm_settings_storage_get_shadowed_storage (sd->storage, NULL),
			                                                                            NULL));
			if (shadowed_storage) {
				/* We have a nmmeta tombstone that indicates that a storage is shadowed.
				 *
				 * This happens when deleting a in-memory profile that was decoupled from
				 * the persitant storage with NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED.
				 * We need to take over this storage again... */
				break;
			}
		}
	}

	if (   shadowed_storage
	    && !new_in_memory) {
		NMSettingsStorage *conflicting_storage;

		conflicting_storage = _sett_conn_entry_storage_find_conflicting_storage (sett_conn_entry,
		                                                                         nm_settings_storage_get_plugin (shadowed_storage),
		                                                                         shadowed_storage,
		                                                                         priv->plugins);
		if (conflicting_storage) {
			/* We cannot add the profile as @shadowed_storage, because there is another, existing storage
			 * that would hide it. Just add it as new storage. In general, this leads to duplication of profiles,
			 * but the circumstances where this happens are very exotic (you need at least one additional settings
			 * plugin, then going through the paths of making shadowed_storage in-memory-detached and delete it,
			 * and finally adding the conflicting storage outside of NM and restart/reload). */
			_LOGT ("ignore shadowed storage "NM_SETTINGS_STORAGE_PRINT_FMT" due to conflicting storage "NM_SETTINGS_STORAGE_PRINT_FMT,
			       NM_SETTINGS_STORAGE_PRINT_ARG (shadowed_storage),
			       NM_SETTINGS_STORAGE_PRINT_ARG (conflicting_storage));
		} else
			update_storage = shadowed_storage;
	}

	shadowed_storage_filename =   (   shadowed_storage
	                               && !update_storage)
	                            ? nm_settings_storage_get_filename_for_shadowed_storage (shadowed_storage)
	                            : NULL;

again_add_connection:

	if (!update_storage) {
		success = _add_connection_to_first_plugin (self,
		                                           sett_conn_entry,
		                                           connection,
		                                           new_in_memory,
		                                           sett_flags,
		                                           shadowed_storage_filename,
		                                           FALSE,
		                                           &new_storage,
		                                           &new_connection,
		                                           &local);
	} else {
		success = _update_connection_to_plugin (self,
		                                        update_storage,
		                                        connection,
		                                        sett_flags,
		                                        FALSE,
		                                        shadowed_storage_filename,
		                                        FALSE,
		                                        &new_storage,
		                                        &new_connection,
		                                        &local);
		if (!success) {
			if (!NMS_IS_KEYFILE_STORAGE (update_storage)) {
				/* hm, the intended storage is not keyfile (it's ifcfg-rh). This settings
				 * plugin may not support the new connection. So step back and retry adding
				 * the profile anew. */
				_LOGT ("failure to add profile as existing storage \"%s\": %s",
				       nm_settings_storage_get_filename (update_storage),
				       local->message);
				update_storage = NULL;
				g_clear_object (&shadowed_storage);
				shadowed_storage_filename = NULL;
				g_clear_error (&local);
				goto again_add_connection;
			}
		}
	}

	if (!success) {
		if (!update_storage) {
			g_set_error (error,
			             NM_SETTINGS_ERROR,
			             NM_SETTINGS_ERROR_FAILED,
			             "failure adding connection: %s",
			             local->message);
		} else {
			g_set_error (error,
			             NM_SETTINGS_ERROR,
			             NM_SETTINGS_ERROR_FAILED,
			             "failure writing connection to existing storage \"%s\": %s",
			             nm_settings_storage_get_filename (update_storage),
			             local->message);
		}
		return FALSE;
	}

	sett_conn_entry = _connection_changed_track (self, new_storage, new_connection, TRUE);

	c_list_for_each_entry (sd, &sett_conn_entry->sd_lst_head, sd_lst) {
		const NMSettingsMetaData *meta_data;
		gs_unref_object NMSettingsStorage *new_tombstone_storage = NULL;
		gboolean in_memory;
		gboolean simulate;

		meta_data = nm_settings_storage_is_meta_data_alive (sd->storage);
		if (   !meta_data
		    || !meta_data->is_tombstone)
			continue;

		if (nm_settings_storage_is_keyfile_run (sd->storage))
			in_memory = TRUE;
		else {
			if (nm_settings_storage_is_keyfile_run (new_storage)) {
				/* Don't remove the file from /etc if we just wrote an in-memory connection */
				continue;
			}
			in_memory = FALSE;
		}

		simulate = FALSE;
again_delete_tombstone:
		if (!nms_keyfile_plugin_set_nmmeta_tombstone (priv->keyfile_plugin,
		                                              simulate,
		                                              uuid,
		                                              in_memory,
		                                              FALSE,
		                                              NULL,
		                                              &new_tombstone_storage,
		                                              NULL)) {
			/* Ups, something went wrong. We really need to get rid of the tombstone. At least
			 * forget about it in-memory. Upong next restart/reload, this might be reverted
			 * however :( .*/
			if (!simulate) {
				simulate = TRUE;
				goto again_delete_tombstone;
			}
		}
		if (new_tombstone_storage)
			_connection_changed_track (self, new_tombstone_storage, NULL, FALSE);
	}

	_connection_changed_process_all_dirty (self,
	                                       FALSE,
	                                       sett_flags,
	                                       _NM_SETTINGS_CONNECTION_INT_FLAGS_PERSISTENT_MASK,
	                                       FALSE,
	                                         NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_SYSTEM_SECRETS
	                                       | NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_AGENT_SECRETS
	                                       | (  NM_FLAGS_HAS (add_reason, NM_SETTINGS_CONNECTION_ADD_REASON_BLOCK_AUTOCONNECT)
	                                          ? NM_SETTINGS_CONNECTION_UPDATE_REASON_BLOCK_AUTOCONNECT
	                                          : NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE));

	nm_assert (sett_conn_entry == _sett_conn_entries_get (self, sett_conn_entry->uuid));
	nm_assert (NM_IS_SETTINGS_CONNECTION (sett_conn_entry->sett_conn));

	NM_SET_OUT (out_sett_conn, _sett_conn_entry_get_conn (sett_conn_entry));
	return TRUE;
}

/*****************************************************************************/

gboolean
nm_settings_update_connection (NMSettings *self,
                               NMSettingsConnection *sett_conn,
                               NMConnection *connection,
                               NMSettingsConnectionPersistMode persist_mode,
                               NMSettingsConnectionIntFlags sett_flags,
                               NMSettingsConnectionIntFlags sett_mask,
                               NMSettingsConnectionUpdateReason update_reason,
                               const char *log_context_name,
                               GError **error)
{
	gs_unref_object NMConnection *connection_cloned_1 = NULL;
	gs_unref_object NMConnection *new_connection_cloned = NULL;
	gs_unref_object NMConnection *new_connection = NULL;
	NMConnection *new_connection_real;
	gs_unref_object NMSettingsStorage *cur_storage = NULL;
	gs_unref_object NMSettingsStorage *new_storage = NULL;
	NMSettingsStorage *drop_storage = NULL;
	SettConnEntry *sett_conn_entry;
	gboolean cur_in_memory;
	gboolean new_in_memory;
	const char *uuid;
	gboolean tombstone_in_memory = FALSE;
	gboolean tombstone_on_disk = FALSE;

	g_return_val_if_fail (NM_IS_SETTINGS (self), FALSE);
	g_return_val_if_fail (NM_IS_SETTINGS_CONNECTION (sett_conn), FALSE);
	g_return_val_if_fail (!connection || NM_IS_CONNECTION (connection), FALSE);

	nm_assert (!NM_FLAGS_ANY (sett_mask, ~_NM_SETTINGS_CONNECTION_INT_FLAGS_PERSISTENT_MASK));
	nm_assert (!NM_FLAGS_ANY (sett_flags, ~sett_mask));
	nm_assert (NM_IN_SET (persist_mode, NM_SETTINGS_CONNECTION_PERSIST_MODE_KEEP,
	                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST,
	                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK,
	                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY,
	                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED,
	                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY));

	cur_storage = g_object_ref (nm_settings_connection_get_storage (sett_conn));

	uuid = nm_settings_storage_get_uuid (cur_storage);

	nm_assert (NM_IS_SETTINGS_STORAGE (cur_storage));

	sett_conn_entry = _sett_conn_entries_get (self, uuid);

	nm_assert (_sett_conn_entry_get_conn (sett_conn_entry) == sett_conn);

	if (connection) {
		gs_free_error GError *local = NULL;

		if (!_nm_connection_ensure_normalized (connection,
		                                       FALSE,
		                                       uuid,
		                                       TRUE,
		                                       &connection_cloned_1,
		                                       &local)) {
			_LOGT ("update[%s]: %s: failed because profile is invalid: %s",
			       nm_settings_storage_get_uuid (cur_storage),
			       log_context_name,
			       local->message);
			g_set_error (error,
			             NM_SETTINGS_ERROR,
			             NM_SETTINGS_ERROR_INVALID_CONNECTION,
			             "connection is invalid: %s",
			             local->message);
			return FALSE;
		}
		if (connection_cloned_1)
			connection = connection_cloned_1;
	} else
		connection = nm_settings_connection_get_connection (sett_conn);

	cur_in_memory = nm_settings_storage_is_keyfile_run (cur_storage);

	if (persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_KEEP) {
		persist_mode =   cur_in_memory
		               ? NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY
		               : NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK;
	}

	if (   NM_FLAGS_HAS (sett_mask, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)
	    && !NM_FLAGS_HAS (sett_flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
		NMDevice *device;

		/* The connection has been changed by the user, it should no longer be
		 * considered a default wired connection, and should no longer affect
		 * the no-auto-default configuration option.
		 */
		device = nm_settings_connection_default_wired_get_device (sett_conn);
		if (device) {

			nm_assert (cur_in_memory);
			nm_assert (NM_FLAGS_HAS (nm_settings_connection_get_flags (sett_conn),
			                         NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED));

			default_wired_clear_tag (self, device, sett_conn, FALSE);

			if (NM_IN_SET (persist_mode, NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST)) {
				/* making a default-wired-connection a regular connection implies persisting
				 * it to disk (unless specified differently).
				 *
				 * Actually, this line is probably unreached, because we should not use
				 * NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST to toggle the nm-generated
				 * flag. */
				nm_assert_not_reached ();
				persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK;
			}
		}
	}

	if (   persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST
	    && NM_FLAGS_ANY (sett_mask,   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
	                                | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
	                                | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL)
	    && NM_FLAGS_ANY ((sett_flags ^ nm_settings_connection_get_flags (sett_conn)) & sett_mask,
	                       NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
	                     | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
	                     | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL)) {
		/* we update the nm-generated/volatile setting of a profile (which is inherently
		 * in-memory. The caller did not request to persist this to disk, however we need
		 * to store the flags in run. */
		nm_assert (cur_in_memory);
		persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY;
	}

	if (persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK)
		new_in_memory = FALSE;
	else if (NM_IN_SET (persist_mode, NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY,
	                                  NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED,
	                                  NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY))
		new_in_memory = TRUE;
	else {
		nm_assert (persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST);
		new_in_memory = cur_in_memory;
	}

	if (!new_in_memory) {
		/* Persistent connections cannot be volatile nor nm-generated.
		 *
		 * That is obviously true for volatile, as it is enforced by Update2() API.
		 *
		 * For nm-generated profiles also, because the nm-generated flag is only stored
		 * for in-memory profiles. If we would persist the profile to /etc it would loose
		 * the nm-generated flag after restart/reload, and that cannot be right. If a profile
		 * ends up on disk, the information who created it gets lost. */
		nm_assert (!NM_FLAGS_ANY (sett_flags,   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
		                                      | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
		                                      | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL));
		sett_mask |=   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
		             | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
		             | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL;
		sett_flags &= ~(  NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
		                | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
		                | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL);
	}

	if (persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST) {
		new_storage = g_object_ref (cur_storage);
		new_connection_real = connection;
		_LOGT ("update[%s]: %s: update profile \"%s\" (not persisted)",
		       nm_settings_storage_get_uuid (cur_storage),
		       log_context_name,
		       nm_connection_get_id (connection));
	} else {
		NMSettingsStorage *shadowed_storage;
		const char *cur_shadowed_storage_filename;
		const char *new_shadowed_storage_filename = NULL;
		gboolean cur_shadowed_owned;
		gboolean new_shadowed_owned = FALSE;
		NMSettingsStorage *update_storage = NULL;
		gs_free_error GError *local = NULL;
		gboolean success;

		cur_shadowed_storage_filename = nm_settings_storage_get_shadowed_storage (cur_storage, &cur_shadowed_owned);

		shadowed_storage = _sett_conn_entry_find_shadowed_storage (sett_conn_entry, cur_shadowed_storage_filename, cur_storage);
		if (!shadowed_storage) {
			cur_shadowed_storage_filename = NULL;
			cur_shadowed_owned = FALSE;
		}

		if (   new_in_memory
		    && persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY) {
			if (cur_in_memory) {
				drop_storage = shadowed_storage;
				update_storage = cur_storage;
			} else
				drop_storage = cur_storage;
		} else if (   !new_in_memory
		           && cur_in_memory
		           && shadowed_storage) {
			drop_storage = cur_storage;
			update_storage = shadowed_storage;
		} else if (new_in_memory != cur_in_memory) {
			if (!new_in_memory)
				drop_storage = cur_storage;
			else if (persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY)
				drop_storage = cur_storage;
			else {
				nm_assert (NM_IN_SET (persist_mode, NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY,
				                                    NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED));
			}
		} else if (nm_settings_storage_is_keyfile_lib (cur_storage)) {
			/* the profile is a keyfile in /usr/lib. It cannot be overwritten, we must migrate it
			 * from /usr/lib to /etc. */
		} else
			update_storage = cur_storage;

		if (new_in_memory) {
			if (persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY) {
				/* pass */
			} else if (!cur_in_memory) {
				new_shadowed_storage_filename = nm_settings_storage_get_filename_for_shadowed_storage (cur_storage);
				if (   new_shadowed_storage_filename
				    && persist_mode != NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED)
					new_shadowed_owned = TRUE;
			} else {
				new_shadowed_storage_filename = cur_shadowed_storage_filename;
				if (   new_shadowed_storage_filename
				    && persist_mode == NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY)
					new_shadowed_owned = TRUE;
			}
		}

		if (!update_storage) {
			success = _add_connection_to_first_plugin (self,
			                                           sett_conn_entry,
			                                           connection,
			                                           new_in_memory,
			                                           sett_flags,
			                                           new_shadowed_storage_filename,
			                                           new_shadowed_owned,
			                                           &new_storage,
			                                           &new_connection,
			                                           &local);
		} else {
			success = _update_connection_to_plugin (self,
			                                        update_storage,
			                                        connection,
			                                        sett_flags,
			                                        update_reason,
			                                        new_shadowed_storage_filename,
			                                        new_shadowed_owned,
			                                        &new_storage,
			                                        &new_connection,
			                                        &local);
		}
		if (!success) {
			gboolean ignore_failure;

			ignore_failure = NM_FLAGS_ANY (update_reason, NM_SETTINGS_CONNECTION_UPDATE_REASON_IGNORE_PERSIST_FAILURE);

			_LOGT ("update[%s]: %s: %sfailure to %s connection \"%s\" on storage: %s",
			       nm_settings_storage_get_uuid (cur_storage),
			       log_context_name,
			       ignore_failure ? "ignore " : "",
			       update_storage ? "update" : "write",
			       nm_connection_get_id (connection),
			       local->message);
			if (!ignore_failure) {
				g_set_error (error,
				             NM_SETTINGS_ERROR,
				             NM_SETTINGS_ERROR_INVALID_CONNECTION,
				             "failed to %s connection: %s",
				             update_storage ? "update" : "write",
				             local->message);
				return FALSE;
			}

			new_storage = g_object_ref (cur_storage);
			new_connection_real = connection;
		} else {
			gs_unref_variant GVariant *agent_owned_secrets = NULL;

			_LOGT ("update[%s]: %s: %s profile \"%s\"",
			       nm_settings_storage_get_uuid (cur_storage),
			       log_context_name,
			       update_storage ? "update" : "write",
			       nm_connection_get_id (connection));

			nm_assert_valid_settings_storage (NULL, new_storage);
			nm_assert (NM_IS_CONNECTION (new_connection));
			nm_assert (nm_streq (uuid, nm_settings_storage_get_uuid (new_storage)));


			agent_owned_secrets = nm_connection_to_dbus (connection,
			                                               NM_CONNECTION_SERIALIZE_ONLY_SECRETS
			                                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_AGENT_OWNED);
			new_connection_real = _connection_changed_normalize_connection (new_storage,
			                                                                new_connection,
			                                                                agent_owned_secrets,
			                                                                &new_connection_cloned);
			if (!new_connection_real) {
				nm_assert_not_reached ();
				new_connection_real = new_connection;
			}
		}
	}

	nm_assert (NM_IS_SETTINGS_STORAGE (new_storage));
	nm_assert (NM_IS_CONNECTION (new_connection_real));

	_connection_changed_track (self, new_storage, new_connection_real, TRUE);

	if (   drop_storage
	    && drop_storage != new_storage) {
		gs_free_error GError *local = NULL;

		if (!nm_settings_plugin_delete_connection (nm_settings_storage_get_plugin (drop_storage),
		                                           drop_storage,
		                                           &local)) {
			const char *filename;

			filename = nm_settings_storage_get_filename (drop_storage);
			_LOGT ("update[%s]: failed to delete moved storage "NM_SETTINGS_STORAGE_PRINT_FMT"%s%s%s: %s",
			       nm_settings_storage_get_uuid (drop_storage),
			       NM_SETTINGS_STORAGE_PRINT_ARG (drop_storage),
			       NM_PRINT_FMT_QUOTED (filename, " (file \"", filename, "\")", ""),
			       local->message);
			/* there is no aborting back form this. We must get rid of the connection and
			 * cannot do better than log a message. Proceed, but remember to write tombstones. */
			if (nm_settings_storage_is_keyfile_run (cur_storage))
				tombstone_in_memory = TRUE;
			else
				tombstone_on_disk = TRUE;
		} else
			_connection_changed_track (self, drop_storage, NULL, FALSE);
	}

	_set_nmmeta_tombstone (self,
	                       uuid,
	                       tombstone_on_disk,
	                       tombstone_in_memory,
	                       NULL);

	_connection_changed_process_all_dirty (self,
	                                       FALSE,
	                                       sett_flags,
	                                       sett_mask,
	                                       FALSE,
	                                       update_reason);

	return TRUE;
}

void
nm_settings_delete_connection (NMSettings *self,
                               NMSettingsConnection *sett_conn,
                               gboolean allow_add_to_no_auto_default)
{
	NMSettingsStorage *cur_storage;
	NMSettingsStorage *shadowed_storage;
	NMSettingsStorage *shadowed_storage_unowned = NULL;
	NMSettingsStorage *drop_storages[2] = { };
	gs_free_error GError *local = NULL;
	SettConnEntry *sett_conn_entry;
	const char *cur_shadowed_storage_filename;
	const char *new_shadowed_storage_filename = NULL;
	gboolean cur_shadowed_owned;
	const char *uuid;
	gboolean tombstone_in_memory = FALSE;
	gboolean tombstone_on_disk = FALSE;
	int i;

	g_return_if_fail (NM_IS_SETTINGS (self));
	g_return_if_fail (NM_IS_SETTINGS_CONNECTION (sett_conn));
	g_return_if_fail (nm_settings_has_connection (self, sett_conn));

	cur_storage = nm_settings_connection_get_storage (sett_conn);

	nm_assert (NM_IS_SETTINGS_STORAGE (cur_storage));

	uuid = nm_settings_storage_get_uuid (cur_storage);
	nm_assert (nm_utils_is_uuid (uuid));

	sett_conn_entry = _sett_conn_entries_get (self, uuid);

	g_return_if_fail (sett_conn_entry);
	nm_assert (sett_conn_entry->sett_conn == sett_conn);
	g_return_if_fail (sett_conn_entry->storage == cur_storage);

	if (NMS_IS_KEYFILE_STORAGE (cur_storage)) {
		NMSKeyfileStorage *s = NMS_KEYFILE_STORAGE (cur_storage);

		if (NM_IN_SET (s->storage_type, NMS_KEYFILE_STORAGE_TYPE_RUN,
		                                NMS_KEYFILE_STORAGE_TYPE_ETC))
			drop_storages[0] = cur_storage;
		else
			tombstone_on_disk = TRUE;
	} else
		drop_storages[0] = cur_storage;

	cur_shadowed_storage_filename = nm_settings_storage_get_shadowed_storage (cur_storage, &cur_shadowed_owned);

	shadowed_storage = _sett_conn_entry_find_shadowed_storage (sett_conn_entry, cur_shadowed_storage_filename, cur_storage);
	if (shadowed_storage) {
		if (!cur_shadowed_owned)
			shadowed_storage_unowned = g_steal_pointer (&shadowed_storage);
	}
	drop_storages[1] = shadowed_storage;

	for (i = 0; i < (int) G_N_ELEMENTS (drop_storages); i++) {
		NMSettingsStorage *storage;
		StorageData *sd;

		storage = drop_storages[i];
		if (!storage)
			continue;

		if (!nm_settings_plugin_delete_connection (nm_settings_storage_get_plugin (storage),
		                                           storage,
		                                           &local)) {
			_LOGT ("delete-connection: failed to delete storage "NM_SETTINGS_STORAGE_PRINT_FMT": %s",
			       NM_SETTINGS_STORAGE_PRINT_ARG (storage),
			       local->message);
			g_clear_error (&local);
			/* there is no aborting back form this. We must get rid of the connection and
			 * cannot do better than log a message. Proceed, but remember to write tombstones. */
			if (nm_settings_storage_is_keyfile_run (cur_storage))
				tombstone_in_memory = TRUE;
			else
				tombstone_on_disk = TRUE;
			sett_conn_entry = _sett_conn_entries_get (self, uuid);
		} else
			sett_conn_entry = _connection_changed_track (self, storage, NULL, FALSE);

		c_list_for_each_entry (sd, &sett_conn_entry->sd_lst_head, sd_lst) {
			if (NM_IN_SET (sd->storage, drop_storages[0],
			                            drop_storages[1]))
				continue;
			if (!_storage_data_is_alive (sd))
				continue;
			if (nm_settings_storage_is_meta_data (sd->storage))
				continue;

			if (sd->storage == shadowed_storage_unowned) {
				/* this only happens if we leak a profile on disk after NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED.
				 * We need to write a tombstone and remember the shadowed-storage. */
				tombstone_in_memory = TRUE;
				new_shadowed_storage_filename = nm_settings_storage_get_filename (shadowed_storage_unowned);
				continue;
			}

			/* we have still conflicting storages. We need to hide them with tombstones. */
			if (nm_settings_storage_is_keyfile_run (sd->storage)) {
				tombstone_in_memory = TRUE;
				continue;
			}
			tombstone_on_disk = TRUE;
		}
	}

	_set_nmmeta_tombstone (self,
	                       uuid,
	                       tombstone_on_disk,
	                       tombstone_in_memory,
	                       new_shadowed_storage_filename);

	_connection_changed_process_all_dirty (self,
	                                       allow_add_to_no_auto_default,
	                                       NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
	                                       NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
	                                       FALSE,
	                                       NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE);
}

/*****************************************************************************/

static void
send_agent_owned_secrets (NMSettings *self,
                          NMSettingsConnection *sett_conn,
                          NMAuthSubject *subject)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_unref_object NMConnection *for_agent = NULL;

	/* Dupe the connection so we can clear out non-agent-owned secrets,
	 * as agent-owned secrets are the only ones we send back to be saved.
	 * Only send secrets to agents of the same UID that called update too.
	 */
	for_agent = nm_simple_connection_new_clone (nm_settings_connection_get_connection (sett_conn));
	_nm_connection_clear_secrets_by_secret_flags (for_agent,
	                                              NM_SETTING_SECRET_FLAG_AGENT_OWNED);
	nm_agent_manager_save_secrets (priv->agent_mgr,
	                               nm_dbus_object_get_path (NM_DBUS_OBJECT (sett_conn)),
	                               for_agent,
	                               subject);
}

static void
pk_add_cb (NMAuthChain *chain,
           GDBusMethodInvocation *context,
           gpointer user_data)
{
	NMSettings *self = NM_SETTINGS (user_data);
	NMAuthCallResult result;
	gs_free_error GError *error = NULL;
	NMConnection *connection = NULL;
	gs_unref_object NMSettingsConnection *added = NULL;
	NMSettingsAddCallback callback;
	gpointer callback_data;
	NMAuthSubject *subject;
	const char *perm;

	nm_assert (G_IS_DBUS_METHOD_INVOCATION (context));

	c_list_unlink (nm_auth_chain_parent_lst_list (chain));

	perm = nm_auth_chain_get_data (chain, "perm");
	nm_assert (perm);

	result = nm_auth_chain_get_result (chain, perm);

	if (result != NM_AUTH_CALL_RESULT_YES) {
		error = g_error_new_literal (NM_SETTINGS_ERROR,
		                             NM_SETTINGS_ERROR_PERMISSION_DENIED,
		                             NM_UTILS_ERROR_MSG_INSUFF_PRIV);
	} else {
		/* Authorized */
		connection = nm_auth_chain_get_data (chain, "connection");
		nm_assert (NM_IS_CONNECTION (connection));

		nm_settings_add_connection (self,
		                            connection,
		                            GPOINTER_TO_UINT (nm_auth_chain_get_data (chain, "persist-mode")),
		                            GPOINTER_TO_UINT (nm_auth_chain_get_data (chain, "add-reason")),
		                            GPOINTER_TO_UINT (nm_auth_chain_get_data (chain, "sett-flags")),
		                            &added,
		                            &error);

		/* The callback may remove the connection from the settings manager (e.g.
		 * because it's found to be incompatible with the device on AddAndActivate).
		 * But we need to keep it alive for a bit longer, precisely to check wehther
		 * it's still known to the setting manager. */
		nm_g_object_ref (added);
	}

	callback = nm_auth_chain_get_data (chain, "callback");
	callback_data = nm_auth_chain_get_data (chain, "callback-data");
	subject = nm_auth_chain_get_data (chain, "subject");

	callback (self, added, error, context, subject, callback_data);

	/* Send agent-owned secrets to the agents */
	if (   added
	    && nm_settings_has_connection (self, added))
		send_agent_owned_secrets (self, added, subject);
}

void
nm_settings_add_connection_dbus (NMSettings *self,
                                 NMConnection *connection,
                                 NMSettingsConnectionPersistMode persist_mode,
                                 NMSettingsConnectionAddReason add_reason,
                                 NMSettingsConnectionIntFlags sett_flags,
                                 NMAuthSubject *subject,
                                 GDBusMethodInvocation *context,
                                 NMSettingsAddCallback callback,
                                 gpointer user_data)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMSettingConnection *s_con;
	NMAuthChain *chain;
	GError *error = NULL, *tmp_error = NULL;
	const char *perm;

	g_return_if_fail (NM_IS_CONNECTION (connection));
	g_return_if_fail (NM_IS_AUTH_SUBJECT (subject));
	g_return_if_fail (G_IS_DBUS_METHOD_INVOCATION (context));

	nm_assert (!NM_FLAGS_ANY (sett_flags, ~_NM_SETTINGS_CONNECTION_INT_FLAGS_PERSISTENT_MASK));

	/* Connection must be valid, of course */
	if (_nm_connection_verify (connection, &tmp_error) != NM_SETTING_VERIFY_SUCCESS) {
		error = g_error_new (NM_SETTINGS_ERROR,
		                     NM_SETTINGS_ERROR_INVALID_CONNECTION,
		                     "The connection was invalid: %s",
		                     tmp_error->message);
		g_error_free (tmp_error);
		goto done;
	}

	if (!nm_auth_is_subject_in_acl_set_error (connection,
	                                          subject,
	                                          NM_SETTINGS_ERROR,
	                                          NM_SETTINGS_ERROR_PERMISSION_DENIED,
	                                          &error))
		goto done;

	/* If the caller is the only user in the connection's permissions, then
	 * we use the 'modify.own' permission instead of 'modify.system'.  If the
	 * request affects more than just the caller, require 'modify.system'.
	 */
	s_con = nm_connection_get_setting_connection (connection);
	nm_assert (s_con);
	if (nm_setting_connection_get_num_permissions (s_con) == 1)
		perm = NM_AUTH_PERMISSION_SETTINGS_MODIFY_OWN;
	else
		perm = NM_AUTH_PERMISSION_SETTINGS_MODIFY_SYSTEM;

	/* Validate the user request */
	chain = nm_auth_chain_new_subject (subject, context, pk_add_cb, self);
	if (!chain) {
		error = g_error_new_literal (NM_SETTINGS_ERROR,
		                             NM_SETTINGS_ERROR_PERMISSION_DENIED,
		                             NM_UTILS_ERROR_MSG_REQ_AUTH_FAILED);
		goto done;
	}

	c_list_link_tail (&priv->auth_lst_head, nm_auth_chain_parent_lst_list (chain));

	nm_auth_chain_set_data (chain, "perm", (gpointer) perm, NULL);
	nm_auth_chain_set_data (chain, "connection", g_object_ref (connection), g_object_unref);
	nm_auth_chain_set_data (chain, "callback", callback, NULL);
	nm_auth_chain_set_data (chain, "callback-data", user_data, NULL);
	nm_auth_chain_set_data (chain, "subject", g_object_ref (subject), g_object_unref);
	nm_auth_chain_set_data (chain, "persist-mode", GUINT_TO_POINTER (persist_mode), NULL);
	nm_auth_chain_set_data (chain, "add-reason", GUINT_TO_POINTER (add_reason), NULL);
	nm_auth_chain_set_data (chain, "sett-flags", GUINT_TO_POINTER (sett_flags), NULL);
	nm_auth_chain_add_call_unsafe (chain, perm, TRUE);
	return;

done:
	nm_assert (error);
	callback (self, NULL, error, context, subject, user_data);
	g_error_free (error);
}

static void
settings_add_connection_add_cb (NMSettings *self,
                                NMSettingsConnection *connection,
                                GError *error,
                                GDBusMethodInvocation *context,
                                NMAuthSubject *subject,
                                gpointer user_data)
{
	gboolean is_add_connection_2 = GPOINTER_TO_INT (user_data);

	if (error) {
		g_dbus_method_invocation_return_gerror (context, error);
		nm_audit_log_connection_op (NM_AUDIT_OP_CONN_ADD, NULL, FALSE, NULL, subject, error->message);
		return;
	}

	if (is_add_connection_2) {
		GVariantBuilder builder;

		g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
		g_dbus_method_invocation_return_value (context,
		                                       g_variant_new ("(oa{sv})",
		                                                      nm_dbus_object_get_path (NM_DBUS_OBJECT (connection)),
		                                                      &builder));
	} else {
		g_dbus_method_invocation_return_value (context,
		                                       g_variant_new ("(o)",
		                                                      nm_dbus_object_get_path (NM_DBUS_OBJECT (connection))));
	}
	nm_audit_log_connection_op (NM_AUDIT_OP_CONN_ADD, connection, TRUE, NULL,
	                            subject, NULL);
}

static void
settings_add_connection_helper (NMSettings *self,
                                GDBusMethodInvocation *context,
                                gboolean is_add_connection_2,
                                GVariant *settings,
                                NMSettingsAddConnection2Flags flags)
{
	gs_unref_object NMConnection *connection = NULL;
	GError *error = NULL;
	gs_unref_object NMAuthSubject *subject = NULL;
	NMSettingsConnectionPersistMode persist_mode;

	connection = _nm_simple_connection_new_from_dbus (settings,
	                                                    NM_SETTING_PARSE_FLAGS_STRICT
	                                                  | NM_SETTING_PARSE_FLAGS_NORMALIZE,
	                                                  &error);

	if (   !connection
	    || !nm_connection_verify_secrets (connection, &error)) {
		g_dbus_method_invocation_take_error (context, error);
		return;
	}

	subject = nm_dbus_manager_new_auth_subject_from_context (context);
	if (!subject) {
		g_dbus_method_invocation_return_error_literal (context,
		                                               NM_SETTINGS_ERROR,
		                                               NM_SETTINGS_ERROR_PERMISSION_DENIED,
		                                               NM_UTILS_ERROR_MSG_REQ_UID_UKNOWN);
		return;
	}

	if (NM_FLAGS_HAS (flags, NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK))
		persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK;
	else {
		nm_assert (NM_FLAGS_HAS (flags, NM_SETTINGS_ADD_CONNECTION2_FLAG_IN_MEMORY));
		persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY;
	}

	nm_settings_add_connection_dbus (self,
	                                 connection,
	                                 persist_mode,
	                                   NM_FLAGS_HAS (flags, NM_SETTINGS_ADD_CONNECTION2_FLAG_BLOCK_AUTOCONNECT)
	                                 ? NM_SETTINGS_CONNECTION_ADD_REASON_BLOCK_AUTOCONNECT
	                                 : NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
	                                 NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
	                                 subject,
	                                 context,
	                                 settings_add_connection_add_cb,
	                                 GINT_TO_POINTER (!!is_add_connection_2));
}

static void
impl_settings_add_connection (NMDBusObject *obj,
                              const NMDBusInterfaceInfoExtended *interface_info,
                              const NMDBusMethodInfoExtended *method_info,
                              GDBusConnection *connection,
                              const char *sender,
                              GDBusMethodInvocation *invocation,
                              GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	gs_unref_variant GVariant *settings = NULL;

	g_variant_get (parameters, "(@a{sa{sv}})", &settings);
	settings_add_connection_helper (self, invocation, FALSE, settings, NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK);
}

static void
impl_settings_add_connection_unsaved (NMDBusObject *obj,
                                      const NMDBusInterfaceInfoExtended *interface_info,
                                      const NMDBusMethodInfoExtended *method_info,
                                      GDBusConnection *connection,
                                      const char *sender,
                                      GDBusMethodInvocation *invocation,
                                      GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	gs_unref_variant GVariant *settings = NULL;

	g_variant_get (parameters, "(@a{sa{sv}})", &settings);
	settings_add_connection_helper (self, invocation, FALSE, settings, NM_SETTINGS_ADD_CONNECTION2_FLAG_IN_MEMORY);
}

static void
impl_settings_add_connection2 (NMDBusObject *obj,
                               const NMDBusInterfaceInfoExtended *interface_info,
                               const NMDBusMethodInfoExtended *method_info,
                               GDBusConnection *connection,
                               const char *sender,
                               GDBusMethodInvocation *invocation,
                               GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	gs_unref_variant GVariant *settings = NULL;
	gs_unref_variant GVariant *args = NULL;
	NMSettingsAddConnection2Flags flags;
	const char *args_name;
	GVariantIter iter;
	guint32 flags_u;

	g_variant_get (parameters, "(@a{sa{sv}}u@a{sv})", &settings, &flags_u, &args);

	if (NM_FLAGS_ANY (flags_u, ~((guint32) (  NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK
	                                        | NM_SETTINGS_ADD_CONNECTION2_FLAG_IN_MEMORY
	                                        | NM_SETTINGS_ADD_CONNECTION2_FLAG_BLOCK_AUTOCONNECT)))) {
		g_dbus_method_invocation_take_error (invocation,
		                                     g_error_new_literal (NM_SETTINGS_ERROR,
		                                                          NM_SETTINGS_ERROR_INVALID_ARGUMENTS,
		                                                          "Unknown flags"));
		return;
	}

	flags = flags_u;

	if (!NM_FLAGS_ANY (flags,   NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK
	                          | NM_SETTINGS_ADD_CONNECTION2_FLAG_IN_MEMORY)) {
		g_dbus_method_invocation_take_error (invocation,
		                                     g_error_new_literal (NM_SETTINGS_ERROR,
		                                                          NM_SETTINGS_ERROR_INVALID_ARGUMENTS,
		                                                          "Requires either to-disk (0x1) or in-memory (0x2) flags"));
		return;
	}

	if (NM_FLAGS_ALL (flags,   NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK
	                         | NM_SETTINGS_ADD_CONNECTION2_FLAG_IN_MEMORY)) {
		g_dbus_method_invocation_take_error (invocation,
		                                     g_error_new_literal (NM_SETTINGS_ERROR,
		                                                          NM_SETTINGS_ERROR_INVALID_ARGUMENTS,
		                                                          "Cannot set to-disk (0x1) and in-memory (0x2) flags together"));
		return;
	}

	nm_assert (g_variant_is_of_type (args, G_VARIANT_TYPE ("a{sv}")));

	g_variant_iter_init (&iter, args);
	while (g_variant_iter_next (&iter, "{&sv}", &args_name, NULL)) {
		g_dbus_method_invocation_take_error (invocation,
		                                     g_error_new (NM_SETTINGS_ERROR,
		                                                  NM_SETTINGS_ERROR_INVALID_ARGUMENTS,
		                                                  "Unsupported argument '%s'", args_name));
		return;
	}

	settings_add_connection_helper (self, invocation, TRUE, settings, flags);
}

/*****************************************************************************/

static void
impl_settings_load_connections (NMDBusObject *obj,
                                const NMDBusInterfaceInfoExtended *interface_info,
                                const NMDBusMethodInfoExtended *method_info,
                                GDBusConnection *dbus_connection,
                                const char *sender,
                                GDBusMethodInvocation *invocation,
                                GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_unref_ptrarray GPtrArray *failures = NULL;
	gs_free const char **filenames = NULL;
	gs_free char *op_result_str = NULL;

	g_variant_get (parameters, "(^a&s)", &filenames);

	/* The permission is already enforced by the D-Bus daemon, but we ensure
	 * that the caller is still alive so that clients are forced to wait and
	 * we'll be able to switch to polkit without breaking behavior.
	 */
	if (!nm_dbus_manager_ensure_uid (nm_dbus_object_get_manager (obj),
	                                 invocation,
	                                 G_MAXULONG,
	                                 NM_SETTINGS_ERROR,
	                                 NM_SETTINGS_ERROR_PERMISSION_DENIED))
		return;

	if (   filenames
	    && filenames[0]) {
		NMSettingsPluginConnectionLoadEntry *entries;
		gsize n_entries;
		gsize i;
		GSList *iter;

		entries = nm_settings_plugin_create_connection_load_entries (filenames, &n_entries);

		for (iter = priv->plugins; iter; iter = iter->next) {
			NMSettingsPlugin *plugin = iter->data;

			nm_settings_plugin_load_connections (plugin,
			                                     entries,
			                                     n_entries,
			                                     _plugin_connections_reload_cb,
			                                     self);
		}

		for (i = 0; i < n_entries; i++) {
			NMSettingsPluginConnectionLoadEntry *entry = &entries[i];

			if (!entry->handled) {
				_LOGW ("load: no settings plugin could load \"%s\"", entry->filename);
				nm_assert (!entry->error);
			} else if (entry->error) {
				_LOGW ("load: failure to load \"%s\": %s", entry->filename, entry->error->message);
				g_clear_error (&entry->error);
			} else
				continue;

			if (!failures)
				failures = g_ptr_array_new ();
			g_ptr_array_add (failures, (char *) entry->filename);
		}

		nm_clear_g_free (&entries);

		_connection_changed_process_all_dirty (self,
		                                       TRUE,
		                                       NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
		                                       NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
		                                       TRUE,
		                                         NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_SYSTEM_SECRETS
		                                       | NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_AGENT_SECRETS);

		for (iter = priv->plugins; iter; iter = iter->next)
			nm_settings_plugin_load_connections_done (iter->data);
	}

	if (failures)
		g_ptr_array_add (failures, NULL);

	nm_audit_log_connection_op (NM_AUDIT_OP_CONNS_LOAD,
	                            NULL,
	                            !failures,
	                            (op_result_str = g_strjoinv (",", (char **) filenames)),
	                            invocation,
	                            NULL);

	g_dbus_method_invocation_return_value (invocation,
	                                       g_variant_new ("(b^as)",
	                                                      (gboolean) (!failures),
	                                                      failures
	                                                        ? (const char **) failures->pdata
	                                                        : NM_PTRARRAY_EMPTY (const char *)));
}

static void
impl_settings_reload_connections (NMDBusObject *obj,
                                  const NMDBusInterfaceInfoExtended *interface_info,
                                  const NMDBusMethodInfoExtended *method_info,
                                  GDBusConnection *connection,
                                  const char *sender,
                                  GDBusMethodInvocation *invocation,
                                  GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);

	/* The permission is already enforced by the D-Bus daemon, but we ensure
	 * that the caller is still alive so that clients are forced to wait and
	 * we'll be able to switch to polkit without breaking behavior.
	 */
	if (!nm_dbus_manager_ensure_uid (nm_dbus_object_get_manager (obj),
	                                 invocation,
	                                 G_MAXULONG,
	                                 NM_SETTINGS_ERROR,
	                                 NM_SETTINGS_ERROR_PERMISSION_DENIED))
		return;

	_plugin_connections_reload (self);

	nm_audit_log_connection_op (NM_AUDIT_OP_CONNS_RELOAD, NULL, TRUE, NULL, invocation, NULL);

	/* We MUST return %TRUE here, otherwise older libnm versions might misbehave. */
	g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", TRUE));
}

/*****************************************************************************/

static void
_clear_connections_cached_list (NMSettingsPrivate *priv)
{
	if (!priv->connections_cached_list)
		return;

	nm_assert (priv->connections_len == NM_PTRARRAY_LEN (priv->connections_cached_list));

#if NM_MORE_ASSERTS
	/* set the pointer to a bogus value. This makes it more apparent
	 * if somebody has a reference to the cached list and still uses
	 * it. That is a bug, this code just tries to make it blow up
	 * more eagerly. */
	memset (priv->connections_cached_list,
	        0x43,
	        sizeof (NMSettingsConnection *) * (priv->connections_len + 1));
#endif

	nm_clear_g_free (&priv->connections_cached_list);
}

static void
impl_settings_list_connections (NMDBusObject *obj,
                                const NMDBusInterfaceInfoExtended *interface_info,
                                const NMDBusMethodInfoExtended *method_info,
                                GDBusConnection *dbus_connection,
                                const char *sender,
                                GDBusMethodInvocation *invocation,
                                GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	gs_free const char **strv = NULL;

	strv = nm_dbus_utils_get_paths_for_clist (&priv->connections_lst_head,
	                                          priv->connections_len,
	                                          G_STRUCT_OFFSET (NMSettingsConnection, _connections_lst),
	                                          TRUE);
	g_dbus_method_invocation_return_value (invocation,
	                                       g_variant_new ("(^ao)", strv));
}

NMSettingsConnection *
nm_settings_get_connection_by_uuid (NMSettings *self, const char *uuid)
{
	g_return_val_if_fail (NM_IS_SETTINGS (self), NULL);
	g_return_val_if_fail (uuid != NULL, NULL);

	return _sett_conn_entry_get_conn (_sett_conn_entries_get (self, uuid));
}

const char *
nm_settings_get_dbus_path_for_uuid (NMSettings *self,
                                    const char *uuid)
{
	NMSettingsConnection *sett_conn;

	sett_conn = nm_settings_get_connection_by_uuid (self, uuid);

	if (!sett_conn)
		return NULL;

	return nm_dbus_object_get_path (NM_DBUS_OBJECT (sett_conn));
}

static void
impl_settings_get_connection_by_uuid (NMDBusObject *obj,
                                      const NMDBusInterfaceInfoExtended *interface_info,
                                      const NMDBusMethodInfoExtended *method_info,
                                      GDBusConnection *dbus_connection,
                                      const char *sender,
                                      GDBusMethodInvocation *invocation,
                                      GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	NMSettingsConnection *sett_conn;
	gs_unref_object NMAuthSubject *subject = NULL;
	GError *error = NULL;
	const char *uuid;

	g_variant_get (parameters, "(&s)", &uuid);

	sett_conn = nm_settings_get_connection_by_uuid (self, uuid);
	if (!sett_conn) {
		error = g_error_new_literal (NM_SETTINGS_ERROR,
		                             NM_SETTINGS_ERROR_INVALID_CONNECTION,
		                             "No connection with the UUID was found.");
		goto error;
	}

	subject = nm_dbus_manager_new_auth_subject_from_context (invocation);
	if (!subject) {
		error = g_error_new_literal (NM_SETTINGS_ERROR,
		                             NM_SETTINGS_ERROR_PERMISSION_DENIED,
		                             NM_UTILS_ERROR_MSG_REQ_UID_UKNOWN);
		goto error;
	}

	if (!nm_auth_is_subject_in_acl_set_error (nm_settings_connection_get_connection (sett_conn),
	                                          subject,
	                                          NM_SETTINGS_ERROR,
	                                          NM_SETTINGS_ERROR_PERMISSION_DENIED,
	                                          &error))
		goto error;

	g_dbus_method_invocation_return_value (invocation,
	                                       g_variant_new ("(o)",
	                                                      nm_dbus_object_get_path (NM_DBUS_OBJECT (sett_conn))));
	return;

error:
	g_dbus_method_invocation_take_error (invocation, error);
}

/**
 * nm_settings_get_connections:
 * @self: the #NMSettings
 * @out_len: (out) (allow-none): returns the number of returned
 *   connections.
 *
 * Returns: (transfer none): a list of NMSettingsConnections. The list is
 * unsorted and NULL terminated. The result is never %NULL, in case of no
 * connections, it returns an empty list.
 * The returned list is cached internally, only valid until the next
 * NMSettings operation.
 */
NMSettingsConnection *const*
nm_settings_get_connections (NMSettings *self, guint *out_len)
{
	NMSettingsPrivate *priv;
	NMSettingsConnection **v;
	NMSettingsConnection *con;
	guint i;

	g_return_val_if_fail (NM_IS_SETTINGS (self), NULL);

	priv = NM_SETTINGS_GET_PRIVATE (self);

	nm_assert (priv->connections_len == c_list_length (&priv->connections_lst_head));

	if (G_UNLIKELY (!priv->connections_cached_list)) {
		v = g_new (NMSettingsConnection *, priv->connections_len + 1);

		i = 0;
		c_list_for_each_entry (con, &priv->connections_lst_head, _connections_lst) {
			nm_assert (i < priv->connections_len);
			v[i++] = con;
		}
		nm_assert (i == priv->connections_len);
		v[i] = NULL;

		priv->connections_cached_list = v;
	}

	NM_SET_OUT (out_len, priv->connections_len);
	return priv->connections_cached_list;
}

/**
 * nm_settings_get_connections_clone:
 * @self: the #NMSetting
 * @out_len: (allow-none): optional output argument
 * @func: caller-supplied function for filtering connections
 * @func_data: caller-supplied data passed to @func
 * @sort_compare_func: (allow-none): optional function pointer for
 *   sorting the returned list.
 * @sort_data: user data for @sort_compare_func.
 *
 * Returns: (transfer container) (element-type NMSettingsConnection):
 *   an NULL terminated array of #NMSettingsConnection objects that were
 *   filtered by @func (or all connections if no filter was specified).
 *   The order is arbitrary.
 *   Caller is responsible for freeing the returned array with free(),
 *   the contained values do not need to be unrefed.
 */
NMSettingsConnection **
nm_settings_get_connections_clone (NMSettings *self,
                                   guint *out_len,
                                   NMSettingsConnectionFilterFunc func,
                                   gpointer func_data,
                                   GCompareDataFunc sort_compare_func,
                                   gpointer sort_data)
{
	NMSettingsConnection *const*list_cached;
	NMSettingsConnection **list;
	guint len, i, j;

	g_return_val_if_fail (NM_IS_SETTINGS (self), NULL);

	list_cached = nm_settings_get_connections (self, &len);

#if NM_MORE_ASSERTS
	nm_assert (list_cached);
	for (i = 0; i < len; i++)
		nm_assert (NM_IS_SETTINGS_CONNECTION (list_cached[i]));
	nm_assert (!list_cached[i]);
#endif

	list = g_new (NMSettingsConnection *, ((gsize) len + 1));
	if (func) {
		for (i = 0, j = 0; i < len; i++) {
			if (func (self, list_cached[i], func_data))
				list[j++] = list_cached[i];
		}
		list[j] = NULL;
		len = j;
	} else
		memcpy (list, list_cached, sizeof (list[0]) * ((gsize) len + 1));

	if (   len > 1
	    && sort_compare_func) {
		g_qsort_with_data (list, len, sizeof (NMSettingsConnection *),
		                   sort_compare_func, sort_data);
	}
	NM_SET_OUT (out_len, len);
	return list;
}

NMSettingsConnection *
nm_settings_get_connection_by_path (NMSettings *self, const char *path)
{
	NMSettingsPrivate *priv;
	NMSettingsConnection *connection;

	g_return_val_if_fail (NM_IS_SETTINGS (self), NULL);
	g_return_val_if_fail (path, NULL);

	priv = NM_SETTINGS_GET_PRIVATE (self);

	connection = nm_dbus_manager_lookup_object (nm_dbus_object_get_manager (NM_DBUS_OBJECT (self)),
	                                            path);
	if (   !connection
	    || !NM_IS_SETTINGS_CONNECTION (connection))
		return NULL;

	nm_assert (c_list_contains (&priv->connections_lst_head, &connection->_connections_lst));
	return connection;
}

gboolean
nm_settings_has_connection (NMSettings *self, NMSettingsConnection *connection)
{
	gboolean has;

	g_return_val_if_fail (NM_IS_SETTINGS (self), FALSE);
	g_return_val_if_fail (NM_IS_SETTINGS_CONNECTION (connection), FALSE);

	has = !c_list_is_empty (&connection->_connections_lst);

	nm_assert (has == nm_c_list_contains_entry (&NM_SETTINGS_GET_PRIVATE (self)->connections_lst_head,
                                                connection,
                                                _connections_lst));
	nm_assert (({
		NMSettingsConnection *candidate = NULL;
		const char *path;

		path = nm_dbus_object_get_path (NM_DBUS_OBJECT (connection));
		if (path)
			candidate = nm_settings_get_connection_by_path (self, path);

		(has == (connection == candidate));
	}));

	return has;
}

/*****************************************************************************/

static void
add_plugin (NMSettings *self,
            NMSettingsPlugin *plugin,
            const char *pname,
            const char *path)
{
	NMSettingsPrivate *priv;

	nm_assert (NM_IS_SETTINGS (self));
	nm_assert (NM_IS_SETTINGS_PLUGIN (plugin));

	nm_assert (pname);
	nm_assert (nm_streq0 (pname, nm_settings_plugin_get_plugin_name (plugin)));

	priv = NM_SETTINGS_GET_PRIVATE (self);

	nm_assert (!g_slist_find (priv->plugins, plugin));

	priv->plugins = g_slist_append (priv->plugins, g_object_ref (plugin));

	nm_shutdown_wait_obj_register_object_full (plugin,
	                                           g_strdup_printf ("%s-settings-plugin", pname),
	                                           TRUE);

	_LOGI ("Loaded settings plugin: %s (%s%s%s)",
	       pname,
	       NM_PRINT_FMT_QUOTED (path, "\"", path, "\"", "internal"));
}

static gboolean
add_plugin_load_file (NMSettings *self, const char *pname, GError **error)
{
	gs_free char *full_name = NULL;
	gs_free char *path = NULL;
	gs_unref_object NMSettingsPlugin *plugin = NULL;
	GModule *module;
	NMSettingsPluginFactoryFunc factory_func;
	struct stat st;
	int errsv;

	full_name = g_strdup_printf ("nm-settings-plugin-%s", pname);
	path = g_module_build_path (NMPLUGINDIR, full_name);

	if (stat (path, &st) != 0) {
		errsv = errno;
		_LOGW ("could not load plugin '%s' from file '%s': %s", pname, path, nm_strerror_native (errsv));
		return TRUE;
	}
	if (!S_ISREG (st.st_mode)) {
		_LOGW ("could not load plugin '%s' from file '%s': not a file", pname, path);
		return TRUE;
	}
	if (st.st_uid != 0) {
		_LOGW ("could not load plugin '%s' from file '%s': file must be owned by root", pname, path);
		return TRUE;
	}
	if (st.st_mode & (S_IWGRP | S_IWOTH | S_ISUID)) {
		_LOGW ("could not load plugin '%s' from file '%s': invalid file permissions", pname, path);
		return TRUE;
	}

	module = g_module_open (path, G_MODULE_BIND_LOCAL);
	if (!module) {
		_LOGW ("could not load plugin '%s' from file '%s': %s",
		     pname, path, g_module_error ());
		return TRUE;
	}

	/* errors after this point are fatal, because we loaded the shared library already. */

	if (!g_module_symbol (module, "nm_settings_plugin_factory", (gpointer) (&factory_func))) {
		g_set_error (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_FAILED,
		             "Could not find plugin '%s' factory function.",
		             pname);
		g_module_close (module);
		return FALSE;
	}

	/* after accessing the plugin we cannot unload it anymore, because the glib
	 * types cannot be properly unregistered. */
	g_module_make_resident (module);

	plugin = (*factory_func) ();
	if (!NM_IS_SETTINGS_PLUGIN (plugin)) {
		g_set_error (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_FAILED,
		             "plugin '%s' returned invalid settings plugin",
		             pname);
		return FALSE;
	}

	add_plugin (self, NM_SETTINGS_PLUGIN (plugin), pname, path);
	return TRUE;
}

static void
add_plugin_keyfile (NMSettings *self)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	if (priv->keyfile_plugin)
		return;
	priv->keyfile_plugin = nms_keyfile_plugin_new ();
	add_plugin (self, NM_SETTINGS_PLUGIN (priv->keyfile_plugin), "keyfile", NULL);
}

static gboolean
load_plugins (NMSettings *self, const char *const*plugins, GError **error)
{
	const char *const*iter;
	gboolean success = TRUE;

	for (iter = plugins; iter && *iter; iter++) {
		const char *pname = *iter;

		if (!*pname || strchr (pname, '/')) {
			_LOGW ("ignore invalid plugin \"%s\"", pname);
			continue;
		}

		if (NM_IN_STRSET (pname, "ifcfg-suse", "ifnet", "ibft", "no-ibft")) {
			_LOGW ("skipping deprecated plugin %s", pname);
			continue;
		}

		/* keyfile plugin is built-in now */
		if (nm_streq (pname, "keyfile")) {
			add_plugin_keyfile (self);
			continue;
		}

		if (nm_utils_strv_find_first ((char **) plugins,
		                              iter - plugins,
		                              pname) >= 0) {
			/* the plugin is already mentioned in the list previously.
			 * Don't load a duplicate. */
			continue;
		}

		success = add_plugin_load_file (self, pname, error);
		if (!success)
			break;
	}

	/* If keyfile plugin was not among configured plugins, add it as the last one */
	if (success)
		add_plugin_keyfile (self);

	return success;
}

/*****************************************************************************/

static void
pk_hostname_cb (NMAuthChain *chain,
                GDBusMethodInvocation *context,
                gpointer user_data)
{
	NMSettings *self = NM_SETTINGS (user_data);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMAuthCallResult result;
	GError *error = NULL;
	const char *hostname;

	nm_assert (G_IS_DBUS_METHOD_INVOCATION (context));

	c_list_unlink (nm_auth_chain_parent_lst_list (chain));

	result = nm_auth_chain_get_result (chain, NM_AUTH_PERMISSION_SETTINGS_MODIFY_HOSTNAME);
	hostname = nm_auth_chain_get_data (chain, "hostname");

	/* If our NMSettingsConnection is already gone, do nothing */
	if (result != NM_AUTH_CALL_RESULT_YES) {
		error = g_error_new_literal (NM_SETTINGS_ERROR,
		                             NM_SETTINGS_ERROR_PERMISSION_DENIED,
		                             NM_UTILS_ERROR_MSG_INSUFF_PRIV);
	} else {
		if (!nm_hostname_manager_write_hostname (priv->hostname_manager, hostname)) {
			error = g_error_new_literal (NM_SETTINGS_ERROR,
			                             NM_SETTINGS_ERROR_FAILED,
			                             "Saving the hostname failed.");
		}
	}

	nm_audit_log_control_op (NM_AUDIT_OP_HOSTNAME_SAVE,
	                         hostname,
	                         !error,
	                         nm_auth_chain_get_subject (chain),
	                         error ? error->message : NULL);

	if (error)
		g_dbus_method_invocation_take_error (context, error);
	else
		g_dbus_method_invocation_return_value (context, NULL);
}

static void
impl_settings_save_hostname (NMDBusObject *obj,
                             const NMDBusInterfaceInfoExtended *interface_info,
                             const NMDBusMethodInfoExtended *method_info,
                             GDBusConnection *connection,
                             const char *sender,
                             GDBusMethodInvocation *invocation,
                             GVariant *parameters)
{
	NMSettings *self = NM_SETTINGS (obj);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMAuthChain *chain;
	const char *hostname;
	const char *error_reason;
	int error_code;

	g_variant_get (parameters, "(&s)", &hostname);

	/* Minimal validation of the hostname */
	if (!nm_hostname_manager_validate_hostname (hostname)) {
		error_code = NM_SETTINGS_ERROR_INVALID_HOSTNAME;
		error_reason = "The hostname was too long or contained invalid characters";
		goto err;
	}

	chain = nm_auth_chain_new_context (invocation, pk_hostname_cb, self);
	if (!chain) {
		error_code = NM_SETTINGS_ERROR_PERMISSION_DENIED;
		error_reason = NM_UTILS_ERROR_MSG_REQ_AUTH_FAILED;
		goto err;
	}

	c_list_link_tail (&priv->auth_lst_head, nm_auth_chain_parent_lst_list (chain));
	nm_auth_chain_add_call (chain, NM_AUTH_PERMISSION_SETTINGS_MODIFY_HOSTNAME, TRUE);
	nm_auth_chain_set_data (chain, "hostname", g_strdup (hostname), g_free);
	return;
err:
	nm_audit_log_control_op (NM_AUDIT_OP_HOSTNAME_SAVE,
	                         hostname,
	                         FALSE,
	                         invocation,
	                         error_reason);
	g_dbus_method_invocation_return_error_literal (invocation,
	                                               NM_SETTINGS_ERROR,
	                                               error_code,
	                                               error_reason);
}

/*****************************************************************************/

static void
_hostname_changed_cb (NMHostnameManager *hostname_manager,
                      GParamSpec *pspec,
                      gpointer user_data)
{
	_notify (user_data, PROP_HOSTNAME);
}

/*****************************************************************************/

static gboolean
have_connection_for_device (NMSettings *self, NMDevice *device)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMSettingsConnection *sett_conn;

	g_return_val_if_fail (NM_IS_SETTINGS (self), FALSE);

	/* Find a wired connection matching for the device, if any */
	c_list_for_each_entry (sett_conn, &priv->connections_lst_head, _connections_lst) {
		NMConnection *connection = nm_settings_connection_get_connection (sett_conn);

		if (!nm_device_check_connection_compatible (device, connection, NULL))
			continue;

		if (nm_settings_connection_default_wired_get_device (sett_conn))
			continue;

		if (NM_FLAGS_ANY (nm_settings_connection_get_flags (sett_conn),
		                    NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
		                  | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL))
			continue;

		return TRUE;
	}

	/* See if there's a known non-NetworkManager configuration for the device */
	if (nm_device_spec_match_list (device, priv->unrecognized_specs))
		return TRUE;

	return FALSE;
}

static void
default_wired_clear_tag (NMSettings *self,
                         NMDevice *device,
                         NMSettingsConnection *sett_conn,
                         gboolean add_to_no_auto_default)
{
	nm_assert (NM_IS_SETTINGS (self));
	nm_assert (NM_IS_DEVICE (device));
	nm_assert (NM_IS_SETTINGS_CONNECTION (sett_conn));
	nm_assert (device == nm_settings_connection_default_wired_get_device (sett_conn));
	nm_assert (sett_conn == g_object_get_qdata (G_OBJECT (device), _default_wired_connection_quark ()));

	_LOGT ("auto-default: forget association between %s (%s) and device %s (%s)",
	       nm_settings_connection_get_uuid (sett_conn),
	       nm_settings_connection_get_id (sett_conn),
	       nm_device_get_iface (device),
	       add_to_no_auto_default ? "persisted" : "temporary");

	nm_settings_connection_default_wired_set_device (sett_conn, NULL);

	g_object_set_qdata (G_OBJECT (device), _default_wired_connection_quark (), NULL);

	if (add_to_no_auto_default)
		nm_config_set_no_auto_default_for_device (NM_SETTINGS_GET_PRIVATE (self)->config, device);
}

static void
device_realized (NMDevice *device, GParamSpec *pspec, NMSettings *self)
{
	gs_unref_object NMConnection *connection = NULL;
	NMSettingsPrivate *priv;
	NMSettingsConnection *added;
	GError *error = NULL;

	if (!nm_device_is_real (device))
		return;

	g_signal_handlers_disconnect_by_func (device,
	                                      G_CALLBACK (device_realized),
	                                      self);

	priv = NM_SETTINGS_GET_PRIVATE (self);

	/* If the device isn't managed or it already has a default wired connection,
	 * ignore it.
	 */
	if (   !NM_DEVICE_GET_CLASS (device)->new_default_connection
	    || !nm_device_get_managed (device, FALSE)
	    || g_object_get_qdata (G_OBJECT (device), _default_wired_connection_blocked_quark ()))
		return;

	/* we only check once whether to create the auto-default connection. If we reach this point,
	 * we mark the creation of the default-wired-connection as blocked. */
	g_object_set_qdata (G_OBJECT (device), _default_wired_connection_blocked_quark (), device);

	if (nm_config_get_no_auto_default_for_device (priv->config, device)) {
		_LOGT ("auto-default: cannot create auto-default connection for device %s: disabled by \"no-auto-default\"",
		       nm_device_get_iface (device));
		return;
	}

	if (have_connection_for_device (self, device)) {
		_LOGT ("auto-default: cannot create auto-default connection for device %s: already has a profile",
		       nm_device_get_iface (device));
		return;
	}

	connection = nm_device_new_default_connection (device);
	if (!connection) {
		_LOGT ("auto-default: cannot create auto-default connection for device %s",
		       nm_device_get_iface (device));
		return;
	}

	_LOGT ("auto-default: creating in-memory connection %s (%s) for device %s",
	       nm_connection_get_uuid (connection),
	       nm_connection_get_id (connection),
	       nm_device_get_iface (device));

	nm_settings_add_connection (self,
	                            connection,
	                            NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
	                            NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
	                            NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
	                            &added,
	                            &error);
	if (!added) {
		if (!g_error_matches (error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_UUID_EXISTS)) {
			_LOGW ("(%s) couldn't create default wired connection: %s",
			       nm_device_get_iface (device),
			       error->message);
		}
		g_clear_error (&error);
		return;
	}

	nm_settings_connection_default_wired_set_device (added, device);

	g_object_set_qdata (G_OBJECT (device), _default_wired_connection_quark (), added);

	_LOGI ("(%s): created default wired connection '%s'",
	       nm_device_get_iface (device),
	       nm_settings_connection_get_id (added));
}

void
nm_settings_device_added (NMSettings *self, NMDevice *device)
{
	if (nm_device_is_real (device))
		device_realized (device, NULL, self);
	else {
		/* FIXME(shutdown): we need to disconnect this signal handler during
		 *   shutdown. */
		g_signal_connect_after (device, "notify::" NM_DEVICE_REAL,
		                        G_CALLBACK (device_realized),
		                        self);
	}
}

void
nm_settings_device_removed (NMSettings *self, NMDevice *device, gboolean quitting)
{
	NMSettingsConnection *connection;

	g_signal_handlers_disconnect_by_func (device,
	                                      G_CALLBACK (device_realized),
	                                      self);

	connection = g_object_get_qdata (G_OBJECT (device), _default_wired_connection_quark ());
	if (connection) {
		default_wired_clear_tag (self, device, connection, FALSE);

		/* Don't delete the default wired connection on shutdown, so that it
		 * remains up and can be assumed if NM starts again.
		 */
		if (quitting == FALSE)
			nm_settings_connection_delete (connection, TRUE);
	}
}

/*****************************************************************************/

static void
session_monitor_changed_cb (NMSessionMonitor *session_monitor,
                            NMSettings *self)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	NMSettingsConnection *const*list;
	guint i, len;
	guint generation;

again:
	list = nm_settings_get_connections (self, &len);
	generation = priv->connections_generation;
	for (i = 0; i < len; i++) {
		gboolean is_visible;

		is_visible = nm_settings_connection_check_visibility (list[i],
		                                                      session_monitor);
		nm_settings_connection_set_flags (list[i],
		                                  NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE,
		                                  is_visible);
		if (generation != priv->connections_generation) {
			/* the cached list was invalidated. Start again.
			 *
			 * Note that nm_settings_connection_recheck_visibility() will do nothing
			 * if the visibility didn't change (including emitting no signals,
			 * and not invalidating the list).
			 *
			 * Hence, for this to be an endless loop, the settings would have
			 * to constantly change the visibility flag and also invalidate the list. */
			goto again;
		}
	}
}

/*****************************************************************************/

G_GNUC_PRINTF (4, 5)
static void
_kf_db_log_fcn (NMKeyFileDB *kf_db,
                int syslog_level,
                gpointer user_data,
                const char *fmt,
                ...)
{
	NMSettings *self = user_data;
	NMLogLevel level = nm_log_level_from_syslog (syslog_level);

	if (_NMLOG_ENABLED (level)) {
		NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
		gs_free char *msg = NULL;
		va_list ap;
		const char *prefix;

		va_start (ap, fmt);
		msg = g_strdup_vprintf (fmt, ap);
		va_end (ap);

		if (priv->kf_db_timestamps == kf_db)
			prefix = "timestamps";
		else if (priv->kf_db_seen_bssids == kf_db)
			prefix = "seen-bssids";
		else {
			nm_assert_not_reached ();
			prefix = "???";
		}

		_NMLOG (level, "[%s-keyfile]: %s", prefix, msg);
	}
}

static gboolean
_kf_db_got_dirty_flush (NMSettings *self,
                        gboolean is_timestamps)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	const char *prefix;
	NMKeyFileDB *kf_db;

	if (is_timestamps) {
		prefix = "timestamps";
		kf_db = priv->kf_db_timestamps;
		priv->kf_db_flush_idle_id_timestamps = 0;
	} else {
		prefix = "seen-bssids";
		kf_db = priv->kf_db_seen_bssids;
		priv->kf_db_flush_idle_id_seen_bssids = 0;
	}

	if (nm_key_file_db_is_dirty (kf_db))
		nm_key_file_db_to_file (kf_db, FALSE);
	else {
		_LOGT ("[%s-keyfile]: skip saving changes to \"%s\"",
		       prefix,
		       nm_key_file_db_get_filename (kf_db));
	}

	return G_SOURCE_REMOVE;
}

static gboolean
_kf_db_got_dirty_flush_timestamps_cb (gpointer user_data)
{
	return _kf_db_got_dirty_flush (user_data,
	                               TRUE);
}

static gboolean
_kf_db_got_dirty_flush_seen_bssids_cb (gpointer user_data)
{
	return _kf_db_got_dirty_flush (user_data,
	                               FALSE);
}

static void
_kf_db_got_dirty_fcn (NMKeyFileDB *kf_db,
                      gpointer user_data)
{
	NMSettings *self = user_data;
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	GSourceFunc idle_func;
	guint *p_id;
	const char *prefix;

	if (priv->kf_db_timestamps == kf_db) {
		prefix = "timestamps";
		p_id = &priv->kf_db_flush_idle_id_timestamps;
		idle_func = _kf_db_got_dirty_flush_timestamps_cb;
	} else if (priv->kf_db_seen_bssids == kf_db) {
		prefix = "seen-bssids";
		p_id = &priv->kf_db_flush_idle_id_seen_bssids;
		idle_func = _kf_db_got_dirty_flush_seen_bssids_cb;
	} else {
		nm_assert_not_reached ();
		return;
	}

	if (*p_id != 0)
		return;
	_LOGT ("[%s-keyfile]: schedule flushing changes to disk", prefix);
	*p_id = g_idle_add_full (G_PRIORITY_LOW, idle_func, self, NULL);
}

void
nm_settings_kf_db_write (NMSettings *self)
{
	NMSettingsPrivate *priv;

	g_return_if_fail (NM_IS_SETTINGS (self));

	priv = NM_SETTINGS_GET_PRIVATE (self);
	if (priv->kf_db_timestamps)
		nm_key_file_db_to_file (priv->kf_db_timestamps, TRUE);
	if (priv->kf_db_seen_bssids)
		nm_key_file_db_to_file (priv->kf_db_seen_bssids, TRUE);
}

/*****************************************************************************/

gboolean
nm_settings_start (NMSettings *self, GError **error)
{
	NMSettingsPrivate *priv;
	gs_strfreev char **plugins = NULL;
	GSList *iter;

	priv = NM_SETTINGS_GET_PRIVATE (self);

	nm_assert (!priv->started);

	priv->startup_complete_start_timestamp_msec = nm_utils_get_monotonic_timestamp_msec ();

	priv->hostname_manager = g_object_ref (nm_hostname_manager_get ());

	priv->kf_db_timestamps = nm_key_file_db_new (NMSTATEDIR "/timestamps",
	                                             "timestamps",
	                                             _kf_db_log_fcn,
	                                             _kf_db_got_dirty_fcn,
	                                             self);
	priv->kf_db_seen_bssids = nm_key_file_db_new (NMSTATEDIR "/seen-bssids",
	                                              "seen-bssids",
	                                              _kf_db_log_fcn,
	                                              _kf_db_got_dirty_fcn,
	                                              self);
	nm_key_file_db_start (priv->kf_db_timestamps);
	nm_key_file_db_start (priv->kf_db_seen_bssids);

	/* Load the plugins; fail if a plugin is not found. */
	plugins = nm_config_data_get_plugins (nm_config_get_data_orig (priv->config), TRUE);

	if (!load_plugins (self, (const char *const*) plugins, error))
		return FALSE;

	for (iter = priv->plugins; iter; iter = iter->next) {
		NMSettingsPlugin *plugin = NM_SETTINGS_PLUGIN (iter->data);

		g_signal_connect (plugin, NM_SETTINGS_PLUGIN_UNMANAGED_SPECS_CHANGED,
		                  G_CALLBACK (_plugin_unmanaged_specs_changed), self);
		g_signal_connect (plugin, NM_SETTINGS_PLUGIN_UNRECOGNIZED_SPECS_CHANGED,
		                  G_CALLBACK (_plugin_unrecognized_specs_changed), self);
	}

	_plugin_unmanaged_specs_changed (NULL, self);
	_plugin_unrecognized_specs_changed (NULL, self);

	_plugin_connections_reload (self);

	g_signal_connect (priv->hostname_manager,
	                  "notify::"NM_HOSTNAME_MANAGER_HOSTNAME,
	                  G_CALLBACK (_hostname_changed_cb),
	                  self);
	if (nm_hostname_manager_get_hostname (priv->hostname_manager))
		_notify (self, PROP_HOSTNAME);

	priv->started = TRUE;
	_startup_complete_check (self, 0);

	/* FIXME(shutdown): we also need a nm_settings_stop() during shutdown.
	 *
	 * In particular, we need to remove all in-memory keyfiles from /run that are nm-generated.
	 * alternatively, the nm-generated flag must also be persisted and loaded to /run. */

	return TRUE;
}

/*****************************************************************************/

static void
get_property (GObject *object, guint prop_id,
              GValue *value, GParamSpec *pspec)
{
	NMSettings *self = NM_SETTINGS (object);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	const char **strv;

	switch (prop_id) {
	case PROP_UNMANAGED_SPECS:
		g_value_take_boxed (value,
		                    _nm_utils_slist_to_strv (nm_settings_get_unmanaged_specs (self),
		                                             TRUE));
		break;
	case PROP_HOSTNAME:
		g_value_set_string (value,
		                      priv->hostname_manager
		                    ? nm_hostname_manager_get_hostname (priv->hostname_manager)
		                    : NULL);
		break;
	case PROP_CAN_MODIFY:
		g_value_set_boolean (value, TRUE);
		break;
	case PROP_CONNECTIONS:
		strv = nm_dbus_utils_get_paths_for_clist (&priv->connections_lst_head,
		                                          priv->connections_len,
		                                          G_STRUCT_OFFSET (NMSettingsConnection, _connections_lst),
		                                          TRUE);
		g_value_take_boxed (value, nm_utils_strv_make_deep_copied (strv));
		break;
	case PROP_STARTUP_COMPLETE:
		g_value_set_boolean (value, !nm_settings_get_startup_complete_blocked_reason (self, FALSE));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
set_property (GObject *object, guint prop_id,
              const GValue *value, GParamSpec *pspec)
{
	NMSettings *self = NM_SETTINGS (object);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	switch (prop_id) {
	case PROP_MANAGER:
		/* construct-only */
		priv->manager = g_value_get_pointer (value);
		nm_assert (NM_IS_MANAGER (priv->manager));
		g_object_add_weak_pointer (G_OBJECT (priv->manager), (gpointer *) &priv->manager);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

/*****************************************************************************/

static void
nm_settings_init (NMSettings *self)
{
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);

	c_list_init (&priv->auth_lst_head);
	c_list_init (&priv->connections_lst_head);
	c_list_init (&priv->startup_complete_scd_lst_head);

	c_list_init (&priv->sce_dirty_lst_head);
	priv->sce_idx = g_hash_table_new_full (nm_pstr_hash, nm_pstr_equal,
	                                       NULL, (GDestroyNotify) _sett_conn_entry_free);

	priv->config = g_object_ref (nm_config_get ());

	priv->agent_mgr = g_object_ref (nm_agent_manager_get ());

	priv->platform = g_object_ref (NM_PLATFORM_GET);

	priv->session_monitor = g_object_ref (nm_session_monitor_get ());
	g_signal_connect (priv->session_monitor,
	                  NM_SESSION_MONITOR_CHANGED,
	                  G_CALLBACK (session_monitor_changed_cb),
	                  self);
}

NMSettings *
nm_settings_new (NMManager *manager)
{
	nm_assert (NM_IS_MANAGER (manager));

	return g_object_new (NM_TYPE_SETTINGS,
	                     NM_SETTINGS_MANAGER, manager,
	                     NULL);
}

static void
dispose (GObject *object)
{
	NMSettings *self = NM_SETTINGS (object);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	CList *iter;

	nm_assert (c_list_is_empty (&priv->sce_dirty_lst_head));
	nm_assert (g_hash_table_size (priv->sce_idx) == 0);

	nm_clear_g_source (&priv->startup_complete_timeout_id);
	nm_clear_pointer (&priv->startup_complete_idx, g_hash_table_destroy);
	nm_assert (c_list_is_empty (&priv->startup_complete_scd_lst_head));

	while ((iter = c_list_first (&priv->auth_lst_head)))
		nm_auth_chain_destroy (nm_auth_chain_parent_lst_entry (iter));

	if (priv->hostname_manager) {
		g_signal_handlers_disconnect_by_func (priv->hostname_manager,
		                                      G_CALLBACK (_hostname_changed_cb),
		                                      self);
		g_clear_object (&priv->hostname_manager);
	}

	if (priv->session_monitor) {
		g_signal_handlers_disconnect_by_func (priv->session_monitor,
		                                      G_CALLBACK (session_monitor_changed_cb),
		                                      self);
		g_clear_object (&priv->session_monitor);
	}

	G_OBJECT_CLASS (nm_settings_parent_class)->dispose (object);
}

static void
finalize (GObject *object)
{
	NMSettings *self = NM_SETTINGS (object);
	NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE (self);
	GSList *iter;

	_clear_connections_cached_list (priv);

	nm_assert (c_list_is_empty (&priv->connections_lst_head));

	nm_assert (c_list_is_empty (&priv->sce_dirty_lst_head));
	nm_assert (g_hash_table_size (priv->sce_idx) == 0);

	nm_clear_pointer (&priv->sce_idx, g_hash_table_destroy);

	g_slist_free_full (priv->unmanaged_specs, g_free);
	g_slist_free_full (priv->unrecognized_specs, g_free);

	while ((iter = priv->plugins)) {
		gs_unref_object NMSettingsPlugin *plugin = iter->data;

		priv->plugins = g_slist_delete_link (priv->plugins, iter);
		g_signal_handlers_disconnect_by_data (plugin, self);
	}

	g_clear_object (&priv->keyfile_plugin);

	g_clear_object (&priv->agent_mgr);

	nm_clear_g_source (&priv->kf_db_flush_idle_id_timestamps);
	nm_clear_g_source (&priv->kf_db_flush_idle_id_seen_bssids);
	nm_key_file_db_to_file (priv->kf_db_timestamps, FALSE);
	nm_key_file_db_to_file (priv->kf_db_seen_bssids, FALSE);
	nm_key_file_db_destroy (priv->kf_db_timestamps);
	nm_key_file_db_destroy (priv->kf_db_seen_bssids);

	G_OBJECT_CLASS (nm_settings_parent_class)->finalize (object);

	g_clear_object (&priv->config);

	g_clear_object (&priv->platform);

	if (priv->manager) {
		g_object_remove_weak_pointer (G_OBJECT (priv->manager), (gpointer *) &priv->manager);
		priv->manager = NULL;
	}
}

static const GDBusSignalInfo signal_info_new_connection = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT (
	"NewConnection",
	.args = NM_DEFINE_GDBUS_ARG_INFOS (
		NM_DEFINE_GDBUS_ARG_INFO ("connection", "o"),
	),
);

static const GDBusSignalInfo signal_info_connection_removed = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT (
	"ConnectionRemoved",
	.args = NM_DEFINE_GDBUS_ARG_INFOS (
		NM_DEFINE_GDBUS_ARG_INFO ("connection", "o"),
	),
);

static const NMDBusInterfaceInfoExtended interface_info_settings = {
	.parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT (
		NM_DBUS_INTERFACE_SETTINGS,
		.methods = NM_DEFINE_GDBUS_METHOD_INFOS (
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"ListConnections",
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("connections", "ao"),
					),
				),
				.handle = impl_settings_list_connections,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"GetConnectionByUuid",
					.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("uuid", "s"),
					),
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("connection", "o"),
					),
				),
				.handle = impl_settings_get_connection_by_uuid,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"AddConnection",
					.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("connection", "a{sa{sv}}"),
					),
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("path", "o"),
					),
				),
				.handle = impl_settings_add_connection,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"AddConnectionUnsaved",
					.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("connection", "a{sa{sv}}"),
					),
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("path", "o"),
					),
				),
				.handle = impl_settings_add_connection_unsaved,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"AddConnection2",
					.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("settings", "a{sa{sv}}"),
						NM_DEFINE_GDBUS_ARG_INFO ("flags",    "u"),
						NM_DEFINE_GDBUS_ARG_INFO ("args",     "a{sv}"),
					),
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("path", "o"),
						NM_DEFINE_GDBUS_ARG_INFO ("result", "a{sv}"),
					),
				),
				.handle = impl_settings_add_connection2,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"LoadConnections",
					.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("filenames", "as"),
					),
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("status", "b"),
						NM_DEFINE_GDBUS_ARG_INFO ("failures", "as"),
					),
				),
				.handle = impl_settings_load_connections,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"ReloadConnections",
					.out_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("status", "b"),
					),
				),
				.handle = impl_settings_reload_connections,
			),
			NM_DEFINE_DBUS_METHOD_INFO_EXTENDED (
				NM_DEFINE_GDBUS_METHOD_INFO_INIT (
					"SaveHostname",
					.in_args = NM_DEFINE_GDBUS_ARG_INFOS (
						NM_DEFINE_GDBUS_ARG_INFO ("hostname", "s"),
					),
				),
				.handle = impl_settings_save_hostname,
			),
		),
		.signals = NM_DEFINE_GDBUS_SIGNAL_INFOS (
			&nm_signal_info_property_changed_legacy,
			&signal_info_new_connection,
			&signal_info_connection_removed,
		),
		.properties = NM_DEFINE_GDBUS_PROPERTY_INFOS (
			NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L ("Connections", "ao", NM_SETTINGS_CONNECTIONS),
			NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L ("Hostname",    "s",  NM_SETTINGS_HOSTNAME),
			NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L ("CanModify",   "b",  NM_SETTINGS_CAN_MODIFY),
		),
	),
	.legacy_property_changed = TRUE,
};

static void
nm_settings_class_init (NMSettingsClass *class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (class);
	NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS (class);

	dbus_object_class->export_path = NM_DBUS_EXPORT_PATH_STATIC (NM_DBUS_PATH_SETTINGS);
	dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS (&interface_info_settings);

	object_class->get_property = get_property;
	object_class->set_property = set_property;
	object_class->dispose = dispose;
	object_class->finalize = finalize;

	obj_properties[PROP_MANAGER] =
	    g_param_spec_pointer (NM_SETTINGS_MANAGER, "", "",
	                          G_PARAM_CONSTRUCT_ONLY |
	                          G_PARAM_WRITABLE |
	                          G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_UNMANAGED_SPECS] =
	    g_param_spec_boxed (NM_SETTINGS_UNMANAGED_SPECS, "", "",
	                        G_TYPE_STRV,
	                        G_PARAM_READABLE |
	                        G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_HOSTNAME] =
	    g_param_spec_string (NM_SETTINGS_HOSTNAME, "", "",
	                         NULL,
	                         G_PARAM_READABLE |
	                         G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_CAN_MODIFY] =
	    g_param_spec_boolean (NM_SETTINGS_CAN_MODIFY, "", "",
	                          FALSE,
	                          G_PARAM_READABLE |
	                          G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_CONNECTIONS] =
	    g_param_spec_boxed (NM_SETTINGS_CONNECTIONS, "", "",
	                        G_TYPE_STRV,
	                        G_PARAM_READABLE |
	                        G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_STARTUP_COMPLETE] =
	    g_param_spec_boolean (NM_SETTINGS_STARTUP_COMPLETE, "", "",
	                          FALSE,
	                          G_PARAM_READABLE |
	                          G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);

	signals[CONNECTION_ADDED] =
	    g_signal_new (NM_SETTINGS_SIGNAL_CONNECTION_ADDED,
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_FIRST,
	                  0, NULL, NULL,
	                  g_cclosure_marshal_VOID__OBJECT,
	                  G_TYPE_NONE, 1, NM_TYPE_SETTINGS_CONNECTION);

	signals[CONNECTION_UPDATED] =
	    g_signal_new (NM_SETTINGS_SIGNAL_CONNECTION_UPDATED,
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_FIRST,
	                  0, NULL, NULL,
	                  NULL,
	                  G_TYPE_NONE, 2, NM_TYPE_SETTINGS_CONNECTION, G_TYPE_UINT);

	signals[CONNECTION_REMOVED] =
	    g_signal_new (NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_FIRST,
	                  0, NULL, NULL,
	                  g_cclosure_marshal_VOID__OBJECT,
	                  G_TYPE_NONE, 1, NM_TYPE_SETTINGS_CONNECTION);

	signals[CONNECTION_FLAGS_CHANGED] =
	    g_signal_new (NM_SETTINGS_SIGNAL_CONNECTION_FLAGS_CHANGED,
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_FIRST,
	                  0, NULL, NULL,
	                  g_cclosure_marshal_VOID__OBJECT,
	                  G_TYPE_NONE, 1, NM_TYPE_SETTINGS_CONNECTION);
}