]> scripts.mit.edu Git - wizard.git/commitdiff
Implement all user information hooks.
authorEdward Z. Yang <ezyang@mit.edu>
Mon, 12 Jul 2010 04:16:27 +0000 (21:16 -0700)
committerEdward Z. Yang <ezyang@mit.edu>
Mon, 12 Jul 2010 04:16:36 +0000 (21:16 -0700)
Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
19 files changed:
README
SCRIPTS
TODO
doc/module/wizard.plugin.rst [new file with mode: 0644]
doc/module/wizard.user.rst
doc/module/wizard.util.rst
doc/plugin.rst
plugins/scripts/setup.py
plugins/scripts/wizard_scripts.py
tests/setup
wizard/command/__init__.py
wizard/command/migrate.py
wizard/command/quota.py
wizard/command/upgrade.py
wizard/git.py
wizard/plugin.py [new file with mode: 0644]
wizard/sql.py
wizard/user.py
wizard/util.py

diff --git a/README b/README
index 4a8a4addcbf41d6f8bee2c999c4d1be4b34b96c9..5d0b88d39507c0d7aca33e20eaacc6d7e815eb14 100644 (file)
--- a/README
+++ b/README
@@ -38,7 +38,7 @@ all error classes at the bottom.
 Notes for repository creation
 -----------------------------
 
-See TODO for full instructions.
+See documentation for full instructions.
 
 - You are not going to be able to doctor a perfect repository
   from scratch.
diff --git a/SCRIPTS b/SCRIPTS
index 8c337e87513e9fbb46f4bda9f8ae15be84f6edbc..31af77abe9b5dc9b61145567deea4f5cdfd005e8 100644 (file)
--- a/SCRIPTS
+++ b/SCRIPTS
@@ -14,22 +14,6 @@ be moved away:
       should instantiate a .wizard directory automatically (which should
       be ignored) that doesn't need to be created in repositories.
 
-    * wizard.scripts is the location that all Scripts specific code
-      will eventually live.
-
-    * [HOOK] wizard.sql does a Script's specific call to get SQL
-      information if no SQL credentials are explicitly passed.
-
-    * In wizard.util
-        - [HOOK] get_dir_owner performs a PTS query if normal pwd
-          querying fails
-        - [HOOK] get_operator_name_from_gssapi requires a patch to SSH
-          that we patched support for
-        - [HOOK] get_operator_info consults Hesiod for more information
-          based on a username.
-        - [HOOK] set_author_env guesses emails using scripts.mit.edu
-          addresses
-
     * [HOOK] wizard.install contains strategies for guessing variables
       for an installation that are Scripts specific
 
diff --git a/TODO b/TODO
index 494c8c368b552f33a345d66456f91b9fded0ea90..bf13a609e926aaaf7b36f2d0cc250b28f42b3b5a 100644 (file)
--- a/TODO
+++ b/TODO
@@ -11,6 +11,8 @@
   to the installation page.
 - The need to run egg_info can make things a little fragile when pulling
   if you forget.  How can we make this less easily forgotten?
+- wizardResolve* files seem to get left in tmp en-mass, and we don't
+  know why.
 
 - Plugin-ify! Hook-ify! In more detail, applications should all be moved
   out into plugins, scripts specific behavior should be moved into
@@ -73,6 +75,7 @@
       to A question is whether or not sending mail actually helps us:
       many users will probably have to come back to us for help; many
       other users won't care.
+    - Whatever happens here should be used to improve user.email()
 
 [ XXX: metadata.rst ]
 
diff --git a/doc/module/wizard.plugin.rst b/doc/module/wizard.plugin.rst
new file mode 100644 (file)
index 0000000..9caff40
--- /dev/null
@@ -0,0 +1,9 @@
+:mod:`wizard.plugin`
+====================
+
+.. automodule:: wizard.plugin
+
+Functions
+---------
+
+.. autofunction:: hook
index 6aa235bc9da4526658081d91b78256bdc0504ff2..d6bfc6c03477452215ead80fa620862d1521448b 100644 (file)
@@ -7,3 +7,12 @@ Functions
 ---------
 
 .. autofunction:: quota
