Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2common/st2common/rbac/syncer.py (1 issue)

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
"""
17
Module for syncing RBAC definitions in the database with the ones from the filesystem.
18
"""
19
from __future__ import absolute_import
20
21
import six
22
23
from itertools import chain
24
25
if six.PY3:
26
    # pylint: disable=no-name-in-module
27
    from itertools import zip_longest as izip_longest  # NOQA
28
else:
29
    # pylint: disable=no-name-in-module
30
    from itertools import izip_longest  # NOQA
31
32
from collections import defaultdict
33
34
from mongoengine.queryset.visitor import Q
35
36
from st2common import log as logging
37
from st2common.models.db.auth import UserDB
38
from st2common.models.db.rbac import UserRoleAssignmentDB
39
from st2common.persistence.auth import User
40
from st2common.persistence.rbac import Role
41
from st2common.persistence.rbac import UserRoleAssignment
42
from st2common.persistence.rbac import PermissionGrant
43
from st2common.persistence.rbac import GroupToRoleMapping
44
from st2common.services import rbac as rbac_services
45
from st2common.util.uid import parse_uid
46
47
48
LOG = logging.getLogger(__name__)
49
50
__all__ = [
51
    'RBACDefinitionsDBSyncer',
52
    'RBACRemoteGroupToRoleSyncer'
53
]
54
55
56
class RBACDefinitionsDBSyncer(object):
57
    """
58
    A class which makes sure that the role definitions and user role assignments in the database
59
    match ones specified in the role definition files.
60
61
    The class works by simply deleting all the obsolete roles (either removed or updated) and
62
    creating new roles (either new roles or one which have been updated).
63
64
    Note #1: Our current datastore doesn't support transactions or similar which means that with
65
    the current data model there is a short time frame during sync when the definitions inside the
66
    DB are out of sync with the ones in the file.
67
68
    Note #2: The operation of this class is idempotent meaning that if it's ran multiple time with
69
    the same dataset, the end result / outcome will be the same.
70
    """
71
72
    def sync(self, role_definition_apis, role_assignment_apis, group_to_role_map_apis):
73
        """
74
        Synchronize all the role definitions, user role assignments and remote group to local roles
75
        maps.
76
        """
77
        result = {}
78
79
        result['roles'] = self.sync_roles(role_definition_apis)
80
        result['role_assignments'] = self.sync_users_role_assignments(role_assignment_apis)
81
        result['group_to_role_maps'] = self.sync_group_to_role_maps(group_to_role_map_apis)
82
83
        return result
84
85
    def sync_roles(self, role_definition_apis):
86
        """
87
        Synchronize all the role definitions in the database.
88
89
        :param role_dbs: RoleDB objects for the roles which are currently in the database.
90
        :type role_dbs: ``list`` of :class:`RoleDB`
91
92
        :param role_definition_apis: RoleDefinition API objects for the definitions loaded from
93
                                     the files.
94
        :type role_definition_apis: ``list`` of :class:RoleDefinitionFileFormatAPI`
95
96
        :rtype: ``tuple``
97
        """
98
        LOG.info('Synchronizing roles...')
99
100
        # Retrieve all the roles currently in the DB
101
        role_dbs = rbac_services.get_all_roles(exclude_system=True)
102
103
        role_db_names = [role_db.name for role_db in role_dbs]
104
        role_db_names = set(role_db_names)
105
        role_api_names = [role_definition_api.name for role_definition_api in role_definition_apis]
106
        role_api_names = set(role_api_names)
107
108
        # A list of new roles which should be added to the database
109
        new_role_names = role_api_names.difference(role_db_names)
110
111
        # A list of roles which need to be updated in the database
112
        updated_role_names = role_db_names.intersection(role_api_names)
113
114
        # A list of roles which should be removed from the database
115
        removed_role_names = (role_db_names - role_api_names)
116
117
        LOG.debug('New roles: %r' % (new_role_names))
