zope.locking 3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1070 @@
1
+ ==========
2
+ System API
3
+ ==========
4
+
5
+ The central approach for the package is that locks and freeze tokens must be
6
+ created and then registered by a token utility. The tokens will not work
7
+ until they have been registered. This gives the ability to definitively know,
8
+ and thus manipulate, all active tokens in a system.
9
+
10
+ The first object we'll introduce, then, is the TokenUtility: the utility that
11
+ is responsible for the registration and the retrieving of tokens.
12
+
13
+ >>> from zope import component, interface
14
+ >>> from zope.locking import interfaces, utility, tokens
15
+ >>> util = utility.TokenUtility()
16
+ >>> from zope.interface.verify import verifyObject
17
+ >>> verifyObject(interfaces.ITokenUtility, util)
18
+ True
19
+
20
+ The utility only has a few methods--`get`, `iterForPrincipalId`,
21
+ `__iter__`, and `register`--which we will look at below. It is expected to be
22
+ persistent, and the included implementation is in fact persistent.Persistent,
23
+ and expects to be installed as a local utility. The utility needs a
24
+ connection to the database before it can register persistent tokens.
25
+
26
+ >>> from zope.locking.testing import Demo
27
+ >>> lock = tokens.ExclusiveLock(Demo(), 'Fantomas')
28
+ >>> util.register(lock)
29
+ Traceback (most recent call last):
30
+ ...
31
+ AttributeError: 'NoneType' object has no attribute 'add'
32
+
33
+ >>> conn = get_connection()
34
+ >>> conn.add(util)
35
+
36
+ If the token provides IPersistent, the utility will add it to its connection.
37
+
38
+ >>> lock._p_jar is None
39
+ True
40
+
41
+ >>> lock = util.register(lock)
42
+ >>> lock._p_jar is util._p_jar
43
+ True
44
+
45
+ >>> lock.end()
46
+ >>> lock = util.register(lock)
47
+
48
+
49
+ The standard token utility can accept tokens for any object that is adaptable
50
+ to IKeyReference.
51
+
52
+ >>> import datetime
53
+ >>> import pytz
54
+ >>> before_creation = datetime.datetime.now(pytz.utc)
55
+ >>> demo = Demo()
56
+
57
+ Now, with an instance of the demo class, it is possible to register lock and
58
+ freeze tokens for demo instances with the token utility.
59
+
60
+ As mentioned above, the general pattern for making a lock or freeze token is
61
+ to create it--at which point most of its methods and attributes are
62
+ unusable--and then to register it with the token utility. After registration,
63
+ the lock is effective and in place.
64
+
65
+ The TokenUtility can actually be used with anything that implements
66
+ zope.locking.interfaces.IAbstractToken, but we'll look at the four tokens that
67
+ come with the zope.locking package: an exclusive lock, a shared lock, a
68
+ permanent freeze, and an endable freeze.
69
+
70
+ Exclusive Locks
71
+ ===============
72
+
73
+ Exclusive locks are tokens that are owned by a single principal. No principal
74
+ may be added or removed: the lock token must be ended and another started for
75
+ another principal to get the benefits of the lock (whatever they have been
76
+ configured to be).
77
+
78
+ Here's an example of creating and registering an exclusive lock: the principal
79
+ with an id of 'john' locks the demo object.
80
+
81
+ >>> lock = tokens.ExclusiveLock(demo, 'john')
82
+ >>> res = util.register(lock)
83
+ >>> res is lock
84
+ True
85
+
86
+ The lock token is now in effect. Registering the token (the lock) fired an
87
+ ITokenStartedEvent, which we'll look at now.
88
+
89
+ (Note that this example uses an events list to look at events that have fired.
90
+ This is simply a list whose `append` method has been added as a subscriber
91
+ to the zope.event.subscribers list. It's included as a global when this file
92
+ is run as a test.)
93
+
94
+ >>> from zope.component.eventtesting import events
95
+ >>> ev = events[-1]
96
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
97
+ True
98
+ >>> ev.object is lock
99
+ True
100
+
101
+ Now that the lock token is created and registered, the token utility knows
102
+ about it. The utilities `get` method simply returns the active token for an
103
+ object or None--it never returns an ended token, and in fact none of the
104
+ utility methods do.
105
+
106
+ >>> util.get(demo) is lock
107
+ True
108
+ >>> util.get(Demo()) is None
109
+ True
110
+
111
+ Note that `get` accepts alternate defaults, like a dictionary.get:
112
+
113
+ >>> util.get(Demo(), util) is util
114
+ True
115
+
116
+ The `iterForPrincipalId` method returns an iterator of active locks for the
117
+ given principal id.
118
+
119
+ >>> list(util.iterForPrincipalId('john')) == [lock]
120
+ True
121
+ >>> list(util.iterForPrincipalId('mary')) == []
122
+ True
123
+
124
+ The util's `__iter__` method simply iterates over all active (non-ended)
125
+ tokens.
126
+
127
+ >>> list(util) == [lock]
128
+ True
129
+
130
+ The token utility disallows registration of multiple active tokens for the
131
+ same object.
132
+
133
+ >>> util.register(tokens.ExclusiveLock(demo, 'mary'))
134
+ ... # doctest: +ELLIPSIS
135
+ Traceback (most recent call last):
136
+ ...
137
+ zope.locking.interfaces.RegistrationError: ...
138
+ >>> util.register(tokens.SharedLock(demo, ('mary', 'jane')))
139
+ ... # doctest: +ELLIPSIS
140
+ Traceback (most recent call last):
141
+ ...
142
+ zope.locking.interfaces.RegistrationError: ...
143
+ >>> util.register(tokens.Freeze(demo))
144
+ ... # doctest: +ELLIPSIS
145
+ Traceback (most recent call last):
146
+ ...
147
+ zope.locking.interfaces.RegistrationError: ...
148
+
149
+ It's also worth looking at the lock token itself. The registered lock token
150
+ implements IExclusiveLock.
151
+
152
+ >>> verifyObject(interfaces.IExclusiveLock, lock)
153
+ True
154
+
155
+ It provides a number of capabilities. Arguably the most important attribute is
156
+ whether the token is in effect or not: `ended`. This token is active, so it
157
+ has not yet ended:
158
+
159
+ >>> lock.ended is None
160
+ True
161
+
162
+ When it does end, the ended attribute is a datetime in UTC of when the token
163
+ ended. We'll demonstrate that below.
164
+
165
+ Later, the `creation`, `expiration`, `duration`, and `remaining_duration` will
166
+ be important; for now we merely note their existence.
167
+
168
+ >>> before_creation <= lock.started <= datetime.datetime.now(pytz.utc)
169
+ True
170
+ >>> lock.expiration is None # == forever
171
+ True
172
+ >>> lock.duration is None # == forever
173
+ True
174
+ >>> lock.remaining_duration is None # == forever
175
+ True
176
+
177
+ The `end` method and the related ending and expiration attributes are all part
178
+ of the IEndable interface--an interface that not all tokens must implement,
179
+ as we will also discuss later.
180
+
181
+ >>> interfaces.IEndable.providedBy(lock)
182
+ True
183
+
184
+ The `context` and `__parent__` attributes point to the locked object--demo in
185
+ our case. `context` is the intended standard API for obtaining the object,
186
+ but `__parent__` is important for the Zope 3 security set up, as discussed
187
+ towards the end of this document.
188
+
189
+ >>> lock.context is demo
190
+ True
191
+ >>> lock.__parent__ is demo # important for security
192
+ True
193
+
194
+ Registering the lock with the token utility set the utility attribute and
195
+ initialized the started attribute to the datetime that the lock began. The
196
+ utility attribute should never be set by any code other than the token
197
+ utility.
198
+
199
+ >>> lock.utility is util
200
+ True
201
+
202
+ Tokens always provide a `principal_ids` attribute that provides an iterable of
203
+ the principals that are part of a token. In our case, this is an exclusive
204
+ lock for 'john', so the value is simple.
205
+
206
+ >>> sorted(lock.principal_ids)
207
+ ['john']
208
+
209
+ The only method on a basic token like the exclusive lock is `end`. Calling it
210
+ without arguments permanently and explicitly ends the life of the token.
211
+
212
+ >>> lock.end()
213
+
214
+ Like registering a token, ending a token fires an event.
215
+
216
+ >>> ev = events[-1]
217
+ >>> verifyObject(interfaces.ITokenEndedEvent, ev)
218
+ True
219
+ >>> ev.object is lock
220
+ True
221
+
222
+ It affects attributes on the token. Again, the most important of these is
223
+ ended, which is now the datetime of ending.
224
+
225
+ >>> lock.ended >= lock.started
226
+ True
227
+ >>> lock.remaining_duration == datetime.timedelta()
228
+ True
229
+
230
+ It also affects queries of the token utility.
231
+
232
+ >>> util.get(demo) is None
233
+ True
234
+ >>> list(util.iterForPrincipalId('john')) == []
235
+ True
236
+ >>> list(util) == []
237
+ True
238
+
239
+ Don't try to end an already-ended token.
240
+
241
+ >>> lock.end()
242
+ Traceback (most recent call last):
243
+ ...
244
+ zope.locking.interfaces.EndedError
245
+
246
+ The other way of ending a token is with an expiration datetime. As we'll see,
247
+ one of the most important caveats about working with timeouts is that a token
248
+ that expires because of a timeout does not fire any expiration event. It
249
+ simply starts providing the `expiration` value for the `ended` attribute.
250
+
251
+ >>> one = datetime.timedelta(hours=1)
252
+ >>> two = datetime.timedelta(hours=2)
253
+ >>> three = datetime.timedelta(hours=3)
254
+ >>> four = datetime.timedelta(hours=4)
255
+ >>> lock = util.register(tokens.ExclusiveLock(demo, 'john', three))
256
+ >>> lock.duration
257
+ datetime.timedelta(seconds=10800)
258
+ >>> three >= lock.remaining_duration >= two
259
+ True
260
+ >>> lock.ended is None
261
+ True
262
+ >>> util.get(demo) is lock
263
+ True
264
+ >>> list(util.iterForPrincipalId('john')) == [lock]
265
+ True
266
+ >>> list(util) == [lock]
267
+ True
268
+
269
+ The expiration time of an endable token is always the creation date plus the
270
+ timeout.
271
+
272
+ >>> lock.expiration == lock.started + lock.duration
273
+ True
274
+ >>> ((before_creation + three) <=
275
+ ... (lock.expiration) <= # this value is the expiration date
276
+ ... (before_creation + four))
277
+ True
278
+
279
+ Expirations can be changed while a lock is still active, using any of
280
+ the `expiration`, `remaining_duration` or `duration` attributes. All changes
281
+ fire events. First we'll change the expiration attribute.
282
+
283
+ >>> lock.expiration = lock.started + one
284
+ >>> lock.expiration == lock.started + one
285
+ True
286
+ >>> lock.duration == one
287
+ True
288
+ >>> ev = events[-1]
289
+ >>> verifyObject(interfaces.IExpirationChangedEvent, ev)
290
+ True
291
+ >>> ev.object is lock
292
+ True
293
+ >>> ev.old == lock.started + three
294
+ True
295
+
296
+ Next we'll change the duration attribute.
297
+
298
+ >>> lock.duration = four
299
+ >>> lock.duration
300
+ datetime.timedelta(seconds=14400)
301
+ >>> four >= lock.remaining_duration >= three
302
+ True
303
+ >>> ev = events[-1]
304
+ >>> verifyObject(interfaces.IExpirationChangedEvent, ev)
305
+ True
306
+ >>> ev.object is lock
307
+ True
308
+ >>> ev.old == lock.started + one
309
+ True
310
+
311
+ Now we'll hack our code to make it think that it is two hours later, and then
312
+ check and modify the remaining_duration attribute.
313
+
314
+ >>> def hackNow():
315
+ ... return (datetime.datetime.now(pytz.utc) +
316
+ ... datetime.timedelta(hours=2))
317
+ ...
318
+ >>> import zope.locking.utils
319
+ >>> oldNow = zope.locking.utils.now
320
+ >>> zope.locking.utils.now = hackNow # make code think it's 2 hours later
321
+ >>> lock.duration
322
+ datetime.timedelta(seconds=14400)
323
+ >>> two >= lock.remaining_duration >= one
324
+ True
325
+ >>> lock.remaining_duration -= one
326
+ >>> one >= lock.remaining_duration >= datetime.timedelta()
327
+ True
328
+ >>> three + datetime.timedelta(minutes=1) >= lock.duration >= three
329
+ True
330
+ >>> ev = events[-1]
331
+ >>> verifyObject(interfaces.IExpirationChangedEvent, ev)
332
+ True
333
+ >>> ev.object is lock
334
+ True
335
+ >>> ev.old == lock.started + four
336
+ True
337
+
338
+ Now, we'll hack our code to make it think that it's a day later. It is very
339
+ important to remember that a lock ending with a timeout ends silently--that
340
+ is, no event is fired.
341
+
342
+ >>> def hackNow():
343
+ ... return (
344
+ ... datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1))
345
+ ...
346
+ >>> zope.locking.utils.now = hackNow # make code think it is a day later
347
+ >>> lock.ended == lock.expiration
348
+ True
349
+ >>> util.get(demo) is None
350
+ True
351
+ >>> util.get(demo, util) is util # alternate default works
352
+ True
353
+ >>> lock.remaining_duration == datetime.timedelta()
354
+ True
355
+ >>> lock.end()
356
+ Traceback (most recent call last):
357
+ ...
358
+ zope.locking.interfaces.EndedError
359
+
360
+ Once a lock has ended, the timeout can no longer be changed.
361
+
362
+ >>> lock.duration = datetime.timedelta(days=2)
363
+ Traceback (most recent call last):
364
+ ...
365
+ zope.locking.interfaces.EndedError
366
+
367
+ We'll undo the hacks, and also end the lock (that is no longer ended once
368
+ the hack is finished).
369
+
370
+ >>> zope.locking.utils.now = oldNow # undo the hack
371
+ >>> lock.end()
372
+
373
+ Make sure to register tokens. Creating a lock but not registering it puts it
374
+ in a state that is not fully initialized.
375
+
376
+ >>> lock = tokens.ExclusiveLock(demo, 'john')
377
+ >>> lock.started # doctest: +ELLIPSIS
378
+ Traceback (most recent call last):
379
+ ...
380
+ zope.locking.interfaces.UnregisteredError: ...
381
+ >>> lock.ended # doctest: +ELLIPSIS
382
+ Traceback (most recent call last):
383
+ ...
384
+ zope.locking.interfaces.UnregisteredError: ...
385
+
386
+
387
+ Shared Locks
388
+ ============
389
+
390
+ Shared locks are very similar to exclusive locks, but take an iterable of one
391
+ or more principals at creation, and can have principals added or removed while
392
+ they are active.
393
+
394
+ In this example, also notice a convenient characteristic of the TokenUtility
395
+ `register` method: it also returns the token, so creation, registration, and
396
+ variable assignment can be chained, if desired.
397
+
398
+ >>> lock = util.register(tokens.SharedLock(demo, ('john', 'mary')))
399
+ >>> ev = events[-1]
400
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
401
+ True
402
+ >>> ev.object is lock
403
+ True
404
+
405
+ Here, principals with ids of 'john' and 'mary' have locked the demo object.
406
+ The returned token implements ISharedLock and provides a superset of the
407
+ IExclusiveLock capabilities. These next operations should all look familiar
408
+ from the discussion of the ExclusiveLock tokens above.
409
+
410
+ >>> verifyObject(interfaces.ISharedLock, lock)
411
+ True
412
+ >>> lock.context is demo
413
+ True
414
+ >>> lock.__parent__ is demo # important for security
415
+ True
416
+ >>> lock.utility is util
417
+ True
418
+ >>> sorted(lock.principal_ids)
419
+ ['john', 'mary']
420
+ >>> lock.ended is None
421
+ True
422
+ >>> before_creation <= lock.started <= datetime.datetime.now(pytz.utc)
423
+ True
424
+ >>> lock.expiration is None
425
+ True
426
+ >>> lock.duration is None
427
+ True
428
+ >>> lock.remaining_duration is None
429
+ True
430
+ >>> lock.end()
431
+ >>> lock.ended >= lock.started
432
+ True
433
+
434
+ As mentioned, though, the SharedLock capabilities are a superset of the
435
+ ExclusiveLock ones. There are two extra methods: `add` and `remove`. These
436
+ are able to add and remove principal ids as shared owners of the lock token.
437
+
438
+ >>> lock = util.register(tokens.SharedLock(demo, ('john',)))
439
+ >>> sorted(lock.principal_ids)
440
+ ['john']
441
+ >>> lock.add(('mary',))
442
+ >>> sorted(lock.principal_ids)
443
+ ['john', 'mary']
444
+ >>> lock.add(('alice',))
445
+ >>> sorted(lock.principal_ids)
446
+ ['alice', 'john', 'mary']
447
+ >>> lock.remove(('john',))
448
+ >>> sorted(lock.principal_ids)
449
+ ['alice', 'mary']
450
+ >>> lock.remove(('mary',))
451
+ >>> sorted(lock.principal_ids)
452
+ ['alice']
453
+
454
+ Adding and removing principals fires appropriate events, as you might expect.
455
+
456
+ >>> lock.add(('mary',))
457
+ >>> sorted(lock.principal_ids)
458
+ ['alice', 'mary']
459
+ >>> ev = events[-1]
460
+ >>> verifyObject(interfaces.IPrincipalsChangedEvent, ev)
461
+ True
462
+ >>> ev.object is lock
463
+ True
464
+ >>> sorted(ev.old)
465
+ ['alice']
466
+ >>> lock.remove(('alice',))
467
+ >>> sorted(lock.principal_ids)
468
+ ['mary']
469
+ >>> ev = events[-1]
470
+ >>> verifyObject(interfaces.IPrincipalsChangedEvent, ev)
471
+ True
472
+ >>> ev.object is lock
473
+ True
474
+ >>> sorted(ev.old)
475
+ ['alice', 'mary']
476
+
477
+ Removing all participants in a lock ends the lock, making it ended.
478
+
479
+ >>> lock.remove(('mary',))
480
+ >>> sorted(lock.principal_ids)
481
+ []
482
+ >>> lock.ended >= lock.started
483
+ True
484
+ >>> ev = events[-1]
485
+ >>> verifyObject(interfaces.IPrincipalsChangedEvent, ev)
486
+ True
487
+ >>> ev.object is lock
488
+ True
489
+ >>> sorted(ev.old)
490
+ ['mary']
491
+ >>> ev = events[-2]
492
+ >>> verifyObject(interfaces.ITokenEndedEvent, ev)
493
+ True
494
+ >>> ev.object is lock
495
+ True
496
+
497
+ As you might expect, trying to add (or remove!) users from an ended lock is
498
+ an error.
499
+
500
+ >>> lock.add(('john',))
501
+ Traceback (most recent call last):
502
+ ...
503
+ zope.locking.interfaces.EndedError
504
+ >>> lock.remove(('john',))
505
+ Traceback (most recent call last):
506
+ ...
507
+ zope.locking.interfaces.EndedError
508
+
509
+ The token utility keeps track of shared lock tokens the same as exclusive lock
510
+ tokens. Here's a quick summary in code.
511
+
512
+ >>> lock = util.register(tokens.SharedLock(demo, ('john', 'mary')))
513
+ >>> util.get(demo) is lock
514
+ True
515
+ >>> list(util.iterForPrincipalId('john')) == [lock]
516
+ True
517
+ >>> list(util.iterForPrincipalId('mary')) == [lock]
518
+ True
519
+ >>> list(util) == [lock]
520
+ True
521
+ >>> util.register(tokens.ExclusiveLock(demo, 'mary'))
522
+ ... # doctest: +ELLIPSIS
523
+ Traceback (most recent call last):
524
+ ...
525
+ zope.locking.interfaces.RegistrationError: ...
526
+ >>> util.register(tokens.SharedLock(demo, ('mary', 'jane')))
527
+ ... # doctest: +ELLIPSIS
528
+ Traceback (most recent call last):
529
+ ...
530
+ zope.locking.interfaces.RegistrationError: ...
531
+ >>> util.register(tokens.Freeze(demo))
532
+ ... # doctest: +ELLIPSIS
533
+ Traceback (most recent call last):
534
+ ...
535
+ zope.locking.interfaces.RegistrationError: ...
536
+ >>> lock.end()
537
+
538
+ Timed expirations work the same as with exclusive locks. We won't repeat that
539
+ here, though look in the annoying.txt document in this package for the actual
540
+ repeated tests.
541
+
542
+
543
+ EndableFreezes
544
+ ==============
545
+
546
+ An endable freeze token is similar to a lock token except that it grants the
547
+ 'lock' to no one.
548
+
549
+ >>> token = util.register(tokens.EndableFreeze(demo))
550
+ >>> verifyObject(interfaces.IEndableFreeze, token)
551
+ True
552
+ >>> ev = events[-1]
553
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
554
+ True
555
+ >>> ev.object is token
556
+ True
557
+ >>> sorted(token.principal_ids)
558
+ []
559
+ >>> token.end()
560
+
561
+ Endable freezes are otherwise identical to exclusive locks. See annoying.txt
562
+ for the comprehensive copy-and-paste tests duplicating the exclusive lock
563
+ tests. Notice that an EndableFreeze will never be a part of an iterable of
564
+ tokens by principal: by definition, a freeze is associated with no principals.
565
+
566
+
567
+ Freezes
568
+ =======
569
+
570
+ Freezes are similar to EndableFreezes, except they are not endable. They are
571
+ intended to be used by system level operations that should permanently disable
572
+ certain changes, such as changes to the content of an archived object version.
573
+
574
+ Creating them is the same...
575
+
576
+ >>> token = util.register(tokens.Freeze(demo))
577
+ >>> verifyObject(interfaces.IFreeze, token)
578
+ True
579
+ >>> ev = events[-1]
580
+ >>> verifyObject(interfaces.ITokenStartedEvent, ev)
581
+ True
582
+ >>> ev.object is token
583
+ True
584
+ >>> sorted(token.principal_ids)
585
+ []
586
+
587
+ But they can't go away...
588
+
589
+ >>> token.end()
590
+ Traceback (most recent call last):
591
+ ...
592
+ AttributeError: 'Freeze' object has no attribute 'end'
593
+
594
+ They also do not have expirations, duration, remaining durations, or ended
595
+ dates. They are permanent, unless you go into the database to muck with
596
+ implementation-specific data structures.
597
+
598
+ There is no API way to end a Freeze. We'll need to make a new object for the
599
+ rest of our demonstrations, and this token will exist through the
600
+ remaining examples.
601
+
602
+ >>> old_demo = demo
603
+ >>> demo = Demo()
604
+
605
+ ===============================
606
+ User API, Adapters and Security
607
+ ===============================
608
+
609
+ The API discussed so far makes few concessions to some of the common use cases
610
+ for locking. Here are some particular needs as yet unfulfilled by the
611
+ discussion so far.
612
+
613
+ - It should be possible to allow and deny per object whether users may
614
+ create and register tokens for the object.
615
+
616
+ - It should often be easier to register an endable token than a permanent
617
+ token.
618
+
619
+ - All users should be able to unlock or modify some aspects of their own
620
+ tokens, or remove their own participation in shared tokens; but it should be
621
+ possible to restrict access to ending tokens that users do not own (often
622
+ called "breaking locks").
623
+
624
+ In the context of the Zope 3 security model, the first two needs are intended
625
+ to be addressed by the ITokenBroker interface, and associated adapter; the last
626
+ need is intended to be addressed by the ITokenHandler, and associated
627
+ adapters.
628
+
629
+
630
+ TokenBrokers
631
+ ============
632
+
633
+ Token brokers adapt an object, which is the object whose tokens are
634
+ brokered, and uses this object as a security context. They provide a few
635
+ useful methods: `lock`, `lockShared`, `freeze`, and `get`. The TokenBroker
636
+ expects to be a trusted adapter.
637
+
638
+ lock
639
+ ----
640
+
641
+ The lock method creates and registers an exclusive lock. Without arguments,
642
+ it tries to create it for the user in the current interaction.
643
+
644
+ This won't work without an interaction, of course. Notice that we start the
645
+ example by registering the utility. We would normally be required to put the
646
+ utility in a site package, so that it would be persistent, but for this
647
+ demonstration we are simplifying the registration.
648
+
649
+ >>> component.provideUtility(util, provides=interfaces.ITokenUtility)
650
+
651
+ >>> import zope.interface.interfaces
652
+ >>> @interface.implementer(zope.interface.interfaces.IComponentLookup)
653
+ ... @component.adapter(interface.Interface)
654
+ ... def siteManager(obj):
655
+ ... return component.getGlobalSiteManager()
656
+ ...
657
+ >>> component.provideAdapter(siteManager)
658
+
659
+ >>> from zope.locking import adapters
660
+ >>> component.provideAdapter(adapters.TokenBroker)
661
+ >>> broker = interfaces.ITokenBroker(demo)
662
+ >>> broker.lock()
663
+ Traceback (most recent call last):
664
+ ...
665
+ ValueError
666
+ >>> broker.lock('joe')
667
+ Traceback (most recent call last):
668
+ ...
669
+ zope.locking.interfaces.ParticipationError
670
+
671
+ If we set up an interaction with one participation, the lock will have a
672
+ better chance.
673
+
674
+ >>> import zope.security.interfaces
675
+ >>> @interface.implementer(zope.security.interfaces.IPrincipal)
676
+ ... class DemoPrincipal(object):
677
+ ... def __init__(self, id, title=None, description=None):
678
+ ... self.id = id
679
+ ... self.title = title
680
+ ... self.description = description
681
+ ...
682
+ >>> joe = DemoPrincipal('joe')
683
+ >>> import zope.security.management
684
+ >>> @interface.implementer(zope.security.interfaces.IParticipation)
685
+ ... class DemoParticipation(object):
686
+ ... def __init__(self, principal):
687
+ ... self.principal = principal
688
+ ... self.interaction = None
689
+ ...
690
+ >>> zope.security.management.endInteraction()
691
+ >>> zope.security.management.newInteraction(DemoParticipation(joe))
692
+
693
+ >>> token = broker.lock()
694
+ >>> interfaces.IExclusiveLock.providedBy(token)
695
+ True
696
+ >>> token.context is demo
697
+ True
698
+ >>> token.__parent__ is demo
699
+ True
700
+ >>> sorted(token.principal_ids)
701
+ ['joe']
702
+ >>> token.started is not None
703
+ True
704
+ >>> util.get(demo) is token
705
+ True
706
+ >>> token.end()
707
+
708
+ You can only specify principals that are in the current interaction.
709
+
710
+ >>> token = broker.lock('joe')
711
+ >>> sorted(token.principal_ids)
712
+ ['joe']
713
+ >>> token.end()
714
+ >>> broker.lock('mary')
715
+ Traceback (most recent call last):
716
+ ...
717
+ zope.locking.interfaces.ParticipationError
718
+
719
+ The method can take a duration.
720
+
721
+ >>> token = broker.lock(duration=two)
722
+ >>> token.duration == two
723
+ True
724
+ >>> token.end()
725
+
726
+ If the interaction has more than one principal, a principal (in the
727
+ interaction) must be specified.
728
+
729
+ >>> mary = DemoPrincipal('mary')
730
+ >>> participation = DemoParticipation(mary)
731
+ >>> zope.security.management.getInteraction().add(participation)
732
+ >>> broker.lock()
733
+ Traceback (most recent call last):
734
+ ...
735
+ ValueError
736
+ >>> broker.lock('susan')
737
+ Traceback (most recent call last):
738
+ ...
739
+ zope.locking.interfaces.ParticipationError
740
+ >>> token = broker.lock('joe')
741
+ >>> sorted(token.principal_ids)
742
+ ['joe']
743
+ >>> token.end()
744
+ >>> token = broker.lock('mary')
745
+ >>> sorted(token.principal_ids)
746
+ ['mary']
747
+ >>> token.end()
748
+ >>> zope.security.management.endInteraction()
749
+
750
+ lockShared
751
+ ----------
752
+
753
+ The `lockShared` method has similar characteristics, except that it can handle
754
+ multiple principals.
755
+
756
+ Without an interaction, principals are either not found, or not part of the
757
+ interaction:
758
+
759
+ >>> broker.lockShared()
760
+ Traceback (most recent call last):
761
+ ...
762
+ ValueError
763
+ >>> broker.lockShared(('joe',))
764
+ Traceback (most recent call last):
765
+ ...
766
+ zope.locking.interfaces.ParticipationError
767
+
768
+ With an interaction, the principals get the lock by default.
769
+
770
+ >>> zope.security.management.newInteraction(DemoParticipation(joe))
771
+
772
+ >>> token = broker.lockShared()
773
+ >>> interfaces.ISharedLock.providedBy(token)
774
+ True
775
+ >>> token.context is demo
776
+ True
777
+ >>> token.__parent__ is demo
778
+ True
779
+ >>> sorted(token.principal_ids)
780
+ ['joe']
781
+ >>> token.started is not None
782
+ True
783
+ >>> util.get(demo) is token
784
+ True
785
+ >>> token.end()
786
+
787
+ You can only specify principals that are in the current interaction.
788
+
789
+ >>> token = broker.lockShared(('joe',))
790
+ >>> sorted(token.principal_ids)
791
+ ['joe']
792
+ >>> token.end()
793
+ >>> broker.lockShared(('mary',))
794
+ Traceback (most recent call last):
795
+ ...
796
+ zope.locking.interfaces.ParticipationError
797
+
798
+ The method can take a duration.
799
+
800
+ >>> token = broker.lockShared(duration=two)
801
+ >>> token.duration == two
802
+ True
803
+ >>> token.end()
804
+
805
+ If the interaction has more than one principal, all are included, unless some
806
+ are singled out.
807
+
808
+ >>> participation = DemoParticipation(mary)
809
+ >>> zope.security.management.getInteraction().add(participation)
810
+ >>> token = broker.lockShared()
811
+ >>> sorted(token.principal_ids)
812
+ ['joe', 'mary']
813
+ >>> token.end()
814
+ >>> token = broker.lockShared(('joe',))
815
+ >>> sorted(token.principal_ids)
816
+ ['joe']
817
+ >>> token.end()
818
+ >>> token = broker.lockShared(('mary',))
819
+ >>> sorted(token.principal_ids)
820
+ ['mary']
821
+ >>> token.end()
822
+ >>> zope.security.management.endInteraction()
823
+
824
+ freeze
825
+ ------
826
+
827
+ The `freeze` method allows users to create an endable freeze. It has no
828
+ requirements on the interaction. It should be protected carefully, from a
829
+ security perspective.
830
+
831
+ >>> token = broker.freeze()
832
+ >>> interfaces.IEndableFreeze.providedBy(token)
833
+ True
834
+ >>> token.context is demo
835
+ True
836
+ >>> token.__parent__ is demo
837
+ True
838
+ >>> sorted(token.principal_ids)
839
+ []
840
+ >>> token.started is not None
841
+ True
842
+ >>> util.get(demo) is token
843
+ True
844
+ >>> token.end()
845
+
846
+ The method can take a duration.
847
+
848
+ >>> token = broker.freeze(duration=two)
849
+ >>> token.duration == two
850
+ True
851
+ >>> token.end()
852
+
853
+ get
854
+ ---
855
+
856
+ The `get` method is exactly equivalent to the token utility's get method:
857
+ it returns the current active token for the object, or None. It is useful
858
+ for protected code, since utilities typically do not get security assertions,
859
+ and this method can get its security assertions from the object, which is
860
+ often the right place.
861
+
862
+ Again, the TokenBroker does embody some policy; if it is not good policy for
863
+ your application, build your own interfaces and adapters that do.
864
+
865
+
866
+ TokenHandlers
867
+ =============
868
+
869
+ TokenHandlers are useful for endable tokens with one or more principals--that
870
+ is, locks, but not freezes. They are intended to be protected with a lower
871
+ external security permission then the usual token methods and attributes, and
872
+ then impose their own checks on the basis of the current interaction. They are
873
+ very much policy, and other approaches may be useful. They are intended to be
874
+ registered as trusted adapters.
875
+
876
+ For exclusive locks and shared locks, then, we have token handlers.
877
+ Generally, token handlers give access to all of the same capabilities as their
878
+ corresponding tokens, with the following additional constraints and
879
+ capabilities:
880
+
881
+ - `expiration`, `duration`, and `remaining_duration` all may be set only if
882
+ all the principals in the current interaction are owners of the wrapped
883
+ token; and
884
+
885
+ - `release` removes some or all of the principals in the interaction if all
886
+ the principals in the current interaction are owners of the wrapped token.
887
+
888
+ Note that `end` is unaffected: this is effectively "break lock", while
889
+ `release` is effectively "unlock". Permissions should be set accordingly.
890
+
891
+ Shared lock handlers have two additional methods that are discussed in their
892
+ section.
893
+
894
+ ExclusiveLockHandlers
895
+ ---------------------
896
+
897
+ Given the general constraints described above, exclusive lock handlers will
898
+ generally only allow access to their special capabilities if the operation
899
+ is in an interaction with only the lock owner.
900
+
901
+ >>> zope.security.management.newInteraction(DemoParticipation(joe))
902
+ >>> component.provideAdapter(adapters.ExclusiveLockHandler)
903
+ >>> lock = broker.lock()
904
+ >>> handler = interfaces.IExclusiveLockHandler(lock)
905
+ >>> verifyObject(interfaces.IExclusiveLockHandler, handler)
906
+ True
907
+ >>> handler.__parent__ is lock
908
+ True
909
+ >>> handler.expiration is None
910
+ True
911
+ >>> handler.duration = two
912
+ >>> lock.duration == two
913
+ True
914
+ >>> handler.expiration = handler.started + three
915
+ >>> lock.expiration == handler.started + three
916
+ True
917
+ >>> handler.remaining_duration = two
918
+ >>> lock.remaining_duration <= two
919
+ True
920
+ >>> handler.release()
921
+ >>> handler.ended >= handler.started
922
+ True
923
+ >>> lock.ended >= lock.started
924
+ True
925
+ >>> lock = util.register(tokens.ExclusiveLock(demo, 'mary'))
926
+ >>> handler = interfaces.ITokenHandler(lock) # for joe's interaction still
927
+ >>> handler.duration = two # doctest: +ELLIPSIS
928
+ Traceback (most recent call last):
929
+ ...
930
+ zope.locking.interfaces.ParticipationError: ...
931
+ >>> handler.expiration = handler.started + three # doctest: +ELLIPSIS
932
+ Traceback (most recent call last):
933
+ ...
934
+ zope.locking.interfaces.ParticipationError: ...
935
+ >>> handler.remaining_duration = two # doctest: +ELLIPSIS
936
+ Traceback (most recent call last):
937
+ ...
938
+ zope.locking.interfaces.ParticipationError: ...
939
+ >>> handler.release() # doctest: +ELLIPSIS
940
+ Traceback (most recent call last):
941
+ ...
942
+ zope.locking.interfaces.ParticipationError: ...
943
+ >>> lock.end()
944
+
945
+ SharedLockHandlers
946
+ ------------------
947
+
948
+ Shared lock handlers let anyone who is an owner of a token set the expiration,
949
+ duration, and remaining_duration values. This is a 'get out of the way' policy
950
+ that relies on social interactions to make sure all the participants are
951
+ represented as they want. Other policies could be written in other adapters.
952
+
953
+ >>> component.provideAdapter(adapters.SharedLockHandler)
954
+ >>> lock = util.register(tokens.SharedLock(demo, ('joe', 'mary')))
955
+ >>> handler = interfaces.ITokenHandler(lock) # for joe's interaction still
956
+ >>> verifyObject(interfaces.ISharedLockHandler, handler)
957
+ True
958
+ >>> handler.__parent__ is lock
959
+ True
960
+ >>> handler.expiration is None
961
+ True
962
+ >>> handler.duration = two
963
+ >>> lock.duration == two
964
+ True
965
+ >>> handler.expiration = handler.started + three
966
+ >>> lock.expiration == handler.started + three
967
+ True
968
+ >>> handler.remaining_duration = two
969
+ >>> lock.remaining_duration <= two
970
+ True
971
+ >>> sorted(handler.principal_ids)
972
+ ['joe', 'mary']
973
+ >>> handler.release()
974
+ >>> sorted(handler.principal_ids)
975
+ ['mary']
976
+ >>> handler.duration = two # doctest: +ELLIPSIS
977
+ Traceback (most recent call last):
978
+ ...
979
+ zope.locking.interfaces.ParticipationError: ...
980
+ >>> handler.expiration = handler.started + three # doctest: +ELLIPSIS
981
+ Traceback (most recent call last):
982
+ ...
983
+ zope.locking.interfaces.ParticipationError: ...
984
+ >>> handler.remaining_duration = two # doctest: +ELLIPSIS
985
+ Traceback (most recent call last):
986
+ ...
987
+ zope.locking.interfaces.ParticipationError: ...
988
+ >>> handler.release() # doctest: +ELLIPSIS
989
+ Traceback (most recent call last):
990
+ ...
991
+ zope.locking.interfaces.ParticipationError: ...
992
+
993
+ The shared lock handler adds two additional methods to a standard handler:
994
+ `join` and `add`. They do similar jobs, but are separate to allow separate
995
+ security settings for each. The `join` method lets some or all of the
996
+ principals in the current interaction join.
997
+
998
+ >>> handler.join()
999
+ >>> sorted(handler.principal_ids)
1000
+ ['joe', 'mary']
1001
+ >>> handler.join(('susan',))
1002
+ Traceback (most recent call last):
1003
+ ...
1004
+ zope.locking.interfaces.ParticipationError
1005
+
1006
+ The `add` method lets any principal ids be added to the lock, but all
1007
+ principals in the current interaction must be a part of the lock.
1008
+
1009
+ >>> handler.add(('susan',))
1010
+ >>> sorted(handler.principal_ids)
1011
+ ['joe', 'mary', 'susan']
1012
+ >>> handler.release()
1013
+ >>> handler.add('jake') # doctest: +ELLIPSIS
1014
+ Traceback (most recent call last):
1015
+ ...
1016
+ zope.locking.interfaces.ParticipationError: ...
1017
+ >>> lock.end()
1018
+ >>> zope.security.management.endInteraction()
1019
+
1020
+
1021
+ Warnings
1022
+ ========
1023
+
1024
+ * The token utility will register a token for an object if it can. It does not
1025
+ check to see if it is actually the local token utility for the given object.
1026
+ This should be arranged by clients of the token utility, and verified
1027
+ externally if desired.
1028
+
1029
+ * Tokens are stored as keys in BTrees, and therefore must be orderable
1030
+ (i.e., they must implement __cmp__).
1031
+
1032
+
1033
+ Intended Security Configuration
1034
+ ===============================
1035
+
1036
+ Utilities are typically unprotected in Zope 3--or more accurately, have
1037
+ no security assertions and are used with no security proxy--and the token
1038
+ utility expects to be so. As such, the broker and handler objects are
1039
+ expected to be the objects used by view code, and so associated with security
1040
+ proxies. All should have appropriate __parent__ attribute values. The
1041
+ ability to mutate the tokens--`end`, `add` and `remove` methods, for
1042
+ instance--should be protected with an administrator-type permission such as
1043
+ 'zope.Security'. Setting the timeout properties on the token should be
1044
+ protected in the same way. Setting the handlers attributes can have a less
1045
+ restrictive setting, since they calculate security themselves on the basis of
1046
+ lock membership.
1047
+
1048
+ On the adapter, the `end` method should be protected with the same or
1049
+ similar permission. Calling methods such as lock and lockShared should be
1050
+ protected with something like 'zope.ManageContent'. Getting attributes should
1051
+ be 'zope.View' or 'zope.Public', and unlocking and setting the timeouts, since
1052
+ they are already protected to make sure the principal is a member of the lock,
1053
+ can probably be 'zope.Public'.
1054
+
1055
+ These settings can be abused relatively easily to create an insecure
1056
+ system--for instance, if a user can get an adapter to IPrincipalLockable for
1057
+ another principal--but are a reasonable start.
1058
+
1059
+ >>> broker.__parent__ is demo
1060
+ True
1061
+ >>> handler.__parent__ is lock
1062
+ True
1063
+
1064
+
1065
+ Random Thoughts
1066
+ ===============
1067
+
1068
+ As a side effect of the design, it is conceivable that multiple lock utilities
1069
+ could be in use at once, governing different aspects of an object; however,
1070
+ this may never itself be of use.