+.. autofunction:: passwd
+.. autofunction:: operator
+.. autofunction:: pwnam
+.. autofunction:: email
+
+Classes
+-------
+
+.. autoclass:: Info
index d8aa162d3ab40cd1546ba5174d5246a1046d71b1..bfe860eefaf4428b0ae80be01ad2170a478374df 100644 (file)
@@ -20,10 +20,7 @@ Functions
 .. autofunction:: chdir
 .. autofunction:: get_exception_name
 .. autofunction:: get_dir_uid
-.. autofunction:: get_dir_owner
 .. autofunction:: get_revision
-.. autofunction:: get_operator_info
-.. autofunction:: get_operator_name_from_gssapi
 .. autofunction:: get_operator_git
 .. autofunction:: set_operator_env
 .. autofunction:: set_author_env
index fb6e418bc1056711ca129a3848bd6c9f34e9a5e8..3b4ba2369c0d5b026d2e42de1005380347ee007b 100644 (file)
@@ -17,6 +17,8 @@ Wizard will automatically use any plugins that it sees registered, so
 plugin authors should take care to "do no harm" if a plugin is unable to
 do anything useful.
 
+
+
 Registering plugins without eggs
 --------------------------------
 
@@ -26,6 +28,8 @@ setuptools (not distutils) ``setup.py`` file that sets ``entry_points``
 to be the plugins that are to be registered, and then run ``python
 setup.py egg_info``.
 
+
+
 ``wizard.app``
 --------------
 
@@ -41,9 +45,7 @@ represent this application (e.g. ``wizard.app.wordpress:Application``)::
 For more information on how to create an application, check
 :doc:`Creating a Repository <create>`.
 
-.. todo::
 
-    Plugins marked with prototype todo messages are not complete yet.
 
 .. _wizard.strategy:
 
@@ -53,6 +55,8 @@ For more information on how to create an application, check
 Used during installation to automatically determine values for
 installation parameters the user may have omitted.
 
+
+
 .. _wizard.sql.auth:
 
 ``wizard.sql.auth``
@@ -68,6 +72,13 @@ user, or the completed URL.  Mutating the passed argument and then
 returning it is the expected modus operandi of a plugin.  The function
 that runs this plugin is :func:`wizard.sql.auth`.
 
+.. note::
+
+    If Wizard is able to determine the login credentials from the
+    application's source files, these plugins will not be run.
+
+
+
 .. _wizard.deploy.web:
 
 ``wizard.deploy.web``
@@ -81,6 +92,13 @@ determint the web URL(s) for, and return a list or generator of
 for an application.  The function that runs this plugin is
 :func:`wizard.deploy.web`.
 
+.. note::
+
+    If Wizard is able to determine the web URL from the application's
+    source files or database, these plugins will not run.
+
+
+
 .. _wizard.user.email:
 
 ``wizard.user.email``
@@ -88,9 +106,18 @@ for an application.  The function that runs this plugin is
 
 Used to determine a user's email address.  By default, we use a
 heuristic approach;  if your system offers this information in a
-canonical form we recommend taking advantage of it.
+canonical form we recommend taking advantage of it.  Plugin should be a
+function that takes a single required argument ``name`` and returns a
+string email address corresponding to that user, or ``None`` if it
+was unable to determine an email address.  The function that runs this
+plugin is :func:`wizard.user.email`.
+
+.. note::
+
+    If the :envvar:`EMAIL` environment variable is set and we are
+    requesting the current user's email, these plugins will not run.
+
 
-.. todo:: Prototype
 
 .. _wizard.user.operator:
 
@@ -98,11 +125,11 @@ canonical form we recommend taking advantage of it.
 ------------------------
 
 If Wizard is running as root or as a superuser masquerading as another