118
        LOG.debug('Updated roles: %r' % (updated_role_names))
119
        LOG.debug('Removed roles: %r' % (removed_role_names))
120
121
        # Build a list of roles to delete
122
        role_names_to_delete = updated_role_names.union(removed_role_names)
123
        role_dbs_to_delete = [role_db for role_db in role_dbs if
124
                              role_db.name in role_names_to_delete]
125
126
        # Build a list of roles to create
127
        role_names_to_create = new_role_names.union(updated_role_names)
128
        role_apis_to_create = [role_definition_api for role_definition_api in role_definition_apis
129
                               if role_definition_api.name in role_names_to_create]
130
131
        ########
132
        # 1. Remove obsolete roles and associated permission grants from the DB
133
        ########
134
135
        # Remove roles
136
        role_ids_to_delete = []
137
        for role_db in role_dbs_to_delete:
138
            role_ids_to_delete.append(role_db.id)
139
140
        LOG.debug('Deleting %s stale roles' % (len(role_ids_to_delete)))
141
        Role.query(id__in=role_ids_to_delete, system=False).delete()
142
        LOG.debug('Deleted %s stale roles' % (len(role_ids_to_delete)))
143
144
        # Remove associated permission grants
145
        permission_grant_ids_to_delete = []
146
        for role_db in role_dbs_to_delete:
147
            permission_grant_ids_to_delete.extend(role_db.permission_grants)
148
149
        LOG.debug('Deleting %s stale permission grants' % (len(permission_grant_ids_to_delete)))
150
        PermissionGrant.query(id__in=permission_grant_ids_to_delete).delete()
151
        LOG.debug('Deleted %s stale permission grants' % (len(permission_grant_ids_to_delete)))
152
153
        ########
154
        # 2. Add new / updated roles to the DB
155
        ########
156
157
        LOG.debug('Creating %s new roles' % (len(role_apis_to_create)))
158
159
        # Create new roles
160
        created_role_dbs = []
161
        for role_api in role_apis_to_create:
162
            role_db = rbac_services.create_role(name=role_api.name,
163
                                                description=role_api.description)
164
165
            # Create associated permission grants
166
            permission_grants = getattr(role_api, 'permission_grants', [])
167
            for permission_grant in permission_grants:
168
                resource_uid = permission_grant.get('resource_uid', None)
169
170
                if resource_uid:
171
                    resource_type, _ = parse_uid(resource_uid)
172
                else:
173
                    resource_type = None
174
175
                permission_types = permission_grant['permission_types']
176
                assignment_db = rbac_services.create_permission_grant(
177
                    role_db=role_db,
178
                    resource_uid=resource_uid,
179
                    resource_type=resource_type,
180
                    permission_types=permission_types)
181
182
                role_db.permission_grants.append(str(assignment_db.id))
183
            created_role_dbs.append(role_db)
184
185
        LOG.debug('Created %s new roles' % (len(created_role_dbs)))
186
        LOG.info('Roles synchronized (%s created, %s updated, %s removed)' %
187
                 (len(new_role_names), len(updated_role_names), len(removed_role_names)))
188
189
        return [created_role_dbs, role_dbs_to_delete]
190
191
    def sync_users_role_assignments(self, role_assignment_apis):
192
        """
193
        Synchronize role assignments for all the users in the database.
194
195
        :param role_assignment_apis: Role assignments API objects for the assignments loaded
196
                                      from the files.
197
        :type role_assignment_apis: ``list`` of :class:`UserRoleAssignmentFileFormatAPI`
198
199
        :return: Dictionary with created and removed role assignments for each user.
200
        :rtype: ``dict``
201
        """
202
        assert isinstance(role_assignment_apis, (list, tuple))
203
204
        LOG.info('Synchronizing users role assignments...')
205
206
        # Note: We exclude remote assignments because sync tool is not supposed to manipulate
207
        # remote assignments
