| /* |
| * |
| * Connection Manager |
| * |
| * Copyright (C) 2012-2014 BMW Car IT GmbH. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| * |
| * 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. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <errno.h> |
| #include <string.h> |
| #include <sys/inotify.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <pwd.h> |
| #include <grp.h> |
| |
| #include <glib.h> |
| |
| #include <gdbus.h> |
| |
| #define CONNMAN_API_SUBJECT_TO_CHANGE |
| #include <connman/plugin.h> |
| #include <connman/log.h> |
| #include <connman/session.h> |
| #include <connman/dbus.h> |
| #include <connman/inotify.h> |
| |
| #include "src/shared/util.h" |
| |
| #define POLICYDIR STORAGEDIR "/session_policy_local" |
| |
| #define MODE (S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | \ |
| S_IXGRP | S_IROTH | S_IXOTH) |
| |
| static DBusConnection *connection; |
| |
| static GHashTable *file_hash; /* (filename, policy_file) */ |
| static GHashTable *session_hash; /* (connman_session, policy_config) */ |
| |
| /* Global lookup tables for mapping sessions to policies */ |
| static GHashTable *selinux_hash; /* (lsm context, policy_group) */ |
| static GHashTable *uid_hash; /* (uid, policy_group) */ |
| static GHashTable *gid_hash; /* (gid, policy_group) */ |
| |
| /* |
| * A instance of struct policy_file is created per file in |
| * POLICYDIR. |
| */ |
| struct policy_file { |
| /* |
| * A valid file is a keyfile with one ore more groups. All |
| * groups are keept in this list. |
| */ |
| GSList *groups; |
| }; |
| |
| struct policy_group { |
| char *selinux; |
| char *uid; |
| char *gid; |
| |
| /* |
| * Each policy_group owns a config and is not shared with |
| * sessions. Instead each session copies the valued from this |
| * object. |
| */ |
| struct connman_session_config *config; |
| |
| /* All 'users' of this policy. */ |
| GSList *sessions; |
| }; |
| |
| /* A struct policy_config object is created and owned by a session. */ |
| struct policy_config { |
| char *selinux; |
| char *selinux_context; |
| char *uid; |
| GSList *gids; |
| |
| /* The policy config owned by the session */ |
| struct connman_session_config *config; |
| |
| /* To which policy belongs this policy_config */ |
| struct connman_session *session; |
| /* |
| * Points to the policy_group when a config has been applied |
| * from a file. |
| */ |
| struct policy_group *group; |
| }; |
| |
| static void copy_session_config(struct connman_session_config *dst, |
| struct connman_session_config *src) |
| { |
| g_slist_free(dst->allowed_bearers); |
| dst->allowed_bearers = g_slist_copy(src->allowed_bearers); |
| dst->ecall = src->ecall; |
| dst->type = src->type; |
| dst->roaming_policy = src->roaming_policy; |
| dst->priority = src->priority; |
| } |
| |
| static void set_policy(struct policy_config *policy, |
| struct policy_group *group) |
| { |
| DBG("policy %p group %p", policy, group); |
| |
| group->sessions = g_slist_prepend(group->sessions, policy); |
| policy->group = group; |
| |
| copy_session_config(policy->config, group->config); |
| } |
| |
| static char *parse_selinux_type(const char *context) |
| { |
| char *ident, **tokens; |
| |
| /* |
| * SELinux combines Role-Based Access Control (RBAC), Type |
| * Enforcment (TE) and optionally Multi-Level Security (MLS). |
| * |
| * When SELinux is enabled all processes and files are labeled |
| * with a contex that contains information such as user, role |
| * type (and optionally a level). E.g. |
| * |
| * $ ls -Z |
| * -rwxrwxr-x. wagi wagi unconfined_u:object_r:haifux_exec_t:s0 session_ui.py |
| * |
| * For identifyng application we (ab)using the type |
| * information. In the above example the haifux_exec_t type |
| * will be transfered to haifux_t as defined in the domain |
| * transition and thus we are able to identify the application |
| * as haifux_t. |
| */ |
| |
| tokens = g_strsplit(context, ":", 0); |
| if (g_strv_length(tokens) < 2) { |
| g_strfreev(tokens); |
| return NULL; |
| } |
| |
| /* Use the SELinux type as identification token. */ |
| ident = g_strdup(tokens[2]); |
| |
| g_strfreev(tokens); |
| |
| return ident; |
| } |
| |
| static void cleanup_config(gpointer user_data); |
| |
| static void finish_create(struct policy_config *policy, |
| connman_session_config_func_t cb, |
| void *user_data) |
| { |
| struct policy_group *group = NULL; |
| GSList *list; |
| |
| if (policy->selinux) |
| group = g_hash_table_lookup(selinux_hash, policy->selinux); |
| |
| if (group) { |
| set_policy(policy, group); |
| |
| policy->config->id_type = CONNMAN_SESSION_ID_TYPE_LSM; |
| policy->config->id = g_strdup(policy->selinux_context); |
| goto done; |
| } |
| |
| if (policy->uid) |
| group = g_hash_table_lookup(uid_hash, policy->uid); |
| |
| if (group) { |
| set_policy(policy, group); |
| |
| policy->config->id_type = CONNMAN_SESSION_ID_TYPE_UID; |
| policy->config->id = g_strdup(policy->uid); |
| goto done; |
| } |
| |
| for (list = policy->gids; list; list = list->next) { |
| char *gid = list->data; |
| |
| group = g_hash_table_lookup(gid_hash, gid); |
| if (!group) |
| continue; |
| |
| set_policy(policy, group); |
| |
| policy->config->id_type = CONNMAN_SESSION_ID_TYPE_GID; |
| policy->config->id = g_strdup(gid); |
| break; |
| } |
| done: |
| g_hash_table_replace(session_hash, policy->session, policy); |
| |
| (*cb)(policy->session, policy->config, user_data, 0); |
| } |
| |
| static void failed_create(struct policy_config *policy, |
| connman_session_config_func_t cb, |
| void *user_data, int err) |
| { |
| (*cb)(policy->session, NULL, user_data, err); |
| |
| cleanup_config(policy); |
| } |
| |
| static void selinux_context_reply(const unsigned char *context, void *user_data, |
| int err) |
| { |
| struct cb_data *cbd = user_data; |
| connman_session_config_func_t cb = cbd->cb; |
| struct policy_config *policy = cbd->data; |
| char *ident = NULL; |
| |
| DBG("session %p", policy->session); |
| |
| if (err == -EIO) { |
| /* No SELinux support, drop back to UID/GID only mode */ |
| finish_create(policy, cb, cbd->user_data); |
| goto done; |
| } |
| |
| if (err < 0) { |
| failed_create(policy, cb, cbd->user_data, err); |
| goto done; |
| } |
| |
| DBG("SELinux context %s", context); |
| |
| policy->selinux_context = g_strdup((const char *)context); |
| ident = parse_selinux_type(policy->selinux_context); |
| if (ident) |
| policy->selinux = g_strdup(ident); |
| |
| finish_create(policy, cb, cbd->user_data); |
| |
| done: |
| g_free(cbd); |
| g_free(ident); |
| } |
| |
| static void get_uid_reply(unsigned int uid, void *user_data, int err) |
| { |
| struct cb_data *cbd = user_data; |
| connman_session_config_func_t cb = cbd->cb; |
| struct policy_config *policy = cbd->data; |
| const char *owner; |
| struct passwd *pwd; |
| struct group *grp; |
| gid_t *groups = NULL; |
| int nrgroups, i; |
| |
| DBG("session %p uid %d", policy->session, uid); |
| |
| if (err < 0) { |
| cleanup_config(policy); |
| goto err; |
| } |
| |
| pwd = getpwuid((uid_t)uid); |
| if (!pwd) { |
| if (errno != 0) |
| err = -errno; |
| else |
| err = -EINVAL; |
| goto err; |
| } |
| |
| policy->uid = g_strdup(pwd->pw_name); |
| |
| nrgroups = 0; |
| getgrouplist(pwd->pw_name, pwd->pw_gid, NULL, &nrgroups); |
| groups = g_try_new0(gid_t, nrgroups); |
| if (!groups) { |
| err = -ENOMEM; |
| goto err; |
| } |
| |
| err = getgrouplist(pwd->pw_name, pwd->pw_gid, groups, &nrgroups); |
| if (err < 0) |
| goto err; |
| |
| for (i = 0; i < nrgroups; i++) { |
| grp = getgrgid(groups[i]); |
| if (!grp) { |
| if (errno != 0) |
| err = -errno; |
| else |
| err = -EINVAL; |
| goto err; |
| } |
| |
| policy->gids = g_slist_prepend(policy->gids, |
| g_strdup(grp->gr_name)); |
| } |
| g_free(groups); |
| |
| owner = connman_session_get_owner(policy->session); |
| |
| err = connman_dbus_get_selinux_context(connection, owner, |
| selinux_context_reply, cbd); |
| if (err == 0) { |
| /* |
| * We are able to ask for a SELinux context. Let's defer the |
| * creation of the session config until we get the answer |
| * from D-Bus. |
| */ |
| return; |
| } |
| |
| finish_create(policy, cb, cbd->user_data); |
| g_free(cbd); |
| |
| return; |
| |
| err: |
| failed_create(NULL, cb, cbd->user_data, err); |
| g_free(cbd); |
| g_free(groups); |
| } |
| |
| static int policy_local_create(struct connman_session *session, |
| connman_session_config_func_t cb, |
| void *user_data) |
| { |
| struct cb_data *cbd = cb_data_new(cb, user_data); |
| struct policy_config *policy; |
| const char *owner; |
| int err; |
| |
| DBG("session %p", session); |
| |
| policy = g_new0(struct policy_config, 1); |
| policy->config = connman_session_create_default_config(); |
| policy->session = session; |
| |
| cbd->data = policy; |
| |
| owner = connman_session_get_owner(session); |
| |
| err = connman_dbus_get_connection_unix_user(connection, owner, |
| get_uid_reply, cbd); |
| if (err < 0) { |
| connman_error("Could not get UID"); |
| cleanup_config(policy); |
| g_free(cbd); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static void policy_local_destroy(struct connman_session *session) |
| { |
| struct policy_data *policy; |
| |
| DBG("session %p", session); |
| |
| policy = g_hash_table_lookup(session_hash, session); |
| if (!policy) |
| return; |
| |
| g_hash_table_remove(session_hash, session); |
| } |
| |
| static struct connman_session_policy session_policy_local = { |
| .name = "session local policy configuration", |
| .priority = CONNMAN_SESSION_POLICY_PRIORITY_DEFAULT, |
| .create = policy_local_create, |
| .destroy = policy_local_destroy, |
| }; |
| |
| static int load_keyfile(const char *pathname, GKeyFile **keyfile) |
| { |
| GError *error = NULL; |
| int err; |
| |
| *keyfile = g_key_file_new(); |
| |
| if (!g_key_file_load_from_file(*keyfile, pathname, 0, &error)) |
| goto err; |
| |
| return 0; |
| |
| err: |
| /* |
| * The fancy G_FILE_ERROR_* codes are identical to the native |
| * error codes. |
| */ |
| err = -error->code; |
| |
| DBG("Unable to load %s: %s", pathname, error->message); |
| g_clear_error(&error); |
| |
| g_key_file_free(*keyfile); |
| *keyfile = NULL; |
| |
| return err; |
| } |
| |
| static int load_policy(GKeyFile *keyfile, const char *groupname, |
| struct policy_group *group) |
| { |
| struct connman_session_config *config = group->config; |
| char *str, **tokens; |
| int i, err = 0; |
| |
| group->selinux = g_key_file_get_string(keyfile, groupname, |
| "selinux", NULL); |
| |
| group->gid = g_key_file_get_string(keyfile, groupname, |
| "gid", NULL); |
| |
| group->uid = g_key_file_get_string(keyfile, groupname, |
| "uid", NULL); |
| |
| if (!group->selinux && !group->gid && !group->uid) |
| return -EINVAL; |
| |
| config->priority = g_key_file_get_boolean(keyfile, groupname, |
| "Priority", NULL); |
| |
| str = g_key_file_get_string(keyfile, groupname, "RoamingPolicy", |
| NULL); |
| if (str) { |
| config->roaming_policy = connman_session_parse_roaming_policy(str); |
| g_free(str); |
| } |
| |
| str = g_key_file_get_string(keyfile, groupname, "ConnectionType", |
| NULL); |
| if (str) { |
| config->type = connman_session_parse_connection_type(str); |
| g_free(str); |
| } |
| |
| config->ecall = g_key_file_get_boolean(keyfile, groupname, |
| "EmergencyCall", NULL); |
| |
| str = g_key_file_get_string(keyfile, groupname, "AllowedBearers", |
| NULL); |
| if (str) { |
| tokens = g_strsplit(str, " ", 0); |
| |
| for (i = 0; tokens[i]; i++) { |
| err = connman_session_parse_bearers(tokens[i], |
| &config->allowed_bearers); |
| if (err < 0) |
| break; |
| } |
| |
| g_free(str); |
| g_strfreev(tokens); |
| } |
| |
| DBG("group %p selinux %s uid %s gid %s", group, group->selinux, |
| group->uid, group->gid); |
| |
| return err; |
| } |
| |
| static void update_session(struct policy_config *policy) |
| { |
| DBG("policy %p session %p", policy, policy->session); |
| |
| if (!policy->session) |
| return; |
| |
| if (connman_session_config_update(policy->session) < 0) |
| connman_session_destroy(policy->session); |
| } |
| |
| static void set_default_config(gpointer user_data) |
| { |
| struct policy_config *policy = user_data; |
| |
| connman_session_set_default_config(policy->config); |
| policy->group = NULL; |
| update_session(policy); |
| } |
| |
| static void cleanup_config(gpointer user_data) |
| { |
| struct policy_config *policy = user_data; |
| |
| DBG("policy %p group %p", policy, policy->group); |
| |
| if (policy->group) |
| policy->group->sessions = |
| g_slist_remove(policy->group->sessions, policy); |
| |
| g_slist_free(policy->config->allowed_bearers); |
| g_free(policy->config->id); |
| g_free(policy->config); |
| g_free(policy->selinux_context); |
| g_free(policy->selinux); |
| g_free(policy->uid); |
| g_slist_free_full(policy->gids, g_free); |
| g_free(policy); |
| } |
| |
| static void cleanup_group(gpointer user_data) |
| { |
| struct policy_group *group = user_data; |
| |
| DBG("group %p", group); |
| |
| g_slist_free_full(group->sessions, set_default_config); |
| |
| g_slist_free(group->config->allowed_bearers); |
| g_free(group->config->id); |
| g_free(group->config); |
| if (group->selinux) |
| g_hash_table_remove(selinux_hash, group->selinux); |
| if (group->uid) |
| g_hash_table_remove(uid_hash, group->uid); |
| if (group->gid) |
| g_hash_table_remove(gid_hash, group->gid); |
| g_free(group->selinux); |
| g_free(group->uid); |
| g_free(group->gid); |
| g_free(group); |
| } |
| |
| static void cleanup_file(gpointer user_data) |
| { |
| struct policy_file *file = user_data; |
| |
| DBG("file %p", file); |
| |
| g_slist_free_full(file->groups, cleanup_group); |
| g_free(file); |
| } |
| |
| static void recheck_sessions(void) |
| { |
| GHashTableIter iter; |
| gpointer value, key; |
| struct policy_group *group = NULL; |
| GSList *list; |
| |
| g_hash_table_iter_init(&iter, session_hash); |
| while (g_hash_table_iter_next(&iter, &key, &value)) { |
| struct policy_config *policy = value; |
| |
| if (policy->group) |
| continue; |
| |
| if (policy->selinux) |
| group = g_hash_table_lookup(selinux_hash, |
| policy->selinux); |
| if (group) { |
| policy->config->id_type = CONNMAN_SESSION_ID_TYPE_LSM; |
| g_free(policy->config->id); |
| policy->config->id = g_strdup(policy->selinux_context); |
| update_session(policy); |
| continue; |
| } |
| |
| group = g_hash_table_lookup(uid_hash, policy->uid); |
| if (group) { |
| set_policy(policy, group); |
| |
| policy->config->id_type = CONNMAN_SESSION_ID_TYPE_UID; |
| g_free(policy->config->id); |
| policy->config->id = g_strdup(policy->uid); |
| update_session(policy); |
| continue; |
| } |
| |
| for (list = policy->gids; list; list = list->next) { |
| char *gid = list->data; |
| group = g_hash_table_lookup(gid_hash, gid); |
| if (group) { |
| set_policy(policy, group); |
| |
| policy->config->id_type = CONNMAN_SESSION_ID_TYPE_GID; |
| g_free(policy->config->id); |
| policy->config->id = g_strdup(gid); |
| update_session(policy); |
| } |
| } |
| } |
| } |
| |
| static int load_file(const char *filename, struct policy_file *file) |
| { |
| GKeyFile *keyfile; |
| struct policy_group *group; |
| char **groupnames; |
| char *pathname; |
| int err = 0, i; |
| |
| DBG("%s", filename); |
| |
| pathname = g_strdup_printf("%s/%s", POLICYDIR, filename); |
| err = load_keyfile(pathname, &keyfile); |
| g_free(pathname); |
| |
| if (err < 0) |
| return err; |
| |
| groupnames = g_key_file_get_groups(keyfile, NULL); |
| |
| for (i = 0; groupnames[i]; i++) { |
| group = g_new0(struct policy_group, 1); |
| group->config = g_new0(struct connman_session_config, 1); |
| |
| err = load_policy(keyfile, groupnames[i], group); |
| if (err < 0) { |
| g_free(group->config); |
| g_free(group); |
| break; |
| } |
| if (group->selinux) |
| g_hash_table_replace(selinux_hash, group->selinux, group); |
| |
| if (group->uid) |
| g_hash_table_replace(uid_hash, group->uid, group); |
| |
| if (group->gid) |
| g_hash_table_replace(gid_hash, group->gid, group); |
| |
| file->groups = g_slist_prepend(file->groups, group); |
| } |
| |
| g_strfreev(groupnames); |
| |
| if (err < 0) |
| g_slist_free_full(file->groups, cleanup_group); |
| |
| g_key_file_free(keyfile); |
| |
| return err; |
| } |
| |
| static bool is_filename_valid(const char *filename) |
| { |
| if (!filename) |
| return false; |
| |
| if (filename[0] == '.') |
| return false; |
| |
| return g_str_has_suffix(filename, ".policy"); |
| } |
| |
| static int read_policies(void) |
| { |
| GDir *dir; |
| const gchar *filename; |
| struct policy_file *file; |
| |
| DBG(""); |
| |
| dir = g_dir_open(POLICYDIR, 0, NULL); |
| if (!dir) |
| return -EINVAL; |
| |
| while ((filename = g_dir_read_name(dir))) { |
| if (!is_filename_valid(filename)) |
| continue; |
| |
| file = g_new0(struct policy_file, 1); |
| if (load_file(filename, file) < 0) { |
| g_free(file); |
| continue; |
| } |
| |
| g_hash_table_replace(file_hash, g_strdup(filename), file); |
| } |
| |
| g_dir_close(dir); |
| |
| return 0; |
| } |
| |
| |
| static void notify_handler(struct inotify_event *event, |
| const char *filename) |
| { |
| struct policy_file *file; |
| |
| DBG("event %x file %s", event->mask, filename); |
| |
| if (event->mask & IN_CREATE) |
| return; |
| |
| if (!is_filename_valid(filename)) |
| return; |
| |
| /* |
| * load_file() will modify the global selinux/uid/gid hash |
| * tables. We need to remove the old entries first before |
| * else the table points to the wrong entries. |
| */ |
| g_hash_table_remove(file_hash, filename); |
| |
| if (event->mask & (IN_DELETE | IN_MOVED_FROM)) |
| return; |
| |
| if (event->mask & (IN_MOVED_TO | IN_MODIFY)) { |
| connman_info("Policy update for '%s'", filename); |
| |
| file = g_new0(struct policy_file, 1); |
| if (load_file(filename, file) < 0) { |
| g_free(file); |
| return; |
| } |
| |
| g_hash_table_replace(file_hash, g_strdup(filename), file); |
| recheck_sessions(); |
| } |
| } |
| |
| static int session_policy_local_init(void) |
| { |
| int err; |
| |
| DBG(""); |
| |
| /* If the dir doesn't exist, create it */ |
| if (!g_file_test(POLICYDIR, G_FILE_TEST_IS_DIR)) { |
| if (mkdir(POLICYDIR, MODE) < 0) { |
| if (errno != EEXIST) |
| return -errno; |
| } |
| } |
| |
| connection = connman_dbus_get_connection(); |
| if (!connection) |
| return -EIO; |
| |
| file_hash = g_hash_table_new_full(g_str_hash, g_str_equal, |
| g_free, cleanup_file); |
| session_hash = g_hash_table_new_full(g_direct_hash, g_direct_equal, |
| NULL, cleanup_config); |
| selinux_hash = g_hash_table_new_full(g_str_hash, g_str_equal, |
| NULL, NULL); |
| uid_hash = g_hash_table_new_full(g_str_hash, g_str_equal, |
| NULL, NULL); |
| gid_hash = g_hash_table_new_full(g_str_hash, g_str_equal, |
| NULL, NULL); |
| |
| err = connman_inotify_register(POLICYDIR, notify_handler); |
| if (err < 0) |
| goto err; |
| |
| err = connman_session_policy_register(&session_policy_local); |
| if (err < 0) |
| goto err_notify; |
| |
| read_policies(); |
| |
| return 0; |
| |
| err_notify: |
| |
| connman_inotify_unregister(POLICYDIR, notify_handler); |
| |
| err: |
| if (file_hash) |
| g_hash_table_destroy(file_hash); |
| |
| if (session_hash) |
| g_hash_table_destroy(session_hash); |
| |
| if (selinux_hash) |
| g_hash_table_destroy(selinux_hash); |
| |
| if (uid_hash) |
| g_hash_table_destroy(uid_hash); |
| |
| if (gid_hash) |
| g_hash_table_destroy(gid_hash); |
| |
| connman_session_policy_unregister(&session_policy_local); |
| |
| dbus_connection_unref(connection); |
| |
| return err; |
| } |
| |
| static void session_policy_local_exit(void) |
| { |
| DBG(""); |
| |
| g_hash_table_destroy(file_hash); |
| g_hash_table_destroy(session_hash); |
| g_hash_table_destroy(selinux_hash); |
| g_hash_table_destroy(uid_hash); |
| g_hash_table_destroy(gid_hash); |
| |
| connman_session_policy_unregister(&session_policy_local); |
| |
| dbus_connection_unref(connection); |
| |
| connman_inotify_unregister(POLICYDIR, notify_handler); |
| } |
| |
| CONNMAN_PLUGIN_DEFINE(session_policy_local, |
| "Session local file policy configuration plugin", |
| VERSION, CONNMAN_PLUGIN_PRIORITY_DEFAULT, |
| session_policy_local_init, session_policy_local_exit) |