OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """Persistence class for user groups. |
| 7 |
| 8 User groups are represented in the database by: |
| 9 - A row in the Users table giving an email address and user ID. |
| 10 (A "group ID" is the user_id of the group in the User table.) |
| 11 - A row in the UserGroupSettings table giving user group settings. |
| 12 |
| 13 Membership of a user X in user group Y is represented as: |
| 14 - A row in the UserGroup table with user_id=X and group_id=Y. |
| 15 """ |
| 16 |
| 17 import collections |
| 18 import logging |
| 19 import re |
| 20 |
| 21 from framework import permissions |
| 22 from framework import sql |
| 23 from proto import usergroup_pb2 |
| 24 from services import caches |
| 25 |
| 26 |
| 27 USERGROUP_TABLE_NAME = 'UserGroup' |
| 28 USERGROUPSETTINGS_TABLE_NAME = 'UserGroupSettings' |
| 29 USERGROUPPROJECTS_TABLE_NAME = 'Group2Project' |
| 30 |
| 31 USERGROUP_COLS = ['user_id', 'group_id', 'role'] |
| 32 USERGROUPSETTINGS_COLS = ['group_id', 'who_can_view_members', |
| 33 'external_group_type', 'last_sync_time'] |
| 34 USERGROUPPROJECTS_COLS = ['group_id', 'project_id'] |
| 35 |
| 36 |
| 37 class MembershipTwoLevelCache(caches.AbstractTwoLevelCache): |
| 38 """Class to manage RAM and memcache for each user's memberships.""" |
| 39 |
| 40 def __init__(self, cache_manager, usergroup_service, group_dag): |
| 41 super(MembershipTwoLevelCache, self).__init__( |
| 42 cache_manager, 'user', 'memberships:', None) |
| 43 self.usergroup_service = usergroup_service |
| 44 self.group_dag = group_dag |
| 45 |
| 46 def _DeserializeMemberships(self, memberships_rows): |
| 47 """Reserialize the DB results into a {user_id: {group_id}}.""" |
| 48 result_dict = collections.defaultdict(set) |
| 49 for user_id, group_id in memberships_rows: |
| 50 result_dict[user_id].add(group_id) |
| 51 |
| 52 return result_dict |
| 53 |
| 54 def FetchItems(self, cnxn, keys): |
| 55 """On RAM and memcache miss, hit the database to get memberships.""" |
| 56 direct_memberships_rows = self.usergroup_service.usergroup_tbl.Select( |
| 57 cnxn, cols=['user_id', 'group_id'], distinct=True, |
| 58 user_id=keys) |
| 59 memberships_set = set() |
| 60 for c_id, p_id in direct_memberships_rows: |
| 61 all_parents = self.group_dag.GetAllAncestors(cnxn, p_id, True) |
| 62 all_parents.append(p_id) |
| 63 memberships_set.update([(c_id, g_id) for g_id in all_parents]) |
| 64 retrieved_dict = self._DeserializeMemberships(list(memberships_set)) |
| 65 |
| 66 # Make sure that every requested user is in the result, and gets cached. |
| 67 retrieved_dict.update( |
| 68 (user_id, set()) for user_id in keys |
| 69 if user_id not in retrieved_dict) |
| 70 return retrieved_dict |
| 71 |
| 72 |
| 73 class UserGroupService(object): |
| 74 """The persistence layer for user group data.""" |
| 75 |
| 76 def __init__(self, cache_manager): |
| 77 """Initialize this service so that it is ready to use. |
| 78 |
| 79 Args: |
| 80 cache_manager: local cache with distributed invalidation. |
| 81 """ |
| 82 self.usergroup_tbl = sql.SQLTableManager(USERGROUP_TABLE_NAME) |
| 83 self.usergroupsettings_tbl = sql.SQLTableManager( |
| 84 USERGROUPSETTINGS_TABLE_NAME) |
| 85 self.usergroupprojects_tbl = sql.SQLTableManager( |
| 86 USERGROUPPROJECTS_TABLE_NAME) |
| 87 |
| 88 self.group_dag = UserGroupDAG(self) |
| 89 |
| 90 # Like a dictionary {user_id: {group_id}} |
| 91 self.memberships_2lc = MembershipTwoLevelCache( |
| 92 cache_manager, self, self.group_dag) |
| 93 |
| 94 ### Group creation |
| 95 |
| 96 def CreateGroup(self, cnxn, services, group_name, who_can_view_members, |
| 97 ext_group_type=None, friend_projects=None): |
| 98 """Create a new user group. |
| 99 |
| 100 Args: |
| 101 cnxn: connection to SQL database. |
| 102 services: connections to backend services. |
| 103 group_name: string email address of the group to create. |
| 104 who_can_view_members: 'owners', 'members', or 'anyone'. |
| 105 ext_group_type: The type of external group to import. |
| 106 friend_projects: The project ids declared as group friends to view its |
| 107 members. |
| 108 |
| 109 Returns: |
| 110 int group_id of the new group. |
| 111 """ |
| 112 friend_projects = friend_projects or [] |
| 113 assert who_can_view_members in ('owners', 'members', 'anyone') |
| 114 if ext_group_type: |
| 115 ext_group_type = str(ext_group_type).lower() |
| 116 assert ext_group_type in ( |
| 117 'chrome_infra_auth', 'mdb', 'baggins'), ( |
| 118 ext_group_type) |
| 119 assert who_can_view_members == 'owners' |
| 120 group_id = services.user.LookupUserID( |
| 121 cnxn, group_name.lower(), autocreate=True, allowgroups=True) |
| 122 group_settings = usergroup_pb2.MakeSettings( |
| 123 who_can_view_members, ext_group_type, 0, friend_projects) |
| 124 self.UpdateSettings(cnxn, group_id, group_settings) |
| 125 return group_id |
| 126 |
| 127 def DeleteGroups(self, cnxn, group_ids): |
| 128 """Delete groups' members and settings. It will NOT delete user entries. |
| 129 |
| 130 Args: |
| 131 cnxn: connection to SQL database. |
| 132 group_ids: list of group ids to delete. |
| 133 """ |
| 134 member_ids_dict, owner_ids_dict = self.LookupMembers(cnxn, group_ids) |
| 135 citizens_id_dict = collections.defaultdict(list) |
| 136 for g_id, user_ids in member_ids_dict.iteritems(): |
| 137 citizens_id_dict[g_id].extend(user_ids) |
| 138 for g_id, user_ids in owner_ids_dict.iteritems(): |
| 139 citizens_id_dict[g_id].extend(user_ids) |
| 140 for g_id, citizen_ids in citizens_id_dict.iteritems(): |
| 141 logging.info('Deleting group %d', g_id) |
| 142 # Remove group members, friend projects and settings |
| 143 self.RemoveMembers(cnxn, g_id, citizen_ids) |
| 144 self.usergroupprojects_tbl.Delete(cnxn, group_id=g_id) |
| 145 self.usergroupsettings_tbl.Delete(cnxn, group_id=g_id) |
| 146 |
| 147 def DetermineWhichUserIDsAreGroups(self, cnxn, user_ids): |
| 148 """From a list of user IDs, identify potential user groups. |
| 149 |
| 150 Args: |
| 151 cnxn: connection to SQL database. |
| 152 user_ids: list of user IDs to examine. |
| 153 |
| 154 Returns: |
| 155 A list with a subset of the given user IDs that are user groups |
| 156 rather than individual users. |
| 157 """ |
| 158 # It is a group if there is any entry in the UserGroupSettings table. |
| 159 group_id_rows = self.usergroupsettings_tbl.Select( |
| 160 cnxn, cols=['group_id'], group_id=user_ids) |
| 161 group_ids = [row[0] for row in group_id_rows] |
| 162 return group_ids |
| 163 |
| 164 ### User memberships in groups |
| 165 |
| 166 def LookupAllMemberships(self, cnxn, user_ids, use_cache=True): |
| 167 """Lookup all the group memberships of a list of users. |
| 168 |
| 169 Args: |
| 170 cnxn: connection to SQL database. |
| 171 user_ids: list of int user IDs to get memberships for. |
| 172 use_cache: set to False to ignore cached values. |
| 173 |
| 174 Returns: |
| 175 A dict {user_id: {group_id}} for the given user_ids. |
| 176 """ |
| 177 result_dict, missed_ids = self.memberships_2lc.GetAll( |
| 178 cnxn, user_ids, use_cache=use_cache) |
| 179 assert not missed_ids |
| 180 return result_dict |
| 181 |
| 182 def LookupMemberships(self, cnxn, user_id): |
| 183 """Return a set of group_ids that this user is a member of.""" |
| 184 membership_dict = self.LookupAllMemberships(cnxn, [user_id]) |
| 185 return membership_dict[user_id] |
| 186 |
| 187 ### Group member addition, removal, and retrieval |
| 188 |
| 189 def RemoveMembers(self, cnxn, group_id, old_member_ids): |
| 190 """Remove the given members/owners from the user group.""" |
| 191 self.usergroup_tbl.Delete( |
| 192 cnxn, group_id=group_id, user_id=old_member_ids) |
| 193 |
| 194 all_affected = self._GetAllMembersInList(cnxn, old_member_ids) |
| 195 |
| 196 self.group_dag.MarkObsolete() |
| 197 self.memberships_2lc.InvalidateAllKeys(cnxn, all_affected) |
| 198 |
| 199 def UpdateMembers(self, cnxn, group_id, member_ids, new_role): |
| 200 """Update role for given members/owners to the user group.""" |
| 201 # Circle detection |
| 202 for mid in member_ids: |
| 203 if self.group_dag.IsChild(cnxn, group_id, mid): |
| 204 raise CircularGroupException( |
| 205 '%s is already an ancestor of group %s.' % (mid, group_id)) |
| 206 |
| 207 self.usergroup_tbl.Delete( |
| 208 cnxn, group_id=group_id, user_id=member_ids) |
| 209 rows = [(member_id, group_id, new_role) for member_id in member_ids] |
| 210 self.usergroup_tbl.InsertRows( |
| 211 cnxn, ['user_id', 'group_id', 'role'], rows) |
| 212 |
| 213 all_affected = self._GetAllMembersInList(cnxn, member_ids) |
| 214 |
| 215 self.group_dag.MarkObsolete() |
| 216 self.memberships_2lc.InvalidateAllKeys(cnxn, all_affected) |
| 217 |
| 218 def _GetAllMembersInList(self, cnxn, group_ids): |
| 219 """Get all direct/indirect members/owners in a list.""" |
| 220 children_member_ids, children_owner_ids = self.LookupAllMembers( |
| 221 cnxn, group_ids) |
| 222 all_members_owners = set() |
| 223 all_members_owners.update(group_ids) |
| 224 for users in children_member_ids.itervalues(): |
| 225 all_members_owners.update(users) |
| 226 for users in children_owner_ids.itervalues(): |
| 227 all_members_owners.update(users) |
| 228 return list(all_members_owners) |
| 229 |
| 230 def LookupAllMembers(self, cnxn, group_ids): |
| 231 """Retrieve user IDs of members/owners of any of the given groups |
| 232 transitively.""" |
| 233 direct_member_rows = self.usergroup_tbl.Select( |
| 234 cnxn, cols=['user_id', 'group_id', 'role'], distinct=True, |
| 235 group_id=group_ids) |
| 236 member_ids_dict = {} |
| 237 owner_ids_dict = {} |
| 238 for gid in group_ids: |
| 239 all_descendants = self.group_dag.GetAllDescendants(cnxn, gid, True) |
| 240 indirect_member_rows = self.usergroup_tbl.Select( |
| 241 cnxn, cols=['user_id'], distinct=True, |
| 242 group_id=all_descendants) |
| 243 |
| 244 # Owners must have direct membership. All indirect users are members. |
| 245 owner_ids_dict[gid] = [m[0] for m in direct_member_rows |
| 246 if m[1] == gid and m[2] == 'owner'] |
| 247 member_ids_list = [r[0] for r in indirect_member_rows] |
| 248 member_ids_list.extend([m[0] for m in direct_member_rows |
| 249 if m[1] == gid and m[2] == 'member']) |
| 250 member_ids_dict[gid] = list(set(member_ids_list)) |
| 251 return member_ids_dict, owner_ids_dict |
| 252 |
| 253 def LookupMembers(self, cnxn, group_ids): |
| 254 """"Retrieve user IDs of direct members/owners of any of the given groups. |
| 255 |
| 256 Args: |
| 257 cnxn: connection to SQL database. |
| 258 group_ids: list of int user IDs for all user groups to be examined. |
| 259 |
| 260 Returns: |
| 261 A dict of member IDs, and a dict of owner IDs keyed by group id. |
| 262 """ |
| 263 member_rows = self.usergroup_tbl.Select( |
| 264 cnxn, cols=['user_id', 'group_id', 'role'], distinct=True, |
| 265 group_id=group_ids) |
| 266 member_ids_dict = {} |
| 267 owner_ids_dict = {} |
| 268 for gid in group_ids: |
| 269 member_ids_dict[gid] = [row[0] for row in member_rows |
| 270 if row[1] == gid and row[2] == 'member'] |
| 271 owner_ids_dict[gid] = [row[0] for row in member_rows |
| 272 if row[1] == gid and row[2] == 'owner'] |
| 273 return member_ids_dict, owner_ids_dict |
| 274 |
| 275 def ExpandAnyUserGroups(self, cnxn, user_ids): |
| 276 """Transitively expand any user groups and return member user IDs. |
| 277 |
| 278 Args: |
| 279 cnxn: connection to SQL database. |
| 280 user_ids: list of user IDs to check. |
| 281 |
| 282 Returns: |
| 283 A pair (individual_user_ids, transitive_ids). individual_user_ids |
| 284 is a list of user IDs that were in the given user_ids list and |
| 285 that identify individual members. transitive_ids is a list of |
| 286 user IDs of the members of any user group in the given list of |
| 287 user_ids and the individual members of any nested groups. |
| 288 """ |
| 289 group_ids = self.DetermineWhichUserIDsAreGroups(cnxn, user_ids) |
| 290 direct_ids = [uid for uid in user_ids if uid not in group_ids] |
| 291 member_ids_dict, owner_ids_dict = self.LookupAllMembers(cnxn, group_ids) |
| 292 indirect_ids = set() |
| 293 for gid in group_ids: |
| 294 indirect_ids.update(member_ids_dict[gid]) |
| 295 indirect_ids.update(owner_ids_dict[gid]) |
| 296 |
| 297 # Note: we return direct and indirect member IDs separately so that |
| 298 # the email notification footer can give more a specific reason for |
| 299 # why the user got an email. E.g., "You were Cc'd" vs. "You are a |
| 300 # member of a user group that was Cc'd". |
| 301 return direct_ids, list(indirect_ids) |
| 302 |
| 303 def LookupVisibleMembers( |
| 304 self, cnxn, group_id_list, perms, effective_ids, services): |
| 305 """"Retrieve the list of user group direct member/owner IDs that the user |
| 306 may see. |
| 307 |
| 308 Args: |
| 309 cnxn: connection to SQL database. |
| 310 group_id_list: list of int user IDs for all user groups to be examined. |
| 311 perms: optional PermissionSet for the user viewing this page. |
| 312 effective_ids: set of int user IDs for that user and all |
| 313 his/her group memberships. |
| 314 services: backend services. |
| 315 |
| 316 Returns: |
| 317 A list of all the member IDs from any group that the user is allowed |
| 318 to view. |
| 319 """ |
| 320 settings_dict = self.GetAllGroupSettings(cnxn, group_id_list) |
| 321 group_ids = settings_dict.keys() |
| 322 (owned_project_ids, membered_project_ids, |
| 323 contrib_project_ids) = services.project.GetUserRolesInAllProjects( |
| 324 cnxn, effective_ids) |
| 325 project_ids = owned_project_ids.union( |
| 326 membered_project_ids).union(contrib_project_ids) |
| 327 # We need to fetch all members/owners to determine whether the requester |
| 328 # has permission to view. |
| 329 direct_member_ids_dict, direct_owner_ids_dict = self.LookupMembers( |
| 330 cnxn, group_ids) |
| 331 all_member_ids_dict, all_owner_ids_dict = self.LookupAllMembers( |
| 332 cnxn, group_ids) |
| 333 visible_member_ids = {} |
| 334 visible_owner_ids = {} |
| 335 for gid in group_ids: |
| 336 member_ids = all_member_ids_dict[gid] |
| 337 owner_ids = all_owner_ids_dict[gid] |
| 338 |
| 339 if permissions.CanViewGroup(perms, effective_ids, settings_dict[gid], |
| 340 member_ids, owner_ids, project_ids): |
| 341 visible_member_ids[gid] = direct_member_ids_dict[gid] |
| 342 visible_owner_ids[gid] = direct_owner_ids_dict[gid] |
| 343 |
| 344 return visible_member_ids, visible_owner_ids |
| 345 |
| 346 ### Group settings |
| 347 |
| 348 def GetAllUserGroupsInfo(self, cnxn): |
| 349 """Fetch (addr, member_count, usergroup_settings) for all user groups.""" |
| 350 group_rows = self.usergroupsettings_tbl.Select( |
| 351 cnxn, cols=['group_id', 'email', 'who_can_view_members', |
| 352 'external_group_type', 'last_sync_time'], |
| 353 left_joins=[('User ON UserGroupSettings.group_id = User.user_id', [])]) |
| 354 count_rows = self.usergroup_tbl.Select( |
| 355 cnxn, cols=['group_id', 'COUNT(*)'], |
| 356 group_by=['group_id']) |
| 357 count_dict = dict(count_rows) |
| 358 group_ids = [g[0] for g in group_rows] |
| 359 friends_dict = self.GetAllGroupFriendProjects(cnxn, group_ids) |
| 360 |
| 361 user_group_info_tuples = [ |
| 362 (email, count_dict.get(group_id, 0), |
| 363 usergroup_pb2.MakeSettings(visiblity, group_type, last_sync_time, |
| 364 friends_dict.get(group_id, [])), |
| 365 group_id) |
| 366 for (group_id, email, visiblity, group_type, last_sync_time) |
| 367 in group_rows] |
| 368 return user_group_info_tuples |
| 369 |
| 370 def GetAllGroupSettings(self, cnxn, group_ids): |
| 371 """Fetch {group_id: group_settings} for the specified groups.""" |
| 372 # TODO(jrobbins): add settings to control who can join, etc. |
| 373 rows = self.usergroupsettings_tbl.Select( |
| 374 cnxn, cols=USERGROUPSETTINGS_COLS, group_id=group_ids) |
| 375 friends_dict = self.GetAllGroupFriendProjects(cnxn, group_ids) |
| 376 settings_dict = { |
| 377 group_id: usergroup_pb2.MakeSettings( |
| 378 vis, group_type, last_sync_time, friends_dict.get(group_id, [])) |
| 379 for group_id, vis, group_type, last_sync_time in rows} |
| 380 return settings_dict |
| 381 |
| 382 def GetGroupSettings(self, cnxn, group_id): |
| 383 """Retrieve group settings for the specified user group. |
| 384 |
| 385 Args: |
| 386 cnxn: connection to SQL database. |
| 387 group_id: int user ID of the user group. |
| 388 |
| 389 Returns: |
| 390 A UserGroupSettings object, or None if no such group exists. |
| 391 """ |
| 392 return self.GetAllGroupSettings(cnxn, [group_id]).get(group_id) |
| 393 |
| 394 def UpdateSettings(self, cnxn, group_id, group_settings): |
| 395 """Update the visiblity settings of the specified group.""" |
| 396 who_can_view_members = str(group_settings.who_can_view_members).lower() |
| 397 ext_group_type = group_settings.ext_group_type |
| 398 assert who_can_view_members in ('owners', 'members', 'anyone') |
| 399 if ext_group_type: |
| 400 ext_group_type = str(group_settings.ext_group_type).lower() |
| 401 assert ext_group_type in ( |
| 402 'chrome_infra_auth', 'mdb', 'baggins'), ( |
| 403 ext_group_type) |
| 404 assert who_can_view_members == 'owners' |
| 405 self.usergroupsettings_tbl.InsertRow( |
| 406 cnxn, group_id=group_id, who_can_view_members=who_can_view_members, |
| 407 external_group_type=ext_group_type, |
| 408 last_sync_time=group_settings.last_sync_time, |
| 409 replace=True) |
| 410 self.usergroupprojects_tbl.Delete( |
| 411 cnxn, group_id=group_id) |
| 412 if group_settings.friend_projects: |
| 413 rows = [(group_id, p_id) for p_id in group_settings.friend_projects] |
| 414 self.usergroupprojects_tbl.InsertRows( |
| 415 cnxn, ['group_id', 'project_id'], rows) |
| 416 |
| 417 def GetAllGroupFriendProjects(self, cnxn, group_ids): |
| 418 """Get {group_id: [project_ids]} for the specified user groups.""" |
| 419 rows = self.usergroupprojects_tbl.Select( |
| 420 cnxn, cols=USERGROUPPROJECTS_COLS, group_id=group_ids) |
| 421 friends_dict = {} |
| 422 for group_id, project_id in rows: |
| 423 friends_dict.setdefault(group_id, []).append(project_id) |
| 424 return friends_dict |
| 425 |
| 426 def GetGroupFriendProjects(self, cnxn, group_id): |
| 427 """Get a list of friend projects for the specified user group.""" |
| 428 return self.GetAllGroupFriendProjects(cnxn, [group_id]).get(group_id) |
| 429 |
| 430 def ValidateFriendProjects(self, cnxn, services, friend_projects): |
| 431 """Validate friend projects. |
| 432 |
| 433 Returns: |
| 434 A list of project ids if no errors, or an error message. |
| 435 """ |
| 436 project_names = filter(None, re.split('; |, | |;|,', friend_projects)) |
| 437 id_dict = services.project.LookupProjectIDs(cnxn, project_names) |
| 438 missed_projects = [] |
| 439 result = [] |
| 440 for p_name in project_names: |
| 441 if p_name in id_dict: |
| 442 result.append(id_dict[p_name]) |
| 443 else: |
| 444 missed_projects.append(p_name) |
| 445 error_msg = '' |
| 446 if missed_projects: |
| 447 error_msg = 'Project(s) %s do not exist' % ', '.join(missed_projects) |
| 448 return None, error_msg |
| 449 else: |
| 450 return result, None |
| 451 |
| 452 # TODO(jrobbins): re-implement FindUntrustedGroups() |
| 453 |
| 454 |
| 455 class UserGroupDAG(object): |
| 456 """A directed-acyclic graph of potentially nested user groups.""" |
| 457 |
| 458 def __init__(self, usergroup_service): |
| 459 self.usergroup_service = usergroup_service |
| 460 self.user_group_parents = collections.defaultdict(list) |
| 461 self.user_group_children = collections.defaultdict(list) |
| 462 self.initialized = False |
| 463 |
| 464 def Build(self, cnxn, circle_detection=False): |
| 465 if not self.initialized: |
| 466 self.user_group_parents.clear() |
| 467 self.user_group_children.clear() |
| 468 group_ids = self.usergroup_service.usergroupsettings_tbl.Select( |
| 469 cnxn, cols=['group_id']) |
| 470 usergroup_rows = self.usergroup_service.usergroup_tbl.Select( |
| 471 cnxn, cols=['user_id', 'group_id'], distinct=True, |
| 472 user_id=[r[0] for r in group_ids]) |
| 473 for user_id, group_id in usergroup_rows: |
| 474 self.user_group_parents[user_id].append(group_id) |
| 475 self.user_group_children[group_id].append(user_id) |
| 476 self.initialized = True |
| 477 |
| 478 if circle_detection: |
| 479 for child_id, parent_ids in self.user_group_parents.iteritems(): |
| 480 for parent_id in parent_ids: |
| 481 if self.IsChild(cnxn, parent_id, child_id): |
| 482 logging.error( |
| 483 'Circle exists between group %d and %d.', child_id, parent_id) |
| 484 |
| 485 def GetAllAncestors(self, cnxn, group_id, circle_detection=False): |
| 486 """Return a list of distinct ancestor group IDs for the given group.""" |
| 487 self.Build(cnxn, circle_detection) |
| 488 result = set() |
| 489 child_ids = [group_id] |
| 490 while child_ids: |
| 491 parent_ids = set() |
| 492 for c_id in child_ids: |
| 493 group_ids = self.user_group_parents[c_id] |
| 494 parent_ids.update(g_id for g_id in group_ids if g_id not in result) |
| 495 result.update(parent_ids) |
| 496 child_ids = list(parent_ids) |
| 497 return list(result) |
| 498 |
| 499 def GetAllDescendants(self, cnxn, group_id, circle_detection=False): |
| 500 """Return a list of distinct descendant group IDs for the given group.""" |
| 501 self.Build(cnxn, circle_detection) |
| 502 result = set() |
| 503 parent_ids = [group_id] |
| 504 while parent_ids: |
| 505 child_ids = set() |
| 506 for p_id in parent_ids: |
| 507 group_ids = self.user_group_children[p_id] |
| 508 child_ids.update(g_id for g_id in group_ids if g_id not in result) |
| 509 result.update(child_ids) |
| 510 parent_ids = list(child_ids) |
| 511 return list(result) |
| 512 |
| 513 def IsChild(self, cnxn, child_id, parent_id): |
| 514 """Returns True if child_id is a direct/indirect child of parent_id.""" |
| 515 all_descendants = self.GetAllDescendants(cnxn, parent_id) |
| 516 return child_id in all_descendants |
| 517 |
| 518 def MarkObsolete(self): |
| 519 """Mark the DAG as uninitialized so it'll be re-built.""" |
| 520 self.initialized = False |
| 521 |
| 522 def __repr__(self): |
| 523 result = {} |
| 524 result['parents'] = self.user_group_parents |
| 525 result['children'] = self.user_group_children |
| 526 return str(result) |
| 527 |
| 528 |
| 529 class Error(Exception): |
| 530 """Base class for errors from this module.""" |
| 531 pass |
| 532 |
| 533 |
| 534 class CircularGroupException(Error): |
| 535 """Circular nested group exception.""" |
| 536 pass |
| 537 |
| 538 |
| 539 class GroupExistsException(Error): |
| 540 """Group already exists exception.""" |
| 541 pass |
OLD | NEW |