208
        role_assignment_dbs = rbac_services.get_all_role_assignments(include_remote=False)
209
210
        user_dbs = User.get_all()
211
212
        username_to_user_db_map = dict([(user_db.name, user_db) for user_db in user_dbs])
213
        username_to_role_assignment_apis_map = defaultdict(list)
214
        username_to_role_assignment_dbs_map = defaultdict(list)
215
216
        for role_assignment_api in role_assignment_apis:
217
            username = role_assignment_api.username
218
            username_to_role_assignment_apis_map[username].append(role_assignment_api)
219
220
        for role_assignment_db in role_assignment_dbs:
221
            username = role_assignment_db.user
222
            username_to_role_assignment_dbs_map[username].append(role_assignment_db)
223
224
        # Note: We process assignments for all the users (ones specified in the assignment files
225
        # and ones which are in the database). We want to make sure assignments are correctly
226
        # deleted from the database for users which existing in the database, but have no
227
        # assignment file on disk and for assignments for users which don't exist in the database.
228
        all_usernames = (list(username_to_user_db_map.keys()) +
229
                         list(username_to_role_assignment_apis_map.keys()) +
230
                         list(username_to_role_assignment_dbs_map.keys()))
231
        all_usernames = list(set(all_usernames))
232
233
        results = {}
234
        for username in all_usernames:
235
            user_db = username_to_user_db_map.get(username, None)
236
237
            if not user_db:
238
                # Note: We allow assignments to be created for the users which don't exist in the
239
                # DB yet because user creation in StackStorm is lazy (we only create UserDB) object
240
                # when user first logs in.
241
                user_db = UserDB(name=username)
242
                LOG.debug(('User "%s" doesn\'t exist in the DB, creating assignment anyway' %
243
                          (username)))
244
245
            role_assignment_apis = username_to_role_assignment_apis_map.get(username, [])
246
            role_assignment_dbs = username_to_role_assignment_dbs_map.get(username, [])
247
248
            # Additional safety assert to ensure we don't accidentally manipulate remote
249
            # assignments
250
            for role_assignment_db in role_assignment_dbs:
251
                assert role_assignment_db.is_remote is False
252
253
            result = self._sync_user_role_assignments(
254
                user_db=user_db, role_assignment_dbs=role_assignment_dbs,
255
                role_assignment_apis=role_assignment_apis)
256
257
            results[username] = result
258
259
        LOG.info('User role assignments synchronized')
260
        return results
261
262
    def sync_group_to_role_maps(self, group_to_role_map_apis):
263
        LOG.info('Synchronizing group to role maps...')
264
265
        # Retrieve all the mappings currently in the db
266
        group_to_role_map_dbs = rbac_services.get_all_group_to_role_maps()
267
268
        # 1. Delete all the existing mappings in the db
269
        group_to_role_map_to_delete = []
270
        for group_to_role_map_db in group_to_role_map_dbs:
271
            group_to_role_map_to_delete.append(group_to_role_map_db.id)
272
273
        GroupToRoleMapping.query(id__in=group_to_role_map_to_delete).delete()
274
275
        # 2. Insert all mappings read from disk
276
        for group_to_role_map_api in group_to_role_map_apis:
277
            source = getattr(group_to_role_map_api, 'file_path', None)
278
            rbac_services.create_group_to_role_map(group=group_to_role_map_api.group,
279
                                                   roles=group_to_role_map_api.roles,
280
                                                   description=group_to_role_map_api.description,
281
                                                   enabled=group_to_role_map_api.enabled,
282
                                                   source=source)
283
284
        LOG.info('Group to role map definitions synchronized.')
285
286
    def _sync_user_role_assignments(self, user_db, role_assignment_dbs, role_assignment_apis):
