Passed
Push — 2.x ( 5ff915...047977 )
by Jordi
06:19
created

  A

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 1
nop 3
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2023 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import six
22
from bika.lims import api
23
from bika.lims import APIError
24
from bika.lims import logger
25
from borg.localrole.default_adapter import DefaultLocalRoleAdapter
26
from plone.memoize.request import cache
27
from senaite.core.behaviors import IClientShareableBehavior
28
from senaite.core.interfaces import IDynamicLocalRoles
29
from zope.component import getAdapters
30
from zope.interface import implementer
31
32
33
def _request_cache(method, *args):
34
    """Decorator that ensures the call to the given method with the given
35
    arguments only takes place once within same request
36
    """
37
    # Extract the path of the context from the instance
38
    try:
39
        path = api.get_path(args[0].context)
40
    except APIError:
41
        path = "no-context"
42
43
    return [
44
        method.__name__,
45
        path,
46
        args[1:],
47
    ]
48
49
50
class DynamicLocalRoleAdapter(DefaultLocalRoleAdapter):
51
    """Gives additional Member local roles based on current user and context
52
    This enables giving additional permissions on items out of the user's
53
    current traverse path
54
    """
55
56
    @property
57
    def request(self):
58
        # Fixture for tests that do not have a regular request!!!
59
        return api.get_request() or api.get_test_request()
60
61
    def getRolesInContext(self, context, principal_id):
62
        """Returns the dynamically calculated 'local' roles for the given
63
        principal and context
64
        @param context: context to calculate roles for the given principal
65
        @param principal_id: User login id
66
        @return List of dynamically calculated local-roles for user and context
67
        """
68
        roles = set()
69
        path = api.get_path(context)
70
        adapters = getAdapters((context,), IDynamicLocalRoles)
71
        for name, adapter in adapters:
72
            local_roles = adapter.getRoles(principal_id)
73
            if local_roles:
74
                logger.debug(u"{}::{}::{}: {}".format(name, path, principal_id,
75
                                                      repr(local_roles)))
76
            roles.update(local_roles)
77
        return list(roles)
78
79
    @cache(get_key=_request_cache, get_request='self.request')
80
    def getRoles(self, principal_id):
81
        """Returns both non-local and local roles for the given principal in
82
        current context
83
        @param principal_id: User login id
84
        @return: list of non-local and local roles for the user and context
85
        """
86
        default_roles = self._rolemap.get(principal_id, [])
87
        if not api.is_object(self.context):
88
            # We only apply dynamic local roles to valid objects
89
            return default_roles[:]
90
91
        if principal_id not in self.getMemberIds():
92
            # We only apply dynamic local roles to existing users
93
            return default_roles[:]
94
95
        # Extend with dynamically computed roles
96
        dynamic_roles = self.getRolesInContext(self.context, principal_id)
97
        return list(set(default_roles + dynamic_roles))
98
99
    def getAllRoles(self):
100
        roles = {}
101
        # Iterate through all members to extract their dynamic local role for
102
        # current context
103
        for principal_id in self.getMemberIds():
104
            user_roles = self.getRoles(principal_id)
105
            if user_roles:
106
                roles.update({principal_id: user_roles})
107
        return six.iteritems(roles)
108
109
    @cache(get_key=_request_cache, get_request='self.request')
110
    def getMemberIds(self):
111
        """Return the list of user ids
112
        """
113
        mtool = api.get_tool("portal_membership")
114
        return mtool.listMemberIds()
115
116
117
@implementer(IDynamicLocalRoles)
118
class ClientShareableLocalRoles(object):
119
    """Adapter for the assignment of roles for content shared across clients
120
    """
121
122
    def __init__(self, context):
123
        self.context = context
124
125
    def getRoles(self, principal_id):
126
        """Returns ["ClientGuest"] local role if the current context is
127
        shareable across clients and the user for the principal_id belongs to
128
        one of the clients for which the context can be shared
129
        """
130
        # Get the clients this context is shared with
131
        behavior = IClientShareableBehavior(self.context)
132
        clients = filter(api.is_uid, behavior.getRawClients())
133
        if not clients:
134
            return []
135
136
        # Check if the user belongs to at least one of the clients
137
        # this context is shared with
138
        query = {
139
            "portal_type": "Contact",
140
            "getUsername": principal_id,
141
            "getParentUID": clients,
142
        }
143
        brains = api.search(query, catalog="portal_catalog")
144
        if len(brains) == 0:
145
            return []
146
147
        return ["ClientGuest"]
148