-user, it is still useful to record who was in front of the screen.  By
-default, we check :envvar:`SUDO_USER`; if your system gives more
+user, it is still useful to record who was in front of the screen.  The default
+fallback is checking :envvar:`SUDO_USER`; if your system gives more
 information we recommend taking advantage of it.
 
-.. todo:: Prototype
+
 
 .. _wizard.user.passwd:
 
@@ -112,21 +139,25 @@ information we recommend taking advantage of it.
 If Wizard is running on a system with a network filesystem (such as NFS
 or AFS), the standard system ``passwd`` database may not actually
 resolve the username from a UID.  Use this hook to implement an
-alternative ``passwd`` lookup.
+alternative ``passwd`` lookup.  Plugin should be a function that takes a
+directory (to determine filesystem off of) and a user ID (to perform the
+lookup on).  The directory is guaranteed to be a real path.  The plugin
+should return an instance of :class:`wizard.user.Info` or ``None``, if
+it was unable to perform the lookup.  The function that runs this plugin
+is :func:`wizard.user.passwd`.
+
 
-.. todo:: Prototype
 
 .. _wizard.user.quota:
 
 ``wizard.user.quota``
 ---------------------
 
-Plugin for :func:`wizard.user.quota`.  Wizard has safeguards for
-avoiding exceeding user quotas when an automated upgrade is being
-performed.  Unfortunately, methods for enforcing quotas are frequently
-highly system dependent.  Use this hook to implement quota usage and
-limit reporting.  Plugin should be a function that takes a single
-required argument ``dir``, which is the directory to determine the quota
-for, and return a tuple ``(quota usage, quota limit)`` or ``(0, None)``
-if it could not determine quota.  The function that runs this plugin is
-:func:`wizard.user.quota`.
+Wizard has safeguards for avoiding exceeding user quotas when an
+automated upgrade is being performed.  Unfortunately, methods for
+enforcing quotas are frequently highly system dependent.  Use this hook
+to implement quota usage and limit reporting.  Plugin should be a
+function that takes a single required argument ``dir``, which is the
+directory to determine the quota for, and return a tuple ``(quota usage,
+quota limit)`` or ``None`` if it could not determine quota.  The
+function that runs this plugin is :func:`wizard.user.quota`.
index e9a07d3265e9a6be63946f3dcca594bb9d285fb4..d190dfb69eb259eeff5815b6b0c991cb94c64301 100644 (file)
@@ -11,6 +11,9 @@ setuptools.setup(
     packages = setuptools.find_packages(),
     entry_points = {
         'wizard.user.quota': 'scripts = wizard_scripts:user_quota',
+        'wizard.user.email': 'scripts = wizard_scripts:user_email',
+        'wizard.user.operator': 'scripts = wizard_scripts:user_operator',
+        'wizard.user.passwd': 'scripts = wizard_scripts:user_passwd',
         'wizard.deploy.web': 'scripts = wizard_scripts:deploy_web',
         'wizard.sql.auth': 'scripts = wizard_scripts:sql_auth',
     }
index a44af215138e8f92f3d24f3e3a1789725031557f..23fb1eea4e7a32c33f0e55511b3713bb02ffc218 100644 (file)
@@ -11,7 +11,7 @@ import time
 import errno
 
 import wizard
-from wizard import shell, util
+from wizard import shell, util, user
 
 def deploy_web(dir):
     # try the directory
@@ -110,3 +110,42 @@ def sql_auth(url):
         except shell.CallError:
             pass
     return None
+
+def user_email(name):
+    # XXX: simplistic strategy which doesn't work most of the time
+    return "%s@scripts.mit.edu" % name
+
+def user_operator():
+    """
+    Returns username of the person operating this script based
+    off of the :envvar:`SSH_GSSAPI_NAME` environment variable.
+
+    .. note::
+
+        :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH
+        distributions.  Scripts servers are patched to support this
+        environment variable.
+    """
+    principal = os.getenv("SSH_GSSAPI_NAME")
+    if not principal:
+        return None
+    instance, _, _ = principal.partition("@")
+    if instance.endswith("/root"):
+        username, _, _ = principal.partition("/")
+    else:
+        username = instance
+    return username
+
+def user_passwd(dir, uid):
+    # XXX: simplistic heuristic for detecting AFS.  The correct thing to
+    # is either to statfs and match magic number, use one of the
+    # vos tools or check mounted directories.
+    if not dir.startswith("/afs/"):
+        return None
+    try:
+        result = shell.eval("hesinfo %d uid", uid)
+    except shell.CallError:
+        return None
+    name, password, uid, gid, gecos, homedir, shell = result.split(":")
+    realname = gecos.split(",")[0]
+    return user.Info(name, uid, gid, realname, homedir, shell)
index 85da3838b408139c4f959f0d9f0283ce5ed7a731..c8c21ef707178a45f2f3ea4d4ebd4e9209751b4d 100644 (file)
@@ -21,7 +21,7 @@ fi
 # APPVERSION is directly interpolated into bash, so it can represent
 # 0 arguments.
 if [ "$VERSION" == "head" ]; then
-    APPVERSION=""
+    APPVERSION="$APP"
 else
     APPVERSION="$APP-$VERSION-scripts" # XXX incorrect if a -scripts2 version exists
 fi
index 5d77be38427ebce61fe24883f367fef24b240fb0..85777767ddb8ae6a8eb02d0972a610b83d3be7e4 100644 (file)
@@ -89,6 +89,8 @@ def security_check_homedir(location):
     This protects against malicious mountpoints, and is roughly equivalent
     to the suexec checks.
     """
+    # XXX: this is a smidge unfriendly to systems who haven't setup
+    # nswitch.
     try:
         uid = util.get_dir_uid(location)
         real = os.path.realpath(location)
index 829261f0866c8d7a87fde8f7f0b4ae8887c45eaf..ebc39cf44a0a8ddab8cd4ec6550268be6256a67d 100644 (file)
@@ -131,7 +131,7 @@ def make_repository(sh, options, repo, tag):
     sh.call("git", "checkout", ".scripts")
     logging.info("Diffstat:\n" + sh.eval("git", "diff", "--stat"))
     # commit user local changes
-    message = "Autoinstall migration of %s locker.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
+    message = "Autoinstall migration.\n\n%s" % util.get_git_footer()
     util.set_git_env()
     try:
         message += "\nMigrated-by: " + util.get_operator_git()
index 11fb04dd57ac207fab78eee0632c39afbf85b98d..3e1454141d67092f1690d58e1efe5de7d87bbdce 100644 (file)
@@ -8,7 +8,7 @@ def main(argv, baton):
     options, args = parse_args(argv, baton)
     dir = os.path.abspath(args[0]) if args else os.getcwd()
     r = user.quota(dir)
-    if r[0] is None or r[1] is None:
+    if r is None:
         sys.exit(1)
     print "%d %d" % r
 
@@ -23,7 +23,7 @@ Output format is:
 
     USED AVAIL
 
-In bytes.  Returns an exit code of 1 if quota could
+in bytes.  Returns an exit code of 1 if quota could
 not be determined."""
     parser = command.WizardOptionParser(usage)
     options, args = parser.parse_all(argv)
index 67f4e9b87563fea25187969acdcf7f8fb62a0055..3eb19c565aa4a0ad80f2be749c6b73f84b855056 100644 (file)
@@ -210,10 +210,12 @@ class Upgrade(object):
             sys.stderr.write("Traceback:\n  (n/a)\nAlreadyUpgraded\n")
             sys.exit(2)
     def preflightQuota(self):
-        usage, limit = user.quota()
-        if limit is not None and (limit - usage) < buffer:
-            logging.info("preflightQuota: limit = %d, usage = %d, buffer = %d", limit, usage, buffer)
-            raise QuotaTooLow
+        r = user.quota()
+        if r is not None:
+            usage, limit = r
+            if limit is not None and (limit - usage) < buffer:
+                logging.info("preflightQuota: limit = %d, usage = %d, buffer = %d", limit, usage, buffer)
+                raise QuotaTooLow
 
     def merge(self):
         if not self.options.dry_run:
@@ -239,7 +241,7 @@ class Upgrade(object):
         for f in added_files:
             if os.path.lexists(f): # broken symbolic links count too!
                 shell.call("git", "add", f)
-        message = "Pre-commit of %s locker before autoinstall upgrade.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
+        message = "Pre-commit before autoinstall upgrade.\n\n%s" % util.get_git_footer()
         try:
             message += "\nPre-commit-by: " + util.get_operator_git()
         except util.NoOperatorInfo:
@@ -373,7 +375,7 @@ class Upgrade(object):
             self.wc.invalidateCache()
             self.wc.verifyVersion()
     def postflightCommitMessage(self):
-        message = "Upgraded autoinstall in %s to %s.\n\n%s" % (util.get_dir_owner(), self.version, util.get_git_footer())
+        message = "Upgraded autoinstall to %s.\n\n%s" % (self.version, util.get_git_footer())
         try:
             message += "\nUpgraded-by: " + util.get_operator_git()
         except util.NoOperatorInfo:
@@ -386,10 +388,12 @@ class Upgrade(object):
         pre_size = int(open(os.path.join(self.temp_wc_dir, ".git/WIZARD_SIZE"), "r").read())
         post_size = util.disk_usage(self.temp_wc_dir)
         backup = self.prod.backup(self.options)
-        usage, limit = user.quota()
-        if limit is not None and (limit - usage) - (post_size - pre_size) < buffer:
-            shutil.rmtree(os.path.join(".scripts/backups", shell.eval("wizard", "restore").splitlines()[0]))
-            raise QuotaTooLow
+        r = user.quota()
+        if r is not None:
+            usage, limit = r
+            if limit is not None and (limit - usage) - (post_size - pre_size) < buffer:
+                shutil.rmtree(os.path.join(".scripts/backups", shell.eval("wizard", "restore").splitlines()[0]))
+                raise QuotaTooLow
         return backup
 
     def upgrade(self, backup):
index d0c50c4cb661fa056abf64141fea6c0ffd9b403f..965327b4caa59b4c33310b60ee5dac38260aebd2 100644 (file)
@@ -13,7 +13,7 @@ def commit_configure():
     Performs a commit of changes performed during configuration of an install
     with an appropriate logfile message.
     """
-    message = "Autoinstall configuration of %s locker.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
+    message = "Autoinstall configuration.\n\n%s" % util.get_git_footer()
     util.set_git_env()
     try:
         message += "\nConfigured-by: " + util.get_operator_git()
diff --git a/wizard/plugin.py b/wizard/plugin.py
new file mode 100644 (file)
index 0000000..c23c4a2
--- /dev/null
@@ -0,0 +1,19 @@
+"""
+Convenience methods for managing plugins.
+"""
+
+import pkg_resources
+
+def hook(name, args):
+    """
+    Runs plugins named ``name`` for this function.  Returns ``None`` if
+    all plugins return ``None``, otherwise returns the result of the
+    first plugin to result that is not ``None``. Assumes that plugins
+    are simple functions that take the arguments ``args``.
+    """
+    for entry in pkg_resources.iter_entry_points(name):
+        func = entry.load()
+        r = func(*args)
+        if r is not None:
+            return r
+    return None
index 548d8a6d399c5b4cd897eb1337e7d732bb970a5f..1c00e3dd615f7f9b2928ed0d689641853dd42aae 100644 (file)
@@ -36,7 +36,6 @@ def auth(url):
     for entry in pkg_resources.iter_entry_points("wizard.sql.auth"):
         func = entry.load()
         r = func(copy.copy(url))
-        print r
         if r is not None:
             return r
     env_dsn = os.getenv("WIZARD_DSN")
index 50fc7272cfe3473a867a5ec3d8edaaf669015d0f..4921bf9a411f5659039c54b6ddee74625847422c 100644 (file)
@@ -1,15 +1,20 @@
 """
-Module for querying information about users.  This mostly asks plugins for
-the extra information, and falls back to using a default that should work
-on most systems (but by no means all systems.)
+Module for querying information about users.  This mostly asks plugins
+for the extra information, and falls back to using a default that should
+work on most systems (but by no means all systems.)
 """
 
 import pkg_resources
 import os
+import socket
+import logging
+import pwd
+
+from wizard import plugin
 
 def quota(dir=None):
     """
-    Returns a tuple (quota usage, quota limit).  Returns ``(0, None)`` if
+    Returns a tuple (quota usage, quota limit).  Returns ``None`` if
     the quota usage is unknown.  If ``dir`` is omitted, the current
     working directory is assumed.  Value returned is in bytes.
 
@@ -18,10 +23,133 @@ def quota(dir=None):
     """
     if dir is None:
         dir = os.getcwd()
-    unknown = (0, None)
-    for entry in pkg_resources.iter_entry_points("wizard.user.quota"):
-        func = entry.load()
-        r = func(dir)
-        if r != unknown:
-            return r
-    return unknown
+    return plugin.hook("wizard.user.quota", [dir])
+
+def email(name=None):
+    """
+    Converts a username into an email address to that user.  If you have
+    a UID, you will have to convert it into a username first.  If no
+    canonical source of information is found, an heuristic approach
+    will be used.  If ``name`` is ``None``, the current user will be
+    used unless it is root, in which case :func:`operator` is tried
+    first to determine the real current user.
+
+    This function implements a plugin interface named
+    :ref:`wizard.user.email`.
+    """
+    if name is None:
+        logging.info("wizard.user.email: Determining email for current user")
+        env_email = os.getenv("EMAIL")
+        if env_email is not None:
+            logging.info("wizard.user.email: Used environment email %s", env_email)
+            return env_email
+        name = operator()
+    # run plugins
+    r = plugin.hook("wizard.user.email", [name])
+    if r is not None:
+        return r
+    # guess an email
+    try:
+        mailname = open("/etc/mailname").read()
+    except OSError:
+        mailname = socket.getfqdn()
+    return name + "@" + mailname
+
+def operator():
+    """
+    Determines the username of the true person who is running this
+    program.  If the process's real uid is nonzero, just do a passwd
+    lookup; otherwise attempt to figure out the user behind the root
+    prompt some other way.
+
+    This function implements a plugin interface named
+    :ref:`wizard.user.operator`.
+    """
+    uid = os.getuid()
+    if uid:
+        pwdentry = pwd.getpwuid(uid)
+        return pwdentry.pw_name
+    # run plugins
+    r = plugin.hook("wizard.user.operator", [])
+    if r is not None:
+        return r
+    # use SUDO_USER
+    sudo_user = os.getenv("SUDO_USER")
+    if not sudo_user:
+        return None
+    pwdentry = pwd.getpwnam(sudo_user)
+    return pwdentry.pw_name
+
+def passwd(path=None, uid=None):
+    """
+    Returns a passwd-like entry (a :class:`Info` object) corresponding
+    to the owner of ``path``.  If ``uid`` is specified, ``path`` is used
+    solely to determine the filesystem ``uid`` was determined from.  It
+    will fall back to the local passwd database, and return ``None``
+    if no information is available.  If ``path`` is omitted, it will
+    fall back to the current working directory.
+
+    This function implements a plugin interface named
+    :ref:`wizard.user.passwd`.
+    """
+    if path is None:
+        path = os.getcwd()
+    path = os.path.realpath(path)
+    if not uid:
+        uid = os.stat(path).st_uid
+    r = plugin.hook("wizard.user.passwd", [path, uid])
+    if r is not None:
+        return r
+    try:
+        return Info.pwentry(pwd.getpwuid(uid))
+    except KeyError:
+        return None
+
+def pwnam(name):
+    """
+    This user converts a username into a :class:`Info` object using
+    *only* the local password database.
+    """
+    return Info.pwentry(pwd.getpwnam(name))
+
+class Info(object):
+    """
+    Object containing information describing a user.  It is analogous to
+    passwd, but has dropped the password field and dedicated the
+    ``gecos`` field for real name information.
+
+    .. note::
+
+        If a platform does not support retrieving information about a
+        field, it may have the value ``None``.
+    """
+    #: Login name
+    name = None
+    #: User ID
+    uid = None
+    #: Group ID
+    gid = None
+    #: Real name
+    realname = None
+    #: Home directory
+    homedir = None
+    #: Default command interpreter
+    shell = None
+    @staticmethod
+    def pwentry(pwentry):
+        return Info(pwentry.pw_name, pwentry.pw_uid, pwentry.pw_gid,
+                pwentry.pw_gecos.split(",")[0], pwentry.pw_dir, pwentry.pw_shell)
+    def __init__(self, name, uid, gid, realname, homedir, shell):
+        self.name = name
+        self.uid = uid
+        self.gid = gid
+        self.realname = realname
+        self.homedir = homedir
+        self.shell = shell
+        self._email = None
+    @property
+    def email(self):
+        """The email of this user, calculated on the fly."""
+        if self._email is None:
+            self._email = email(self.name)
+        return self._email
index 419e9d92485cd21004b1426dbf180cd52d868462..876f8a985cf257ecdddba2c305ce0c7aa8e70463 100644 (file)
@@ -23,6 +23,7 @@ import random
 import string
 
 import wizard
+from wizard import user
 
 class ChangeDirectory(object):
     """
@@ -202,24 +203,6 @@ def get_dir_uid(dir):
     """Finds the uid of the person who owns this directory."""
     return os.stat(dir).st_uid
 
-def get_dir_owner(dir = "."):
-    """
-    Finds the name of the locker this directory is in.
-
-    .. note::
-
-        When querying AFS servers, this function only works if
-        you're on a Scripts server (which has the correct passwd
-        database) or if you're on a Debathena machine.
-    """
-    uid = get_dir_uid(dir)
-    try:
-        pwentry = pwd.getpwuid(uid)
-        return pwentry.pw_name
-    except KeyError:
-        # do an pts query to get the name
-        return subprocess.Popen(['pts', 'examine', str(uid)], stdout=subprocess.PIPE).communicate()[0].partition(",")[0].partition(": ")[2]
-
 def get_revision():
     """Returns the commit ID of the current Wizard install."""
     # If you decide to convert this to use wizard.shell, be warned
@@ -228,91 +211,42 @@ def get_revision():
     wizard_git = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".git")
     return subprocess.Popen(["git", "--git-dir=" + wizard_git, "rev-parse", "HEAD"], stdout=subprocess.PIPE).communicate()[0].rstrip()
 
