• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / AccessControl / 10579446198

27 Aug 2024 01:47PM UTC coverage: 81.555%. First build
10579446198

Pull #155

github

dataflake
- apply latest meta/config templates
Pull Request #155: Apply latest meta/config templates

998 of 1520 branches covered (65.66%)

Branch coverage included in aggregate %.

90 of 91 new or added lines in 10 files covered. (98.9%)

5055 of 5902 relevant lines covered (85.65%)

5.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.39
/src/AccessControl/SecurityInfo.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE
11
#
12
##############################################################################
13
"""SecurityInfo objects and access control constants.
6✔
14

15
   SecurityInfo objects are used in class definitions to allow
16
   a declarative style of associating access control information
17
   with class attributes.
18

19
   More information on using SecurityInfo and guide to Zope security
20
   for developers can be found in the dev.zope.org "Declarative Security"
21
   project:
22

23
   http://dev.zope.org/Wikis/DevSite/Projects/DeclarativeSecurity
24

25
   While SecurityInfo objects largely remove the need for Python
26
   programmers to care about the underlying implementation, there
27
   are several constants defined that should be used by code that
28
   must set __roles__ attributes directly. (the constants are also
29
   accessible from the AccessControl namespace). The defined access
30
   control constants and their meanings are:
31

32
   ACCESS_PUBLIC:  accessible from restricted code and possibly
33
                   through the web (if object has a docstring)
34

35
   ACCESS_PRIVATE: accessible only from python code
36

37
   ACCESS_NONE:    no access
38

39
"""
40

41
import sys
6✔
42
from logging import getLogger
6✔
43

44
from Acquisition import Implicit
6✔
45
from Persistence import Persistent
6✔
46

47
from AccessControl.class_init import InitializeClass
6✔
48
from AccessControl.ImplPython import _what_not_even_god_should_do
6✔
49

50

51
# always patch Persistent before ClassSecurityInfo is used
52
Persistent.__class_init__ = InitializeClass
6✔
53

54
LOG = getLogger('SecurityInfo')
6✔
55

56
# Security constants - these are imported into the AccessControl
57
# namespace and can be referenced as AccessControl.PUBLIC etc.
58

59
ACCESS_NONE = _what_not_even_god_should_do
6✔
60
ACCESS_PRIVATE = ()
6✔
61
ACCESS_PUBLIC = None
6✔
62

63
_marker = []
6✔
64

65

66
class SecurityInfo(Implicit):
6✔
67
    """Encapsulate security information."""
68

69
    __security_info__ = 1
6✔
70

71
    __roles__ = ACCESS_PRIVATE
6✔
72

73
    def __init__(self):
6✔
74
        self.names = {}
6✔
75
        self.roles = {}
6✔
76
        self._unused_protected_decorators = set()
6✔
77

78
    def _setaccess(self, names, access):
6✔
79
        for name in names:
6✔
80
            if self.names.get(name, access) != access:
6!
81
                LOG.warning(
×
82
                    f'Conflicting security declarations for {name!r}')
83
                self._warnings = 1
×
84
            self.names[name] = access
6✔
85

86
    declarePublic__roles__ = ACCESS_PRIVATE
6✔
87

88
    def declarePublic(self, name, *names):
6✔
89
        """Declare names to be publicly accessible."""
90
        self._setaccess((name,) + names, ACCESS_PUBLIC)
6✔
91

92
    declarePrivate__roles__ = ACCESS_PRIVATE
6✔
93

94
    def declarePrivate(self, name, *names):
6✔
95
        """Declare names to be inaccessible to restricted code."""
96
        self._setaccess((name,) + names, ACCESS_PRIVATE)
6✔
97

98
    declareProtected__roles__ = ACCESS_PRIVATE
6✔
99

100
    def declareProtected(self, permission_name, name, *names):
6✔
101
        """Declare names to be associated with a permission."""
102
        self._setaccess((name,) + names, permission_name)
6✔
103

104
    declareObjectPublic__roles__ = ACCESS_PRIVATE
6✔
105

106
    def declareObjectPublic(self):