287
        """
288
        Synchronize role assignments for a particular user.
289
290
        :param user_db: User to synchronize the assignments for.
291
        :type user_db: :class:`UserDB`
292
293
        :param role_assignment_dbs: Existing user role assignments.
294
        :type role_assignment_dbs: ``list`` of :class:`UserRoleAssignmentDB`
295
296
        :param role_assignment_apis: List of user role assignments to apply.
297
        :param role_assignment_apis: ``list`` of :class:`UserRoleAssignmentFileFormatAPI`
298
299
        :rtype: ``tuple``
300
        """
301
        db_roles = set([(entry.role, entry.source) for entry in role_assignment_dbs])
302
303
        api_roles = [
304
            list(izip_longest(entry.roles, [], fillvalue=entry.file_path))
305
            for entry in role_assignment_apis
306
        ]
307
308
        api_roles = set(list(chain.from_iterable(api_roles)))
309
310
        # A list of new assignments which should be added to the database
311
        new_roles = api_roles.difference(db_roles)
312
313
        # A list of assignments which need to be updated in the database
314
        updated_roles = db_roles.intersection(api_roles)
315
316
        # A list of assignments which should be removed from the database
317
        removed_roles = (db_roles - api_roles)
318
319
        LOG.debug('New assignments for user "%s": %r' % (user_db.name, new_roles))
320
        LOG.debug('Updated assignments for user "%s": %r' % (user_db.name, updated_roles))
321
        LOG.debug('Removed assignments for user "%s": %r' % (user_db.name, removed_roles))
322
323
        # Build a list of role assignments to delete
324
        roles_to_delete = updated_roles.union(removed_roles)
325
326
        role_assignment_dbs_to_delete = [
327
            role_assignment_db for role_assignment_db in role_assignment_dbs
328
            if (role_assignment_db.role, role_assignment_db.source) in roles_to_delete
329
        ]
330
331
        for role_name, assignment_source in roles_to_delete:
332
            queryset_filter = (
333
                Q(user=user_db.name) &
334
                Q(role=role_name) &
335
                Q(source=assignment_source) &
336
                (Q(is_remote=False) | Q(is_remote__exists=False))
337
            )
338
339
            UserRoleAssignmentDB.objects(queryset_filter).delete()
340
341
            LOG.debug('Removed role "%s" from "%s" for user "%s".' % (role_name, assignment_source,
342
                                                                      user_db.name))
343
344
        # Build a list of roles assignments to create
345
        roles_to_create = new_roles.union(updated_roles)
346
        created_role_assignment_dbs = []
347
348
        for role_name, assignment_source in roles_to_create:
349
            role_db = Role.get(name=role_name)
350
            if not role_db:
351
                msg = 'Role "%s" referenced in assignment file "%s" doesn\'t exist'
352
                raise ValueError(msg % (role_name, assignment_source))
353
354
            role_assignment_api = [r for r in role_assignment_apis if
355
                                   r.file_path == assignment_source][0]
356
            description = getattr(role_assignment_api, 'description', None)
357
358
            assignment_db = rbac_services.assign_role_to_user(
359
                role_db=role_db, user_db=user_db, source=assignment_source, description=description)
360
361
            created_role_assignment_dbs.append(assignment_db)
362
363
            LOG.debug('Assigned role "%s" from "%s" for user "%s".' %
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
364
                (role_name, assignment_source, user_db.name))
365
366
        return (created_role_assignment_dbs, role_assignment_dbs_to_delete)
367
368
369
class RBACRemoteGroupToRoleSyncer(object):
370
    """
371
    Class which writes remote user role assignments based on the user group membership information
372
    provided by the auth backend and based on the group to role mapping definitions on disk.
373
    """
374
375
    def sync(self, user_db, groups):
376
        """
377
        :param user_db: User to sync the assignments for.
378
        :type user: :class:`UserDB`
379
380
        :param groups: A list of remote groups user is a member of.
381
        :type groups: ``list`` of ``str``
382
383
        :return: A list of mappings which have been created.
384
        :rtype: ``list`` of :class:`UserRoleAssignmentDB`
385
        """
