/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2014 Red Hat, Inc.
 */

/**
 * SECTION:nm-polkit-listener
 * @short_description: A polkit agent listener
 *
 * #NMPolkitListener is the polkit agent listener used by nmcli and nmtui.
 * http://www.freedesktop.org/software/polkit/docs/latest/index.html
 *
 * For an example polkit agent you can look at polkit source tree:
 * http://cgit.freedesktop.org/polkit/tree/src/polkitagent/polkitagenttextlistener.c
 * http://cgit.freedesktop.org/polkit/tree/src/programs/pkttyagent.c
 * or LXDE polkit agent:
 * http://git.lxde.org/gitweb/?p=debian/lxpolkit.git;a=blob;f=src/lxpolkit-listener.c
 * https://github.com/lxde/lxqt-policykit/tree/master/src
 */

#include "libnm-client-aux-extern/nm-default-client.h"

#include "nm-polkit-listener.h"

#include <gio/gio.h>
#include <pwd.h>
#include <fcntl.h>

#include "libnm-glib-aux/nm-dbus-aux.h"
#include "libnm-glib-aux/nm-str-buf.h"
#include "libnm-glib-aux/nm-secret-utils.h"
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-core-aux-intern/nm-auth-subject.h"
#include "c-list/src/c-list.h"

#define LOGIND_BUS_NAME "org.freedesktop.login1"
#define POLKIT_BUS_NAME "org.freedesktop.PolicyKit1"

#define POLKIT_AUTHORITY_OBJ_PATH   "/org/freedesktop/PolicyKit1/Authority"
#define POLKIT_AUTHORITY_IFACE_NAME "org.freedesktop.PolicyKit1.Authority"

#define POLKIT_AGENT_OBJ_PATH       "/org/freedesktop/PolicyKit1/AuthenticationAgent"
#define POLKIT_AGENT_DBUS_INTERFACE "org.freedesktop.PolicyKit1.AuthenticationAgent"

#define LOGIND_OBJ_PATH          "/org/freedesktop/login1"
#define LOGIND_MANAGER_INTERFACE "org.freedesktop.login1.Manager"

#define NM_POLKIT_LISTENER_DBUS_CONNECTION "dbus-connection"
#define NM_POLKIT_LISTENER_SESSION_AGENT   "session-agent"

#define POLKIT_DBUS_ERROR_FAILED "org.freedesktop.PolicyKit1.Error.Failed"

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

enum { REGISTERED, REQUEST_SYNC, ERROR, LAST_SIGNAL };

static guint signals[LAST_SIGNAL] = {0};

struct _NMPolkitListener {
    GObject          parent;
    GDBusConnection *dbus_connection;
    char            *name_owner;
    GCancellable    *cancellable;
    GMainContext    *main_context;
    CList            request_lst_head;
    guint            pk_auth_agent_reg_id;
    guint            name_owner_changed_id;
    bool             session_agent : 1;
};

struct _NMPolkitListenerClass {
    GObjectClass parent;
};

G_DEFINE_TYPE(NMPolkitListener, nm_polkit_listener, G_TYPE_OBJECT);

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

typedef struct {
    CList request_lst;

    NMPolkitListener      *listener;
    GSource               *child_stdout_watch_source;
    GSource               *child_stdin_watch_source;
    GDBusMethodInvocation *dbus_invocation;
    char                  *action_id;
    char                  *message;
    char                  *username;
    char                  *cookie;
    NMStrBuf               in_buffer;
    NMStrBuf               out_buffer;
    gsize                  out_buffer_offset;

    int child_stdout;
    int child_stdin;

    bool request_any_response : 1;
    bool request_is_completed : 1;
} AuthRequest;

