3 ### Operations and policy switch
5 ### (c) 2013 Mark Wooding
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of Chopwood: a password-changing service.
12 ### Chopwood is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU Affero General Public License as
14 ### published by the Free Software Foundation; either version 3 of the
15 ### License, or (at your option) any later version.
17 ### Chopwood is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU Affero General Public License for more details.
22 ### You should have received a copy of the GNU Affero General Public
23 ### License along with Chopwood; if not, see
24 ### <http://www.gnu.org/licenses/>.
28 import config as CONF; CFG = CONF.CFG
31 ### The objective here is to be able to insert a policy layer between the UI,
32 ### which is where the user makes requests to change a bunch of accounts, and
33 ### the backends, which make requested changes without thinking too much
34 ### about whether they're a good idea.
36 ### Here, we convert between (nearly) user-level /requests/, which involve
37 ### doing things to multiple service/user pairs, and /operations/, which
38 ### represent a single change to be made to a particular service. (This is
39 ### slightly nontrivial in the case of reset requests, since the intended
40 ### semantics may be that the services are all assigned the /same/ random
43 ###--------------------------------------------------------------------------
46 OPS = ['set', 'reset', 'clear']
47 ## A list of the available operations.
49 class polswitch (U.struct):
50 """A small structure holding a value for each operation."""
53 ###--------------------------------------------------------------------------
54 ### Operation protocol.
56 ## An operation deals with a single service/user pair. The protocol works
57 ## like this. The constructor is essentially passive, storing information
58 ## about the operation but not actually performing it. The `perform' method
59 ## attempts to perform the operation, and stores information about the
60 ## outcome in attributes:
62 ## error Either `None' or an `ExpectedError' instance indicating what
65 ## result Either `None' or a string providing additional information
66 ## about the successful completion of the operation.
68 ## svc The service object on which the operation was attempted.
70 ## user The user name on which the operation was attempted.
72 class BaseOperation (object):
74 Base class for individual operations.
76 This is where the basic operation protocol is implemented. Subclasses
77 should store any additional attributes necessary during initialization, and
78 implement a method `_perform' which takes no parameters, performs the
79 operation, and returns any necessary result.
82 def __init__(me, svc, user, *args, **kw):
83 """Initialize the operation, storing the SVC and USER in attributes."""
84 super(BaseOperation, me).__init__(*args, **kw)
89 """Perform the operation, and return whether it was successful."""
91 ## Set up the `result' and `error' slots here, rather than earlier, to
92 ## catch callers referencing them too early.
93 me.result = me.error = None
95 ## Perform the operation, and stash the result.
98 try: me.result = me._perform()
99 except (IOError, OSError), e: raise U.ExpectedError, (500, str(e))
100 except U.ExpectedError, e:
106 CONF.export('BaseOperation')
108 class SetOperation (BaseOperation):
109 """Operation to set a given password on an account."""
110 def __init__(me, svc, user, passwd, *args, **kw):
111 super(SetOperation, me).__init__(svc, user, *args, **kw)
114 me.svc.setpasswd(me.user, me.passwd)
115 CONF.export('SetOperation')
117 class ClearOperation (BaseOperation):
118 """Operation to clear a password from an account, preventing logins."""
120 me.svc.clearpasswd(me.user)
121 CONF.export('ClearOperation')
123 class FailOperation (BaseOperation):
124 """A fake operation which just raises an exception."""
125 def __init__(me, svc, user, exc):
133 CONF.export('FailOperation')
135 ###--------------------------------------------------------------------------
138 ## A request object represents a single user-level operation targetted at
139 ## multiple services. The user might be known under a different alias by
140 ## each service, so requests operate on service/user pairs, bundled in an
143 ## Request methods are as follows.
145 ## check() Verify that the request complies with policy. Note that
146 ## checking that any particular user has authority over the
147 ## necessary accounts has already been done. One might want to
148 ## check that the passwords are sufficiently long and
149 ## complicated (though that rapidly becomes problematic, and I
150 ## don't really recommend it) or that particular services are or
151 ## aren't processed at the same time.
153 ## perform() Actually perform the request. A list of completed operation
154 ## objects is left in the `ops' attribute.
156 ## Performing the operation may leave additional information in attributes.
157 ## The `INFO' class attribute contains a dictionary mapping attribute names
158 ## to human-readable descriptions of this additional information.
160 ## Note that the request object has a fairly free hand in choosing how to
161 ## implement the request in terms of operations. In particular, it might
162 ## process additional services. Callers must not assume that they can
163 ## predict what the resulting operations list will look like.
165 class acct (U.struct):
166 """A simple pairing of a service SVC and USER name."""
167 __slots__ = ['svc', 'user']
169 class BaseRequest (object):
171 Base class for requests, provides basic protocol. In particular, it
172 provides an empty `INFO' map, a trivial `check' method, and the obvious
173 `perform' method which assumes that the `ops' list has already been
179 Check the request to make sure we actually want to proceed.
182 def makeop(me, optype, svc, user, **kw):
184 Hook for making operations. A policy class can substitute a
185 `FailOperation' to partially disallow a request.
187 return optype(svc, user, **kw)
190 Perform the queued-up operations.
192 for op in me.ops: op.perform()
194 CONF.export('BaseRequest', ExpectedError = U.ExpectedError)
196 class SetRequest (BaseRequest):
198 Request to set the password for the given ACCTS to NEW.
200 The new password is kept in the object's `new' attribute for easy
201 inspection. The `check' method ensures that the password is not empty, but
202 imposes no other policy restrictions.
204 def __init__(me, accts, new):
206 me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = new)
210 raise U.ExpectedError, (400, "Empty password not permitted")
211 super(SetRequest, me).check()
212 CONF.export('SetRequest')
214 class ResetRequest (BaseRequest):
216 Request to set the password for the given ACCTS to something new but
217 nonspeific. The new password is generated based on a number of class
218 attributes which subclasses can usefully override.
220 ENCODING Encoding to apply to random data.
222 PWBYTES Number of random bytes to collect.
224 Alternatively, subclasses can override the `pwgen' method.
227 ## Password generation parameters.
231 ## Additional information.
232 INFO = dict(new = 'New password')
234 def __init__(me, accts):
236 me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = me.new)
240 return U.ENCODINGS[me.ENCODING].encode(OS.urandom(me.PWBYTES)) \
242 CONF.export('ResetRequest')
244 class ClearRequest (BaseRequest):
246 Request to clear the password for the given ACCTS.
248 def __init__(me, accts):
249 me.ops = [me.makeop(ClearOperation, acct.svc, acct.user)
251 CONF.export('ClearRequest')
253 ###--------------------------------------------------------------------------
254 ### Master policy switch.
256 CONF.DEFAULTS.update(
258 ## Map a request type `set', `reset', or `clear', to the appropriate
260 RQCLASS = polswitch(**dict((i, None) for i in OPS)),
262 ## Alternatively, set this to a mixin class to apply common policy to all
263 ## the kinds of requests.
267 def set_policy_classes():
268 for op, base in [('set', SetRequest),
269 ('reset', ResetRequest),
270 ('clear', ClearRequest)]:
271 if getattr(CFG.RQCLASS, op): continue
273 cls = type('Custom%sPolicy' % op.title(), (base, CFG.RQMIXIN), {})
276 setattr(CFG.RQCLASS, op, cls)
280 class outcome (U.struct):
281 __slots__ = ['rc', 'nwin', 'nlose']
287 class info (U.struct):
288 __slots__ = ['desc', 'value']
290 def operate(op, accts, *args, **kw):
292 Perform a request through the policy switch.
294 The operation may be one of `set', `reset' or `clear'. An instance of the
295 appropriate request class is constructed, and additional arguments are
296 passed directly to the request class constructor; the request is checked
297 for policy compliance; and then performed.
299 The return values are:
301 * an `outcome' object holding the general outcome, and a count of the
302 winning and losing operations;
304 * a list of `info' objects holding additional information from the
307 * the request object itself; and
309 * a list of the individual operation objects.
311 rq = getattr(CFG.RQCLASS, op)(accts, *args, **kw)
316 if o.error: nlose += 1
319 if nlose: rc = outcome.PARTIAL
320 else: rc = outcome.OK
322 if nlose: rc = outcome.FAIL
323 else: rc = outcome.NOTHING
324 ii = [info(v, getattr(rq, k)) for k, v in rq.INFO.iteritems()]
325 return outcome(rc, nwin, nlose), ii, rq, ops
327 ###----- That's all, folks --------------------------------------------------