###--------------------------------------------------------------------------
### 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.
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.
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, \
## 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:
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):
- 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
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.
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():
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.
--- /dev/null
+=======================================
+ 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