6✔
107
        """Declare the object to be publicly accessible."""
108
        self._setaccess(('',), ACCESS_PUBLIC)
×
109

110
    declareObjectPrivate__roles__ = ACCESS_PRIVATE
6✔
111

112
    def declareObjectPrivate(self):
6✔
113
        """Declare the object to be inaccessible to restricted code."""
114
        self._setaccess(('',), ACCESS_PRIVATE)
×
115

116
    declareObjectProtected__roles__ = ACCESS_PRIVATE
6✔
117

118
    def declareObjectProtected(self, permission_name):
6✔
119
        """Declare the object to be associated with a permission."""
120
        self._setaccess(('',), permission_name)
×
121

122
    public__roles__ = ACCESS_PRIVATE
6✔
123

124
    def public(self, func):
6✔
125
        """Decorate a function to be publicly accessible."""
126
        self.declarePublic(func.__name__)  # NOQA: D001
6✔
127
        return func
6✔
128

129
    private__roles__ = ACCESS_PRIVATE
6✔
130

131
    def private(self, func):
6✔
132
        """Decorate a function to be inaccessible to restricted code."""
133
        self.declarePrivate(func.__name__)  # NOQA: D001
6✔
134
        return func
6✔
135

136
    protected__roles__ = ACCESS_PRIVATE
6✔
137

138
    def protected(self, permission_name):
6✔
139
        """Return a decorator to associate a function with a permission."""
140
        # the decorator returned is remembered in a set and will
141
        # remove itself upon call. self.apply will check for an empty
142
        # set and raise an AssertionError otherwise.
143
        key = f"'{permission_name}':{id(lambda x: x)}"
6!
144

145
        def decor(func):
6✔
146
            self.declareProtected(permission_name, func.__name__)  # NOQA: D001
6✔
147
            self._unused_protected_decorators.remove(key)
6✔
148
            return func
6✔
149
        # make sure our key algo creates unique-enough keys
150
        if key in self._unused_protected_decorators:
6!
NEW
151
            raise KeyError(f"Duplicate key: {key}")
×
152
        self._unused_protected_decorators.add(key)
6✔
153
        return decor
6✔
154

155
    setPermissionDefault__roles__ = ACCESS_PRIVATE
6✔
156

157
    def setPermissionDefault(self, permission_name, roles):
6✔
158
        """Declare default roles for a permission"""
159
        rdict = {}
6✔
160
        for role in roles:
6✔
161
            rdict[role] = 1
6✔
162
        if self.roles.get(permission_name, rdict) != rdict:
6!
163
            LOG.warning('Conflicting default role'
×
164
                        ' declarations for permission "%s"' % permission_name)
165
            self._warnings = 1
×
166
        self.roles[permission_name] = rdict
6✔
167

168
    setDefaultAccess__roles__ = ACCESS_PRIVATE
6✔
169

170
    def setDefaultAccess(self, access):
6✔
171
        """Declare default attribute access policy.
172

173
        This should be a boolean value, a map of attribute names to
174
        booleans, or a callable (name, value) -> boolean.
175
        """
176
        if isinstance(access, str):
6!
177
            access = access.lower()
6✔
178
            if access == 'allow':
6!
179
                access = 1
6✔
180
            elif access == 'deny':
×
181
                access = 0
×
182
            else:
183
                raise ValueError("'allow' or 'deny' expected")
×
184
        self.access = access
6✔
185

186

187
class ClassSecurityInfo(SecurityInfo):
6✔
188
    """Encapsulate security information for class objects."""
189

190
    __roles__ = ACCESS_PRIVATE
6✔
191

192
    apply__roles__ = ACCESS_PRIVATE
6✔
193

194
    def apply(self, classobj):
6✔
195
        """Apply security information to the given class object."""
196

197
        # make sure all decorators handed out by security.protected were used
198
        if self._unused_protected_decorators:
6✔
199
            msg = "Class '%r' has %d non-decorator security.protected calls!"
6✔
200
            raise AssertionError(msg % (classobj,
201
                                 len(self._unused_protected_decorators)))
202