static const GDBusInterfaceInfo interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
    POLKIT_AGENT_DBUS_INTERFACE,
    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
        NM_DEFINE_GDBUS_METHOD_INFO("BeginAuthentication",
                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                                        NM_DEFINE_GDBUS_ARG_INFO("action_id", "s"),
                                        NM_DEFINE_GDBUS_ARG_INFO("message", "s"),
                                        NM_DEFINE_GDBUS_ARG_INFO("icon_name", "s"),
                                        NM_DEFINE_GDBUS_ARG_INFO("details", "a{ss}"),
                                        NM_DEFINE_GDBUS_ARG_INFO("cookie", "s"),
                                        NM_DEFINE_GDBUS_ARG_INFO("identities", "a(sa{sv})"), ), ),
        NM_DEFINE_GDBUS_METHOD_INFO("CancelAuthentication",
                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                                        NM_DEFINE_GDBUS_ARG_INFO("cookie", "s"), ), ), ), );

static void
auth_request_complete(AuthRequest *request, gboolean success)
{
    c_list_unlink(&request->request_lst);

    if (success)
        g_dbus_method_invocation_return_value(request->dbus_invocation, NULL);
    else {
        g_dbus_method_invocation_return_dbus_error(request->dbus_invocation,
                                                   POLKIT_DBUS_ERROR_FAILED,
                                                   "");
    }

    nm_clear_g_free(&request->action_id);
    nm_clear_g_free(&request->message);
    nm_clear_g_free(&request->username);
    nm_clear_g_free(&request->cookie);
    nm_clear_g_source_inst(&request->child_stdout_watch_source);
    nm_clear_g_source_inst(&request->child_stdin_watch_source);

    nm_str_buf_destroy(&request->in_buffer);
    nm_str_buf_destroy(&request->out_buffer);

    if (request->child_stdout != -1) {
        nm_close(request->child_stdout);
        request->child_stdout = -1;
    }

    if (request->child_stdin != -1) {
        nm_close(request->child_stdin);
        request->child_stdin = -1;
    }

    nm_g_slice_free(request);
}

static gboolean
uid_to_name(uid_t uid, gboolean *out_cached, char **out_name)
{
    if (!*out_cached) {
        *out_cached = TRUE;
        *out_name   = nm_utils_uid_to_name(uid);
    }
    return !!(*out_name);
}

static char *
choose_identity(GVariant *identities)
{
    GVariantIter  identity_iter;
    GVariant     *details_tmp;
    const char   *kind;
    gs_free char *username_first = NULL;
    gs_free char *username_root  = NULL;
    const char   *user;

    /* Choose identity. First try current user, then root, and else
     * take the first one we find. */

    user = getenv("USER");

    g_variant_iter_init(&identity_iter, identities);
    while (g_variant_iter_next(&identity_iter, "(&s@a{sv})", &kind, &details_tmp)) {
        gs_unref_variant GVariant *details = g_steal_pointer(&details_tmp);

        if (nm_streq(kind, "unix-user")) {
            gs_unref_variant GVariant *v = NULL;

            v = g_variant_lookup_value(details, "uid", G_VARIANT_TYPE_UINT32);
            if (v) {
                guint32       uid    = g_variant_get_uint32(v);
                gs_free char *u      = NULL;
                gboolean      cached = FALSE;

                if (user) {
                    if (!uid_to_name(uid, &cached, &u))
                        continue;
                    if (nm_streq(u, user))
                        return g_steal_pointer(&u);
                }
                if (!username_root && uid == 0) {
                    if (!uid_to_name(uid, &cached, &u))
                        continue;
                    username_root = g_strdup(u);
                    if (!user)
                        break;
                }
                if (!username_root && !username_first) {
                    if (!uid_to_name(uid, &cached, &u))
                        continue;
                    username_first = g_strdup(u);
                }
            }
        }
    }

    if (username_root)
        return g_steal_pointer(&username_root);

    if (username_first)
        return g_steal_pointer(&username_first);

    return NULL;
}

static void
agent_register_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMPolkitListener          *listener        = NM_POLKIT_LISTENER(user_data);
    GDBusConnection           *dbus_connection = G_DBUS_CONNECTION(source_object);
    gs_free_error GError      *error           = NULL;
    gs_unref_variant GVariant *ret             = NULL;

    ret = g_dbus_connection_call_finish(dbus_connection, res, &error);

    if (nm_utils_error_is_cancelled(error)) {
        return;
    }

    if (ret) {
        g_signal_emit(listener, signals[REGISTERED], 0);
    } else {
        g_signal_emit(listener, signals[ERROR], 0, error->message);
    }
}

