diff options
author | Ryan Lortie <desrt@desrt.ca> | 2013-06-26 12:39:05 -0400 |
---|---|---|
committer | Ray Strode <rstrode@redhat.com> | 2013-10-01 11:17:43 -0400 |
commit | 0a7e6f638e643fca3e08c3cf47fbc7aa3c5c1582 (patch) | |
tree | 8a753b1836e60ca4225e22c0f30fc9cfc56af5bc | |
parent | 499bfeb4bce5841dc67118737d78eb8aead14ef0 (diff) |
Clean up user classification logic
Bring back the simple login.defs-based check for if a user is human or
not and enable it by default.
Add a build option --enable-user-heuristics to get the old behaviour
back again.
Split out all human vs. system user divination into a new file,
user-classify.c in order to clean up daemon.c a bit.
-rw-r--r-- | configure.ac | 6 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/daemon.c | 123 | ||||
-rw-r--r-- | src/daemon.h | 4 | ||||
-rw-r--r-- | src/user-classify.c | 248 | ||||
-rw-r--r-- | src/user-classify.h | 32 | ||||
-rw-r--r-- | src/user.c | 9 |
7 files changed, 293 insertions, 131 deletions
diff --git a/configure.ac b/configure.ac index d555bef..47e9c67 100644 --- a/configure.ac +++ b/configure.ac @@ -54,6 +54,12 @@ AS_IF([test x$enable_admin_group = xauto], [ ]) AC_DEFINE_UNQUOTED([ADMIN_GROUP], ["$enable_admin_group"], [Define to the group for administrator users]) +AC_ARG_ENABLE(user-heuristics, + [AS_HELP_STRING([--enable-user-heuristics],[Enable heuristics for guessing system vs. human users])], + [if test "$enableval" = yes; then + AC_DEFINE([ENABLE_USER_HEURISTICS], , [System vs. human user heuristics enabled]) + fi]) + dnl --------------------------------------------------------------------------- dnl - Warnings dnl --------------------------------------------------------------------------- diff --git a/src/Makefile.am b/src/Makefile.am index 13c57ca..a5bfcf9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -35,6 +35,8 @@ accounts_daemon_SOURCES = \ daemon.h \ daemon.c \ extensions.c \ + user-classify.h \ + user-classify.c \ user.h \ user.c \ util.h \ diff --git a/src/daemon.c b/src/daemon.c index cd09e2c..eb75ec9 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -43,49 +43,17 @@ #include <gio/gio.h> #include <polkit/polkit.h> +#include "user-classify.h" #include "daemon.h" #include "util.h" #define PATH_PASSWD "/etc/passwd" #define PATH_SHADOW "/etc/shadow" -#define PATH_NOLOGIN "/sbin/nologin" -#define PATH_FALSE "/bin/false" #define PATH_GDM_CUSTOM "/etc/gdm/custom.conf" #ifdef HAVE_UTMPX_H #define PATH_WTMP _PATH_WTMPX #endif -static const char *default_excludes[] = { - "bin", - "root", - "daemon", - "adm", - "lp", - "sync", - "shutdown", - "halt", - "mail", - "news", - "uucp", - "operator", - "nobody", - "nobody4", - "noaccess", - "postgres", - "pvm", - "rpm", - "nfsnobody", - "pcap", - "mysql", - "ftp", - "games", - "man", - "at", - "gdm", - "gnome-initial-setup", - NULL -}; - enum { PROP_0, PROP_DAEMON_VERSION @@ -96,7 +64,6 @@ struct DaemonPrivate { GDBusProxy *bus_proxy; GHashTable *users; - GHashTable *exclusions; User *autologin; @@ -167,78 +134,6 @@ error_get_type (void) return etype; } -gboolean -daemon_local_user_is_excluded (Daemon *daemon, - const gchar *username, - const gchar *shell, - const gchar *password_hash) -{ - int ret; - - if (g_hash_table_lookup (daemon->priv->exclusions, username)) { - return TRUE; - } - - ret = FALSE; - - if (shell != NULL) { - char *basename, *nologin_basename, *false_basename; - -#ifdef HAVE_GETUSERSHELL - char *valid_shell; - - ret = TRUE; - setusershell (); - while ((valid_shell = getusershell ()) != NULL) { - if (g_strcmp0 (shell, valid_shell) != 0) - continue; - ret = FALSE; - } - endusershell (); -#endif - - basename = g_path_get_basename (shell); - nologin_basename = g_path_get_basename (PATH_NOLOGIN); - false_basename = g_path_get_basename (PATH_FALSE); - - if (shell[0] == '\0') { - ret = TRUE; - } else if (g_strcmp0 (basename, nologin_basename) == 0) { - ret = TRUE; - } else if (g_strcmp0 (basename, false_basename) == 0) { - ret = TRUE; - } - - g_free (basename); - g_free (nologin_basename); - g_free (false_basename); - } - - if (password_hash != NULL) { - /* skip over the account-is-locked '!' prefix if present */ - if (password_hash[0] == '!') - password_hash++; - - if (password_hash[0] != '\0') { - /* modern hashes start with "$n$" */ - if (password_hash[0] == '$') { - if (strlen (password_hash) < 4) - ret = TRUE; - - /* DES crypt is base64 encoded [./A-Za-z0-9]* - */ - } else if (!g_ascii_isalnum (password_hash[0]) && - password_hash[0] != '.' && - password_hash[0] != '/') { - ret = TRUE; - } - } - - } - - return ret; -} - #ifdef HAVE_UTMPX_H typedef struct { @@ -520,7 +415,7 @@ load_entries (Daemon *daemon, break; /* Skip system users... */ - if (daemon_local_user_is_excluded (daemon, pwent->pw_name, pwent->pw_shell, NULL)) { + if (!user_classify_is_human (pwent->pw_uid, pwent->pw_name, pwent->pw_shell, NULL)) { g_debug ("skipping user: %s", pwent->pw_name); continue; } @@ -750,25 +645,13 @@ on_gdm_monitor_changed (GFileMonitor *monitor, static void daemon_init (Daemon *daemon) { - gint i; GFile *file; GError *error; daemon->priv = DAEMON_GET_PRIVATE (daemon); - daemon->priv->exclusions = g_hash_table_new_full (g_str_hash, - g_str_equal, - g_free, - NULL); - daemon->priv->extension_ifaces = daemon_read_extension_ifaces (); - for (i = 0; default_excludes[i] != NULL; i++) { - g_hash_table_insert (daemon->priv->exclusions, - g_strdup (default_excludes[i]), - GUINT_TO_POINTER (TRUE)); - } - daemon->priv->users = create_users_hash_table (); file = g_file_new_for_path (PATH_PASSWD); @@ -1092,7 +975,7 @@ finish_list_cached_users (gpointer user_data) uid = user_get_uid (user); shell = user_get_shell (user); - if (daemon_local_user_is_excluded (data->daemon, name, shell, NULL)) { + if (!user_classify_is_human (uid, name, shell, NULL)) { g_debug ("user %s %ld excluded", name, (long) uid); continue; } diff --git a/src/daemon.h b/src/daemon.h index 6792f50..b7e072e 100644 --- a/src/daemon.h +++ b/src/daemon.h @@ -76,10 +76,6 @@ User *daemon_local_find_user_by_id (Daemon *daemon, User *daemon_local_find_user_by_name (Daemon *daemon, const gchar *name); User *daemon_local_get_automatic_login_user (Daemon *daemon); -gboolean daemon_local_user_is_excluded (Daemon *daemon, - const gchar *name, - const gchar *shell, - const gchar *password_hash); typedef void (*AuthorizedCallback) (Daemon *daemon, User *user, diff --git a/src/user-classify.c b/src/user-classify.c new file mode 100644 index 0000000..b68c9ae --- /dev/null +++ b/src/user-classify.c @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2009-2010 Red Hat, Inc. + * Copyright (C) 2013 Canonical Limited + * + * 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 3 of the licence, 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. + * + * 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 + * + * Authors: Ryan Lortie <desrt@desrt.ca> + * Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include "user-classify.h" + +#include <string.h> + +#ifdef ENABLE_USER_HEURISTICS +static const char *default_excludes[] = { + "bin", + "root", + "daemon", + "adm", + "lp", + "sync", + "shutdown", + "halt", + "mail", + "news", + "uucp", + "operator", + "nobody", + "nobody4", + "noaccess", + "postgres", + "pvm", + "rpm", + "nfsnobody", + "pcap", + "mysql", + "ftp", + "games", + "man", + "at", + "gdm", + "gnome-initial-setup" +}; + +#define PATH_NOLOGIN "/sbin/nologin" +#define PATH_FALSE "/bin/false" + +static gboolean +user_classify_is_excluded_by_heuristics (const gchar *username, + const gchar *shell, + const gchar *password_hash) +{ + static GHashTable *exclusions; + gboolean ret = FALSE; + + if (exclusions == NULL) { + guint i; + + exclusions = g_hash_table_new (g_str_hash, g_str_equal); + + for (i = 0; i < G_N_ELEMENTS (default_excludes); i++) { + g_hash_table_add (exclusions, (gpointer) default_excludes[i]); + } + } + + if (g_hash_table_contains (exclusions, username)) { + return TRUE; + } + + if (shell != NULL) { + char *basename, *nologin_basename, *false_basename; + +#ifdef HAVE_GETUSERSHELL + char *valid_shell; + + ret = TRUE; + setusershell (); + while ((valid_shell = getusershell ()) != NULL) { + if (g_strcmp0 (shell, valid_shell) != 0) + continue; + ret = FALSE; + } + endusershell (); +#endif + + basename = g_path_get_basename (shell); + nologin_basename = g_path_get_basename (PATH_NOLOGIN); + false_basename = g_path_get_basename (PATH_FALSE); + + if (shell[0] == '\0') { + ret = TRUE; + } else if (g_strcmp0 (basename, nologin_basename) == 0) { + ret = TRUE; + } else if (g_strcmp0 (basename, false_basename) == 0) { + ret = TRUE; + } + + g_free (basename); + g_free (nologin_basename); + g_free (false_basename); + } + + if (password_hash != NULL) { + /* skip over the account-is-locked '!' prefix if present */ + if (password_hash[0] == '!') + password_hash++; + + if (password_hash[0] != '\0') { + /* modern hashes start with "$n$" */ + if (password_hash[0] == '$') { + if (strlen (password_hash) < 4) + ret = TRUE; + + /* DES crypt is base64 encoded [./A-Za-z0-9]* + */ + } else if (!g_ascii_isalnum (password_hash[0]) && + password_hash[0] != '.' && + password_hash[0] != '/') { + ret = TRUE; + } + } + + } + + return ret; +} + +#else /* ENABLE_USER_HEURISTICS */ + +static gboolean +user_classify_parse_login_defs_field (const gchar *contents, + const gchar *key, + uid_t *result) +{ + gsize key_len; + gint64 value; + gchar *end; + + key_len = strlen (key); + + for (;;) { + /* Our key has to be at the start of the line, followed by whitespace */ + if (strncmp (contents, key, key_len) == 0 && g_ascii_isspace (contents[key_len])) { + /* Found it. Move contents past the key itself and break out. */ + contents += key_len; + break; + } + + /* Didn't find it. Find the end of the line. */ + contents = strchr (contents, '\n'); + + /* EOF? */ + if (!contents) { + /* We didn't find the field... */ + return FALSE; + } + + /* Start at the beginning of the next line on next iteration. */ + contents++; + } + + /* 'contents' now points at the whitespace character just after + * the field name. strtoll can deal with that. + */ + value = g_ascii_strtoll (contents, &end, 10); + + if (*end && !g_ascii_isspace (*end)) { + g_warning ("Trailing junk after '%s' field in login.defs", key); + return FALSE; + } + + if (value <= 0 || value >= G_MAXINT32) { + g_warning ("Value for '%s' field out of range", key); + return FALSE; + } + + *result = value; + + return TRUE; +} + +static void +user_classify_read_login_defs (uid_t *min_uid, + uid_t *max_uid) +{ + GError *error = NULL; + char *contents; + + if (!g_file_get_contents ("/etc/login.defs", &contents, NULL, &error)) { + g_warning ("Could not open /etc/login.defs: %s. Falling back to default human uid range of %d to %d", + error->message, (int) *min_uid, (int) *max_uid); + g_error_free (error); + return; + } + + if (!user_classify_parse_login_defs_field (contents, "UID_MIN", min_uid)) { + g_warning ("Could not find UID_MIN value in login.defs. Using default of %d", (int) *min_uid); + } + + if (!user_classify_parse_login_defs_field (contents, "UID_MAX", max_uid)) { + g_warning ("Could not find UID_MIN value in login.defs. Using default of %d", (int) *max_uid); + } + + g_free (contents); +} + +static gboolean +user_classify_is_in_human_range (uid_t uid) +{ + static uid_t min_uid = 1000, max_uid = 60000; + static gboolean initialised; + + if (!initialised) { + user_classify_read_login_defs (&min_uid, &max_uid); + initialised = TRUE; + } + + return min_uid <= uid && uid <= max_uid; +} +#endif /* ENABLE_USER_HEURISTICS */ + +gboolean +user_classify_is_human (uid_t uid, + const gchar *username, + const gchar *shell, + const gchar *password_hash) +{ +#ifdef ENABLE_USER_HEURISTICS + return !user_classify_is_excluded_by_heuristics (username, shell, password_hash); +#else + return user_classify_is_in_human_range (uid); +#endif +} diff --git a/src/user-classify.h b/src/user-classify.h new file mode 100644 index 0000000..3e3a391 --- /dev/null +++ b/src/user-classify.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 Canonical Limited + * + * 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 3 of the licence, 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. + * + * 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 + * + * Author: Ryan Lortie <desrt@desrt.ca> + */ + +#ifndef __USER_CLASSIFY_H__ +#define __USER_CLASSIFY_H__ + +#include <sys/types.h> +#include <glib.h> + +gboolean user_classify_is_human (uid_t uid, + const gchar *username, + const gchar *shell, + const gchar *password_hash); + +#endif /* __USER_CLASSIFY_H__ */ @@ -42,6 +42,7 @@ #include <gio/gunixinputstream.h> #include <polkit/polkit.h> +#include "user-classify.h" #include "daemon.h" #include "user.h" #include "accounts-user-generated.h" @@ -291,13 +292,7 @@ user_update_from_pwent (User *user, g_object_notify (G_OBJECT (user), "password-mode"); } - /* FIXME: this relies on heuristics that don't always come out - * right. - */ - user->system_account = daemon_local_user_is_excluded (user->daemon, - user->user_name, - pwent->pw_shell, - passwd); + user->system_account = !user_classify_is_human (user->uid, user->user_name, pwent->pw_shell, passwd); g_object_thaw_notify (G_OBJECT (user)); |