-
Notifications
You must be signed in to change notification settings - Fork 20
/
SecurityInfo.py
389 lines (299 loc) · 13.5 KB
/
SecurityInfo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""SecurityInfo objects and access control constants.
SecurityInfo objects are used in class definitions to allow
a declarative style of associating access control information
with class attributes.
More information on using SecurityInfo and guide to Zope security
for developers can be found in the dev.zope.org "Declarative Security"
project:
http://dev.zope.org/Wikis/DevSite/Projects/DeclarativeSecurity
While SecurityInfo objects largely remove the need for Python
programmers to care about the underlying implementation, there
are several constants defined that should be used by code that
must set __roles__ attributes directly. (the constants are also
accessible from the AccessControl namespace). The defined access
control constants and their meanings are:
ACCESS_PUBLIC: accessible from restricted code and possibly
through the web (if object has a docstring)
ACCESS_PRIVATE: accessible only from python code
ACCESS_NONE: no access
"""
import sys
from logging import getLogger
from Acquisition import Implicit
from Persistence import Persistent
from AccessControl.class_init import InitializeClass
from AccessControl.ImplPython import _what_not_even_god_should_do
# always patch Persistent before ClassSecurityInfo is used
Persistent.__class_init__ = InitializeClass
LOG = getLogger('SecurityInfo')
# Security constants - these are imported into the AccessControl
# namespace and can be referenced as AccessControl.PUBLIC etc.
ACCESS_NONE = _what_not_even_god_should_do
ACCESS_PRIVATE = ()
ACCESS_PUBLIC = None
_marker = []
class SecurityInfo(Implicit):
"""Encapsulate security information."""
__security_info__ = 1
__roles__ = ACCESS_PRIVATE
def __init__(self):
self.names = {}
self.roles = {}
self._unused_protected_decorators = set()
def _setaccess(self, names, access):
for name in names:
if self.names.get(name, access) != access:
LOG.warn('Conflicting security declarations for "%s"' % name)
self._warnings = 1
self.names[name] = access
declarePublic__roles__ = ACCESS_PRIVATE
def declarePublic(self, name, *names):
"""Declare names to be publicly accessible."""
self._setaccess((name,) + names, ACCESS_PUBLIC)
declarePrivate__roles__ = ACCESS_PRIVATE
def declarePrivate(self, name, *names):
"""Declare names to be inaccessible to restricted code."""
self._setaccess((name,) + names, ACCESS_PRIVATE)
declareProtected__roles__ = ACCESS_PRIVATE
def declareProtected(self, permission_name, name, *names):
"""Declare names to be associated with a permission."""
self._setaccess((name,) + names, permission_name)
declareObjectPublic__roles__ = ACCESS_PRIVATE
def declareObjectPublic(self):
"""Declare the object to be publicly accessible."""
self._setaccess(('',), ACCESS_PUBLIC)
declareObjectPrivate__roles__ = ACCESS_PRIVATE
def declareObjectPrivate(self):
"""Declare the object to be inaccessible to restricted code."""
self._setaccess(('',), ACCESS_PRIVATE)
declareObjectProtected__roles__ = ACCESS_PRIVATE
def declareObjectProtected(self, permission_name):
"""Declare the object to be associated with a permission."""
self._setaccess(('',), permission_name)
public__roles__ = ACCESS_PRIVATE
def public(self, func):
"""Decorate a function to be publicly accessible."""
self.declarePublic(func.__name__) # NOQA: D001
return func
private__roles__ = ACCESS_PRIVATE
def private(self, func):
"""Decorate a function to be inaccessible to restricted code."""
self.declarePrivate(func.__name__) # NOQA: D001
return func
protected__roles__ = ACCESS_PRIVATE
def protected(self, permission_name):
"""Return a decorator to associate a function with a permission."""
# the decorator returned is remembered in a set and will
# remove itself upon call. self.apply will check for an empty
# set and raise an AssertionError otherwise.
key = "'%s':%s" % (permission_name, id(lambda x: x))
def decor(func):
self.declareProtected(permission_name, func.__name__) # NOQA: D001
self._unused_protected_decorators.remove(key)
return func
# make sure our key algo creates unique-enough keys
if key in self._unused_protected_decorators:
raise KeyError("Duplicate key: %s" % (key,))
self._unused_protected_decorators.add(key)
return decor
setPermissionDefault__roles__ = ACCESS_PRIVATE
def setPermissionDefault(self, permission_name, roles):
"""Declare default roles for a permission"""
rdict = {}
for role in roles:
rdict[role] = 1
if self.roles.get(permission_name, rdict) != rdict:
LOG.warn('Conflicting default role'
'declarations for permission "%s"' % permission_name)
self._warnings = 1
self.roles[permission_name] = rdict
setDefaultAccess__roles__ = ACCESS_PRIVATE
def setDefaultAccess(self, access):
"""Declare default attribute access policy.
This should be a boolean value, a map of attribute names to
booleans, or a callable (name, value) -> boolean.
"""
if isinstance(access, str):
access = access.lower()
if access == 'allow':
access = 1
elif access == 'deny':
access = 0
else:
raise ValueError("'allow' or 'deny' expected")
self.access = access
class ClassSecurityInfo(SecurityInfo):
"""Encapsulate security information for class objects."""
__roles__ = ACCESS_PRIVATE
apply__roles__ = ACCESS_PRIVATE
def apply(self, classobj):
"""Apply security information to the given class object."""
# make sure all decorators handed out by security.protected were used
if self._unused_protected_decorators:
msg = "Class '%r' has %d non-decorator security.protected calls!"
raise AssertionError(msg % (classobj,
len(self._unused_protected_decorators)))
cdict = classobj.__dict__
# Check the class for an existing __ac_permissions__ and
# incorporate that if present to support older classes or
# classes that haven't fully switched to using SecurityInfo.
if '__ac_permissions__' in cdict:
for item in cdict['__ac_permissions__']:
permission_name = item[0]
self._setaccess(item[1], permission_name)
if len(item) > 2:
self.setPermissionDefault(permission_name, item[2])
# Set __roles__ for attributes declared public or private.
# Collect protected attribute names in ac_permissions.
ac_permissions = {}
for name, access in self.names.items():
if access in (ACCESS_PRIVATE, ACCESS_PUBLIC, ACCESS_NONE):
setattr(classobj, '%s__roles__' % name, access)
else:
if access not in ac_permissions:
ac_permissions[access] = []
ac_permissions[access].append(name)
# Now transform our nested dict structure into the nested tuple
# structure expected of __ac_permissions__ attributes and set
# it on the class object.
getRoles = self.roles.get
__ac_permissions__ = []
permissions = sorted(ac_permissions.items())
for permission_name, names in permissions:
roles = getRoles(permission_name, ())
if len(roles):
entry = (permission_name, tuple(names), tuple(roles.keys()))
else:
entry = (permission_name, tuple(names))
__ac_permissions__.append(entry)
for permission_name, roles in self.roles.items():
if permission_name not in ac_permissions:
entry = (permission_name, (), tuple(roles.keys()))
__ac_permissions__.append(entry)
setattr(classobj, '__ac_permissions__', tuple(__ac_permissions__))
# Take care of default attribute access policy
access = getattr(self, 'access', _marker)
if access is not _marker:
setattr(classobj, '__allow_access_to_unprotected_subobjects__',
access)
if getattr(self, '_warnings', None):
LOG.warn('Class "%s" had conflicting '
'security declarations' % classobj.__name__)
class ClassSecurityInformation(ClassSecurityInfo):
# Default policy is disallow
access = 0
_moduleSecurity = {}
_appliedModuleSecurity = {}
def secureModule(mname, *imp):
modsec = _moduleSecurity.get(mname, None)
if modsec is None:
if mname in _appliedModuleSecurity:
return sys.modules[mname]
return # no MSI, no module
if imp:
__import__(mname, *imp)
try:
del _moduleSecurity[mname]
except KeyError:
# In case of access from multiple threads, the del might fail, but that
# is OK.
pass
module = sys.modules[mname]
modsec.apply(module.__dict__)
_appliedModuleSecurity[mname] = modsec
return module
def ModuleSecurityInfo(module_name=None):
if module_name is not None:
modsec = _moduleSecurity.get(module_name, None)
if modsec is not None:
return modsec
modsec = _appliedModuleSecurity.get(module_name, None)
if modsec is not None:
# Move security info back to to-apply dict (needed for product
# refresh). Also invoke this check for parent packages already
# applied
del _appliedModuleSecurity[module_name]
_moduleSecurity[module_name] = modsec
dot = module_name.rfind('.')
if dot > 0:
ModuleSecurityInfo(module_name[:dot])
return modsec
dot = module_name.rfind('.')
if dot > 0:
# If the module is in a package, recursively make sure
# there are security declarations for the package steps
# leading to the module
modname = module_name[dot + 1:]
pmodsec = ModuleSecurityInfo(module_name[:dot])
if modname not in pmodsec.names:
pmodsec.declarePublic(modname) # NOQA: D001
return _ModuleSecurityInfo(module_name)
class _ModuleSecurityInfo(SecurityInfo):
"""Encapsulate security information for modules."""
__roles__ = ACCESS_PRIVATE
access = False # deny by default, prevent acquiring 'access' from module
def __init__(self, module_name=None):
self.names = {}
if module_name is not None:
global _moduleSecurity
_moduleSecurity[module_name] = self
__call____roles__ = ACCESS_PRIVATE
def __call__(self, name, value):
"""Callback for __allow_access_to_unprotected_subobjects__ hook."""
access = self.names.get(name, _marker)
if access is not _marker:
return access == ACCESS_PUBLIC
return getattr(self, 'access', 0)
apply__roles__ = ACCESS_PRIVATE
def apply(self, dict):
"""Apply security information to the given module dict."""
# Start with default attribute access policy
access = getattr(self, 'access', _marker)
if access is not _marker or len(self.names):
dict['__allow_access_to_unprotected_subobjects__'] = self
if getattr(self, '_warnings', None):
LOG.warn('Module "%s" had conflicting '
'security declarations' % dict['__name__'])
declareProtected__roles__ = ACCESS_PRIVATE
def declareProtected(self, permission_name, *names):
"""Cannot declare module names protected."""
pass
declareObjectProtected__roles__ = ACCESS_PRIVATE
def declareObjectProtected(self, permission_name):
"""Cannot declare module protected."""
pass
setDefaultRoles__roles__ = ACCESS_PRIVATE
def setDefaultRoles(self, permission_name, roles):
"""Cannot set default roles for permissions in a module."""
pass
# Handy little utility functions
def allow_module(module_name):
"""Allow a module and all its contents to be used from a
restricted Script. The argument module_name may be a simple
or dotted module or package name. Note that if a package
path is given, all modules in the path will be available."""
ModuleSecurityInfo(module_name).setDefaultAccess(1)
dot = module_name.find('.')
while dot > 0:
ModuleSecurityInfo(module_name[:dot]).setDefaultAccess(1)
dot = module_name.find('.', dot + 1)
def allow_class(Class):
"""Allow a class and all of its methods to be used from a
restricted Script. The argument Class must be a class."""
Class._security = sec = ClassSecurityInfo()
sec.declareObjectPublic()
sec.setDefaultAccess(1)
sec.apply(Class)
InitializeClass(Class)