chiark / gitweb /
backend.py: Use configured delimiter for joining fields.
[chopwood] / service.py
index 9ba9fca5d24e2b916ab364de0f46509a78b40670..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')
 
 ###--------------------------------------------------------------------------
@@ -300,38 +318,53 @@ 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._default = default
+    me._opmap = opmap
 
   def _describe(me):
     """Description of the remote service."""
@@ -343,16 +376,43 @@ class CommandRemoteService (BasicRemoteService):
 
   def _mkcmd(me, cmd, map):
     """Construct the command to be executed, by substituting placeholders."""
+    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', state = dict(u = user))
+    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, state = dict(u = user))
+    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')