[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Commit configurability work for 0.0.5. (needs testing)



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv19199/lib/mixminion

Modified Files:
	ClientMain.py Common.py Config.py Main.py test.py 
Log Message:
Commit configurability work for 0.0.5.  (needs testing)

- Add ability to disable directory paranoia (disabled by default on
  cygwin, win32).
- Make directory paranoia messages more helpful.
- Change min,default,max public key overlap from (0,2,6) hours to (6,24,72) 
  hours.



Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.92
retrieving revision 1.93
diff -u -d -r1.92 -r1.93
--- ClientMain.py	26 Jun 2003 03:23:53 -0000	1.92
+++ ClientMain.py	26 Jun 2003 17:43:27 -0000	1.93
@@ -1095,6 +1095,8 @@
 #ShredCommand: rm -f
 ## Use this option to specify a nonstandard entropy source.
 #EntropySource: /dev/urandom
+## Set this option to 'no' to disable permission checking
+#FileParanoia: yes
 
 [DirectoryServers]
 # Not yet implemented

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.92
retrieving revision 1.93
diff -u -d -r1.92 -r1.93
--- Common.py	26 Jun 2003 03:23:53 -0000	1.92
+++ Common.py	26 Jun 2003 17:43:27 -0000	1.93
@@ -30,6 +30,7 @@
 import signal
 import stat
 import statvfs
+import string
 import sys
 import threading
 import time
@@ -42,6 +43,11 @@
 del Queue
 del Empty
 
+try:
+    import pwd, grp
+except ImportError:
+    pwd = grp = None
+
 from types import StringType
 
 class MixError(Exception):
@@ -64,6 +70,10 @@
     """Exception class for failed authentication to a server."""
     pass
 
+class MixFilePermissionError(MixFatalError):
+    """Exception raised when a file has the wrong owner or permissions."""
+    pass
+
 class UIError(MixError):
     """Exception raised for an error that should be reported to the user,
        not dumped as a stack trace."""
@@ -161,6 +171,31 @@
     else:
         return "".join(pieces)
 
+def englishSequence(lst, empty="none"):
+    """Given a sequence of items, return the sequence formatted
+       according to ordinary English conventions of punctuation.
+
+       If the list is empty, the value of 'empty' will be returned."""
+
+    if len(lst) == 0:
+        return empty
+    elif len(lst) == 1:
+        return lst[0]
+
+    punc = ", "
+    for item in lst:
+        if "," in item or stringContains(item, " and "):
+            punc = "; "
+            break
+
+    if len(lst) == 2:
+        if punc == ", ":
+            return "%s and %s" % tuple(lst)
+        else:
+            return "%s; and %s" % tuple(lst)
+    else:
+        return "%s%sand %s" % (punc.join(lst[0:-1]), punc, lst[-1])
+
 #----------------------------------------------------------------------
 # Functions to generate and parse OpenPGP-style ASCII armor
 
@@ -266,6 +301,46 @@
     raise MixFatalError("Unreachable code somehow reached.")
 
 #----------------------------------------------------------------------
+
+#----------------------------------------------------------------------
+
+# A set of directories we've issued warnings about -- we won't check
+# them again.
+_WARNED_DIRECTORIES = {}
+# A set of directories that have checked out -- we won't check them again.
+_VALID_DIRECTORIES = {}
+# A list of user IDs
+_TRUSTED_UIDS = [ 0 ]
+
+# Flags: what standard Unix access controls should we check for?
+_CHECK_UID = 1
+_CHECK_GID = 1
+_CHECK_MODE = 1
+
+if sys.platform in ('cygwin', 'win32'):
+    # Under windows, we can't do anything sensible with permissions AFAICT.
+    _CHECK_UID = _CHECK_GID = _CHECK_MODE = 0
+elif os.environ.get("MM_NO_FILE_PARANOIA"):
+    _CHECK_UID = _CHECK_GID = _CHECK_MODE = 0
+
+def _uidToName(uid):
+    """Helper function: given a uid, return a username or descriptive
+       string."""
+    try:
+        return pwd.getpwuid(uid)[0]
+    except (KeyError, AttributeError):
+        # KeyError: no such pwent.  AttributeError: pwd module not loaded.
+        return "user %s"%uid
+
+def _gidToName(gid):
+    """Helper function: given a gid, return a groupname or descriptive string
+       """
+    try:
+        return grp.getgrgid(gid)[0]
+    except (KeyError, AttributeError):
+        # KeyError: no such grpent.  AttributeError: grp module not loaded.
+        return "group %s"%gid
+ 
 def checkPrivateFile(fn, fix=1):
     """Checks whether f is a file owned by this uid, set to mode 0600 or
        0700, and all its parents pass checkPrivateDir.  Raises MixFatalError
@@ -285,12 +360,17 @@
     if not os.path.isfile(fn):
         raise MixFatalError("%s is not a regular file" % fn)
     me = os.getuid()
-    if st[stat.ST_UID] != me:
-        raise MixFatalError("File %s must have owner %s" % (fn, me))
+    if _CHECK_UID and st[stat.ST_UID] != me:
+        ownerName = _uidToName( st[stat.ST_UID])
+        myName = _uidToName(me)
+        raise MixFilePermissionError(
+            "File %s is owned by %s, but Mixminion is running as %s" 
+            % (fn, ownrerName, myName))
     mode = st[stat.ST_MODE] & 0777
-    if mode not in (0700, 0600):
+    if _CHECK_MODE and mode not in (0700, 0600):
         if not fix:
-            raise MixFatalError("Bad mode %o on file %s" % (mode & 0777, fn))
+            raise MixFilePermissionError("Bad mode %o on file %s" 
+                                         % (mode & 0777, fn))
         newmode = {0:0600,0100:0700}[(mode & 0100)]
         LOG.warn("Repairing permissions on file %s" % fn)
         os.chmod(fn, newmode)
@@ -304,14 +384,11 @@
         try:
             os.makedirs(d, 0700)
         except OSError, e:
-            raise MixFatalError("Unable to create directory %s: %s" % (d, e))
+            raise MixFatalError(
+                "Unable to create directory %s: %s" % (d, e))
 
     checkPrivateDir(d)
 
-_WARNED_DIRECTORIES = {}
-_VALID_DIRECTORIES = {}
-_TRUSTED_UIDS = [ 0 ]
-
 def checkPrivateDir(d, recurse=1):
     """Check whether d is a directory owned by this uid, set to mode
        0700. All of d's parents must not be writable or owned by anybody but
@@ -330,11 +407,15 @@
 
     st = os.stat(d)
     # check permissions
-    if st[stat.ST_MODE] & 0777 != 0700:
-        raise MixFatalError("Directory %s must be mode 0700" % d)
+    if _CHECK_MODE and st[stat.ST_MODE] & 0777 != 0700:
+        raise MixFilePermissionError("Directory %s must be mode 0700" % d)
 
-    if st[stat.ST_UID] != me:
-        raise MixFatalError("Directory %s has must have owner %s" %(d, me))
+    if _CHECK_UID and st[stat.ST_UID] != me:
+        ownerName = _uidToName( st[stat.ST_UID])
+        myName = _uidToName(me)
+        raise MixFilePermissionError(
+            "Directory %s is owned by %s, but Mixminion is running as %s"
+            %(d,ownerName,myName))
 
     if not recurse:
         return
@@ -351,35 +432,34 @@
         st = os.stat(d)
         mode = st[stat.ST_MODE]
         owner = st[stat.ST_UID]
-        if owner not in trusted_uids:
-            raise MixFatalError("Bad owner (uid=%s) on directory %s"
-                                % (owner, d))
-        if (mode & 02) and not (mode & stat.S_ISVTX):
-            raise MixFatalError("Bad mode (%o) on directory %s" %
-                                (mode&0777, d))
+        if _CHECK_UID and owner not in trusted_uids:
+            ownerName = _uidToName(owner)
+            trustedNames = map(_uidToName,trusted_uids)
+            raise MixFilePermissionError(
+                "Directory %s is owned by %s, but I only trust %s"
+                % (d, ownerName, englishSequence(trustedNames, "(nobody)")))
+        if _CHECK_MODE and (mode & 02) and not (mode & stat.S_ISVTX):
+            raise MixFilePermissionError(
+                "Bad permissions (mode %o) on directory %s" %
+                (mode&0777, d))
 
-        if (mode & 020) and not (mode & stat.S_ISVTX):
+        if _CHECK_MODE and (mode & 020) and not (mode & stat.S_ISVTX):
             # FFFF We may want to give an even stronger error here.
-            if not _WARNED_DIRECTORIES.has_key(d):
+            if _CHECK_GID and not _WARNED_DIRECTORIES.has_key(d):
                 group = grp.getgrgid(st[stat.ST_GID])[0]
+                groupName = _gidToName(group)
                 LOG.warn("Directory %s is writable by group %s (mode %o)",
-                         d, group, mode&0777)
-            _WARNED_DIRECTORIES[d] = 1
+                         d, groupName, mode&0777)
+                _WARNED_DIRECTORIES[d] = 1
 
 def configureTrustedUsers(config):
     users = config['Host']['TrustedUser']
     if not users:
         return