-def get_operator_info():
-    """
-    Returns tuple of ``(realname, email)`` about the person running
-    the script.  If run from a scripts server, get info from Hesiod.
-    Otherwise, use the passwd database (email generated probably won't
-    actually accept mail).  Useful when generating commit messages.
-    """
-    username = get_operator_name_from_gssapi()
-    if username:
-        # scripts approach
-        hesinfo = subprocess.Popen(["hesinfo", username, "passwd"],stdout=subprocess.PIPE).communicate()[0]
-        fields = hesinfo.partition(",")[0]
-        realname = fields.rpartition(":")[2]
-        return realname, username + "@mit.edu"
-    else:
-        # more traditional approach, but the email probably doesn't work
-        uid = os.getuid()
-        if not uid:
-            # since root isn't actually a useful designation, but maybe
-            # SUDO_USER contains something helpful
-            sudo_user = os.getenv("SUDO_USER")
-            if not sudo_user:
-                raise NoOperatorInfo
-            pwdentry = pwd.getpwnam(sudo_user)
-        else:
-            pwdentry = pwd.getpwuid(uid)
-        # XXX: error checking might be nice
-        # We follow the Ubuntu convention of gecos being a comma split field
-        # with the person's realname being the first entry.
-        return pwdentry.pw_gecos.split(",")[0], pwdentry.pw_name + "@" + socket.gethostname()
-
 def get_operator_git():
     """
     Returns ``Real Name <username@mit.edu>`` suitable for use in
-    Git ``Something-by:`` string.
+    Git ``Something-by:`` string.  Throws :exc:`NoOperatorInfo` if
+    no operator information is available.
     """