static void
agent_register(NMPolkitListener *self, const char *session_id)
{
    const char                    *locale          = NULL;
    gs_unref_object NMAuthSubject *subject         = NULL;
    GVariant                      *subject_variant = NULL;

    locale = g_getenv("LANG");
    if (locale == NULL) {
        locale = "en_US.UTF-8";
    }

    if (self->session_agent) {
        subject = nm_auth_subject_new_unix_session(session_id);
    } else {
        subject = nm_auth_subject_new_unix_process_self();
    }
    subject_variant = nm_auth_subject_unix_to_polkit_gvariant(subject);

    g_dbus_connection_call(
        self->dbus_connection,
        self->name_owner,
        POLKIT_AUTHORITY_OBJ_PATH,
        POLKIT_AUTHORITY_IFACE_NAME,
        "RegisterAuthenticationAgent",
        g_variant_new("(@(sa{sv})ss)", subject_variant, locale, POLKIT_AGENT_OBJ_PATH),
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        self->cancellable,
        agent_register_cb,
        self);
}

static void
agent_unregister(NMPolkitListener *self)
{
    gs_unref_object NMAuthSubject *subject         = NULL;
    GVariant                      *subject_variant = NULL;

    subject         = nm_auth_subject_new_unix_process_self();
    subject_variant = nm_auth_subject_unix_to_polkit_gvariant(subject);

    g_dbus_connection_call(self->dbus_connection,
                           self->name_owner,
                           POLKIT_AUTHORITY_OBJ_PATH,
                           POLKIT_AUTHORITY_IFACE_NAME,
                           "UnregisterAuthenticationAgent",
                           g_variant_new("(@(sa{sv})s)", subject_variant, POLKIT_AGENT_OBJ_PATH),
                           NULL,
                           G_DBUS_CALL_FLAGS_NONE,
                           -1,
                           NULL,
                           NULL,
                           self);
}

static void
retrieve_session_id_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMPolkitListener                       *listener = NM_POLKIT_LISTENER(user_data);
    char                                   *session_id;
    guint32                                 session_uid;
    nm_auto_free_variant_iter GVariantIter *iter    = NULL;
    gs_unref_variant GVariant              *ret     = NULL;
    gs_free_error GError                   *error   = NULL;
    gs_free char                           *err_str = NULL;
    uid_t                                   uid     = getuid();

    ret = g_dbus_connection_call_finish(listener->dbus_connection, res, &error);

    if (nm_utils_error_is_cancelled(error)) {
        return;
    }

    if (ret) {
        g_variant_get_child(ret, 0, "a(susso)", &iter);

        while (
            g_variant_iter_next(iter, "(&su@s@s@o)", &session_id, &session_uid, NULL, NULL, NULL)) {
            if (session_uid == uid) {
                agent_register(listener, session_id);
                return;
            }
        }
        err_str = g_strdup_printf(_("Could not find any session id for uid %d"), uid);
    } else {
        err_str = g_strdup_printf(_("Could not retrieve session id: %s"), error->message);
    }

    g_signal_emit(listener, signals[ERROR], 0, err_str);
}

static void
retrieve_session_id(NMPolkitListener *self)
{
    g_dbus_connection_call(self->dbus_connection,
                           LOGIND_BUS_NAME,
                           LOGIND_OBJ_PATH,
                           LOGIND_MANAGER_INTERFACE,
                           "ListSessions",
                           NULL,
                           G_VARIANT_TYPE("(a(susso))"),
                           G_DBUS_CALL_FLAGS_NONE,
                           -1,
                           self->cancellable,
                           retrieve_session_id_cb,
                           self);
}

