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 ###--------------------------------------------------------------------------
44 ### Operation protocol.
46 ## An operation deals with a single service/user pair. The protocol works
47 ## like this. The constructor is essentially passive, storing information
48 ## about the operation but not actually performing it. The `perform' method
49 ## attempts to perform the operation, and stores information about the
50 ## outcome in attributes:
52 ## error Either `None' or an `ExpectedError' instance indicating what
55 ## result Either `None' or a string providing additional information
56 ## about the successful completion of the operation.
58 ## svc The service object on which the operation was attempted.
60 ## user The user name on which the operation was attempted.
62 class BaseOperation (object):
64 Base class for individual operations.
66 This is where the basic operation protocol is implemented. Subclasses
67 should store any additional attributes necessary during initialization, and
68 implement a method `_perform' which takes no parameters, performs the
69 operation, and returns any necessary result.
72 def __init__(me, svc, user, *args, **kw):
73 """Initialize the operation, storing the SVC and USER in attributes."""
74 super(BaseOperation, me).__init__(*args, **kw)
79 """Perform the operation, and return whether it was successful."""
81 ## Set up the `result' and `error' slots here, rather than earlier, to
82 ## catch callers referencing them too early.
83 me.result = me.error = None
85 ## Perform the operation, and stash the result.
88 try: me.result = me._perform()
89 except (IOError, OSError), e: raise U.ExpectedError, (500, str(e))
90 except U.ExpectedError, e:
96 CONF.export('BaseOperation')
98 class SetOperation (BaseOperation):
99 """Operation to set a given password on an account."""
100 def __init__(me, svc, user, passwd, *args, **kw):
101 super(SetOperation, me).__init__(svc, user, *args, **kw)
104 me.svc.setpasswd(me.user, me.passwd)
105 CONF.export('SetOperation')
107 class ClearOperation (BaseOperation):
108 """Operation to clear a password from an account, preventing logins."""
110 me.svc.clearpasswd(me.user)
111 CONF.export('ClearOperation')
113 class FailOperation (BaseOperation):
114 """A fake operation which just raises an exception."""
115 def __init__(me, svc, user, exc):
123 CONF.export('FailOperation')
125 ###--------------------------------------------------------------------------
128 ## A request object represents a single user-level operation targetted at
129 ## multiple services. The user might be known under a different alias by
130 ## each service, so requests operate on service/user pairs, bundled in an
133 ## Request methods are as follows.
135 ## check() Verify that the request complies with policy. Note that
136 ## checking that any particular user has authority over the
137 ## necessary accounts has already been done. One might want to
138 ## check that the passwords are sufficiently long and
139 ## complicated (though that rapidly becomes problematic, and I
140 ## don't really recommend it) or that particular services are or
141 ## aren't processed at the same time.
143 ## perform() Actually perform the request. A list of completed operation
144 ## objects is left in the `ops' attribute.
146 ## Performing the operation may leave additional information in attributes.
147 ## The `INFO' class attribute contains a dictionary mapping attribute names
148 ## to human-readable descriptions of this additional information.
150 ## Note that the request object has a fairly free hand in choosing how to
151 ## implement the request in terms of operations. In particular, it might
152 ## process additional services. Callers must not assume that they can
153 ## predict what the resulting operations list will look like.
155 class acct (U.struct):
156 """A simple pairing of a service SVC and USER name."""
157 __slots__ = ['svc', 'user']
159 class BaseRequest (object):
161 Base class for requests, provides basic protocol. In particular, it
162 provides an empty `INFO' map, a trivial `check' method, and the obvious
163 `perform' method which assumes that the `ops' list has already been
169 Check the request to make sure we actually want to proceed.
172 def makeop(me, optype, svc, user, **kw):
174 Hook for making operations. A policy class can substitute a
175 `FailOperation' to partially disallow a request.
177 return optype(svc, user, **kw)
180 Perform the queued-up operations.
182 for op in me.ops: op.perform()
184 CONF.export('BaseRequest', ExpectedError = U.ExpectedError)
186 class SetRequest (BaseRequest):
188 Request to set the password for the given ACCTS to NEW.
190 The new password is kept in the object's `new' attribute for easy
191 inspection. The `check' method ensures that the password is not empty, but
192 imposes no other policy restrictions.
194 def __init__(me, accts, new):
196 me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = new)
200 raise U.ExpectedError, (400, "Empty password not permitted")
201 super(SetRequest, me).check()
202 CONF.export('SetRequest')
204 class ResetRequest (BaseRequest):
206 Request to set the password for the given ACCTS to something new but
207 nonspeific. The new password is generated based on a number of class
208 attributes which subclasses can usefully override.
210 ENCODING Encoding to apply to random data.
212 PWBYTES Number of random bytes to collect.
214 Alternatively, subclasses can override the `pwgen' method.
217 ## Password generation parameters.
221 ## Additional information.
222 INFO = dict(new = 'New password')
224 def __init__(me, accts):
226 me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = me.new)
230 return U.ENCODINGS[me.ENCODING].encode(OS.urandom(me.PWBYTES)) \
232 CONF.export('ResetRequest')
234 class ClearRequest (BaseRequest):
236 Request to clear the password for the given ACCTS.
238 def __init__(me, accts):
239 me.ops = [me.makeop(ClearOperation, acct.svc, acct.user)
241 CONF.export('ClearRequest')
243 ###--------------------------------------------------------------------------
244 ### Master policy switch.
246 class polswitch (U.struct):
247 __slots__ = ['set', 'reset', 'clear']
249 CONF.DEFAULTS.update(
251 ## Map a request type `set', `reset', or `clear', to the appropriate
253 RQCLASS = polswitch(None, None, None),
255 ## Alternatively, set this to a mixin class to apply common policy to all
256 ## the kinds of requests.
260 def set_policy_classes():
261 for op, base in [('set', SetRequest),
262 ('reset', ResetRequest),
263 ('clear', ClearRequest)]:
264 if getattr(CFG.RQCLASS, op): continue
266 cls = type('Custom%sPolicy' % op.title(), (base, CFG.RQMIXIN), {})
269 setattr(CFG.RQCLASS, op, cls)
273 class outcome (U.struct):
274 __slots__ = ['rc', 'nwin', 'nlose']
280 class info (U.struct):
281 __slots__ = ['desc', 'value']
283 def operate(op, accts, *args, **kw):
285 Perform a request through the policy switch.
287 The operation may be one of `set', `reset' or `clear'. An instance of the
288 appropriate request class is constructed, and additional arguments are
289 passed directly to the request class constructor; the request is checked
290 for policy compliance; and then performed.
292 The return values are:
294 * an `outcome' object holding the general outcome, and a count of the
295 winning and losing operations;
297 * a list of `info' objects holding additional information from the
300 * the request object itself; and
302 * a list of the individual operation objects.
304 rq = getattr(CFG.RQCLASS, op)(accts, *args, **kw)
309 if o.error: nlose += 1
312 if nlose: rc = outcome.PARTIAL
313 else: rc = outcome.OK
315 if nlose: rc = outcome.FAIL
316 else: rc = outcome.NOTHING
317 ii = [info(v, getattr(rq, k)) for k, v in rq.INFO.iteritems()]
318 return outcome(rc, nwin, nlose), ii, rq, ops
320 ###----- That's all, folks --------------------------------------------------