chiark / gitweb /
Found in crybaby's working tree. mdw/wip.crybaby.2016-05-05
authorMark Wooding <mdw@distorted.org.uk>
Thu, 5 May 2016 09:27:24 +0000 (10:27 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Fri, 7 Jul 2017 21:58:58 +0000 (22:58 +0100)
backend.py
chpwd.rst [new file with mode: 0644]

index fda32e0b8e37d259090e71f4d6ac864bd407e069..2596306363e7c6171107bdbfa8d26b81de8f40c9 100644 (file)
@@ -44,7 +44,8 @@ CONF.DEFAULTS.update(
 ###--------------------------------------------------------------------------
 ### Utilities.
 
 ###--------------------------------------------------------------------------
 ### Utilities.
 
-def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
+def fill_in_fields(fno_user, fno_passwd, fno_map,
+                   user, passwd, args, defaults = None):
   """
   Return a vector of filled-in fields.
 
   """
   Return a vector of filled-in fields.
 
@@ -53,6 +54,10 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
   is a sequence of (NAME, POS) pairs.  The USER and PASSWD arguments give the
   actual user name and password values; ARGS are the remaining arguments,
   maybe in the form `NAME=VALUE'.
   is a sequence of (NAME, POS) pairs.  The USER and PASSWD arguments give the
   actual user name and password values; ARGS are the remaining arguments,
   maybe in the form `NAME=VALUE'.
+
+  If DEFAULTS is given, it is a fully-filled-in sequence of records.  The
+  user name and password are taken from the DEFAULTS if USER and PASSWD are
+  `None' on entry; they cannot be set from ARGS.
   """
 
   ## Prepare the result vector, and set up some data structures.
   """
 
   ## Prepare the result vector, and set up some data structures.
@@ -63,7 +68,8 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
   if fno_user >= n or fno_passwd >= n: ok = False
   for k, i in fno_map:
     fmap[k] = i
   if fno_user >= n or fno_passwd >= n: ok = False
   for k, i in fno_map:
     fmap[k] = i
-    rmap[i] = "`%s'" % k
+    if i in rmap: ok = False
+    else: rmap[i] = "`%s'" % k
     if i >= n: ok = False
   if not ok:
     raise U.ExpectedError, \
     if i >= n: ok = False
   if not ok:
     raise U.ExpectedError, \
@@ -71,8 +77,12 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
 
   ## Prepare the new record's fields.
   f = [None]*n
 
   ## Prepare the new record's fields.
   f = [None]*n
-  f[fno_user] = user
-  f[fno_passwd] = passwd
+  if user is not None: f[fno_user] = user
+  elif defaults is not None: f[no_user] = defaults[no_user]
+  else: raise U.ExpectedError, (500, "No user name given")
+  if passwd is not None: f[fno_passwd] = passwd
+  elif defaults is not None: f[no_passwd] = defaults[no_passwd]
+  else: raise U.ExpectedError, (500, "No password given")
 
   for a in args:
     if '=' in a:
 
   for a in args:
     if '=' in a:
@@ -89,10 +99,12 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
       raise U.ExpectedError, (400, "Field %s is already set" % rmap[i])
     f[i] = v
 
       raise U.ExpectedError, (400, "Field %s is already set" % rmap[i])
     f[i] = v
 
-  ## Check that the vector of fields is properly set up.
+  ## Check that the vector of fields is properly set up.  Copy unset values
+  ## from the defaults if they're available.
   for i in xrange(n):
   for i in xrange(n):
-    if f[i] is None:
-      raise U.ExpectedError, (500, "Field %s is unset" % rmap[i])
+    if f[i] is not None: pass
+    elif defaults is not None: f[i] = defaults[i]
+    else: raise U.ExpectedError, (500, "Field %s is unset" % rmap[i])
 
   ## Done.
   return f
 
   ## Done.
   return f
@@ -165,7 +177,7 @@ class FlatFileRecord (BasicRecord):
   is careful to do this).
   """
 
   is careful to do this).
   """
 
-  def __init__(me, line, delim, fmap, *args, **kw):
+  def __init__(me, line, delim, fno_user, fno_passwd, fmap, *args, **kw):
     """
     Initialize the record, splitting the LINE into fields separated by DELIM,
     and setting attributes under control of FMAP.
     """
     Initialize the record, splitting the LINE into fields separated by DELIM,
     and setting attributes under control of FMAP.
@@ -174,6 +186,8 @@ class FlatFileRecord (BasicRecord):
     line = line.rstrip('\n')
     fields = line.split(delim)
     me._delim = delim
     line = line.rstrip('\n')
     fields = line.split(delim)
     me._delim = delim
+    me._fno_user = fno_user
+    me._fno_passwd = fno_passwd
     me._fmap = fmap
     me._raw = fields
     for k, v in fmap.iteritems():
     me._fmap = fmap
     me._raw = fields
     for k, v in fmap.iteritems():
@@ -199,6 +213,14 @@ class FlatFileRecord (BasicRecord):
       fields[v] = val
     return me._delim.join(fields) + '\n'
 
       fields[v] = val
     return me._delim.join(fields) + '\n'
 
+  def edit(me, args):
+    """
+    Modify the record as described by the ARGS.
+
+    Fields other than the user name and password can be modified.
+    """
+    ff = fill_in_fields(
+
 class FlatFileBackend (object):
   """
   Password storage in a flat passwd(5)-style file.
 class FlatFileBackend (object):
   """
   Password storage in a flat passwd(5)-style file.
diff --git a/chpwd.rst b/chpwd.rst
new file mode 100644 (file)
index 0000000..909576b
--- /dev/null
+++ b/chpwd.rst
@@ -0,0 +1,185 @@
+=======================================
+ Chopwood: a password changing service
+=======================================
+
+:Author: Mark Wooding
+
+.. sectnum::
+       :depth: 3
+
+.. contents::
+
+Introduction
+############
+
+
+Congratulations.  You've just installed Squid, or nginx, or Exim, or
+Dovecot, or, well, something.  And it has a marvellous access control
+system with users and passwords and stuff.  Yet another little file with
+*user*\ **:**\ *password* lines, or maybe a database or something.
+Which will all be great.  Just as soon as your users can actually set
+passwords, or change them.  Chopwood is a service for letting them do
+that.
+
+Of course, you could set up some single sign-on system.  They're quite
+complicated.  Also, they probably aren't really what you wanted, because
+these various services that you've set up probably have rather different
+security levels and it's just not a good plan to feed high-value
+credentials to a low-security service.
+
+Chopwood isn't a single sign-on system: in fact it's the opposite.  It's
+a tool to let users manage a number of passwords on the same system.
+
+Chopwood has three user interfaces.
+
+  * Local users can communicate with it as a `GNU Userv`_ service.  Most
+    of the necessary plumbing is provided.  Local users don't need to do
+    authenticate explicitly via Userv, so this is simple, convenient,
+    reliable, and relatively pain free.  The only downside is that you
+    have to give users shell access to your server.
+
+  * Remote users can reach it using SSH, using OpenSSH_\ 's forced
+    command feature.  Maintaining the ``authorized_keys`` file is a bit
+    of a nuisance, especially if they need to keep changing their keys
+    [#keymgmt]_.
+
+  * Remote users can also get to it with a web browser, using good
+    old-fashioned CGI.  Unfortunately, we don't have a better approach
+    for authenticating users than more passwords.  Of course, Chopwood
+    can manage its own password, but that's not really the point.
+
+.. [#keymgmt] Maybe someone should build a similar tool for managing
+             authorized keys for SSH services.
+
+.. _GNU Userv: http://www.gnu.org/software/userv/
+
+.. _OpenSSH: http://www.openssh.org/
+
+
+
+Installation
+############
+
+
+Chopwood is written in Python, and doesn't depend on any additional
+libraries.  The program is tested with Python 2.7, but I expect that it
+works with Python 2.6, and it's intended to work with 2.5 too.
+
+You'll also need GNU Make and at least of
+
+  * GNU Userv;
+
+  * OpenSSH, or some other SSH server with a forced-command feature;
+
+  * a web server which can run CGI scripts as some other user; or
+
+  * some willingness to get your hands dirty hooking Chopwood up to some
+    other interface.
+
+Chopwood is designed to be run from a Git working tree.  You can clone
+it from
+
+  * <git://git.distorted.org.uk/~mdw/chopwood/>
+  * <http://git.distorted.org.uk/~mdw/chopwood/>
+  * <https://git.distorted.org.uk/~mdw/chopwood/>
+
+I recommend that you use the HTTPS transport if you can; unfortunately,
+at the moment, that server doesn't have a certificate from a well-known
+CA.  Please check the certificate against the TLSA record, which is
+secured using DNSsec.
+
+The working tree needs a very quick build step, which you can accomplish
+by running ::
+
+       $ make
+
+This does three things:
+
+  * it generates the ``auto.py`` file which tells Chopwood its
+    installation directory, version number, and various other
+    automatically discovered things; and
+
+  * it generates the static files for the web interface; and
+
+  * it causes Python to byte-compile the various Python modules.
+
+
+Basic configuration
+-------------------
+
+Great so far.  Now it's time to shut up the warning about not having a
+configuration file.  
+
+
+Configuration parameters
+------------------------
+
+**ALLOWOP**:
+       An object holding three boolean attributes.  If **ALLOWOP.set**
+       is true then users are allowed to set passwords to values of
+       their choosing; if **ALLOWOP.reset** is true then they're
+       allowed to ask for passwords to be reset to a value chosen by
+       Chopwood; and if **ALLOWOP.clear** is true then they're allowed
+       to clear passwords, leaving them unable to log into the
+       applicable service.
+
+**AUTHHASH**:
+       The hash function to use when making HTTP authentication
+       cookies.  This should be a function (or other callable) which
+       behaves like one of the **hashlib** hash functions.  The default
+       is **hashlib**.\ **sha256**, which is likely to be good enough.
+
+**DB**:
+       The database which Chopwood should use to store its state.  This
+       should be a pair (*module*, *modargs*), where *module* names a
+       PEP 249 database module and *modargs* is a sequence or
+       dictionary of arguments to pass to its **connect** function.
+       The default is to create a SQLite3 database in Chopwood's
+       working directory.
+
+**HASH**:
+       The password hash to use for Chopwood's own user database.  See
+       `Chopwood password hashes`_ for more information about the
+       password hashes provided and the protocol in which hash objects
+       participate.  The default is to use a **crypt**\ (3)-style hash
+       with MD5.
+
+**LOCKDIR**:
+       A string naming the directory in which Chopwood should store
+       its lockfiles.
+
+**RQCLASS** and **RQMIXIN**:
+       Classes used to determine password policies.  See `Applying
+       local policy`_ for details.  The default is to allow everything.
+
+**SCRIPT_NAME**:
+       A (possibly relative) URL referring to Chopwood's CGI script.
+       This will usually be set appropriately by the webserver, but
+       this setting exists so that you can override it if it's wrong.
+
+**SECRETFRESH**:
+       The time in seconds for which a cookie authentication key is
+       used for issuing fresh cookies.  After this, a new key is
+       generated, and the old ones kept and used to validate existing
+       cookies until their lifetime expires (see **SECRETLIFE** below).
+       The default is five minutes.
+
+**SECRETLIFE**:
+       The total lifetime, in seconds, of a key used to ensure
+       cryptographically the integrity of session cookies.  See
+       **SECRETFRESH** above.  Users' browsers are instructed to
+       discard the cookie after `**SECRETLIFE** - **SECRETFRESH**`:m:
+       seconds.  The default is half an hour.
+
+**STATIC**:
+       The URL prefix for static content: the location of static
+       resource *file* is reported to the user agent as *STATIC*\
+       **/**\ *file*.  The default is to attempt (see **SCRIPT_NAME**
+       above) to link back to Chopwood's CGI script to generate the
+       content dynamically.  You can improve responsiveness by
+       configuring Chopwood and your webserver to serve actual static
+       files instead.
+
+
+..  LocalWords:  nginx www userv CGI keymgmt https TLSA py ALLOWOP sha
+..  LocalWords:  AUTHHASH RQCLASS RQMIXIN modargs