Passed
Push — 2.x ( b4b250...e86af7 )
by Ramon
05:31
created

senaite.core.adapters.localroles._request_cache()   A

Complexity

Conditions 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 14
rs 9.95
c 0
b 0
f 0
cc 2
nop 2
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-2022 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 bika.lims.utils import get_client
26
from borg.localrole.default_adapter import DefaultLocalRoleAdapter
27
from plone.memoize.request import cache
28
from senaite.core.behaviors import IClientShareableBehavior
29
from senaite.core.interfaces import IDynamicLocalRoles
30
from zope.component import getAdapters
31
from zope.interface import implementer
32
33
34
def _request_cache(method, *args):
35
    """Decorator that ensures the call to the given method with the given
36
    arguments only takes place once within same request
37
    """
38
    # Extract the path of the context from the instance
39
    try:
40
        path = api.get_path(args[0].context)
41
    except APIError:
42
        path = "no-context"
43
44
    return [
45
        method.__name__,
46
        path,
47
        args[1:],
48
    ]
49
50
51
class DynamicLocalRoleAdapter(DefaultLocalRoleAdapter):
52
    """Gives additional Member local roles based on current user and context
53
    This enables giving additional permissions on items out of the user's
54
    current traverse path
55
    """
56
57
    @property
58
    def request(self):
59
        # Fixture for tests that do not have a regular request!!!
60
        return api.get_request() or api.get_test_request()
61
62
    def getRolesInContext(self, context, principal_id):
63
        """Returns the dynamically calculated 'local' roles for the given
64
        principal and context
65
        @param context: context to calculate roles for the given principal
66
        @param principal_id: User login id
67
        @return List of dynamically calculated local-roles for user and context
68
        """
69
        roles = set()
70
        path = api.get_path(context)
71
        adapters = getAdapters((context,), IDynamicLocalRoles)
72
        for name, adapter in adapters:
73
            local_roles = adapter.getRoles(principal_id)
74
            if local_roles:
75
                logger.info(u"{}::{}::{}: {}".format(name, path, principal_id,
76
                                                     repr(local_roles)))
77
            roles.update(local_roles)
78
        return list(roles)
79
80
    @cache(get_key=_request_cache, get_request='self.request')
81
    def getRoles(self, principal_id):
82
        """Returns both non-local and local roles for the given principal in
83
        current context
84
        @param principal_id: User login id
85
        @return: list of non-local and local roles for the user and context
86
        """
87
        default_roles = self._rolemap.get(principal_id, [])
88
        if not api.is_object(self.context):
89
            # We only apply dynamic local roles to valid objects
90
            return default_roles[:]
91
92
        if principal_id not in self.getMemberIds():
93
            # We only apply dynamic local roles to existing users
94
            return default_roles[:]
95
96
        # Extend with dynamically computed roles
97
        dynamic_roles = self.getRolesInContext(self.context, principal_id)
98
        return list(set(default_roles + dynamic_roles))
99
100
    def getAllRoles(self):
101
        roles = {}
102
        # Iterate through all members to extract their dynamic local role for
103
        # current context
104
        for principal_id in self.getMemberIds():
105
            user_roles = self.getRoles(principal_id)
106
            if user_roles:
107
                roles.update({principal_id: user_roles})
108
        return six.iteritems(roles)
109
110
    @cache(get_key=_request_cache, get_request='self.request')
111
    def getMemberIds(self):
112
        """Return the list of user ids
113
        """
114
        mtool = api.get_tool("portal_membership")
115
        return mtool.listMemberIds()
116
117
118
@implementer(IDynamicLocalRoles)
119
class ClientAwareLocalRoles(object):
120
    """Adapter for the assignment of dynamic local roles to users that are
121
    linked to a ClientContact for objects that belong to same client
122
    """
123
124
    def __init__(self, context):
125
        self.context = context
126
127
    def hasContact(self, client, principal_id):
128
        """Returns whether the client passed in has a contact linked to a user
129
        with the given principal_id
130
        """
131
        query = {
132
            "portal_type": "Contact",
133
            "getUsername": principal_id,
134
            "getParentUID": api.get_uid(client),
135
        }
136
        brains = api.search(query, catalog="portal_catalog")
137
        return len(brains) == 1
138
139
    def getRoles(self, principal_id):
140
        """Returns ["Owner"] local role if the user is linked to a Client
141
        Contact that belongs to the same client as the current context
142
        """
143
        # Get the client of current context, if any
144
        client = get_client(self.context)
145
        if not client:
146
            return []
147
148
        # Check if the user belongs to same client as context
149
        if not self.hasContact(client, principal_id):
150
            return []
151
152
        return ["Owner"]
153
154
155
@implementer(IDynamicLocalRoles)
156
class ClientShareableLocalRoles(object):
157
    """Adapter for the assignment of roles for content shared across clients
158
    """
159
160
    def __init__(self, context):
161
        self.context = context
162
163
    def getRoles(self, principal_id):
164
        """Returns ["ClientGuest"] local role if the current context is
165
        shareable across clients and the user for the principal_id belongs to
166
        one of the clients for which the context can be shared
167
        """
168
        # Get the clients this context is shared with
169
        behavior = IClientShareableBehavior(self.context)
170
        clients = filter(api.is_uid, behavior.getRawClients())
171
        if not clients:
172
            return []
173
174
        # Check if the user belongs to at least one of the clients
175
        # this context is shared with
176
        query = {
177
            "portal_type": "Contact",
178
            "getUsername": principal_id,
179
            "getParentUID": clients,
180
        }
181
        brains = api.search(query, catalog="portal_catalog")
182
        if len(brains) == 0:
183
            return []
184
185
        return ["ClientGuest"]
186