+    if not _CHECK_UID:
+        return
 
-    for u in users:
-        u = u.strip()
-        try:
-            ent = pwd.getpwnam(u)
-        except KeyError:
-            LOG.warn("TrustedUser: No such user as %s", u)
-            continue
-
-        uid = ent[2]
+    for uid in users:
         _TRUSTED_UIDS.append(uid)
 
 #----------------------------------------------------------------------

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.49
retrieving revision 1.50
diff -u -d -r1.49 -r1.50
--- Config.py	25 Jun 2003 17:03:11 -0000	1.49
+++ Config.py	26 Jun 2003 17:43:27 -0000	1.50
@@ -56,6 +56,11 @@
 import os
 import re
 import socket # for inet_aton and error
+try:
+    import pwd
+except ImportError:
+    pwd = None
+
 from cStringIO import StringIO
 
 import mixminion.Common
@@ -369,6 +374,17 @@
 
     return os.path.expanduser(s)
 
+def _parseUser(s):
+    """Validation function.  Matches a username or UID.  Returns a UID."""
+    s = s.strip()
+    try:
+        return pwd.getpwnam(s)[2]
+    except (KeyError,AttributeError):
+        try:
+            return _parseInt(s)
+        except ConfigError:
+            raise ConfigError("Expected a user name or UID, but got %r",s)
+
 #----------------------------------------------------------------------
 
 # Regular expression to match a section header.
