|
19 | 19 | import time |
20 | 20 | import re |
21 | 21 |
|
22 | | - |
23 | 22 | ''' |
24 | 23 | TODO: |
25 | | - idempotency |
26 | | - handle user deletion if removed from config? |
27 | 24 | MVI / other apps? |
28 | 25 | unit tests |
29 | 26 |
|
30 | | - where are we going to run this from? Needs to run in cluster, so a Job in an app |
31 | | - which app though? Manage? |
32 | | - Perhaps we do the core bits in a core app (suite workspace?) |
33 | | - And app-specific bits in the apps themselves |
34 | | - but if we create a user before manage is ready, sync fails, and no way to trigger resync in MAS < 9.1.0? |
35 | | - # could do it all from a core app, but check for readiness of apps |
36 | | - Run in a dedicated instance-root app in the final syncwave |
37 | | -
|
38 | | - how ot cope with users that have been soft-deleted? tolerate / skip - ensure they are removed from secret so |
| 27 | + how to cope with users that have been soft-deleted? tolerate / skip - ensure they are removed from secret so |
39 | 28 | we don't get caught in an infinite loop? |
40 | 29 |
|
41 | 30 | ''' |
@@ -282,6 +271,47 @@ def get_or_create_user(self, payload): |
282 | 271 |
|
283 | 272 | raise Exception(f"{response.status_code} {response.text}") |
284 | 273 |
|
| 274 | + def update_user(self, payload): |
| 275 | + user_id = payload["id"] |
| 276 | + self.logger.debug(f"Updating user {user_id}") |
| 277 | + url = f"{self.mas_api_url_internal}/v3/users/{user_id}" |
| 278 | + headers = { |
| 279 | + "Accept": "application/json", |
| 280 | + "x-access-token": self.superuser_auth_token |
| 281 | + } |
| 282 | + response = requests.put( |
| 283 | + url, |
| 284 | + headers=headers, |
| 285 | + json=payload, |
| 286 | + verify=self.core_internal_ca_pem_file_path |
| 287 | + ) |
| 288 | + |
| 289 | + if response.status_code == 200: |
| 290 | + return response.json() |
| 291 | + |
| 292 | + raise Exception(f"{response.status_code} {response.text}") |
| 293 | + |
| 294 | + def update_user_display_name(self, user_id, display_name): |
| 295 | + self.logger.debug(f"Updating user display name {user_id}") |
| 296 | + url = f"{self.mas_api_url_internal}/v3/users/{user_id}" |
| 297 | + headers = { |
| 298 | + "Accept": "application/json", |
| 299 | + "x-access-token": self.superuser_auth_token |
| 300 | + } |
| 301 | + response = requests.patch( |
| 302 | + url, |
| 303 | + headers=headers, |
| 304 | + json={ |
| 305 | + "displayName": display_name |
| 306 | + }, |
| 307 | + verify=self.core_internal_ca_pem_file_path |
| 308 | + ) |
| 309 | + |
| 310 | + if response.status_code == 200: |
| 311 | + return response.json() |
| 312 | + |
| 313 | + raise Exception(f"{response.status_code} {response.text}") |
| 314 | + |
285 | 315 | def link_user_to_local_idp(self, user_id, email_password=False): |
286 | 316 | ''' |
287 | 317 | Checks if user already has a local identity, no-op if so. |
@@ -437,49 +467,41 @@ def set_user_application_permission(self, user_id, application_id, role): |
437 | 467 |
|
438 | 468 | raise Exception(f"{response.status_code} {response.text}") |
439 | 469 |
|
440 | | - def check_user_sync(self, user_id, application_id, timeout_secs=60 * 10): |
| 470 | + def check_user_sync(self, user_id, application_id, timeout_secs=60 * 10, retry_interval_secs=5): |
441 | 471 | t_end = time.time() + timeout_secs |
442 | 472 | self.logger.info(f"Awaiting user {user_id} sync status \"SUCCESS\" for app {application_id}: {t_end - time.time():.2f} seconds remaining") |
443 | 473 | while time.time() < t_end: |
444 | 474 | user = self.get_user(user_id) |
445 | | - sync_state = user["applications"][application_id]["sync"]["state"] |
446 | | - if sync_state == "SUCCESS": |
447 | | - return |
448 | | - elif sync_state == "ERROR": |
449 | | - # coreapi >= 25.2.3, not in mas 9.0.9, mas >= 9.1 only? |
450 | | - # self.resync_users([user_id]) |
451 | | - # time.sleep(8) |
452 | | - # alternative mechanism to kick off a user resync? |
453 | | - # if not, bomb out here since we'll never get SUCCESS? |
454 | | - # TODO: I think you can just set user roles against to retrigger user sync |
455 | | - raise Exception(f"User {user_id} sync failed, aborting") |
| 475 | + |
| 476 | + if "applications" not in user or application_id not in user["applications"] or "sync" not in user["applications"][application_id] or "state" not in user["applications"][application_id]["sync"]: |
| 477 | + self.logger.warning(f"User {user_id} does not have any sync state for application {application_id}, triggering resync") |
| 478 | + self.resync_users([user_id]) |
| 479 | + time.sleep(retry_interval_secs) |
456 | 480 | else: |
457 | | - self.logger.info(f"User {user_id} sync has not been completed yet for app {application_id} (currrently {sync_state}): {t_end - time.time():.2f} seconds remaining") |
458 | | - time.sleep(5) |
| 481 | + sync_state = user["applications"][application_id]["sync"]["state"] |
| 482 | + if sync_state == "SUCCESS": |
| 483 | + return |
| 484 | + elif sync_state == "ERROR": |
| 485 | + self.logger.warning(f"User {user_id} sync state for {application_id} was {sync_state}, triggering resync") |
| 486 | + self.resync_users([user_id]) |
| 487 | + time.sleep(retry_interval_secs) |
| 488 | + else: |
| 489 | + self.logger.info(f"User {user_id} sync has not been completed yet for app {application_id} (currrently {sync_state}): {t_end - time.time():.2f} seconds remaining") |
| 490 | + time.sleep(retry_interval_secs) |
459 | 491 | raise Exception(f"User {user_id} sync failed to complete for app within {timeout_secs} seconds") |
460 | 492 |
|
461 | | - # coreapi >= 25.2.3, not in mas 9.0.9, mas >= 9.1 only? |
| 493 | + def resync_user(self, user_ids): |
| 494 | + self.logger.info(f"Issuing resync request(s) for user(s) {user_ids}") |
462 | 495 |
|
463 | | - def resync_users(self, user_ids): |
464 | | - self.logger.info(f"Issuing resync request for user(s) {user_ids}") |
465 | | - url = f"{self.mas_api_url_internal}/v3/users/utils/resync" |
466 | | - querystring = {} |
467 | | - payload = { |
468 | | - "users": user_ids |
469 | | - } |
470 | | - headers = { |
471 | | - "Content-Type": "application/json", |
472 | | - "x-access-token": self.superuser_auth_token |
473 | | - } |
474 | | - response = requests.put( |
475 | | - url, |
476 | | - json=payload, |
477 | | - headers=headers, |
478 | | - params=querystring, |
479 | | - verify=self.core_internal_ca_pem_file_path |
480 | | - ) |
481 | | - if response.status_code != 204: |
482 | | - raise Exception(response.text) |
| 496 | + # The "/v3/users/utils/resync" API is only available in MAS Core >= 9.1 (coreapi >= 25.2.3) |
| 497 | + # Until it is available in all supported versions of MAS, |
| 498 | + # we instead perform a no-op update to the user to achieve the same effect |
| 499 | + # (the "update user profile" API is used as this is this allows us to isolate the displayName field, |
| 500 | + # which reduces the impact of concurrent updates leading to race conditions) |
| 501 | + |
| 502 | + for user_id in user_ids: |
| 503 | + user = self.get_user(user_id) |
| 504 | + self.update_user_display_name(user_id, user["displayName"]) |
483 | 505 |
|
484 | 506 | def create_or_get_manage_api_key_for_user(self, user_id): |
485 | 507 | self.logger.debug(f"Attempting to create Manage API Key for user {user_id}") |
|
0 commit comments