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

zopefoundation / AccessControl / 11251390227

09 Oct 2024 07:10AM UTC coverage: 81.668% (+0.09%) from 81.58%
11251390227

push

github

perrinjerome
Align behavior with objects raising in `__getattr__`

The observed problem was a behavior different between C and python
implementation on python 3, happening with Zope python script. When the
context can not be accessed by the current user, Zope binds a
`Shared.DC.Scripts.Bindings.UnauthorizedBinding`, a class that raises an
Unauthorized error when the context is actually accessed, in order to
postpone the Unauthorized if something is actually accessed. This class
does implements this by raising Unauthorized in `__getattr__`.

The python implementation of `rolesForPermissionOn` used `hasattr` and
`hasattr` has changed between python2 and python3, on python2 it was
ignoring all exceptions, including potential Unauthorized errors and
just returning False, but on python3 these errors are now raised.
This change of behavior of python causes `rolesForPermissionOn` to
behave differently: when using python implementation on python2 or when
using C implementation, such Unauthorized errors were gracefully handled
and caused `checkPermission` to return False, but on python3 the
Unauthorized is raised.

The C implementation of `rolesForPermissionOn` uses a construct
equivalent to the python2 version of `hasattr`. For consistency - and
because ignoring errors is usually not good - we also want to change it
to be have like the python3 implementation.

This change make this scenario behave the same between python and C
implementations:
 - `Unauthorized` errors raised in `__getattr__` are supported on py3.
 - Other errors  than `AttributeError` and `Unauthorized` raised in
    `__getattr__` are no longer ignored in the C implementation.

1004 of 1526 branches covered (65.79%)

Branch coverage included in aggregate %.

32 of 32 new or added lines in 3 files covered. (100.0%)

81 existing lines in 3 files now uncovered.

5095 of 5942 relevant lines covered (85.75%)

5.14 hits per line

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

67.37
/src/AccessControl/ImplPython.py
1
##############################################################################
2
#
3
# Copyright (c) 2003 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
# isort:skip_file
14
"""Python implementation of the access control machinery."""
6✔
15

16
from AccessControl.ZopeSecurityPolicy import getRoles  # XXX
6✔
17
from AccessControl.unauthorized import Unauthorized
6✔
18
from AccessControl.SimpleObjectPolicies import _noroles
6✔
19
from AccessControl.SimpleObjectPolicies import Containers
6✔
20
from AccessControl.SecurityManagement import getSecurityManager
6✔
21
from AccessControl.Permission import getPermissionIdentifier
6✔
22
from AccessControl.interfaces import ISecurityPolicy
6✔
23
from AccessControl.interfaces import ISecurityManager
6✔
24
import os
6✔
25
from logging import getLogger
6✔
26

27
from Acquisition import aq_acquire
6✔
28
from Acquisition import aq_base
6✔
29
from Acquisition import aq_inContextOf
6✔
30
from Acquisition import aq_inner
6✔
31
from Acquisition import aq_parent
6✔
32
from ExtensionClass import Base
6✔
33
from zope.interface import implementer
6✔
34
from zExceptions import Unauthorized as zExceptions_Unauthorized
6✔
35

36
PURE_PYTHON = int(os.environ.get('PURE_PYTHON', '0'))
6✔
37
if PURE_PYTHON:
6✔
38
    # We need our own to not depend on the C implementation:
39
    _what_not_even_god_should_do = []
6✔
40
else:
41
    from AccessControl.cAccessControl import _what_not_even_god_should_do
6✔
42

43
from AccessControl.ZopeGuards import guarded_getitem  # NOQA
6✔
44

45

46
# AccessControl.ZopeSecurityPolicy
47
# --------------------------------
48
#
49
#   TODO:  implement this in cAccessControl, and have Implementation
50
#          do the indirection.
51
#
52

53

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

56
# AccessControl.PermissionRole
57
# ----------------------------
58

59
_default_roles = ('Manager',)
6✔
60

61
# If _embed_permission_in_roles is enabled, computed __roles__
62
# attributes will often include a special role that encodes the name
63
# of the permission from which the roles were derived.  This is useful
64
# for verbose security exceptions.
65
_embed_permission_in_roles = 0
6✔
66

67

68
def rolesForPermissionOn(perm, object, default=_default_roles, n=None):
6✔
69
    """Return the roles that have the given permission on the given object
70
    """