386
        groups = list(set(groups))
387
388
        extra = {'user_db': user_db, 'groups': groups}
389
        LOG.info('Synchronizing remote role assignments for user "%s"' % (str(user_db)),
390
                 extra=extra)
391
392
        # 1. Retrieve group to role mappings for the provided groups
393
        all_mapping_dbs = GroupToRoleMapping.query(group__in=groups)
394
        enabled_mapping_dbs = [mapping_db for mapping_db in all_mapping_dbs if
395
                               mapping_db.enabled]
396
        disabled_mapping_dbs = [mapping_db for mapping_db in all_mapping_dbs if
397
                                not mapping_db.enabled]
398
399
        if not all_mapping_dbs:
400
            LOG.debug('No group to role mappings found for user "%s"' % (str(user_db)), extra=extra)
401
402
        # 2. Remove all the existing remote role assignments
403
        remote_assignment_dbs = UserRoleAssignment.query(user=user_db.name, is_remote=True)
404
405
        existing_role_names = [assignment_db.role for assignment_db in remote_assignment_dbs]
406
        existing_role_names = set(existing_role_names)
407
        current_role_names = set([])
408
409
        for mapping_db in all_mapping_dbs:
410
            for role in mapping_db.roles:
411
                current_role_names.add(role)
412
413
        # A list of new role assignments which should be added to the database
414
        new_role_names = current_role_names.difference(existing_role_names)
415
416
        # A list of role assignments which need to be updated in the database
417
        updated_role_names = existing_role_names.intersection(current_role_names)
418
419
        # A list of role assignments which should be removed from the database
420
        removed_role_names = (existing_role_names - new_role_names)
421
422
        # Also remove any assignments for mappings which are disabled in the database
423
        for mapping_db in disabled_mapping_dbs:
424
            for role in mapping_db.roles:
425
                removed_role_names.add(role)
426
427
        LOG.debug('New role assignments: %r' % (new_role_names))
428
        LOG.debug('Updated role assignments: %r' % (updated_role_names))
429
        LOG.debug('Removed role assignments: %r' % (removed_role_names))
430
431
        # Build a list of role assignments to delete
432
        role_names_to_delete = updated_role_names.union(removed_role_names)
433
        role_assignment_dbs_to_delete = [role_assignment_db for role_assignment_db
434
                                         in remote_assignment_dbs
435
                                         if role_assignment_db.role in role_names_to_delete]
436
437
        UserRoleAssignment.query(user=user_db.name, role__in=role_names_to_delete,
438
                                 is_remote=True).delete()
439
440
        # 3. Create role assignments for all the current groups
441
        created_assignments_dbs = []
442
        for mapping_db in enabled_mapping_dbs:
443
            extra['mapping_db'] = mapping_db
444
445
            for role_name in mapping_db.roles:
446
                role_db = rbac_services.get_role_by_name(name=role_name)
447
448
                if not role_db:
449
                    # Gracefully skip assignment for role which doesn't exist in the db
450
                    LOG.info('Role with name "%s" for mapping "%s" not found, skipping assignment.'
451
                             % (role_name, str(mapping_db)), extra=extra)
452
                    continue
453
454
                description = ('Automatic role assignment based on the remote user membership in '
455
                               'group "%s"' % (mapping_db.group))
456
                assignment_db = rbac_services.assign_role_to_user(role_db=role_db, user_db=user_db,
457
                                                                  description=description,
458
                                                                  is_remote=True,
459
                                                                  source=mapping_db.source,
460
                                                                  ignore_already_exists_error=True)
461
                assert assignment_db.is_remote is True
462
                created_assignments_dbs.append(assignment_db)
463
464
        LOG.debug('Created %s new remote role assignments for user "%s"' %
465
                  (len(created_assignments_dbs), str(user_db)), extra=extra)
466
467
        return (created_assignments_dbs, role_assignment_dbs_to_delete)
468