/*
 * Copyright (C) 2009 Kay Sievers <kay.sievers@vrfy.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details:
 */

#include <acl/libacl.h>
#include <sys/stat.h>
#include <errno.h>
#include <getopt.h>
#include <glib.h>
#include <inttypes.h>
#include <libudev.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libintl.h>
#include <locale.h>

static int debug;

enum{
        ACTION_NONE = 0,
        ACTION_REMOVE,
        ACTION_ADD,
        ACTION_CHANGE
};

static int set_facl(const char* filename, uid_t uid, int add)
{
        int get;
        acl_t acl;
        acl_entry_t entry = NULL;
        acl_entry_t e;
        acl_permset_t permset;
        int ret;

        /* don't touch ACLs for root */
        if (uid == 0)
                return 0;

        /* read current record */
        acl = acl_get_file(filename, ACL_TYPE_ACCESS);
        if (!acl)
                return -1;

        /* locate ACL_USER entry for uid */
        get = acl_get_entry(acl, ACL_FIRST_ENTRY, &e);
        while (get == 1) {
                acl_tag_t t;

                acl_get_tag_type(e, &t);
                if (t == ACL_USER) {
                        uid_t *u;

                        u = (uid_t*)acl_get_qualifier(e);
                        if (u == NULL) {
                                ret = -1;
                                goto out;
                        }
                        if (*u == uid) {
                                entry = e;
                                acl_free(u);
                                break;
                        }
                        acl_free(u);
                }

                get = acl_get_entry(acl, ACL_NEXT_ENTRY, &e);
        }

        /* remove ACL_USER entry for uid */
        if (!add) {
                if (entry == NULL) {
                        ret = 0;
                        goto out;
                }
                acl_delete_entry(acl, entry);
                goto update;
        }

        /* create ACL_USER entry for uid */
        if (entry == NULL) {
                ret = acl_create_entry(&acl, &entry);
                if (ret != 0)
                        goto out;
                acl_set_tag_type(entry, ACL_USER);
                acl_set_qualifier(entry, &uid);
        }

        /* add permissions for uid */
        acl_get_permset(entry, &permset);
        acl_add_perm(permset, ACL_READ|ACL_WRITE);
update:
        /* update record */
        if (debug)
                printf("%c%u %s\n", add ? '+' : '-', uid, filename);
        acl_calc_mask(&acl);
        ret = acl_set_file(filename, ACL_TYPE_ACCESS, acl);

out:
        acl_free(acl);
        return ret;
}

/* check if a given uid is listed */
static int uid_in_list(GSList *list, uid_t uid)
{
        GSList *l;

        for (l = list; l != NULL; l = g_slist_next(l))
                if (uid == GPOINTER_TO_UINT(l->data))
                        return 1;
        return 0;
}

/* return list of current uids of local active sessions */
static GSList *uids_with_local_active_session(const char *own_id)
{
        GSList *list = NULL;
        GKeyFile *keyfile;

        keyfile = g_key_file_new();
        if (g_key_file_load_from_file(keyfile, RUNDIR "/ConsoleKit/database", 0, NULL)) {
                gchar **groups;

                groups = g_key_file_get_groups(keyfile, NULL);
                if (groups != NULL) {
                        int i;

                        for (i = 0; groups[i] != NULL; i++) {
                                uid_t u;

                                if (!g_str_has_prefix(groups[i], "Session "))
                                        continue;
                                if (own_id != NULL &&g_str_has_suffix(groups[i], own_id))
                                        continue;
                                if (!g_key_file_get_boolean(keyfile, groups[i], "is_local", NULL))
                                        continue;
                                if (!g_key_file_get_boolean(keyfile, groups[i], "is_active", NULL))
                                        continue;
                                u = g_key_file_get_integer(keyfile, groups[i], "uid", NULL);
                                if (u > 0 && !uid_in_list(list, u))
                                        list = g_slist_prepend(list, GUINT_TO_POINTER(u));
                        }
                        g_strfreev(groups);
                }
        }
        g_key_file_free(keyfile);

        return list;
}