71
    n = n or getPermissionIdentifier(perm)
6✔
72
    r = None
6✔
73

74
    while True:
4✔
75
        try:
6✔
76
            roles = getattr(object, n)
6✔
77
        except (AttributeError, zExceptions_Unauthorized):
6✔
78
            pass
6✔
79
        else:
80
            if roles is None:
6✔
81
                if _embed_permission_in_roles:
6!
UNCOV
82
                    return (('Anonymous',), n)
×
83
                return ('Anonymous',)
6✔
84

85
            t = type(roles)
6✔
86
            if t is tuple:
6✔
87
                # If we get a tuple, then we don't acquire
88
                if r is None:
6✔
89
                    if _embed_permission_in_roles:
6!
UNCOV
90
                        return roles + (n,)
×
91
                    return roles
6✔
92
                if _embed_permission_in_roles:
6!
UNCOV
93
                    return r + list(roles) + [n]
×
94
                return r + list(roles)
6✔
95

96
            if t is str:
6!
97
                # We found roles set to a name.  Start over
98
                # with the new permission name.  If the permission
99
                # name is '', then treat as private!
UNCOV
100
                if roles:
×
UNCOV
101
                    n = roles
×
102
                else:
UNCOV
103
                    return _what_not_even_god_should_do
×
104

105
            elif roles:
6!
106
                if r is None:
6!
107
                    r = list(roles)
6✔
108
                else:
UNCOV
109
                    r = r + list(roles)
×
110

111
        object = aq_inner(object)
6✔
112
        if object is None:
6✔
113
            break
6✔
114
        object = aq_parent(object)
6✔
115

116
    if r is None:
6!
117
        if _embed_permission_in_roles:
6!
UNCOV
118
            if default:
×
UNCOV
119
                if isinstance(default, tuple):
×
UNCOV
120
                    return default + (n,)
×
121
                else:
122
                    return default + [n]
×
123
            else:
124
                return [n]
×
125
        return default
6✔
126

UNCOV
127
    if _embed_permission_in_roles:
×
128
        return r + [n]
×
UNCOV
129
    return r
×
130

131

132
class PermissionRole(Base):
6✔
133
    """Implement permission-based roles.
134

135
    Under normal circumstances, our __of__ method will be
136
    called with an unwrapped object.  The result will then be called
137
    with a wrapped object, if the original object was wrapped.
138
    To deal with this, we have to create an intermediate object.
139

140
    """
141

142
    def __init__(self, name, default=('Manager',)):
6✔
143
        self.__name__ = name
6✔
144
        self._p = getPermissionIdentifier(name)
6✔
145
        self._d = self.__roles__ = default
6✔
146

147
    def __of__(self, parent):
6✔
148
        r = imPermissionRole()
6✔
149
        r._p = self._p
6✔
150
        r._pa = parent
6✔
151
        r._d = self._d
6✔
152
        p = getattr(parent, 'aq_inner', None)
6✔
153
        if p is not None:
6✔
154
            return r.__of__(p)
6✔
155
        else:
156
            return r
6✔
157

158
    def rolesForPermissionOn(self, value):
6✔
159
        return rolesForPermissionOn(None, value, self._d, self._p)
6✔
160

161

162
class imPermissionRole(Base):
6✔
163
    """Implement permission-based roles"""
164

165
    def __of__(self, value):
6✔
166
        return rolesForPermissionOn(None, value, self._d, self._p)
6✔
167
    rolesForPermissionOn = __of__
6✔
168

169
    # The following methods are needed in the unlikely case that an unwrapped
170
    # object is accessed:
171
    def __getitem__(self, i):
6✔
172
        try:
6✔
173
            v = self._v
6✔
174
        except:  # noqa: E722 do not use bare 'except'
6✔
175
            v = self._v = self.__of__(self._pa)
6✔
176
            del self._pa
6✔
177

178
        return v[i]
6✔
179

180
    def __len__(self):
6✔
181
        try:
6✔
182
            v = self._v
6✔
183
        except:  # noqa: E722 do not use bare 'except'
6✔
184
            v = self._v = self.__of__(self._pa)
6✔
185
            del self._pa
6✔
186

187
        return len(v)
6✔
188

189

190
@implementer(ISecurityPolicy)
6✔
191
class ZopeSecurityPolicy:
6✔
192

