/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * 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 "src/core/nm-default-daemon.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 "libnm-core-aux-intern/nm-common-macros.h"
#include "libnm-glib-aux/nm-uuid.h"
#include "libnm-glib-aux/nm-keyfile-aux.h"
#include "libnm-core-intern/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 "libnm-core-intern/nm-core-internal.h"

#include "libnm-std-aux/c-list-util.h"
#include "libnm-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 "libnm-core-aux-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_uuid_is_normalized(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_uuid_is_normalized(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).
 *
 * @storage_check_ignore is optional, and if given then it skips this particular
 * storage.
 *
 * 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,
                                                  NMSettingsStorage *storage_check_ignore,
                                                  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_ignore) {
            /* We ignore this one, because we're in the process of
             * replacing it. */
            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_STATIC_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;
    NMSettingsConnection **connections_cached_list_sorted_by_autoconnect_priority;

    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 kf_db_pruned_timestamps;
    bool kf_db_pruned_seen_bssid;

    bool started : 1;

    /* Whether NMSettingsConnections changed in a way that affects the comparison
     * with nm_settings_connection_cmp_autoconnect_priority_with_data(). In that case,
     * we may need to re-sort the connections_cached_list_sorted_by_autoconnect_priority
     * list. */
    bool sorted_by_autoconnect_priority_maybe_changed : 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) {
        gs_free_error GError *error = NULL;

        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;
        }

        /* Check that device is compatible with the device. We are also happy
         * with a device compatible but for which the connection is disallowed
         * by NM configuration. */
        if (!nm_device_check_connection_compatible(device, conn, &error)
            && !g_error_matches(error,
                                NM_UTILS_ERROR,
                                NM_UTILS_ERROR_CONNECTION_AVAILABLE_DISALLOWED))
            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;
    }

    nm_clear_g_source(&priv->startup_complete_timeout_id);

    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);

    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)));

    nm_assert_connection_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
            | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET);

    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,
                                const char                  *plugin_name,
                                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,
                                NMSettingsStorage           *drop_storage,
                                GError                     **error)
{
    NMSettingsPrivate    *priv        = NM_SETTINGS_GET_PRIVATE(self);
    gs_free_error GError *first_error = NULL;
    GSList               *iter;
    const char           *uuid;
    gboolean              no_plugin = TRUE;

    uuid = nm_connection_get_uuid(new_connection);

    nm_assert(nm_uuid_is_normalized(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 (plugin_name && strcmp(plugin_name, nm_settings_plugin_get_plugin_name(plugin))) {
            /* Not the plugin we're confined to. Ignore. */
            continue;
        }

        if (!in_memory) {
            NMSettingsStorage *conflicting_storage;

            conflicting_storage = _sett_conn_entry_storage_find_conflicting_storage(sett_conn_entry,
                                                                                    plugin,
                                                                                    NULL,
                                                                                    drop_storage,
                                                                                    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);
        }

        no_plugin = FALSE;

        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_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;
    }

    if (no_plugin) {
        nm_assert(plugin_name);
        nm_assert(!first_error);
        g_set_error(error,
                    NM_SETTINGS_ERROR,
                    NM_SETTINGS_ERROR_INVALID_ARGUMENTS,
                    "a plugin by the name of '%s' is not available",
                    plugin_name);
    } else {
        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,
                           const char                     *plugin,
                           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 persistent 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,
            NULL,
            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,
                                                  plugin,
                                                  sett_conn_entry,
                                                  connection,
                                                  new_in_memory,
                                                  sett_flags,
                                                  shadowed_storage_filename,
                                                  FALSE,
                                                  &new_storage,
                                                  &new_connection,
                                                  NULL,
                                                  &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_CLEAR_AGENT_SECRETS
            | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET
            | (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,
                              const char                      *plugin_name,
                              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 && plugin_name) {
            NMSettingsPlugin *plugin = nm_settings_storage_get_plugin(update_storage);

            if (strcmp(plugin_name, nm_settings_plugin_get_plugin_name(plugin))) {
                /* We're updating a connection, we're confined to a particular
                 * plugin, but the connection is currently using a different one.
                 * We need to migrate. Drop the existing storage and look out for
                 * a new one. */
                drop_storage   = update_storage;
                update_storage = NULL;
            }
        }

        if (!update_storage) {
            success = _add_connection_to_first_plugin(self,
                                                      plugin_name,
                                                      sett_conn_entry,
                                                      connection,
                                                      new_in_memory,
                                                      sett_flags,
                                                      new_shadowed_storage_filename,
                                                      new_shadowed_owned,
                                                      &new_storage,
                                                      &new_connection,
                                                      drop_storage,
                                                      &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_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_uuid_is_normalized(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,
                                   nm_auth_chain_get_data(chain, "plugin"),
                                   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,
                                const char                     *plugin,
                                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;

    chain = nm_auth_chain_new_subject(subject, context, pk_add_cb, self);

    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_set_data(chain, "plugin", g_strdup(plugin), g_free);
    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,
                               const char                   *plugin,
                               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,
        plugin,
        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,
                                   NULL,
                                   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,
                                   NULL,
                                   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;
    gs_free char                 *plugin   = NULL;
    NMSettingsAddConnection2Flags flags;
    const char                   *args_name;
    GVariant                     *args_value;
    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, &args_value)) {
        if (plugin == NULL && nm_streq(args_name, "plugin")
            && g_variant_is_of_type(args_value, G_VARIANT_TYPE_STRING)) {
            plugin = g_variant_dup_string(args_value, NULL);
            continue;
        }

        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, plugin, 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
                | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET);

        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));
}

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

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

    priv->sorted_by_autoconnect_priority_maybe_changed = TRUE;
}

