From 330797e682d90776f5e8bb030533f94fe00b4245 Mon Sep 17 00:00:00 2001
From: Ryan Lortie <desrt@desrt.ca>
Date: Wed, 26 Jun 2013 12:39:05 -0400
Subject: [PATCH] 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.
---
 configure.ac        |   6 ++
 src/Makefile.am     |   2 +
 src/daemon.c        | 123 +-------------------------
 src/daemon.h        |   4 -
 src/user-classify.c | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/user-classify.h |  32 +++++++
 src/user.c          |   9 +-
 7 files changed, 293 insertions(+), 131 deletions(-)
 create mode 100644 src/user-classify.c
 create mode 100644 src/user-classify.h

Index: accountsservice-0.6.34/configure.ac
===================================================================
--- accountsservice-0.6.34.orig/configure.ac	2013-06-26 20:02:05.731804883 +0200
+++ accountsservice-0.6.34/configure.ac	2013-06-26 20:02:05.715804882 +0200
@@ -57,6 +57,12 @@
 ])
 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 ---------------------------------------------------------------------------
Index: accountsservice-0.6.34/src/Makefile.am
===================================================================
--- accountsservice-0.6.34.orig/src/Makefile.am	2013-06-26 20:02:05.731804883 +0200
+++ accountsservice-0.6.34/src/Makefile.am	2013-06-26 20:02:05.719804882 +0200
@@ -42,6 +42,8 @@
 	daemon.h		\
 	daemon.c		\
 	extensions.c		\
+	user-classify.h		\
+	user-classify.c		\
 	user.h			\
 	user.c			\
 	util.h			\
Index: accountsservice-0.6.34/src/daemon.c
===================================================================
--- accountsservice-0.6.34.orig/src/daemon.c	2013-06-26 20:02:05.731804883 +0200
+++ accountsservice-0.6.34/src/daemon.c	2013-06-26 20:02:05.719804882 +0200
@@ -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 @@
         GDBusProxy *bus_proxy;
 
         GHashTable *users;
-        GHashTable *exclusions;
 
         User *autologin;
 
@@ -169,78 +136,6 @@
   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 @@
                         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 @@
 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 @@
                 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;
                 }
Index: accountsservice-0.6.34/src/daemon.h
===================================================================
--- accountsservice-0.6.34.orig/src/daemon.h	2013-06-26 20:02:05.731804883 +0200
+++ accountsservice-0.6.34/src/daemon.h	2013-06-26 20:02:05.719804882 +0200
@@ -77,10 +77,6 @@
 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,
Index: accountsservice-0.6.34/src/user-classify.c
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ accountsservice-0.6.34/src/user-classify.c	2013-06-26 20:02:05.723804882 +0200
@@ -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
+}
Index: accountsservice-0.6.34/src/user-classify.h
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ accountsservice-0.6.34/src/user-classify.h	2013-06-26 20:02:05.723804882 +0200
@@ -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__ */
Index: accountsservice-0.6.34/src/user.c
===================================================================
--- accountsservice-0.6.34.orig/src/user.c	2013-06-26 20:02:05.731804883 +0200
+++ accountsservice-0.6.34/src/user.c	2013-06-26 20:02:05.727804883 +0200
@@ -45,6 +45,7 @@
 #define GCR_API_SUBJECT_TO_CHANGE
 #include <gcr/gcr-base.h>
 
+#include "user-classify.h"
 #include "daemon.h"
 #include "user.h"
 #include "accounts-user-generated.h"
@@ -314,13 +315,7 @@
                 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));
 