193
    def __init__(self, ownerous=1, authenticated=1, verbose=0):
6✔
194
        """Create a Zope security policy.
195

196
        Optional arguments may be provided:
197

198
        ownerous -- Untrusted users can create code
199
                    (e.g. Python scripts or templates),
200
                    so check that code owners can access resources.
201
                    The argument must have a truth value.
202
                    The default is true.
203

204
        authenticated -- Allow access to resources based on the
205
                    privaledges of the authenticated user.
206
                    The argument must have a truth value.
207
                    The default is true.
208

209
                    This (somewhat experimental) option can be set
210
                    to false on sites that allow only public
211
                    (unauthenticated) access. An anticipated
212
                    scenario is a ZEO configuration in which some
213
                    clients allow only public access and other
214
                    clients allow full management.
215

216
        verbose --  Include debugging information in Unauthorized
217
                    exceptions.  Not suitable for public sites.
218
        """
219
        self._ownerous = ownerous
6✔
220
        self._authenticated = authenticated
6✔
221
        self._verbose = verbose
6✔
222

223
    def validate(self, accessed, container, name, value, context,
6✔
224
                 roles=_noroles, getattr=getattr, _noroles=_noroles,
225
                 valid_aq_=('aq_parent', 'aq_inner', 'aq_explicit')):
226

227
        ############################################################
228
        # Provide special rules for the acquisition attributes
229
        if isinstance(name, str):
6✔
230
            if name.startswith('aq_') and name not in valid_aq_:
6✔
231
                if self._verbose:
6!
UNCOV
232
                    raiseVerbose(
×
233
                        'aq_* names (other than %s) are not allowed'
234
                        % ', '.join(valid_aq_),
235
                        accessed,
236
                        container,
237
                        name,
238
                        value,
239
                        context,
240
                    )
241
                raise Unauthorized(name, value)
6✔
242

243
        containerbase = aq_base(container)
6✔
244
        accessedbase = aq_base(accessed)
6✔
245
        if accessedbase is accessed:
6✔
246
            # accessed is not a wrapper, so assume that the
247
            # value could not have been acquired.
248
            accessedbase = container
6✔
249

250
        ############################################################
251
        # If roles weren't passed in, we'll try to get them from the object
252

253
        if roles is _noroles:
6!
254
            roles = getRoles(container, name, value, _noroles)
6✔
255

256
        ############################################################
257
        # We still might not have any roles
258

259
        if roles is _noroles:
6✔
260

261
            ############################################################
262
            # We have an object without roles and we didn't get a list
263
            # of roles passed in. Presumably, the value is some simple
264
            # object like a string or a list.  We'll try to get roles
265
            # from its container.
266
            if container is None:
6!
267
                # Either container or a list of roles is required
268
                # for ZopeSecurityPolicy to know whether access is
269
                # allowable.
UNCOV
270
                if self._verbose:
×
UNCOV
271
                    raiseVerbose(
×
272
                        'No container provided',
273
                        accessed, container, name, value, context)
274
                raise Unauthorized(name, value)
×
275

276
            roles = getattr(container, '__roles__', roles)
6✔
277
            if roles is _noroles:
6✔
278
                if containerbase is container:
6✔
279
                    # Container is not wrapped.
280
                    if containerbase is not accessedbase:
6!
UNCOV
281
                        if self._verbose:
×
UNCOV
282
                            raiseVerbose(
×
283
                                'Unable to find __roles__ in the container '
284
                                'and the container is not wrapped',
285
                                accessed,
286
                                container,
287
                                name,
288
                                value,
289
                                context,
290
                            )
UNCOV
291
                        raise Unauthorized(name, value)
×
292
                else:
293
                    # Try to acquire roles
294
                    try:
6✔
295
                        roles = aq_acquire(container, '__roles__')
6✔
296
                    except AttributeError:
6✔
297
                        if containerbase is not accessedbase:
6!
UNCOV
298
                            if self._verbose:
×
UNCOV
299
                                raiseVerbose(
×
300
                                    'Unable to find or acquire __roles__ '
301
                                    'from the container',
302
                                    accessed, container, name, value, context)
303
                            raise Unauthorized(name, value)
×
304

305
            # We need to make sure that we are allowed to
306
            # get unprotected attributes from the container. We are