-    return "%s <%s>" % get_operator_info()
-
-def get_operator_name_from_gssapi():
-    """
-    Returns username of the person operating this script based
-    off of the :envvar:`SSH_GSSAPI_NAME` environment variable.
-
-    .. note::
-
-        :envvar:`SSH_GSSAPI_NAME` is not set by a vanilla OpenSSH
-        distributions.  Scripts servers are patched to support this
-        environment variable.
-    """
-    principal = os.getenv("SSH_GSSAPI_NAME")
-    if not principal:
-        return None
-    instance, _, _ = principal.partition("@")
-    if instance.endswith("/root"):
-        username, _, _ = principal.partition("/")
-    else:
-        username = instance
-    return username
+    op = user.operator()
+    if op is None:
+        raise NoOperatorInfo
+    info = user.pwnam(op)
+    return "%s <%s>" % (info.realname, info.email)
 
 def set_operator_env():
     """
     Sets :envvar:`GIT_COMMITTER_NAME` and :envvar:`GIT_COMMITTER_EMAIL`
-    environment variables if applicable.  Does nothing if
-    :func:`get_operator_info` throws :exc:`NoOperatorInfo`.
+    environment variables if applicable.  Does nothing if no information
+    is available
     """
-    try:
-        op_realname, op_email = get_operator_info()
-        os.putenv("GIT_COMMITTER_NAME", op_realname)
-        os.putenv("GIT_COMMITTER_EMAIL", op_email)
-    except NoOperatorInfo:
-        pass
+    op = user.operator()
+    if op is None:
+        return
+    info = user.pwnam(op)
+    os.putenv("GIT_COMMITTER_NAME", info.realname)
+    os.putenv("GIT_COMMITTER_EMAIL", info.email)
 
 def set_author_env():
     """
-    Sets :envvar:`GIT_AUTHOR_NAME` and :envvar:`GIT_AUTHOR_EMAIL` environment
-    variables if applicable. Does nothing if :func:`get_dir_owner` fails.
+    Sets :envvar:`GIT_AUTHOR_NAME` and :envvar:`GIT_AUTHOR_EMAIL`
+    environment variables if applicable. Does nothing if
+    :func:`wizard.user.passwd` fails.
     """
-    try:
-        # XXX: should check if the directory is in AFS, and if not, use
-        # a more traditional metric
-        lockername = get_dir_owner()
-        os.putenv("GIT_AUTHOR_NAME", "%s locker" % lockername)
-        os.putenv("GIT_AUTHOR_EMAIL", "%s@scripts.mit.edu" % lockername)
-    except KeyError: # XXX: This doesn't actually make sense
-        pass
+    info = user.passwd()
+    if info is None:
+        return
+    os.putenv("GIT_AUTHOR_NAME", "%s" % info.realname)
+    os.putenv("GIT_AUTHOR_EMAIL", "%s" % info.email)
 
 def set_git_env():
     """Sets all appropriate environment variables for Git commits."""
@@ -360,8 +294,7 @@ def makedirs(path):
 
 def fetch(host, path, subpath, post=None):
     try:
-        # XXX: Special case if it's https; not sure why this data isn't
-        # passed
+        # XXX: Should use urllib instead
         h = httplib.HTTPConnection(host)
         fullpath = path.rstrip("/") + "/" + subpath.lstrip("/") # to be lenient about input we accept
         if post: