chiark / gitweb /
backend.py: Use configured delimiter for joining fields.
[chopwood] / service.py
index c04e99e3218b9de4dd9ad26769cf1cee8cacf147..ddd28b9a121cadd76617cdc39ddefb460d42fce7 100644 (file)
@@ -40,11 +40,10 @@ import util as U
 ### Protocol.
 ###
 ### A service is a thing for which a user might have an account, with a login
-### name and password.  The service protocol is fairly straightforward: a
-### password can be set to a particular value using `setpasswd' (which
-### handles details of hashing and so on), or cleared (i.e., preventing
-### logins using a password) using `clearpasswd'.  Services also present
-### `friendly' names, used by the user interface.
+### name and password.  The service protocol is fairly straightforward: there
+### are methods corresponding to the various low-level operations which can
+### be performed on services.  Services also present `friendly' names, used
+### by the user interface.
 ###
 ### A service may be local or remote.  Local services are implemented in
 ### terms of a backend and hashing scheme.  Information about a particular
@@ -76,12 +75,18 @@ class IncorrectPassword (Exception):
 class BasicService (object):
   """
   A simple base class for services.
+
+  The `manage_pwent_p' flag indicates whether administration commands should
+  attempt to add or remove password entries in the corresponding database
+  when users are added or removed.
   """
 
-  def __init__(me, friendly, name = None, *args, **kw):
+  def __init__(me, friendly, name = None, manage_pwent_p = True,
+               *args, **kw):
     super(BasicService, me).__init__(*args)
     me.name = name
     me.friendly = friendly
+    me.manage_pwent_p = manage_pwent_p
     me.meta = kw
 
 ###--------------------------------------------------------------------------
@@ -128,6 +133,10 @@ class Account (object):
     me._rec.passwd = passwd
     me._rec.write()
 
+  def remove(me):
+    """Service protocol: remove the user's password entry."""
+    me._rec.remove()
+
 class LocalService (BasicService):
   """
   A local service has immediate knowledge of a hashing scheme and a password
@@ -162,6 +171,15 @@ class LocalService (BasicService):
     """Service protocol: clear USER's password, preventing logins."""
     me.find(user).clearpasswd()
 
+  def mkpwent(me, user, passwd, fields):
+    """Service protocol: create a record for USER."""
+    if me.hash.NULL is not None: passwd = me.hash.NULL
+    me._be.create(user, passwd, fields)
+
+  def rmpwent(me, user):
+    """Service protocol: delete the record for USER."""
+    me.find(user).remove()
+
 CONF.export('LocalService')
 
 ###--------------------------------------------------------------------------
@@ -183,17 +201,17 @@ class BasicRemoteService (BasicService):
   configuration.
   """
 
-  def _run(me, cmd, input = None):
+  def _run(me, cmd, input = None, state = None):
     """
     This is the core of the remote service machinery.  It issues a command
     and parses the response.  It will generate strings of informational
     output from the command; error responses cause appropriate exceptions to
     be raised.
 
-    The command is determined by passing the CMD argument to the `_mkcmd'
-    method, which a subclass must implement; it should return a list of
-    command-line arguments suitable for `subprocess.Popen'.  The INPUT is a
-    string to make available on the command's stdin; if None, then no input
+    The command is determined by passing the CMD and STATE arguments to the
+    `_mkcmd' method, which a subclass must implement; it should return a list
+    of command-line arguments suitable for `subprocess.Popen'.  The INPUT is
+    string to make available on the command's stdin; if None, then no input
     is provided to the command.  The `_describe' method must provide a
     description of the remote service for use in timeout messages.
 
@@ -207,7 +225,7 @@ class BasicRemoteService (BasicService):
 
     ## Run the command and collect its output and status.
     with U.timeout(30, "waiting for remote service %s" % me._describe()):
-      proc = SUB.Popen(me._mkcmd(cmd),
+      proc = SUB.Popen(me._mkcmd(cmd, state),
                        stdin = input is not None and SUB.PIPE or None,
                        stdout = SUB.PIPE, stderr = SUB.PIPE)
       out, err = proc.communicate(input)
@@ -248,9 +266,9 @@ class BasicRemoteService (BasicService):
     if not win:
       raise U.ExpectedError, (500, 'No reply from remote service')
 
-  def _run_noout(me, cmd, input = None):
+  def _run_noout(me, cmd, input = None, state = None):
     """Like `_run', but expect no output."""
-    for _ in me._run(cmd, input):
+    for _ in me._run(cmd, input, state):
       raise U.ExpectedError, (500, 'Unexpected output from remote service')
 
 class SSHRemoteService (BasicRemoteService):
@@ -300,59 +318,101 @@ class SSHRemoteService (BasicRemoteService):
     """Service protocol: clear the USER's password."""
     me._run_noout(['clear', me.name, user])
 
+  def mkpwent(me, user, passwd, fields):
+    """Service protocol: create a record for USER."""
+    me._run_noout(['mkpwent', user, me.name] + fields, passwd + '\n')
+
+  def rmpwent(me, user):
+    """Service protocol: delete the record for USER."""
+    me._run_noout(['rmpwent', user, me.name])
+
 CONF.export('SSHRemoteService')
 
 class CommandRemoteService (BasicRemoteService):
   """
   A remote service transported over a standard Unix command.
 
-  This is left rather generic.  We need to know some command lists SET and
-  CLEAR containing the relevant service names and arguments.  These are
-  simply executed, after simple placeholder substitution.
-
-  The SET command should read a password as its first line on stdin, and set
-  that as the user's new password.  The CLEAR command should simply prevent
-  the user from logging in with a password.  On success, the commands should
-  print a line `OK' to standard output, and on any kind of anticipated
-  failure, they should print `ERR' followed by an HTTP status code and a
-  message; in either case, the program should exit with status zero.  In
-  disastrous cases, it's acceptable to print an error message to stderr
-  and/or exit with a nonzero status.
-
-  The placeholders are as follows.
+  This is left rather generic.  Two strategies are available (and can be
+  combined using appropriate configuration).  A DEFAULT command list can be
+  specified, and will be invoked as `DEFAULT OP ARGS...', where OP ARGS form
+  a Chopwood remote command.  Additionally, an OPMAP dictionary can be
+  provided, mapping OP names (remote command names) to command lists
+  containing `%' placeholders, as follows:
 
   `%u'          the user's name
   `%%'          a single `%' character
+
+  On success, the commands should print a line `OK' to standard output, and
+  on any kind of anticipated failure, they should print `ERR' followed by an
+  HTTP status code and a message; in either case, the program should exit
+  with status zero.  In disastrous cases, it's acceptable to print an error
+  message to stderr and/or exit with a nonzero status.
+
+  Configuration hint: if you can only handle some subset of the available
+  commands, then your easy approach is to set commands for the operations you
+  can handle in the OPMAP, and set the DEFAULT to something like
+
+        ['echo', 'ERR 500', 'unsupported command:']
+
+  to reject other commands.
   """
 
   R_PAT = RX.compile('%(.)')
 
-  def __init__(me, set, clear, *args, **kw):
+  def __init__(me, default = ['ERR', '500', 'unimplemented command:'],
+               opmap = {}, *args, **kw):
     """Initialize the command remote service."""
     super(CommandRemoteService, me).__init__(*args, **kw)
-    me._set = set
-    me._clear = clear
-    me._map = dict(u = user)
+    me._default = default
+    me._opmap = opmap
 
   def _describe(me):
     """Description of the remote service."""
     return "`%s' command service (%s)" % (me.name, ' '.join(me._default))
 
-  def _subst(me, c):
+  def _subst(me, c, map):
     """Return the substitution for the placeholder `%C'."""
-    return me._map.get(c, c)
+    return map.get(c, c)
 
-  def _mkcmd(me, cmd):
+  def _mkcmd(me, cmd, map):
     """Construct the command to be executed, by substituting placeholders."""
-    return [me.R_PAT.sub(lambda m: me._subst(m.group(1))) for arg in cmd]
+    if map is None: return cmd
+    return [me.R_PAT.sub(lambda m: me._subst(m.group(1), map), arg)
+            for arg in cmd]
+
+  def _dispatch(me, func, op, args, input = None):
+    """
+    Work out how to invoke a particular command.
+
+    Invoke FUNC, which works like `_run', with appropriate arguments.  The OP
+    is a remote command name; ARGS is a sequence of (C, ARG) pairs, where C
+    is a placeholder character and ARG is a string value; INPUT is the text
+    to provide to the command on standard input.
+    """
+    try:
+      cmd = me._opmap[op]
+    except KeyError:
+      cmd = me._default + [op] + [v for k, v in args]
+      map = None
+    else:
+      map = dict(args)
+    return func(cmd, input = input, state = map)
 
   def setpasswd(me, user, passwd):
     """Service protocol: set the USER's password to PASSWD."""
-    me._run_noout(me._set, passwd + '\n')
+    me._dispatch(me._run_noout, 'set', [('u', user)], passwd + '\n')
 
   def clearpasswd(me, user):
     """Service protocol: clear the USER's password."""
-    me._run_noout(me._clear)
+    me._dispatch(me._run_noout, 'clear', [('u', user)])
+
+  def mkpwent(me, user, passwd, fields):
+    """Service protocol: create a record for USER."""
+    me._dispatch(me._run_noout, 'mkpwent', [('u', user)])
+
+  def rmpwent(me, user):
+    """Service protocol: delete the record for USER."""
+    me._dispatch(me._run_noout, 'rmpwent', [('u', user)])
 
 CONF.export('CommandRemoteService')