307
            # allowed for certain simple containers and if the
308
            # container says we can. Simple containers
309
            # may also impose name restrictions.
310
            p = Containers(type(container), None)
6✔
311
            if p is None:
6✔
312
                p = getattr(container,
6✔
313
                            '__allow_access_to_unprotected_subobjects__',
314
                            None)
315

316
            if p is not None:
6✔
317
                if not isinstance(p, (bool, int)):
6✔
318
                    if isinstance(p, dict):
6✔
319

320
                        if isinstance(name, str):
6!
321
                            p = p.get(name)
6✔
322
                        else:
UNCOV
323
                            p = 1
×
324
                    else:
325
                        p = p(name, value)
6✔
326

327
            if not p:
6✔
328
                if self._verbose:
6!
UNCOV
329
                    raiseVerbose(
×
330
                        'The container has no security assertions',
331
                        accessed,
332
                        container,
333
                        name,
334
                        value,
335
                        context,
336
                    )
337
                raise Unauthorized(name, value)
6✔
338

339
            if roles is _noroles:
6✔
340
                return 1
6✔
341

342
            # We are going to need a security-aware object to pass
343
            # to allowed(). We'll use the container.
344
            value = container
6✔
345

346
        # Short-circuit tests if we can:
347
        try:
6✔
348
            if roles is None or 'Anonymous' in roles:
6✔
349
                return 1
6✔
UNCOV
350
        except TypeError:
×
351
            # 'roles' isn't a sequence
UNCOV
352
            LOG.error(
×
353
                "'%r' passed as roles"
354
                " during validation of '%s' is not a sequence." % (
355
                    roles,
356
                    name,
357
                ),
358
            )
UNCOV
359
            raise
×
360

361
        # Check executable security
362
        stack = context.stack
6✔
363
        if stack:
6✔
364
            eo = stack[-1]
6✔
365

366
            # If the executable had an owner, can it execute?
367
            if self._ownerous:
6!
368
                owner = eo.getOwner()
6✔
369
                if (owner is not None) and not owner.allowed(value, roles):
6✔
370
                    # We don't want someone to acquire if they can't
371
                    # get an unacquired!
372
                    if self._verbose:
6!
UNCOV
373
                        if len(roles) < 1:
×
UNCOV
374
                            raiseVerbose(
×
375
                                "The object is marked as private",
376
                                accessed,
377
                                container,
378
                                name,
379
                                value,
380
                                context,
381
                            )
UNCOV
382
                        elif userHasRolesButNotInContext(owner, value, roles):
×
UNCOV
383
                            raiseVerbose(
×
384
                                "The owner of the executing script is defined "
385
                                "outside the context of the object being "
386
                                "accessed",
387
                                accessed,
388
                                container,
389
                                name,
390
                                value,
391
                                context,
392
                                required_roles=roles,
393
                                eo_owner=owner,
394
                                eo=eo,
395
                            )
396
                        else:
UNCOV
397
                            raiseVerbose(
×
398
                                "The owner of the executing script does not "
399
                                "have the required permission",
400
                                accessed,
401
                                container,
402
                                name,
403
                                value,
404
                                context,
405
                                required_roles=roles,
406
                                eo_owner=owner,
407
                                eo=eo,
408
                                eo_owner_roles=getUserRolesInContext(owner,
409
                                                                     value),
410
                            )
411
                    raise Unauthorized(name, value)
6✔
412

413
            # Proxy roles, which are a lot safer now.
414
            proxy_roles = getattr(eo, '_proxy_roles', None)
6✔
415
            if proxy_roles:
6✔
416
                # Verify that the owner actually can state the proxy role
417
                # in the context of the accessed item; users in subfolders
418
                # should not be able to use proxy roles to access items
419
                # above their subfolder!
420
                owner = eo.getWrappedOwner()
6✔
421

422
                if owner is not None:
6✔
423
                    if container is not containerbase:
6!
424
                        # Unwrapped objects don't need checking
425
                        if not owner._check_context(container):
6✔
426
                            # container is higher up than the owner,
427
                            # deny access
428
                            if self._verbose:
6!
UNCOV
429
                                raiseVerbose(
×
430
                                    "The owner of the executing script is "
431
                                    "defined outside the context of the "
432
                                    "object being accessed.  The script has "
433
                                    "proxy roles, but they do not apply in "
434
                                    "this context.",
435
                                    accessed, container, name, value, context,
436
                                    required_roles=roles, eo_owner=owner,
437
                                    eo=eo)