203
        cdict = classobj.__dict__
6✔
204

205
        # Check the class for an existing __ac_permissions__ and
206
        # incorporate that if present to support older classes or
207
        # classes that haven't fully switched to using SecurityInfo.
208
        if '__ac_permissions__' in cdict:
6✔
209
            for item in cdict['__ac_permissions__']:
6!
210
                permission_name = item[0]
×
211
                self._setaccess(item[1], permission_name)
×
212
                if len(item) > 2:
×
213
                    self.setPermissionDefault(permission_name, item[2])
×
214

215
        # Set __roles__ for attributes declared public or private.
216
        # Collect protected attribute names in ac_permissions.
217
        ac_permissions = {}
6✔
218
        for name, access in self.names.items():
6✔
219
            if access in (ACCESS_PRIVATE, ACCESS_PUBLIC, ACCESS_NONE):
6✔
220
                setattr(classobj, '%s__roles__' % name, access)
6✔
221
            else:
222
                if access not in ac_permissions:
6✔
223
                    ac_permissions[access] = []
6✔
224
                ac_permissions[access].append(name)
6✔
225

226
        # Now transform our nested dict structure into the nested tuple
227
        # structure expected of __ac_permissions__ attributes and set
228
        # it on the class object.
229
        getRoles = self.roles.get
6✔
230
        __ac_permissions__ = []
6✔
231
        permissions = sorted(ac_permissions.items())
6✔
232
        for permission_name, names in permissions:
6✔
233
            roles = getRoles(permission_name, ())
6✔
234
            if len(roles):
6✔
235
                entry = (permission_name, tuple(names), tuple(roles.keys()))
6✔
236
            else:
237
                entry = (permission_name, tuple(names))
6✔
238
            __ac_permissions__.append(entry)
6✔
239
        for permission_name, roles in self.roles.items():
6✔
240
            if permission_name not in ac_permissions:
6✔
241
                entry = (permission_name, (), tuple(roles.keys()))
6✔
242
                __ac_permissions__.append(entry)
6✔
243
        setattr(classobj, '__ac_permissions__', tuple(__ac_permissions__))
6✔
244

245
        # Take care of default attribute access policy
246
        access = getattr(self, 'access', _marker)
6✔
247
        if access is not _marker:
6!
248
            setattr(classobj, '__allow_access_to_unprotected_subobjects__',
×
249
                    access)
250

251
        if getattr(self, '_warnings', None):
6!
252
            LOG.warning('Class "%s" had conflicting'
×
253
                        ' security declarations' % classobj.__name__)
254

255

256
class ClassSecurityInformation(ClassSecurityInfo):
6✔
257
    # Default policy is disallow
258
    access = 0
6✔
259

260

261
_moduleSecurity = {}
6✔
262
_appliedModuleSecurity = {}
6✔
263

264

265
def secureModule(mname, *imp):
6✔
266
    modsec = _moduleSecurity.get(mname, None)
6✔
267
    if modsec is None:
6✔
268
        if mname in _appliedModuleSecurity:
6✔
269
            return sys.modules[mname]
6✔
270
        return  # no MSI, no module
6✔
271

272
    if imp:
6✔
273
        __import__(mname, *imp)
6✔
274
    module = sys.modules[mname]
6✔
275
    modsec.apply(module.__dict__)
6✔
276
    _appliedModuleSecurity[mname] = modsec
6✔
277
    try:
6✔
278
        del _moduleSecurity[mname]
6✔
279
    except KeyError:
6✔
280
        # In case of access from multiple threads, the del might fail, but that
281
        # is OK.
282
        pass
6✔
283
    return module
6✔
284

285

286
def ModuleSecurityInfo(module_name=None):
6✔
287
    if module_name is not None:
6!
288
        modsec = _moduleSecurity.get(module_name, None)
6✔
289
        if modsec is not None:
6✔
290
            return modsec
6✔
291
        modsec = _appliedModuleSecurity.get(module_name, None)
6✔
292
        if modsec is not None:
6✔
293
            # Move security info back to to-apply dict (needed for product
294
            # refresh). Also invoke this check for parent packages already