@@ -724,7 +740,8 @@
         'Host' : { '__SECTION__' : ('ALLOW', None, None),
                    'ShredCommand': ('ALLOW', _parseCommand, None),
                    'EntropySource': ('ALLOW', _parseFilename, "/dev/urandom"),
-                   'TrustedUser': ('ALLOW*', None, None),
+                   'TrustedUser': ('ALLOW*', _parseUser, None),
+                   'FileParanoia': ('Allow', _parseBoolean, "yes"),
                    },
         'DirectoryServers' :
                    { '__SECTION__' : ('REQUIRE', None, None),

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.51
retrieving revision 1.52
diff -u -d -r1.51 -r1.52
--- Main.py	26 Jun 2003 03:23:53 -0000	1.51
+++ Main.py	26 Jun 2003 17:43:27 -0000	1.52
@@ -225,6 +225,7 @@
     # command implementation code, we catch all UIError exceptions here.
     commonModule = __import__('mixminion.Common', {}, {}, ['UIError'])
     uiErrorClass = getattr(commonModule, 'UIError')
+    filePermissionErrorClass = getattr(commonModule, 'MixFilePermissionError')
 
     # Read the module and function.
     command_module, command_fn = _COMMANDS[args[1]]
@@ -242,6 +243,12 @@
         func(commandStr, ["--help"])
     except uiErrorClass, e:
         e.dumpAndExit()
+    except filePermissionErrorClass, e:
+        print str(e)
+        print "(You can disable file permission checking by setting",
+        print "the MM_NO_FILE_PARANOIA"
+        print "environment variable."
+        sys.exit(1)
     except KeyboardInterrupt:
         print "Interrupted."
     except ImportError, e:

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.124
retrieving revision 1.125
diff -u -d -r1.124 -r1.125
--- test.py	26 Jun 2003 03:19:29 -0000	1.124
+++ test.py	26 Jun 2003 17:43:27 -0000	1.125
@@ -4971,8 +4971,10 @@
         self.assertEquals(vu, keyring.getNextKeyRotation())
 
         # Check the second key we created.
-        key2time = vu+3600
-        va, vu, curKey = keyring._getLiveKeys(key2time)[0]
+        key2time = vu+3600*48
+        live = keyring._getLiveKeys(key2time)
+        self.assertEquals(len(live),1)
+        va, vu, curKey = live[0]
         self.assertEquals(va, finish)
         self.assertEquals(vu, mixminion.Common.previousMidnight(
             finish+10*24*60*60+60))
@@ -5015,7 +5017,7 @@
         # In case we started very close to midnight, remove keys as if it
         # were a little in the future; otherwise, we won't remove the
         # just-expired keys.
-        keyring.removeDeadKeys(now+360)
+        keyring.removeDeadKeys(now+24*60*60)
         self.assertEquals(3, len(keyring.keySets))
 
         if USE_SLOW_MODE:
@@ -5875,7 +5877,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(MMTPTests))
+        suite.addTest(tc(ServerKeysTests))
         return suite
 
     suite.addTest(tc(MiscTests))