cgi.py, operation.py, list.fhtml: Request-level policy switch.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 16 Mar 2013 00:35:34 +0000 (00:35 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 16 Mar 2013 00:58:13 +0000 (00:58 +0000)
  * Introduce a new configuration variable `ALLOWOP' with a policy flag
    for each request type;

  * have `BaseRequest.check' ensure that the corresponding policy flag
    is set;

  * export this policy switch to the template language; and

  * only show widgets for the permitted operations in the web interface.

The commands still appear in the userv/SSH interface, which is a bit
gnarly.

cgi.py
list.fhtml
operation.py

diff --git a/cgi.py b/cgi.py
index 3b3d441..26295e0 100644 (file)
--- a/cgi.py
+++ b/cgi.py
@@ -197,7 +197,8 @@ def set_template_keywords():
     package = PACKAGE,
     version = VERSION,
     script = CFG.SCRIPT_NAME,
-    static = CFG.STATIC)
+    static = CFG.STATIC,
+    allowop = CFG.ALLOWOP)
 
 class TemplateFinder (object):
   """
index 3753313..735f63c 100644 (file)
         FORMS.acct = {
           elts: ['list'],
           check: function () { return null; }
-        }
+        }~={allowop.set}:[
+
+        // Password setting is forbidden, so here's a stub function.
+        function check_partial_passwd() { return null; }~;~]
 --></script>
 </div>
 
 <div class=expand-reference>
 
-<h2>Set a new password</h2>
+~={allowop.set}:[~;<h2>Set a new password</h2>
 <table>
 <tr>
   <td class=label>
@@ -92,9 +95,9 @@
             return null;
           }
         }
---></script>
+--></script>~2%~]~
 
-<h2>Generate a new password</h2>
+~={allowop.reset}:[~;<h2>Generate a new password</h2>
 <button type=submit id=reset-submit accesskey=g
         name=%act value=reset><u>G</u>enerate</button>
 <span class=whinge id=reset-whinge>OK</span>
             return check_accounts() || check_partial_passwd();
           }
         }
---></script>
+--></script>~2%~]~
 
-<h2>Clear the existing passwords</h2>
+~={allowop.clear}:[~;<h2>Clear the existing passwords</h2>
 <button type=submit id=clear-submit accesskey=c
         name=%act value=clear><u>C</u>lear</button>
 <span class=whinge id=clear-whinge>OK</span>
             return check_accounts() || check_partial_passwd();
           }
         }
---></script>
+--></script>~2%~]~
 
 </div>
 </div>
index e10a1b5..9a2d1c0 100644 (file)
@@ -135,6 +135,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
@@ -168,29 +174,40 @@ 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 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):
@@ -201,14 +218,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):
@@ -224,6 +246,8 @@ class ResetRequest (BaseRequest):
   Alternatively, subclasses can override the `pwgen' method.
   """
 
+  OP = 'reset'
+
   ## Password generation parameters.
   PWBYTES = 16
   ENCODING = 'base32'
@@ -239,15 +263,20 @@ 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')
 
 ###--------------------------------------------------------------------------