static void
_clear_connections_cached_list(NMSettingsPrivate *priv)
{
    if (priv->connections_cached_list) {
        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);
    }
    if (priv->connections_cached_list_sorted_by_autoconnect_priority) {
        nm_assert(priv->connections_len
                  == NM_PTRARRAY_LEN(priv->connections_cached_list_sorted_by_autoconnect_priority));

#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_sorted_by_autoconnect_priority,
               0x42,
               sizeof(NMSettingsConnection *) * (priv->connections_len + 1));
#endif

        nm_clear_g_free(&priv->connections_cached_list_sorted_by_autoconnect_priority);
    }
}

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;
}

NMSettingsConnection *const *
nm_settings_get_connections_sorted_by_autoconnect_priority(NMSettings *self, guint *out_len)
{
    NMSettingsPrivate *priv;
    gboolean           needs_sort = FALSE;

    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));
    nm_assert(
        !priv->connections_cached_list_sorted_by_autoconnect_priority
        || (priv->connections_len
            == NM_PTRARRAY_LEN(priv->connections_cached_list_sorted_by_autoconnect_priority)));

    if (!priv->connections_cached_list_sorted_by_autoconnect_priority) {
        NMSettingsConnection *const *list_cached;
        guint                        len;

        list_cached = nm_settings_get_connections(self, &len);
        priv->connections_cached_list_sorted_by_autoconnect_priority =
            nm_memdup(list_cached, sizeof(NMSettingsConnection *) * (len + 1));
        needs_sort = (len > 1);
    } else if (priv->sorted_by_autoconnect_priority_maybe_changed) {
        if (!nm_utils_ptrarray_is_sorted(
                (gconstpointer *) priv->connections_cached_list_sorted_by_autoconnect_priority,
                priv->connections_len,
                FALSE,
                nm_settings_connection_cmp_autoconnect_priority_with_data,
                NULL)) {
            /* We cache the sorted list, but we don't monitor all entries whether they
             * get modified to invalidate the sort order. So every time we have to check
             * whether the sort order is still correct. The vast majority of the time it
             * is, and this check is faster than sorting anew. */
            needs_sort = TRUE;
        }
    } else {
        nm_assert(nm_utils_ptrarray_is_sorted(
            (gconstpointer *) priv->connections_cached_list_sorted_by_autoconnect_priority,
            priv->connections_len,
            TRUE,
            nm_settings_connection_cmp_autoconnect_priority_with_data,
            NULL));
    }

    priv->sorted_by_autoconnect_priority_maybe_changed = FALSE;
    if (needs_sort) {
        g_qsort_with_data(priv->connections_cached_list_sorted_by_autoconnect_priority,
                          priv->connections_len,
                          sizeof(NMSettingsConnection *),
                          nm_settings_connection_cmp_autoconnect_priority_p_with_data,
                          NULL);
    }

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