438
                            raise Unauthorized(name, value)
6✔
439

440
                for r in proxy_roles:
6✔
441
                    if r in roles:
6✔
442
                        return 1
6✔
443

444
                # Proxy roles actually limit access!
445
                if self._verbose:
6!
UNCOV
446
                    if len(roles) < 1:
×
UNCOV
447
                        raiseVerbose(
×
448
                            "The object is marked as private",
449
                            accessed, container, name, value, context)
450
                    else:
451
                        raiseVerbose(
×
452
                            "The proxy roles set on the executing script "
453
                            "do not allow access",
454
                            accessed, container, name, value, context,
455
                            eo=eo, eo_proxy_roles=proxy_roles,
456
                            required_roles=roles)
457
                raise Unauthorized(name, value)
6✔
458

459
        try:
6✔
460
            if self._authenticated and context.user.allowed(value, roles):
6✔
461
                return 1
6✔
UNCOV
462
        except AttributeError:
×
UNCOV
463
            pass
×
464

465
        if self._verbose:
6!
466
            if len(roles) < 1:
×
467
                raiseVerbose(
×
468
                    "The object is marked as private",
469
                    accessed, container, name, value, context)
470
            elif not self._authenticated:
×
471
                raiseVerbose(
×
472
                    "Authenticated access is not allowed by this "
473
                    "security policy",
474
                    accessed, container, name, value, context)
475
            elif userHasRolesButNotInContext(context.user, value, roles):
×
UNCOV
476
                raiseVerbose(
×
477
                    "Your user account is defined outside "
478
                    "the context of the object being accessed",
479
                    accessed, container, name, value, context,
480
                    required_roles=roles, user=context.user)
481
            else:
UNCOV
482
                raiseVerbose(
×
483
                    "Your user account does not "
484
                    "have the required permission",
485
                    accessed, container, name, value, context,
486
                    required_roles=roles, user=context.user,
487
                    user_roles=getUserRolesInContext(context.user, value))
488
        raise Unauthorized(name, value)
6✔
489

490
    def checkPermission(self, permission, object, context):
6✔
491
        roles = rolesForPermissionOn(permission, object)
6✔
492
        if isinstance(roles, str):
6!
UNCOV
493
            roles = [roles]
×
494

495
        # check executable owner and proxy roles
496
        stack = context.stack
6✔
497
        if stack:
6✔
498
            eo = stack[-1]
6✔
499
            # If the executable had an owner, can it execute?
500
            if self._ownerous:
6✔
501
                owner = eo.getOwner()
6✔
502
                if (owner is not None) and not owner.allowed(object, roles):
6✔
503
                    # We don't want someone to acquire if they can't
504
                    # get an unacquired!
505
                    return 0
6✔
506
            proxy_roles = getattr(eo, '_proxy_roles', None)
6✔
507
            if proxy_roles:
6✔
508
                # Verify that the owner actually can state the proxy role
509
                # in the context of the accessed item; users in subfolders
510
                # should not be able to use proxy roles to access items
511
                # above their subfolder!
512
                owner = eo.getWrappedOwner()
6✔
513
                if owner is not None:
6!
514
                    if object is not aq_base(object):
6!
515
                        if not owner._check_context(object):
6✔
516
                            # object is higher up than the owner,
517
                            # deny access
518
                            return 0
6✔
519

520
                for r in proxy_roles:
6✔
521
                    if r in roles:
6✔
522
                        return 1
6✔
523
                return 0
6✔
524

525
        return context.user.allowed(object, roles)
6✔
526

527
# AccessControl.SecurityManager
528
# -----------------------------
529

530

531
# There is no corresponding control in the C implementation of the
532
# access control machinery (cAccessControl.c); this should probably go
533
# away in a future version.  If you're concerned about the size of
534
# security stack, you probably have bigger problems.
535
#
536
try:
6✔
537
    max_stack_size = int(os.environ.get('Z_MAX_STACK_SIZE', '100'))
6✔
UNCOV
538
except:  # noqa: E722 do not use bare 'except'
×
UNCOV
539
    max_stack_size = 100
×
540

541

542
def setDefaultBehaviors(ownerous, authenticated, verbose):
6✔
543
    global _defaultPolicy
