_sync_user_role_assignments()   F
last analyzed

Complexity

Conditions 10

Size

Total Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
dl 0
loc 81
rs 3.7499
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like RBACDefinitionsDBSyncer._sync_user_role_assignments() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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