/* ConsoleKit calls us with special variables */
static int consolekit_called(const char *ck_action, uid_t *uid, uid_t *uid2, const char **remove_session_id, int *action)
{
        int a = ACTION_NONE;
        uid_t u = 0;
        uid_t u2 = 0;
        const char *s;
        const char *s2;
        const char *old_session = NULL;

        if (ck_action == NULL || strcmp(ck_action, "seat_active_session_changed") != 0)
                return -1;

        /* We can have one of: remove, add, change, no-change */
        s = getenv("CK_SEAT_OLD_SESSION_ID");
        s2 = getenv("CK_SEAT_SESSION_ID");
        if (s == NULL && s2 == NULL) {
                return -1;
        } else if (s2 == NULL) {
                a = ACTION_REMOVE;
        } else if (s == NULL) {
                a = ACTION_ADD;
        } else {
                a = ACTION_CHANGE;
        }

        switch (a) {
        case ACTION_ADD:
                s = getenv("CK_SEAT_SESSION_USER_UID");
                if (s == NULL)
                        return -1;
                u = strtoul(s, NULL, 10);

                s = getenv("CK_SEAT_SESSION_IS_LOCAL");
                if (s == NULL)
                        return -1;
                if (strcmp(s, "true") != 0)
                        return 0;

                break;
        case ACTION_REMOVE:
                s = getenv("CK_SEAT_OLD_SESSION_USER_UID");
                if (s == NULL)
                        return -1;
                u = strtoul(s, NULL, 10);

                s = getenv("CK_SEAT_OLD_SESSION_IS_LOCAL");
                if (s == NULL)
                        return -1;
                if (strcmp(s, "true") != 0)
                        return 0;

                old_session = getenv("CK_SEAT_OLD_SESSION_ID");
                if (old_session == NULL)
                        return -1;

                break;
        case ACTION_CHANGE:
                s = getenv("CK_SEAT_OLD_SESSION_USER_UID");
                if (s == NULL)
                        return -1;
                u = strtoul(s, NULL, 10);
                s = getenv("CK_SEAT_SESSION_USER_UID");
                if (s == NULL)
                        return -1;
                u2 = strtoul(s, NULL, 10);

                s = getenv("CK_SEAT_OLD_SESSION_IS_LOCAL");
                s2 = getenv("CK_SEAT_SESSION_IS_LOCAL");
                if (s == NULL || s2 == NULL)
                        return -1;
                /* don't process non-local session changes */
                if (strcmp(s, "true") != 0 && strcmp(s2, "true") != 0)
                        return 0;

                if (strcmp(s, "true") == 0 && strcmp(s2, "true") == 0) {
                        /* process the change */
                        if (u == u2) {
                                /* special case: we noop if we are
                                 * changing between local sessions for
                                 * the same uid */
                                a = ACTION_NONE;
                        }
                        old_session = getenv("CK_SEAT_OLD_SESSION_ID");
                        if (old_session == NULL)
                                return -1;
                } else if (strcmp(s, "true") == 0) {
                        /* only process the removal */
                        a = ACTION_REMOVE;
                        old_session = getenv("CK_SEAT_OLD_SESSION_ID");
                        if (old_session == NULL)
                                return -1;
                } else if (strcmp(s2, "true") == 0) {
                        /* only process the addition */
                        a = ACTION_ADD;
                        u = u2;
                }
                break;
        }

        *remove_session_id = old_session;
        *uid = u;
        *uid2 = u2;
        *action = a;
        return 0;
}

/* add or remove a ACL for a given uid from all matching devices */
static void apply_acl_to_devices(uid_t uid, int add)
{
        struct udev *udev;
        struct udev_enumerate *enumerate;
        struct udev_list_entry *list_entry;

        /* iterate over all devices tagged with ACL_SET */
        udev = udev_new();
        enumerate = udev_enumerate_new(udev);
        udev_enumerate_add_match_tag(enumerate, "udev-acl");
        udev_enumerate_scan_devices(enumerate);
        udev_list_entry_foreach(list_entry, udev_enumerate_get_list_entry(enumerate)) {
                struct udev_device *device;
                const char *node;

                device = udev_device_new_from_syspath(udev_enumerate_get_udev(enumerate),
                                                      udev_list_entry_get_name(list_entry));
                if (device == NULL)
                        continue;
                node = udev_device_get_devnode(device);
                if (node == NULL) {
                        udev_device_unref(device);
                        continue;
                }
                set_facl(node, uid, add);
                udev_device_unref(device);
        }
        udev_enumerate_unref(enumerate);
        udev_unref(udev);
}