544
    global _embed_permission_in_roles
545
    _defaultPolicy = ZopeSecurityPolicy(
6✔
546
        ownerous=ownerous,
547
        authenticated=authenticated,
548
        verbose=verbose)
549
    _embed_permission_in_roles = verbose
6✔
550

551

552
setDefaultBehaviors(True, True, False)
6✔
553

554

555
@implementer(ISecurityManager)
6✔
556
class SecurityManager:
6✔
557
    """A security manager provides methods for checking access and managing
558
    executable context and policies
559
    """
560
    __allow_access_to_unprotected_subobjects__ = {
6✔
561
        'validate': 1,
562
        'checkPermission': 1,
563
        'getUser': 1,
564
        'calledByExecutable': 1,
565
    }
566

567
    def __init__(self, thread_id, context):
6✔
568
        self._thread_id = thread_id
6✔
569
        self._context = context
6✔
570
        self._policy = _defaultPolicy
6✔
571

572
    def validate(self,
6✔
573
                 accessed=None,
574
                 container=None,
575
                 name=None,
576
                 value=None,
577
                 roles=_noroles,
578
                 ):
579
        """Validate access.
580

581
        Arguments:
582

583
        accessed -- the object that was being accessed
584

585
        container -- the object the value was found in
586

587
        name -- The name used to access the value
588

589
        value -- The value retrieved though the access.
590

591
        roles -- The roles of the object if already known.
592

593
        The arguments may be provided as keyword arguments. Some of these
594
        arguments may be ommitted, however, the policy may reject access
595
        in some cases when arguments are ommitted.  It is best to provide
596
        all the values possible.
597
        """
598
        policy = self._policy
6✔
599
        if roles is _noroles:
6✔
600
            return policy.validate(accessed, container, name, value,
6✔
601
                                   self._context)
602
        else:
603
            return policy.validate(accessed, container, name, value,
6✔
604
                                   self._context, roles)
605

606
    def DTMLValidate(self,
6✔
607
                     accessed=None,
608
                     container=None,
609
                     name=None,
610
                     value=None,
611
                     md=None,
612
                     ):
613
        """Validate access.
614
        * THIS EXISTS FOR DTML COMPATIBILITY *
615

616
        Arguments:
617

618
        accessed -- the object that was being accessed
619

620
        container -- the object the value was found in
621

622
        name -- The name used to access the value
623

624
        value -- The value retrieved though the access.
625

626
        md -- multidict for DTML (ignored)
627

628
        The arguments may be provided as keyword arguments. Some of these
629
        arguments may be ommitted, however, the policy may reject access
630
        in some cases when arguments are ommitted.  It is best to provide
631
        all the values possible.
632

633
        """
634
        policy = self._policy
6✔
635
        return policy.validate(accessed, container, name, value, self._context)
6✔
636

637
    def checkPermission(self, permission, object):
6✔
638
        """Check whether the security context allows the given permission on
639
        the given object.
640

641
        Arguments:
642

643
        permission -- A permission name
644

645
        object -- The object being accessed according to the permission
646
        """
647
        policy = self._policy
6✔
648
        return policy.checkPermission(permission, object, self._context)
6✔
649

650
    def addContext(self, anExecutableObject,
6✔
651
                   getattr=getattr):
652
        """Add an ExecutableObject to the current security
653
        context. Optionally, add a new SecurityPolicy as well.
654
        """
655
        stack = self._context.stack
6✔
656
        if len(stack) > max_stack_size:
6!
UNCOV
657
            raise SystemError('Excessive recursion')
×
658
        stack.append(anExecutableObject)
6✔
659
        p = getattr(anExecutableObject, '_customSecurityPolicy', None)
6✔
660
        if p is not None:
6✔
661
            p = p()
6✔
662
        else:
663
            p = _defaultPolicy
6✔
664
        self._policy = p
6✔
665

666
    def removeContext(self, anExecutableObject):
6✔
667
        """Remove an ExecutableObject, and optionally, a
668
        SecurityPolicy, from the current security context.
669
        """
670
        stack = self._context.stack
6✔
671
        if not stack:
6!
UNCOV
672
            return
×
673
        top = stack[-1]
6✔
674
        if top is anExecutableObject:
6✔
675
            del stack[-1]
6✔
676
        else:
677
            indexes = range(len(stack))
6✔
678
            for i in reversed(indexes):
6!
679
                top = stack[i]
6✔
680
                if top is anExecutableObject:
6✔
681
                    del stack[i:]
6✔
682
                    break
6✔
683
            else:
UNCOV
684
                return
×
685

686
        if stack:
6✔
687
            top = stack[-1]
6✔
688
            p = getattr(top, '_customSecurityPolicy', None)
6✔
689
            if p is not None:
6✔
690
                p = p()
6✔
691
            else:
692
                p = _defaultPolicy
6✔
693
            self._policy = p
6✔
694
        else:
695
            self._policy = _defaultPolicy
6✔
696

697
    def getUser(self):
6✔
698
        """Get the current authenticated user"""
699
        return self._context.user
6✔
700

701
    def calledByExecutable(self):
6✔
702
        """Return a boolean value indicating if this context was called
703
        by an executable"""
704
        return len(self._context.stack)
6✔
705

706

707
# AccessControl.ZopeGuards
708
# ------------------------
709

710

711
def aq_validate(inst, object, name, v, validate):
6✔
712
    return validate(inst, object, name, v)
6✔
713

714

715
_marker = object()
6✔
716

717

718
def guarded_getattr(inst, name, default=_marker):
6✔
719
    """Retrieves an attribute, checking security in the process.
720

721
    Raises Unauthorized if the attribute is found but the user is
722
    not allowed to access the attribute.
723
    """
724
    if name[:1] == '_':
6✔
725
        raise Unauthorized(name)
6✔
726

727
    # Try to get the attribute normally so that unusual
728
    # exceptions are caught early.
729
    try:
6✔
730
        v = getattr(inst, name)
6✔
731
    except AttributeError:
6✔
732
        if default is not _marker:
6!
UNCOV
733
            return default
×
734
        raise
6✔
735

736
    try:
6✔
737
        container = v.__self__
6✔
738
    except AttributeError:
6✔
739
        container = aq_parent(aq_inner(v)) or inst
6✔
740

741
    assertion = Containers(type(container))
6✔
742

743
    if isinstance(assertion, dict):
6✔
744
        # We got a table that lets us reason about individual
745
        # attrs
746
        assertion = assertion.get(name)
6✔
747
        if assertion:
6✔
748
            # There's an entry, but it may be a function.
749
            if callable(assertion):
6✔
750
                return assertion(inst, name)
6✔
751

752
            # Nope, it's boolean
753
            return v
6✔
754
        raise Unauthorized(name)
6✔
755

756
    if assertion:
6✔
757
        if callable(assertion):
6✔
758
            factory = assertion(name, v)
6✔
759
            if callable(factory):
6✔
760
                return factory(inst, name)
6✔
761
            assert factory == 1
6✔
762
        else:
763
            assert assertion == 1
6✔
764
        return v
6✔
765

766
    # See if we can get the value doing a filtered acquire.
767
    # aq_acquire will either return the same value as held by
768
    # v or it will return an Unauthorized raised by validate.
769
    validate = getSecurityManager().validate
6✔
770
    aq_acquire(inst, name, aq_validate, validate)
6✔
771

772
    return v
6✔
773

774

775
# Helpers for verbose authorization exceptions
776
# --------------------------------------------
777

778

779
def item_repr(ob):
6✔
780
    """Generates a repr without angle brackets (to avoid HTML quoting)"""
UNCOV
781
    return repr(ob).replace('<', '(').replace('>', ')')
×
782

783

784
def simplifyRoles(roles):
6✔
785
    """Sorts and removes duplicates from a role list."""
UNCOV
786
    return sorted(set(roles))
×
787

788

789
def raiseVerbose(msg, accessed, container, name, value, context,
6✔
790
                 required_roles=None,
791
                 user_roles=None,
792
                 user=None,
793
                 eo=None,
794
                 eo_owner=None,
795
                 eo_owner_roles=None,
796
                 eo_proxy_roles=None,
797
                 ):
798
    """Raises an Unauthorized error with a verbose explanation."""
799

UNCOV
800
    s = '{}.  Access to {} of {}'.format(
×
801
        msg, repr(name), item_repr(container))
UNCOV
802
    if aq_base(container) is not aq_base(accessed):
×
UNCOV
803
        s += ', acquired through %s,' % item_repr(accessed)
