permissions.py 9.1 KB
Newer Older
1
#!/usr/bin/python
2
3
4
# -*- coding: utf-8 -*-

from flask_security import current_user
5
from flask import request, current_app
Pablo Panero's avatar
Oauth    
Pablo Panero committed
6
from invenio_search import current_search_client
7

8
9
from cern_search_rest_api.modules.cernsearch.utils import get_user_provides, cern_search_record_to_index, \
    get_index_from_request
Pablo Panero's avatar
Pablo Panero committed
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"""Access control for CERN Search."""


def record_permission_factory(record=None, action=None):
    """Record permission factory."""
    return RecordPermission.create(record, action)


def record_create_permission_factory(record=None):
    """Create permission factory."""
    return record_permission_factory(record=record, action='create')


def record_read_permission_factory(record=None):
    """Read permission factory."""
    return record_permission_factory(record=record, action='read')


29
def record_list_permission_factory(record=None):
30
    """Read permission factory."""
31
    return record_permission_factory(record=record, action='list')
32
33


34
35
36
37
38
39
40
41
42
43
44
45
def record_update_permission_factory(record=None):
    """Update permission factory."""
    return record_permission_factory(record=record, action='update')


def record_delete_permission_factory(record=None):
    """Delete permission factory."""
    return record_permission_factory(record=record, action='delete')


class RecordPermission(object):
    """Record permission.
46
47
    - Create action given to owners only.
    - Read access given to everyone if public, according to a record ownership if not.
48
    - Update access given to record owners.
49
    - Delete access given to record owners.
50
51
52
53
    """

    create_actions = ['create']
    read_actions = ['read']
54
    list_actions = ['list']
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    update_actions = ['update']
    delete_actions = ['delete']

    def __init__(self, record, func, user):
        """Initialize a file permission object."""
        self.record = record
        self.func = func
        self.user = user or current_user

    def can(self):
        """Determine access."""
        return self.func(self.user, self.record)

    @classmethod
    def create(cls, record, action, user=None):
        """Create a record permission."""
        # Allow everything for testing
72
        if action in cls.list_actions:
73
            return cls(record, has_list_permission, user)
74
        elif action in cls.create_actions:
75
            return cls(record, has_owner_permission, user)
76
        elif action in cls.read_actions:
Pablo Panero's avatar
Oauth    
Pablo Panero committed
77
            return cls(record, has_read_record_permission, user)
78
        elif action in cls.update_actions:
Pablo Panero's avatar
Oauth    
Pablo Panero committed
79
            return cls(record, has_update_permission, user)
80
        elif action in cls.delete_actions:
Pablo Panero's avatar
Oauth    
Pablo Panero committed
81
            return cls(record, has_delete_permission, user)
82
83
84
85
        else:
            return cls(record, deny, user)


86
def has_owner_permission(user, record=None):
87
    """Check if user is authenticated and has create access"""
88
    log_action(user, 'CREATE/OWNER')
89
90
91
92
93

    # First authentication phase, decorator level
    if not record:
        return user.is_authenticated
    # Second authentication phase, record level
94
95
96
    if user.is_authenticated:
        # Allow based in the '_access' key
        user_provides = get_user_provides()
Pablo Panero's avatar
Search    
Pablo Panero committed
97
        es_index, doc = get_index_from_request(record)
Pablo Panero's avatar
Pablo Panero committed
98
        current_app.logger.debug('Using index {idx} and doc {doc}'.format(idx=es_index, doc=doc))
Pablo Panero's avatar
Search    
Pablo Panero committed
99
        if current_search_client.indices.exists([es_index]):
Pablo Panero's avatar
Oauth    
Pablo Panero committed
100
101
            mapping = current_search_client.indices.get_mapping([es_index])
            if mapping is not None:
102
                current_app.logger.debug('Using mapping for {idx}'.format(idx=es_index))
Pablo Panero's avatar
Pablo Panero committed
103
                current_app.logger.debug('Mapping {mapping}'.format(mapping=mapping))
Pablo Panero's avatar
Oauth    
Pablo Panero committed
104
                # set.isdisjoint() is faster than set.intersection()
Pablo Panero's avatar
Search    
Pablo Panero committed
105
                create_access_groups = mapping[es_index]['mappings'][doc]['_meta']['_owner'].split(',')
Pablo Panero's avatar
Oauth    
Pablo Panero committed
106
                if user_provides and not set(user_provides).isdisjoint(set(create_access_groups)):
107
                    current_app.logger.debug('User authenticated correctly')
Pablo Panero's avatar
Oauth    
Pablo Panero committed
108
                    return True
109
                current_app.logger.debug('Could not authenticate user, group sets are disjoint')
110
    return False
111
112


113
114
def has_list_permission(user, record=None):
    """Check if user is authenticated and has create access"""