/**
 * 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);

    if (sort_compare_func == nm_settings_connection_cmp_autoconnect_priority_p_with_data) {
        list_cached       = nm_settings_get_connections_sorted_by_autoconnect_priority(self, &len);
        sort_compare_func = NULL;
    } else
        list_cached = nm_settings_get_connections(self, &len);

#if NM_MORE_ASSERTS > 10
    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_with_type(nm_dbus_object_get_manager(NM_DBUS_OBJECT(self)),
                                                NM_TYPE_SETTINGS_CONNECTION,
                                                path);
    if (!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, gboolean ignore_not_found, 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;
        if (!ignore_not_found) {
            _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_strv_find_first(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, FALSE, 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_utils_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
_static_hostname_changed_cb(NMHostnameManager *hostname_manager,
                            GParamSpec        *pspec,
                            gpointer           user_data)
{
    _notify(user_data, PROP_STATIC_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,
                               NULL,
                               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;
        }
    }
}

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

static gboolean
_kf_db_prune_predicate(const char *uuid, gpointer user_data)
{
    return !!nm_settings_get_connection_by_uuid(user_data, uuid);
}

static void
_kf_db_to_file(NMSettings *self, gboolean is_timestamps, gboolean force_write)
{
    NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
    NMKeyFileDB       *kf_db;
    bool              *p_kf_db_pruned;

    if (is_timestamps) {
        kf_db          = priv->kf_db_timestamps;
        p_kf_db_pruned = &priv->kf_db_pruned_timestamps;
    } else {
        kf_db          = priv->kf_db_seen_bssids;
        p_kf_db_pruned = &priv->kf_db_pruned_seen_bssid;
    }

    if (!*p_kf_db_pruned) {
        /* we only prune the DB once, because afterwards every
         * add/remove of an connection will lead to a direct update. */
        *p_kf_db_pruned = TRUE;
        nm_key_file_db_prune(kf_db, _kf_db_prune_predicate, self);

        /* once we also go over the directory, and see whether we
         * have any left over temporary files to delete. */
        nm_key_file_db_prune_tmp_files(kf_db);
    }

    nm_key_file_db_to_file(kf_db, force_write);
}

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))
        _kf_db_to_file(self, is_timestamps, 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)
{
    g_return_if_fail(NM_IS_SETTINGS(self));

    _kf_db_to_file(self, TRUE, TRUE);
    _kf_db_to_file(self, FALSE, 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 (plugins && plugins[0]) {
        if (!load_plugins(self, (const char *const *) plugins, error))
            return FALSE;
    } else {
        add_plugin_keyfile(self);
#if WITH_CONFIG_PLUGIN_IFCFG_RH
        add_plugin_load_file(self, "ifcfg-rh", TRUE, NULL);
#endif
#if WITH_CONFIG_PLUGIN_IFUPDOWN
        add_plugin_load_file(self, "ifupdown", TRUE, NULL);
#endif
    }

    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_STATIC_HOSTNAME,
                     G_CALLBACK(_static_hostname_changed_cb),
                     self);
    if (nm_hostname_manager_get_static_hostname(priv->hostname_manager))
        _notify(self, PROP_STATIC_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_STATIC_HOSTNAME:
        g_value_set_string(value,
                           priv->hostname_manager
                               ? nm_hostname_manager_get_static_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_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(_static_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);
    _kf_db_to_file(self, TRUE, FALSE);
    _kf_db_to_file(self, FALSE, 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(&signal_info_new_connection,
                                                &signal_info_connection_removed, ),
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Connections",
                                                           "ao",
                                                           NM_SETTINGS_CONNECTIONS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Hostname",
                                                           "s",
                                                           NM_SETTINGS_STATIC_HOSTNAME),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("CanModify",
                                                           "b",
                                                           NM_SETTINGS_CAN_MODIFY), ), ),
};

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_STATIC_HOSTNAME] =
        g_param_spec_string(NM_SETTINGS_STATIC_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);
}