×
804
    info = [s + ' denied.']
×
805

806
    if user is not None:
×
807
        try:
×
808
            ufolder = '/'.join(aq_parent(aq_inner(user)).getPhysicalPath())
×
UNCOV
809
        except:  # noqa: E722 do not use bare 'except'
×
810
            ufolder = '(unknown)'
×
811
        info.append('Your user account, {}, exists at {}.'.format(
×
812
            str(user), ufolder))
813

814
    if required_roles is not None:
×
815
        p = None
×
UNCOV
816
        required_roles = list(required_roles)
×
UNCOV
817
        for r in required_roles:
×
818
            if r.startswith('_') and r.endswith('_Permission'):
×
819
                p = r[1:]
×
820
                required_roles.remove(r)
×
821
                break
×
822
        sr = simplifyRoles(required_roles)
×
823
        if p:
×
824
            # got a permission name
825
            info.append('Access requires %s, '
×
826
                        'granted to the following roles: %s.' %
827
                        (p, sr))
828
        else:
829
            # permission name unknown
UNCOV
830
            info.append('Access requires one of the following roles: %s.'
×
831
                        % sr)
832

UNCOV
833
    if user_roles is not None:
×
834
        info.append(
×
835
            'Your roles in this context are %s.' % simplifyRoles(user_roles))
836

837
    if eo is not None:
×
838
        s = 'The executing script is %s' % item_repr(eo)
×
UNCOV
839
        if eo_proxy_roles is not None:
×
UNCOV
840
            s += ', with proxy roles: %s' % simplifyRoles(eo_proxy_roles)
×
841
        if eo_owner is not None:
×
842
            s += ', owned by %s' % repr(eo_owner)
×
843
        if eo_owner_roles is not None:
×
844
            s += ', who has the roles %s' % simplifyRoles(eo_owner_roles)
×
845
        info.append(s + '.')
×
846

847
    text = ' '.join(info)
×
848
    LOG.debug('Unauthorized: %s' % text)
×
849
    raise Unauthorized(text)
×
850

851

852
def getUserRolesInContext(user, context):
6✔
853
    """Returns user roles for a context."""
UNCOV
854
    if hasattr(aq_base(user), 'getRolesInContext'):
×
UNCOV
855
        return user.getRolesInContext(context)
×
856
    else:
UNCOV
857
        return ()
×
858

859

860
def userHasRolesButNotInContext(user, object, object_roles):
6✔
861
    '''Returns 1 if the user has any of the listed roles but
862
    is not defined in a context which is not an ancestor of object.
863
    '''
UNCOV
864
    if object_roles is None or 'Anonymous' in object_roles:
×
UNCOV
865
        return 0
×
UNCOV
866
    usr_roles = getUserRolesInContext(user, object)
×
UNCOV
867
    for role in object_roles:
×
868
        if role in usr_roles:
×
869
            # User has the roles.
870
            return (not verifyAcquisitionContext(
×
871
                user, object, object_roles))
872
    return 0
×
873

874

875
def verifyAcquisitionContext(user, object, object_roles=None):
6✔
876
    """Mimics the relevant section of User.allowed().
877

878
    Returns true if the object is in the context of the user's user folder.
879
    """
880
    ufolder = aq_parent(user)
6✔
881
    ucontext = aq_parent(ufolder)
6✔
882
    if ucontext is not None:
6!
883
        if object is None:
6!
884
            # This is a strange rule, though
885
            # it doesn't cause any security holes. SDH
UNCOV
886
            return 1
×
887
        if getattr(object, '__self__', None) is not None:
6!
888
            # This is a method.  Grab its self.
UNCOV
889
            object = object.__self__
×
890
        if not aq_inContextOf(object, ucontext, 1):
6!
UNCOV
891
            if 'Shared' in object_roles:
×
892
                # Old role setting. Waaa
893
                object_roles = user._shared_roles(object)
×
UNCOV
894
                if 'Anonymous' in object_roles:
×
895
                    return 1
×
UNCOV
896
            return None
×
897
    # Note that if the user were not wrapped, it would
898
    # not be possible to determine the user's context
899
    # and this method would return 1.
900
    # However, as long as user folders always return
901
    # wrapped user objects, this is safe.
902
    return 1
6✔
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

© 2026 Coveralls, Inc