static void
remove_uid (uid_t uid, const char *remove_session_id)
{
        /*
         * Remove ACL for given uid from all matching devices
         * when there is currently no local active session.
         */
        GSList *list;

        list = uids_with_local_active_session(remove_session_id);
        if (!uid_in_list(list, uid))
                apply_acl_to_devices(uid, 0);
        g_slist_free(list);
}

int main (int argc, char* argv[])
{
        static const struct option options[] = {
                { "action", required_argument, NULL, 'a' },
                { "device", required_argument, NULL, 'D' },
                { "user", required_argument, NULL, 'u' },
                { "debug", no_argument, NULL, 'd' },
                { "help", no_argument, NULL, 'h' },
                {}
        };
        int action = -1;
        const char *device = NULL;
        bool uid_given = false;
        uid_t uid = 0;
        uid_t uid2 = 0;
        const char* remove_session_id = NULL;
        int rc = 0;

        while (1) {
                int option;

                option = getopt_long(argc, argv, "+a:D:u:dh", options, NULL);
                if (option == -1)
                        break;

                switch (option) {
                case 'a':
                        if (strcmp(optarg, "remove") == 0)
                                action = ACTION_REMOVE;
                        else
                                action = ACTION_ADD;
                        break;
                case 'D':
                        device = optarg;
                        break;
                case 'u':
                        uid_given = true;
                        uid = strtoul(optarg, NULL, 10);
                        break;
                case 'd':
                        debug = 1;
                        break;
                case 'h':
                        printf("Usage: udev-acl --action=ACTION [--device=DEVICEFILE] [--user=UID]\n\n");
                        goto out;
                }
        }

        if (action < 0 && device == NULL && !uid_given)
                if (!consolekit_called(argv[optind], &uid, &uid2, &remove_session_id, &action))
                        uid_given = true;

        if (action < 0) {
                fprintf(stderr, "missing action\n\n");
                rc = 2;
                goto out;
        }

        if (device != NULL && uid_given) {
                fprintf(stderr, "only one option, --device=DEVICEFILE or --user=UID expected\n\n");
                rc = 3;
                goto out;
        }

        if (uid_given) {
                switch (action) {
                case ACTION_ADD:
                        /* Add ACL for given uid to all matching devices. */
                        apply_acl_to_devices(uid, 1);
                        break;
                case ACTION_REMOVE:
                        remove_uid(uid, remove_session_id);
                        break;
                case ACTION_CHANGE:
                        remove_uid(uid, remove_session_id);
                        apply_acl_to_devices(uid2, 1);
                        break;
                case ACTION_NONE:
                        goto out;
                        break;
                default:
                        g_assert_not_reached();
                        break;
                }
        } else if (device != NULL) {
                /*
                 * Add ACLs for all current session uids to a given device.
                 *
                 * Or remove ACLs for uids which do not have any current local
                 * active session. Remove is not really interesting, because in
                 * most cases the device node is removed anyway.
                 */
                GSList *list;
                GSList *l;

                list = uids_with_local_active_session(NULL);
                for (l = list; l != NULL; l = g_slist_next(l)) {
                        uid_t u;

                        u = GPOINTER_TO_UINT(l->data);
                        if (action == ACTION_ADD || !uid_in_list(list, u))
                                set_facl(device, u, action == ACTION_ADD);
                }
                g_slist_free(list);
        } else {
                fprintf(stderr, "--device=DEVICEFILE or --user=UID expected\n\n");
                rc = 3;
        }
out:
        return rc;
}
