Commit | Line | Data |
---|---|---|
a2916c06 MW |
1 | ### -*-python-*- |
2 | ### | |
3 | ### Operations and policy switch | |
4 | ### | |
5 | ### (c) 2013 Mark Wooding | |
6 | ### | |
7 | ||
8 | ###----- Licensing notice --------------------------------------------------- | |
9 | ### | |
10 | ### This file is part of Chopwood: a password-changing service. | |
11 | ### | |
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. | |
16 | ### | |
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. | |
21 | ### | |
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/>. | |
25 | ||
26 | import os as OS | |
27 | ||
28 | import config as CONF; CFG = CONF.CFG | |
29 | import util as U | |
30 | ||
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. | |
35 | ### | |
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 | |
41 | ### password.) | |
42 | ||
43 | ###-------------------------------------------------------------------------- | |
44 | ### Operation protocol. | |
45 | ||
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: | |
51 | ## | |
52 | ## error Either `None' or an `ExpectedError' instance indicating what | |
53 | ## went wrong. | |
54 | ## | |
55 | ## result Either `None' or a string providing additional information | |
56 | ## about the successful completion of the operation. | |
57 | ## | |
58 | ## svc The service object on which the operation was attempted. | |
59 | ## | |
60 | ## user The user name on which the operation was attempted. | |
61 | ||
62 | class BaseOperation (object): | |
63 | """ | |
64 | Base class for individual operations. | |
65 | ||
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. | |
70 | """ | |
71 | ||
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) | |
75 | me.svc = svc | |
76 | me.user = user | |
77 | ||
78 | def perform(me): | |
79 | """Perform the operation, and return whether it was successful.""" | |
80 | ||
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 | |
84 | ||
85 | ## Perform the operation, and stash the result. | |
86 | ok = True | |
87 | try: | |
88 | try: me.result = me._perform() | |
89 | except (IOError, OSError), e: raise U.ExpectedError, (500, str(e)) | |
90 | except U.ExpectedError, e: | |
91 | me.error = e | |
92 | ok = False | |
93 | ||
94 | ## Done. | |
95 | return ok | |
96 | CONF.export('BaseOperation') | |
97 | ||
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) | |
102 | me.passwd = passwd | |
103 | def _perform(me): | |
104 | me.svc.setpasswd(me.user, me.passwd) | |
105 | CONF.export('SetOperation') | |
106 | ||
107 | class ClearOperation (BaseOperation): | |
108 | """Operation to clear a password from an account, preventing logins.""" | |
109 | def _perform(me): | |
110 | me.svc.clearpasswd(me.user) | |
111 | CONF.export('ClearOperation') | |
112 | ||
113 | class FailOperation (BaseOperation): | |
114 | """A fake operation which just raises an exception.""" | |
115 | def __init__(me, svc, user, exc): | |
116 | me.svc = svc | |
117 | me.uesr = user | |
118 | me.exc = exc | |
119 | def perform(me): | |
120 | me.result = None | |
121 | me.error = me.exc | |
122 | return False | |
123 | CONF.export('FailOperation') | |
124 | ||
125 | ###-------------------------------------------------------------------------- | |
126 | ### Requests. | |
127 | ||
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 | |
131 | ## `acct' object. | |
132 | ## | |
133 | ## Request methods are as follows. | |
134 | ## | |
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. | |
142 | ## | |
143 | ## perform() Actually perform the request. A list of completed operation | |
144 | ## objects is left in the `ops' attribute. | |
145 | ## | |
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. | |
149 | ## | |
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. | |
154 | ||
155 | class acct (U.struct): | |
156 | """A simple pairing of a service SVC and USER name.""" | |
157 | __slots__ = ['svc', 'user'] | |
158 | ||
159 | class BaseRequest (object): | |
160 | """ | |
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 | |
164 | constructed. | |
165 | """ | |
166 | INFO = {} | |
167 | def check(me): | |
168 | """ | |
169 | Check the request to make sure we actually want to proceed. | |
170 | """ | |
171 | pass | |
172 | def makeop(me, optype, svc, user, **kw): | |
173 | """ | |
174 | Hook for making operations. A policy class can substitute a | |
175 | `FailOperation' to partially disallow a request. | |
176 | """ | |
177 | return optype(svc, user, **kw) | |
178 | def perform(me): | |
179 | """ | |
180 | Perform the queued-up operations. | |
181 | """ | |
182 | for op in me.ops: op.perform() | |
183 | return me.ops | |
184 | CONF.export('BaseRequest', ExpectedError = U.ExpectedError) | |
185 | ||
186 | class SetRequest (BaseRequest): | |
187 | """ | |
188 | Request to set the password for the given ACCTS to NEW. | |
189 | ||
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. | |
193 | """ | |
194 | def __init__(me, accts, new): | |
195 | me.new = new | |
196 | me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = new) | |
197 | for acct in accts] | |
198 | def check(me): | |
199 | if me.new == '': | |
200 | raise U.ExpectedError, (400, "Empty password not permitted") | |
201 | super(SetRequest, me).check() | |
202 | CONF.export('SetRequest') | |
203 | ||
204 | class ResetRequest (BaseRequest): | |
205 | """ | |
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. | |
209 | ||
210 | ENCODING Encoding to apply to random data. | |
211 | ||
212 | PWBYTES Number of random bytes to collect. | |
213 | ||
214 | Alternatively, subclasses can override the `pwgen' method. | |
215 | """ | |
216 | ||
217 | ## Password generation parameters. | |
218 | PWBYTES = 16 | |
219 | ENCODING = 'base32' | |
220 | ||
221 | ## Additional information. | |
222 | INFO = dict(new = 'New password') | |
223 | ||
224 | def __init__(me, accts): | |
225 | me.new = me.pwgen() | |
226 | me.ops = [me.makeop(SetOperation, acct.svc, acct.user, passwd = me.new) | |
227 | for acct in accts] | |
228 | ||
229 | def pwgen(me): | |
230 | return U.ENCODINGS[me.ENCODING].encode(OS.urandom(me.PWBYTES)) \ | |
231 | .rstrip('=') | |
232 | CONF.export('ResetRequest') | |
233 | ||
234 | class ClearRequest (BaseRequest): | |
235 | """ | |
236 | Request to clear the password for the given ACCTS. | |
237 | """ | |
238 | def __init__(me, accts): | |
239 | me.ops = [me.makeop(ClearOperation, acct.svc, acct.user) | |
240 | for acct in accts] | |
241 | CONF.export('ClearRequest') | |
242 | ||
243 | ###-------------------------------------------------------------------------- | |
244 | ### Master policy switch. | |
245 | ||
246 | class polswitch (U.struct): | |
247 | __slots__ = ['set', 'reset', 'clear'] | |
248 | ||
249 | CONF.DEFAULTS.update( | |
250 | ||
251 | ## Map a request type `set', `reset', or `clear', to the appropriate | |
252 | ## request class. | |
253 | RQCLASS = polswitch(None, None, None), | |
254 | ||
255 | ## Alternatively, set this to a mixin class to apply common policy to all | |
256 | ## the kinds of requests. | |
257 | RQMIXIN = None) | |
258 | ||
259 | @CONF.hook | |
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 | |
265 | if CFG.RQMIXIN: | |
266 | cls = type('Custom%sPolicy' % op.title(), (base, CFG.RQMIXIN), {}) | |
267 | else: | |
268 | cls = base | |
269 | setattr(CFG.RQCLASS, op, cls) | |
270 | ||
271 | ## Outcomes. | |
272 | ||
273 | class outcome (U.struct): | |
274 | __slots__ = ['rc', 'nwin', 'nlose'] | |
275 | OK = 0 | |
276 | PARTIAL = 1 | |
277 | FAIL = 2 | |
278 | NOTHING = 3 | |
279 | ||
280 | class info (U.struct): | |
281 | __slots__ = ['desc', 'value'] | |
282 | ||
283 | def operate(op, accts, *args, **kw): | |
284 | """ | |
285 | Perform a request through the policy switch. | |
286 | ||
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. | |
291 | ||
292 | The return values are: | |
293 | ||
294 | * an `outcome' object holding the general outcome, and a count of the | |
295 | winning and losing operations; | |
296 | ||
297 | * a list of `info' objects holding additional information from the | |
298 | request; | |
299 | ||
300 | * the request object itself; and | |
301 | ||
302 | * a list of the individual operation objects. | |
303 | """ | |
304 | rq = getattr(CFG.RQCLASS, op)(accts, *args, **kw) | |
305 | rq.check() | |
306 | ops = rq.perform() | |
307 | nwin = nlose = 0 | |
308 | for o in ops: | |
309 | if o.error: nlose += 1 | |
310 | else: nwin += 1 | |
311 | if nwin: | |
312 | if nlose: rc = outcome.PARTIAL | |
313 | else: rc = outcome.OK | |
314 | else: | |
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 | |
319 | ||
320 | ###----- That's all, folks -------------------------------------------------- |