295
            # applied
296
            del _appliedModuleSecurity[module_name]
6✔
297
            _moduleSecurity[module_name] = modsec
6✔
298
            dot = module_name.rfind('.')
6✔
299
            if dot > 0:
6✔
300
                ModuleSecurityInfo(module_name[:dot])
6✔
301
            return modsec
6✔
302
        dot = module_name.rfind('.')
6✔
303
        if dot > 0:
6✔
304
            # If the module is in a package, recursively make sure
305
            # there are security declarations for the package steps
306
            # leading to the module
307
            modname = module_name[dot + 1:]
6✔
308
            pmodsec = ModuleSecurityInfo(module_name[:dot])
6✔
309
            if modname not in pmodsec.names:
6!
310
                pmodsec.declarePublic(modname)  # NOQA: D001
6✔
311
    return _ModuleSecurityInfo(module_name)
6✔
312

313

314
class _ModuleSecurityInfo(SecurityInfo):
6✔
315
    """Encapsulate security information for modules."""
316

317
    __roles__ = ACCESS_PRIVATE
6✔
318
    access = False  # deny by default, prevent acquiring 'access' from module
6✔
319

320
    def __init__(self, module_name=None):
6✔
321
        self.names = {}
6✔
322
        if module_name is not None:
6!
323
            global _moduleSecurity
324
            _moduleSecurity[module_name] = self
6✔
325

326
    __call____roles__ = ACCESS_PRIVATE
6✔
327

328
    def __call__(self, name, value):
6✔
329
        """Callback for __allow_access_to_unprotected_subobjects__ hook."""
330
        access = self.names.get(name, _marker)
6✔
331
        if access is not _marker:
6✔
332
            return access == ACCESS_PUBLIC
6✔
333

334
        return getattr(self, 'access', 0)
6✔
335

336
    apply__roles__ = ACCESS_PRIVATE
6✔
337

338
    def apply(self, dict):
6✔
339
        """Apply security information to the given module dict."""
340

341
        # Start with default attribute access policy
342
        access = getattr(self, 'access', _marker)
6✔
343
        if access is not _marker or len(self.names):
6!
344
            dict['__allow_access_to_unprotected_subobjects__'] = self
6✔
345

346
        if getattr(self, '_warnings', None):
6!
347
            LOG.warning('Module "%s" had conflicting'
×
348
                        ' security declarations' % dict['__name__'])
349

350
    declareProtected__roles__ = ACCESS_PRIVATE
6✔
351

352
    def declareProtected(self, permission_name, *names):
6✔
353
        """Cannot declare module names protected."""
354
        pass
×
355

356
    declareObjectProtected__roles__ = ACCESS_PRIVATE
6✔
357

358
    def declareObjectProtected(self, permission_name):
6✔
359
        """Cannot declare module protected."""
360
        pass
×
361

362
    setDefaultRoles__roles__ = ACCESS_PRIVATE
6✔
363

364
    def setDefaultRoles(self, permission_name, roles):
6✔
365
        """Cannot set default roles for permissions in a module."""
366
        pass
×
367

368
# Handy little utility functions
369

370

371
def allow_module(module_name):
6✔
372
    """Allow a module and all its contents to be used from a
373
    restricted Script. The argument module_name may be a simple
374
    or dotted module or package name. Note that if a package
375
    path is given, all modules in the path will be available."""
376
    ModuleSecurityInfo(module_name).setDefaultAccess(1)
×
377
    dot = module_name.find('.')
×
378
    while dot > 0:
×
379
        ModuleSecurityInfo(module_name[:dot]).setDefaultAccess(1)
×
380
        dot = module_name.find('.', dot + 1)
×
381

382

383
def allow_class(Class):
6✔
384
    """Allow a class and all of its methods to be used from a
385
    restricted Script.  The argument Class must be a class."""
386
    Class._security = sec = ClassSecurityInfo()
×
387
    sec.declareObjectPublic()
×
388
    sec.setDefaultAccess(1)
×
389
    sec.apply(Class)
×
390
    InitializeClass(Class)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc