agpl.py: Python 2.5 compatibility.
[chopwood] / service.py
index aa77388..b1f170c 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
 ### 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
 ###
 ### 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.
 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
     super(BasicService, me).__init__(*args)
     me.name = name
     me.friendly = friendly
+    me.manage_pwent_p = manage_pwent_p
     me.meta = kw
 
 ###--------------------------------------------------------------------------
     me.meta = kw
 
 ###--------------------------------------------------------------------------
@@ -128,6 +133,10 @@ class Account (object):
     me._rec.passwd = passwd
     me._rec.write()
 
     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
 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()
 
     """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')
 
 ###--------------------------------------------------------------------------
 CONF.export('LocalService')
 
 ###--------------------------------------------------------------------------
@@ -266,6 +284,14 @@ class SSHRemoteService (BasicRemoteService):
   `clear SERVICE USER'
         Clear the USER's password for SERVICE.
 
   `clear SERVICE USER'
         Clear the USER's password for SERVICE.
 
+  `mkpwent USER SERVICE [FIELDS ...]'
+        Install a record for USER in the SERVICE, supplying any other
+        necessary FIELDS in the appropriate format.  The user's password is
+        provided on the next line of standard input.
+
+  `rmpwent USER SERVICE'
+        Remove USER's password record for SERVICE.
+
   Arguments are form-url-encoded, since SSH doesn't preserve token boundaries
   in its argument list.
 
   Arguments are form-url-encoded, since SSH doesn't preserve token boundaries
   in its argument list.
 
@@ -300,6 +326,14 @@ class SSHRemoteService (BasicRemoteService):
     """Service protocol: clear the USER's password."""
     me._run_noout(['clear', me.name, user])
 
     """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):
 CONF.export('SSHRemoteService')
 
 class CommandRemoteService (BasicRemoteService):
@@ -314,8 +348,14 @@ class CommandRemoteService (BasicRemoteService):
   containing `%' placeholders, as follows:
 
   `%u'          the user's name
   containing `%' placeholders, as follows:
 
   `%u'          the user's name
+  `%f'          a user record field (list-valued)
   `%%'          a single `%' character
 
   `%%'          a single `%' character
 
+  If a template word contains placeholders for list-valued arguments, then
+  one output word is produced for each element of each list, with the
+  rightmost placeholder varying fastest.  If any list is empty then no output
+  words are produced.
+
   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
   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
@@ -326,14 +366,15 @@ class CommandRemoteService (BasicRemoteService):
   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
 
   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:']
+        ['echo', 'ERR', '500', 'unsupported command:']
 
   to reject other commands.
   """
 
   R_PAT = RX.compile('%(.)')
 
 
   to reject other commands.
   """
 
   R_PAT = RX.compile('%(.)')
 
-  def __init__(me, default = ['ERR', '500', 'unimplemented command:'],
+  def __init__(me,
+               default = ['echo', 'ERR', '500', 'unimplemented command:'],
                opmap = {}, *args, **kw):
     """Initialize the command remote service."""
     super(CommandRemoteService, me).__init__(*args, **kw)
                opmap = {}, *args, **kw):
     """Initialize the command remote service."""
     super(CommandRemoteService, me).__init__(*args, **kw)
@@ -344,15 +385,60 @@ class CommandRemoteService (BasicRemoteService):
     """Description of the remote service."""
     return "`%s' command service (%s)" % (me.name, ' '.join(me._default))
 
     """Description of the remote service."""
     return "`%s' command service (%s)" % (me.name, ' '.join(me._default))
 
-  def _subst(me, c, map):
-    """Return the substitution for the placeholder `%C'."""
-    return map.get(c, c)
+  def _mkcmd(me, cmd, argmap):
+    """
+    Construct the command to be executed, by substituting placeholders.
 
 
-  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]
+    The ARGMAP is a dictionary mapping placeholder letters to lists of
+    arguments.  These are substituted cartesian-product style into the
+    command words.
+    """
+
+    ## No command map, so assume someone's already done the hard word.
+    if argmap is None: return cmd
+
+    ## Start on building a list of arguments.
+    ww = []
+
+    ## Work through each template argument in turn...
+    for w in cmd:
+
+      ## Firstly, build a list of lists.  We'll then take the cartesian
+      ## product of these, and concatenate each of the results.
+      pc = []
+      last = 0
+      for m in me.R_PAT.finditer(w):
+        start, end = m.start(0), m.end(0)
+        if start > last: pc.append([w[last:start]])
+        ch = m.group(1)
+        if ch == '%':
+          pc.append(['%'])
+        else:
+          try: pc.append(argmap[m.group(1)])
+          except KeyError: raise U.ExpectedError, (
+            500, "Unknown placeholder `%%%s' in command `%s'" % (ch, cmd))
+        last = end
+      if last < len(w): pc.append([w[last:]])
+
+      ## If any of the components is empty then there's nothing to do for
+      ## this word.
+      if not all(pc): continue
+
+      ## Now do all the substitutions.
+      ii = len(pc)*[0]
+      while True:
+        ww.append(''.join(map(lambda v, i: v[i], pc, ii)))
+        i = len(ii) - 1
+        while i >= 0:
+          ii[i] += 1
+          if ii[i] < len(pc[i]): break
+          ii[i] = 0
+          i -= 1
+        else:
+          break
+
+    ## And finally we're done.
+    return ww
 
   def _dispatch(me, func, op, args, input = None):
     """
 
   def _dispatch(me, func, op, args, input = None):
     """
@@ -360,13 +446,14 @@ class CommandRemoteService (BasicRemoteService):
 
     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
 
     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.
+    is a placeholder character and ARG is a list of string values; INPUT is
+    the text to provide to the command on standard input.
     """
     try:
       cmd = me._opmap[op]
     except KeyError:
     """
     try:
       cmd = me._opmap[op]
     except KeyError:
-      cmd = me._default + [op] + [v for k, v in args]
+      cmd = me._default + [op] + reduce(lambda x, y: x + y,
+                                        [v for k, v in args], [])
       map = None
     else:
       map = dict(args)
       map = None
     else:
       map = dict(args)
@@ -374,11 +461,21 @@ class CommandRemoteService (BasicRemoteService):
 
   def setpasswd(me, user, passwd):
     """Service protocol: set the USER's password to PASSWD."""
 
   def setpasswd(me, user, passwd):
     """Service protocol: set the USER's password to PASSWD."""
-    me._dispatch(me._run_noout, 'set', [('u', user)], passwd + '\n')
+    me._dispatch(me._run_noout, 'set', [('u', [user])],
+                 input = passwd + '\n')
 
   def clearpasswd(me, user):
     """Service protocol: clear the USER's password."""
 
   def clearpasswd(me, user):
     """Service protocol: clear the USER's password."""
-    me._dispatch(me._run_noout, 'clear', [('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]), ('f', fields)],
+                 input = passwd + '\n')
+
+  def rmpwent(me, user):
+    """Service protocol: delete the record for USER."""
+    me._dispatch(me._run_noout, 'rmpwent', [('u', [user])])
 
 CONF.export('CommandRemoteService')
 
 
 CONF.export('CommandRemoteService')