static gboolean
io_watch_can_write(int fd, GIOCondition condition, gpointer user_data)
{
    AuthRequest *request = user_data;
    gssize       n_written;

    if (NM_FLAGS_ANY(condition, (G_IO_HUP | G_IO_ERR)))
        goto done;

    n_written =
        write(request->child_stdin,
              &((nm_str_buf_get_str_unsafe(&request->out_buffer))[request->out_buffer_offset]),
              request->out_buffer.len - request->out_buffer_offset);

    if (n_written < 0 && errno != EAGAIN)
        goto done;

    if (n_written > 0) {
        if ((gsize) n_written >= (request->out_buffer.len - request->out_buffer_offset)) {
            nm_assert((gsize) n_written == (request->out_buffer.len - request->out_buffer_offset));
            goto done;
        }
        request->out_buffer_offset += (gsize) n_written;
    }

    return G_SOURCE_CONTINUE;

done:
    nm_str_buf_set_size(&request->out_buffer, 0, TRUE, FALSE);
    request->out_buffer_offset = 0;
    nm_clear_g_source_inst(&request->child_stdin_watch_source);

    if (request->request_is_completed)
        auth_request_complete(request, TRUE);

    return G_SOURCE_CONTINUE;
}

static void
queue_string_to_helper(AuthRequest *request, const char *response)
{
    g_return_if_fail(response);

    if (!nm_str_buf_is_initalized(&request->out_buffer))
        request->out_buffer = NM_STR_BUF_INIT(strlen(response) + 2u, TRUE);

    nm_str_buf_append(&request->out_buffer, response);
    nm_str_buf_ensure_trailing_c(&request->out_buffer, '\n');

    if (!request->child_stdin_watch_source) {
        request->child_stdin_watch_source = nm_g_unix_fd_source_new(request->child_stdin,
                                                                    G_IO_OUT | G_IO_ERR | G_IO_HUP,
                                                                    G_PRIORITY_DEFAULT,
                                                                    io_watch_can_write,
                                                                    request,
                                                                    NULL);
        g_source_attach(request->child_stdin_watch_source, request->listener->main_context);
    }
}

static gboolean
io_watch_have_data(int fd, GIOCondition condition, gpointer user_data)
{
    AuthRequest *request = user_data;
    gboolean     auth_result;
    gssize       n_read;

    if (NM_FLAGS_ANY(condition, G_IO_HUP | G_IO_ERR))
        n_read = -EIO;
    else
        n_read = nm_utils_fd_read(fd, &request->in_buffer);

    if (n_read <= 0) {
        if (n_read == -EAGAIN) {
            /* wait longer. */
            return G_SOURCE_CONTINUE;
        }

        /* Either an error or EOF happened. The data we parsed so far was not relevant.
         * Regardless of what we still have unprocessed in the receive buffers, we are done.
         *
         * We would expect that the other side completed with SUCCESS or FAILURE. Apparently
         * it didn't. If we had any good request, we assume success. */
        auth_result = request->request_any_response;
        goto out;
    }

    while (TRUE) {
        char       *line_terminator;
        const char *line;

        line            = nm_str_buf_get_str(&request->in_buffer);
        line_terminator = (char *) strchr(line, '\n');
        if (!line_terminator) {
            /* We don't have a complete line. Wait longer. */
            return G_SOURCE_CONTINUE;
        }
        line_terminator[0] = '\0';

        if (NM_STR_HAS_PREFIX(line, "PAM_PROMPT_ECHO_OFF ")
            || NM_STR_HAS_PREFIX(line, "PAM_PROMPT_ECHO_ON ")) {
            nm_auto_free_secret char *response = NULL;

            /* FIXME(cli-async): emit signal and wait for response (blocking) */
            g_signal_emit(request->listener,
                          signals[REQUEST_SYNC],
                          0,
                          request->action_id,
                          request->message,
                          request->username,
                          &response);

            if (response) {
                queue_string_to_helper(request, response);
                request->request_any_response = TRUE;
                goto erase_line;
            }
            auth_result = FALSE;
        } else if (nm_streq(line, "SUCCESS"))
            auth_result = TRUE;
        else if (nm_streq(line, "FAILURE"))
            auth_result = FALSE;
        else if (NM_STR_HAS_PREFIX(line, "PAM_ERROR_MSG ")
                 || NM_STR_HAS_PREFIX(line, "PAM_TEXT_INFO ")) {
            /* ignore. */
            goto erase_line;
        } else {
            /* unknown command. Fail. */
            auth_result = FALSE;
        }

        break;
erase_line:
        nm_str_buf_erase(&request->in_buffer, 0, line_terminator - line + 1u, TRUE);
    }

out:
    request->request_is_completed = TRUE;
    nm_clear_g_source_inst(&request->child_stdout_watch_source);
    if (auth_result && request->child_stdin_watch_source) {
        /* we need to wait for the buffer to send the response. */
    } else
        auth_request_complete(request, auth_result);

    return G_SOURCE_CONTINUE;
}

