X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/chopwood/blobdiff_plain/a2916c0635fec5b45ad742904db9f5769b48f53d..8f6848e2f6be4cde36f1e3599de0e7ef0457a30b:/operation.py?ds=sidebyside diff --git a/operation.py b/operation.py index 1184e3d..fccd172 100644 --- a/operation.py +++ b/operation.py @@ -24,6 +24,7 @@ ### . 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,16 @@ 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() + if not CFG.OPTS.ignpol: + 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 +363,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