From 4e7866aba8adeec7d5613387d106fb1519108a25 Mon Sep 17 00:00:00 2001 Message-Id: <4e7866aba8adeec7d5613387d106fb1519108a25.1714453422.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sat, 16 Mar 2013 00:35:34 +0000 Subject: [PATCH] cgi.py, operation.py, list.fhtml: Request-level policy switch. Organization: Straylight/Edgeware From: Mark Wooding * 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 | 3 ++- list.fhtml | 17 ++++++++++------- operation.py | 39 ++++++++++++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/cgi.py b/cgi.py index 3b3d441..26295e0 100644 --- 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): """ diff --git a/list.fhtml b/list.fhtml index 3753313..735f63c 100644 --- a/list.fhtml +++ b/list.fhtml @@ -48,13 +48,16 @@ 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; }~;~] -->
-

Set a new password

+~={allowop.set}:[~;

Set a new password

@@ -92,9 +95,9 @@ return null; } } ---> +-->~2%~]~ -

Generate a new password

+~={allowop.reset}:[~;

Generate a new password

OK @@ -104,9 +107,9 @@ return check_accounts() || check_partial_passwd(); } } ---> +-->~2%~]~ -

Clear the existing passwords

+~={allowop.clear}:[~;

Clear the existing passwords

OK @@ -116,7 +119,7 @@ return check_accounts() || check_partial_passwd(); } } ---> +-->~2%~]~ diff --git a/operation.py b/operation.py index e10a1b5..9a2d1c0 100644 --- a/operation.py +++ b/operation.py @@ -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') ###-------------------------------------------------------------------------- -- [mdw]