static void
begin_authentication(AuthRequest *request)
{
    const char *helper_argv[] = {
        POLKIT_AGENT_HELPER_1_PATH,
        request->username,
        NULL,
    };

    if (!request->username) {
        auth_request_complete(request, FALSE);
        return;
    }

    if (!g_spawn_async_with_pipes(NULL,
                                  (char **) helper_argv,
                                  NULL,
                                  G_SPAWN_CLOEXEC_PIPES | G_SPAWN_STDERR_TO_DEV_NULL,
                                  NULL,
                                  NULL,
                                  NULL,
                                  &request->child_stdin,
                                  &request->child_stdout,
                                  NULL,
                                  NULL)) {
        /* not findind the PolicyKit setuid helper is a critical error */
        request->child_stdin  = -1;
        request->child_stdout = -1;
        g_signal_emit(request->listener,
                      signals[ERROR],
                      0,
                      "The PolicyKit setuid helper 'polkit-agent-helper-1' has not been found");

        auth_request_complete(request, FALSE);
        return;
    }

    nm_io_fcntl_setfl_update_nonblock(request->child_stdin);
    nm_io_fcntl_setfl_update_nonblock(request->child_stdout);

    request->child_stdout_watch_source = nm_g_unix_fd_source_new(request->child_stdout,
                                                                 G_IO_IN | G_IO_ERR | G_IO_HUP,
                                                                 G_PRIORITY_DEFAULT,
                                                                 io_watch_have_data,
                                                                 request,
                                                                 NULL);
    g_source_attach(request->child_stdout_watch_source, request->listener->main_context);

    /* Write the cookie on stdin so it can't be seen by other processes */
    queue_string_to_helper(request, request->cookie);

    return;
}

static AuthRequest *
get_request(NMPolkitListener *listener, const char *cookie)
{
    AuthRequest *request;

    c_list_for_each_entry (request, &listener->request_lst_head, request_lst) {
        if (nm_streq0(cookie, request->cookie)) {
            return request;
        }
    }
    return NULL;
}

static AuthRequest *
create_request(NMPolkitListener      *listener,
               GDBusMethodInvocation *invocation,
               const char            *action_id,
               const char            *message,
               char                  *username_take,
               const char            *cookie)
{
    AuthRequest *request;

    request  = g_slice_new(AuthRequest);
    *request = (AuthRequest) {
        .listener             = listener,
        .dbus_invocation      = invocation,
        .action_id            = g_strdup(action_id),
        .message              = g_strdup(message),
        .username             = g_steal_pointer(&username_take),
        .cookie               = g_strdup(cookie),
        .request_any_response = FALSE,
        .request_is_completed = FALSE,
        .in_buffer            = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE),
    };

    c_list_link_tail(&listener->request_lst_head, &request->request_lst);
    return request;
}