115
116
117
118
119
    if user:
        log_action(user, 'LIST')
        return user.is_authenticated
    else:
        return False
120
121


122
def has_update_permission(user, record):
123
    """Check if user is authenticated and has update access"""
124
    log_action(user, 'UPDATE')
125
126
127
128
    if user.is_authenticated:
        # Allow based in the '_access' key
        user_provides = get_user_provides()
        # set.isdisjoint() is faster than set.intersection()
129
        update_access_groups = record['_access']['update']
130
        if check_elasticsearch(record) and user_provides and has_owner_permission(user) and \
131
132
133
134
135
                (
                        not set(user_provides).isdisjoint(set(update_access_groups))
                        or is_admin(user)
                ):
            current_app.logger.debug('Group sets not disjoint, user allowed')
136
137
            return True
    return False
138
139


140
141
def has_read_record_permission(user, record):
    """Check if user is authenticated and has read access. This implies reading one document"""
142
    log_action(user, 'READ')
143
144
145
146
    if user.is_authenticated:
        # Allow based in the '_access' key
        user_provides = get_user_provides()
        # set.isdisjoint() is faster than set.intersection()
147
148
149
150
151
152
153
154
155
156
        try:
            read_access_groups = record['_access']['read']
            if check_elasticsearch(record) and user_provides and has_owner_permission(user) and \
                    (
                            not set(user_provides).isdisjoint(set(read_access_groups))
                            or is_admin(user)
                    ):
                current_app.logger.debug('Group sets not disjoint, user allowed')
                return True
        except KeyError:
157
158
            return True
    return False
159
160


161
162
def has_delete_permission(user, record):
    """Check if user is authenticated and has delete access"""
163
    log_action(user, 'DELETE')
164
165
166
167
    if user.is_authenticated:
        # Allow based in the '_access' key
        user_provides = get_user_provides()
        # set.isdisjoint() is faster than set.intersection()
168
        delete_access_groups = record['_access']['delete']
169
        if check_elasticsearch(record) and user_provides and has_owner_permission(user) and \
170
171
172
173
174
                (
                        not set(user_provides).isdisjoint(set(delete_access_groups))
                        or is_admin(user)
                ):
            current_app.logger.debug('Group sets not disjoint, user allowed')
175
176
            return True
    return False
177
178


Pablo Panero's avatar
Pablo Panero committed
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
"""Access control for CERN Search Admin Web UI."""


def admin_permission_factory(view):
    """Record permission factory."""
    return AdminPermission.create(view=view)


class AdminPermission(object):

    def __init__(self, func, user, view):
        """Initialize a file permission object."""
        self.user = user or current_user
        self.func = func
        self.view = view

    def can(self):
        """Determine access."""
        return self.func(self.user)

    @classmethod
    def create(cls, user=None, view=None):
        """Create a record permission."""
        # Allow everything for testing
        return cls(has_admin_view_permission, user, view)


def has_admin_view_permission(user):
    admin_access_groups = current_app.config['ADMIN_VIEW_ACCESS_GROUPS']
    if user.is_authenticated and admin_access_groups:
        # Allow based in the '_access' key
        user_provides = get_user_provides()
        # set.isdisjoint() is faster than set.intersection()
        admin_access_groups = admin_access_groups.split(',')
        if user_provides and not set(user_provides).isdisjoint(set(admin_access_groups)):
214
            current_app.logger.debug('User has admin view access')
Pablo Panero's avatar
Pablo Panero committed
215
216
217
218
            return True
    return False


219
# Utility functions
220
221


222
223
224
225
226
227
228
229
230
231
def deny(user, record):
    """Deny access."""
    return False


def allow(user, record):
    """Allow access."""
    return True


232
233
234
235
def is_admin(user):
    """Check if the user is administrator"""
    admin_user = current_app.config['ADMIN_USER']
    if user.email == admin_user or user.email.replace('@cern.ch', '') == admin_user:
236
        current_app.logger.debug('User {user} is admin'.format(user=user.email))
237
238
239
240
        return True
    return False


241
242
243
244
245
246
def is_public(data, action):
    """Check if the record is fully public.
    In practice this means that the record doesn't have the ``access`` key or
    the action is not inside access or is empty.
    """
    return '_access' not in data or not data.get('_access', {}).get(action)
Pablo Panero's avatar
Search    
Pablo Panero committed
247
248
249
250
251
252
253
254
255


def check_elasticsearch(record=None):
    if record is not None:
        """Try to search for given record."""
        search = request._methodview.search_class()
        search = search.get_record(str(record.id))
        return search.count() == 1
    return False
256
257
258
259
260
261
262
263
264
265
266
267


def log_action(user, action):
    try:
        email = user.email
    except AttributeError:
        email = 'Anonymous'
    current_app.logger.debug('Action {action} -  user {usr} authenticated: {status}'.format(
        action=action,
        usr=email,
        status=user.is_authenticated
    ))