chiark / gitweb /
agpl.py: Python 2.5 compatibility.
[chopwood] / operation.py
index 1184e3d17996f6406ce29bf79c8873291dd76632..50d79527f923e4b96108ac4387dbcbaaf35133cf 100644 (file)
@@ -24,6 +24,7 @@
 ### <http://www.gnu.org/licenses/>.
 
 import os as OS
+import syslog as L
 
 import config as CONF; CFG = CONF.CFG
 import util as U
@@ -40,6 +41,16 @@ import util as U
 ### semantics may be that the services are all assigned the /same/ random
 ### password.)
 
+###--------------------------------------------------------------------------
+### Some utilities.
+
+OPS = ['set', 'reset', 'clear']
+## A list of the available operations.
+
+class polswitch (U.struct):
+  """A small structure holding a value for each operation."""
+  __slots__ = OPS
+
 ###--------------------------------------------------------------------------
 ### Operation protocol.
 
@@ -114,7 +125,7 @@ class FailOperation (BaseOperation):
   """A fake operation which just raises an exception."""
   def __init__(me, svc, user, exc):
     me.svc = svc
-    me.uesr = user
+    me.user = user
     me.exc = exc
   def perform(me):
     me.result = None
@@ -125,6 +136,12 @@ CONF.export('FailOperation')
 ###--------------------------------------------------------------------------
 ### Requests.
 
+CONF.DEFAULTS.update(
+
+  ## A boolean switch for each operation to tell us whether it's allowed.  By
+  ## default, they all are.
+  ALLOWOP = polswitch(**dict((i, True) for i in OPS)))
+
 ## A request object represents a single user-level operation targetted at
 ## multiple services.  The user might be known under a different alias by
 ## each service, so requests operate on service/user pairs, bundled in an
@@ -158,29 +175,43 @@ class acct (U.struct):
 
 class BaseRequest (object):
   """
-  Base class for requests, provides basic protocol.  In particular, it
-  provides an empty `INFO' map, a trivial `check' method, and the obvious
-  `perform' method which assumes that the `ops' list has already been
-  constructed.
+  Base class for requests, provides basic protocol.
+
+  It provides an empty `INFO' map; a simple `check' method which checks the
+  operation name (in the class attribute `OP') against the configured policy
+  `CFG'ALLOWOP'; and the obvious `perform' method which assumes that the
+  `ops' list has already been constructed.
   """
+
   INFO = {}
+  ## A dictionary describing the additional information returned by the
+  ## request: it maps attribute names to human-readable descriptions.
+
   def check(me):
     """
     Check the request to make sure we actually want to proceed.
     """
-    pass
+    if not getattr(CFG.ALLOWOP, me.OP):
+      raise U.ExpectedError, \
+          (401, "Operation `%s' forbidden by policy" % me.OP)
+
   def makeop(me, optype, svc, user, **kw):
     """
     Hook for making operations.  A policy class can substitute a
     `FailOperation' to partially disallow a request.
     """
     return optype(svc, user, **kw)
+
+  def describe(me):
+    return me.OP
+
   def perform(me):
     """
     Perform the queued-up operations.
     """
     for op in me.ops: op.perform()
     return me.ops
+
 CONF.export('BaseRequest', ExpectedError = U.ExpectedError)
 
 class SetRequest (BaseRequest):
@@ -191,14 +222,19 @@ class SetRequest (BaseRequest):
   inspection.  The `check' method ensures that the password is not empty, but
   imposes no other policy restrictions.
   """
+
+  OP = 'set'
+
   def __init__(me, accts, new):
     me.new = new
     me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = new)
               for acct in accts]
+
   def check(me):
     if me.new == '':
       raise U.ExpectedError, (400, "Empty password not permitted")
     super(SetRequest, me).check()
+
 CONF.export('SetRequest')
 
 class ResetRequest (BaseRequest):
@@ -214,6 +250,8 @@ class ResetRequest (BaseRequest):
   Alternatively, subclasses can override the `pwgen' method.
   """
 
+  OP = 'reset'
+
   ## Password generation parameters.
   PWBYTES = 16
   ENCODING = 'base32'
@@ -229,28 +267,30 @@ class ResetRequest (BaseRequest):
   def pwgen(me):
     return U.ENCODINGS[me.ENCODING].encode(OS.urandom(me.PWBYTES)) \
            .rstrip('=')
+
 CONF.export('ResetRequest')
 
 class ClearRequest (BaseRequest):
   """
   Request to clear the password for the given ACCTS.
   """
+
+  OP = 'clear'
+
   def __init__(me, accts):
     me.ops = [me.makeop(ClearOperation, acct.svc, acct.user)
               for acct in accts]
+
 CONF.export('ClearRequest')
 
 ###--------------------------------------------------------------------------
 ### Master policy switch.
 
-class polswitch (U.struct):
-  __slots__ = ['set', 'reset', 'clear']
-
 CONF.DEFAULTS.update(
 
   ## Map a request type `set', `reset', or `clear', to the appropriate
   ## request class.
-  RQCLASS = polswitch(None, None, None),
+  RQCLASS = polswitch(**dict((i, None) for i in OPS)),
 
   ## Alternatively, set this to a mixin class to apply common policy to all
   ## the kinds of requests.
@@ -302,7 +342,15 @@ def operate(op, accts, *args, **kw):
     * a list of the individual operation objects.
   """
   rq = getattr(CFG.RQCLASS, op)(accts, *args, **kw)
-  rq.check()
+  desc = rq.describe()
+  try:
+    rq.check()
+  except U.ExpectedError, e:
+    L.syslog('REFUSE %s %s: %s' %
+             (desc,
+              ', '.join(['%s@%s' % (o.user, o.svc.name) for o in rq.ops]),
+              e))
+    raise
   ops = rq.perform()
   nwin = nlose = 0
   for o in ops:
@@ -314,6 +362,12 @@ def operate(op, accts, *args, **kw):
   else:
     if nlose: rc = outcome.FAIL
     else: rc = outcome.NOTHING
+  L.syslog('%s %s: %s' % (['OK', 'PARTIAL', 'FAIL', 'NOTHING'][rc],
+                            desc,
+                            '; '.join(['%s@%s %s' % (o.user, o.svc.name,
+                                                     not o.error and 'OK' or
+                                                     'ERR %s' % o.error)
+                                       for o in ops])))
   ii = [info(v, getattr(rq, k)) for k, v in rq.INFO.iteritems()]
   return outcome(rc, nwin, nlose), ii, rq, ops