static void
dbus_method_call_cb(GDBusConnection       *connection,
                    const char            *sender,
                    const char            *object_path,
                    const char            *interface_name,
                    const char            *method_name,
                    GVariant              *parameters,
                    GDBusMethodInvocation *invocation,
                    gpointer               user_data)
{
    NMPolkitListener          *listener = NM_POLKIT_LISTENER(user_data);
    const char                *action_id;
    const char                *message;
    const char                *cookie;
    AuthRequest               *request;
    gs_unref_variant GVariant *identities_gvariant = NULL;

    if (nm_streq(method_name, "BeginAuthentication")) {
        g_variant_get(parameters,
                      "(&s&s&s@a{ss}&s@a(sa{sv}))",
                      &action_id,
                      &message,
                      NULL,
                      NULL,
                      &cookie,
                      &identities_gvariant);

        request = create_request(listener,
                                 invocation,
                                 action_id,
                                 message,
                                 choose_identity(identities_gvariant),
                                 cookie);
        begin_authentication(request);
        return;
    }

    if (nm_streq(method_name, "CancelAuthentication")) {
        g_variant_get(parameters, "&s", &cookie);
        request = get_request(listener, cookie);

        if (!request) {
            gs_free char *msg = NULL;

            msg = g_strdup_printf("No pending authentication request for cookie '%s'", cookie);
            g_dbus_method_invocation_return_dbus_error(invocation, POLKIT_DBUS_ERROR_FAILED, msg);
            return;
        }

        /* Complete a cancelled request with success. */
        auth_request_complete(request, TRUE);
        return;
    }

    g_dbus_method_invocation_return_error(invocation,
                                          G_DBUS_ERROR,
                                          G_DBUS_ERROR_UNKNOWN_METHOD,
                                          "Unknown method %s",
                                          method_name);
}

static gboolean
export_dbus_iface(NMPolkitListener *self, GError **error)
{
    GDBusInterfaceVTable interface_vtable = {
        .method_call  = dbus_method_call_cb,
        .set_property = NULL,
        .get_property = NULL,
    };

    g_return_val_if_fail(NM_IS_POLKIT_LISTENER(self), FALSE);
    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

    /* Agent listener iface has been exported already */
    if (self->pk_auth_agent_reg_id) {
        return TRUE;
    }

    self->pk_auth_agent_reg_id =
        g_dbus_connection_register_object(self->dbus_connection,
                                          POLKIT_AGENT_OBJ_PATH,
                                          (GDBusInterfaceInfo *) &interface_info,
                                          &interface_vtable,
                                          self,
                                          NULL,
                                          error);
    if (!self->pk_auth_agent_reg_id) {
        g_signal_emit(self,
                      signals[ERROR],
                      0,
                      "Could not register as a PolicyKit Authentication Agent");
    }
    return self->pk_auth_agent_reg_id;
}

static void
name_owner_changed(NMPolkitListener *self, const char *name_owner)
{
    gs_free_error GError *error = NULL;

    name_owner = nm_str_not_empty(name_owner);

    if (nm_streq0(self->name_owner, name_owner)) {
        return;
    }

    g_free(self->name_owner);
    self->name_owner = g_strdup(name_owner);

    if (!self->name_owner) {
        return;
    }

    if (export_dbus_iface(self, &error)) {
        if (self->session_agent) {
            retrieve_session_id(self);
        } else {
            agent_register(self, NULL);
        }
    } else {
        g_signal_emit(self,
                      signals[ERROR],
                      0,
                      "Could not export the PolicyKit Authentication Agent DBus interface");
    }
}

static void
name_owner_changed_cb(GDBusConnection *connection,
                      const char      *sender_name,
                      const char      *object_path,
                      const char      *interface_name,
                      const char      *signal_name,
                      GVariant        *parameters,
                      gpointer         user_data)
{
    NMPolkitListener *self = user_data;
    const char       *new_owner;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) {
        return;
    }

    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);

    name_owner_changed(self, new_owner);
}

static void
get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
{
    if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
        return;
    }
    name_owner_changed(user_data, name_owner);
}

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

NM_GOBJECT_PROPERTIES_DEFINE(NMPolkitListener, PROP_DBUS_CONNECTION, PROP_SESSION_AGENT, );

static void
set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    NMPolkitListener *self = NM_POLKIT_LISTENER(object);

    switch (prop_id) {
    case PROP_DBUS_CONNECTION:
        self->dbus_connection = g_value_dup_object(value);
        break;
    case PROP_SESSION_AGENT:
        self->session_agent = g_value_get_boolean(value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
nm_polkit_listener_init(NMPolkitListener *self)
{
    c_list_init(&self->request_lst_head);
    self->main_context = g_main_context_ref_thread_default();
}

static void
constructed(GObject *object)
{
    NMPolkitListener *self = NM_POLKIT_LISTENER(object);

    self->cancellable = g_cancellable_new();

    self->name_owner_changed_id =
        nm_dbus_connection_signal_subscribe_name_owner_changed(self->dbus_connection,
                                                               POLKIT_BUS_NAME,
                                                               name_owner_changed_cb,
                                                               self,
                                                               NULL);

    nm_dbus_connection_call_get_name_owner(self->dbus_connection,
                                           POLKIT_BUS_NAME,
                                           -1,
                                           self->cancellable,
                                           get_name_owner_cb,
                                           self);

    G_OBJECT_CLASS(nm_polkit_listener_parent_class)->constructed(object);
}

/**
 * nm_polkit_listener_new:
 * @dbus_connection: a open DBus connection
 * @session_agent: TRUE if a session agent is wanted, FALSE for a process agent
 *
 * Creates a new #NMPolkitListener and registers it as a polkit agent.
 *
 * Returns: a new #NMPolkitListener
 */
NMPolkitListener *
nm_polkit_listener_new(GDBusConnection *dbus_connection, gboolean session_agent)
{
    return g_object_new(NM_TYPE_POLKIT_LISTENER,
                        NM_POLKIT_LISTENER_DBUS_CONNECTION,
                        dbus_connection,
                        NM_POLKIT_LISTENER_SESSION_AGENT,
                        session_agent,
                        NULL);
}

static void
dispose(GObject *object)
{
    NMPolkitListener *self = NM_POLKIT_LISTENER(object);
    AuthRequest      *request;

    nm_clear_g_cancellable(&self->cancellable);

    while ((request = c_list_first_entry(&self->request_lst_head, AuthRequest, request_lst)))
        auth_request_complete(request, FALSE);

    if (self->dbus_connection) {
        nm_clear_g_dbus_connection_signal(self->dbus_connection, &self->name_owner_changed_id);
        g_dbus_connection_unregister_object(self->dbus_connection, self->pk_auth_agent_reg_id);
        agent_unregister(self);
        nm_clear_g_free(&self->name_owner);
        g_clear_object(&self->dbus_connection);
    }

    nm_clear_pointer(&self->main_context, g_main_context_unref);

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

static void
nm_polkit_listener_class_init(NMPolkitListenerClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    object_class->set_property = set_property;
    object_class->constructed  = constructed;
    object_class->dispose      = dispose;

    obj_properties[PROP_DBUS_CONNECTION] =
        g_param_spec_object(NM_POLKIT_LISTENER_DBUS_CONNECTION,
                            "",
                            "",
                            G_TYPE_DBUS_CONNECTION,
                            G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_SESSION_AGENT] =
        g_param_spec_boolean(NM_POLKIT_LISTENER_SESSION_AGENT,
                             "",
                             "",
                             FALSE,
                             G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);

    signals[REQUEST_SYNC] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_REQUEST_SYNC,
                                         NM_TYPE_POLKIT_LISTENER,
                                         G_SIGNAL_RUN_LAST,
                                         0,
                                         NULL,
                                         NULL,
                                         NULL,
                                         G_TYPE_STRING,
                                         3,
                                         G_TYPE_STRING,
                                         G_TYPE_STRING,
                                         G_TYPE_STRING);

    signals[REGISTERED] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_REGISTERED,
                                       NM_TYPE_POLKIT_LISTENER,
                                       G_SIGNAL_RUN_FIRST,
                                       0,
                                       NULL,
                                       NULL,
                                       NULL,
                                       G_TYPE_NONE,
                                       0);

    signals[ERROR] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_ERROR,
                                  NM_TYPE_POLKIT_LISTENER,
                                  G_SIGNAL_RUN_FIRST,
                                  0,
                                  NULL,
                                  NULL,
                                  NULL,
                                  G_TYPE_NONE,
                                  1,
                                  G_TYPE_STRING);
}
