From f0e689f8ad35fdfbf34830f93d4d9ca29e9f5e82 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Wed, 25 Mar 2026 20:00:33 +0530 Subject: [PATCH 01/35] [minor] initial-user created with wrong entitlement Issue: #MASCORE-11368 --- bin/mas-devops-create-initial-users-for-saas | 5 +- setup.py | 1 + src/mas/devops/users.py | 310 +++++++++++++++---- 3 files changed, 254 insertions(+), 62 deletions(-) diff --git a/bin/mas-devops-create-initial-users-for-saas b/bin/mas-devops-create-initial-users-for-saas index 7372f495..312003e8 100644 --- a/bin/mas-devops-create-initial-users-for-saas +++ b/bin/mas-devops-create-initial-users-for-saas @@ -34,6 +34,7 @@ if __name__ == "__main__": parser.add_argument("--coreapi-port", required=False, default=443) parser.add_argument("--admin-dashboard-port", required=False, default=443) parser.add_argument("--manage-api-port", required=False, default=443) + parser.add_argument("--mas_version", required=False, default="9.1") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--initial-users-yaml-file") @@ -56,6 +57,7 @@ if __name__ == "__main__": mas_instance_id = args.mas_instance_id mas_workspace_id = args.mas_workspace_id + mas_version = args.mas_version initial_users_yaml_file = args.initial_users_yaml_file initial_users_secret_name = args.initial_users_secret_name coreapi_port = args.coreapi_port @@ -66,6 +68,7 @@ if __name__ == "__main__": logger.info("--------------") logger.info(f"mas_instance_id: {mas_instance_id}") logger.info(f"mas_workspace_id: {mas_workspace_id}") + logger.info(msg=f"mas_version: {mas_version}") logger.info(f"initial_users_yaml_file: {initial_users_yaml_file}") logger.info(f"initial_users_secret_name: {initial_users_secret_name}") logger.info(f"log_level: {log_level}") @@ -83,7 +86,7 @@ if __name__ == "__main__": config.load_kube_config() logger.debug("Loaded kubeconfig file") - user_utils = MASUserUtils(mas_instance_id, mas_workspace_id, client.api_client.ApiClient(), coreapi_port=coreapi_port, admin_dashboard_port=admin_dashboard_port, manage_api_port=manage_api_port) + user_utils = MASUserUtils(mas_instance_id, mas_workspace_id, client.api_client.ApiClient(), mas_version, coreapi_port=coreapi_port, admin_dashboard_port=admin_dashboard_port, manage_api_port=manage_api_port) if initial_users_secret_name is not None: diff --git a/setup.py b/setup.py index e914b8e5..58f99a2b 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ def get_version(rel_path): 'semver', # BSD License 'boto3', # Apache Software License 'slack_sdk', # MIT License + "packaging", # Apache Software License ], extras_require={ 'dev': [ diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index a4c6f997..8f3f66c0 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -18,6 +18,7 @@ import os import time import re +from packaging.version import Version class MASUserUtils(): @@ -43,7 +44,7 @@ class MASUserUtils(): MAXADMIN = "MAXADMIN" - def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: client.api_client.ApiClient, coreapi_port: int = 443, admin_dashboard_port: int = 443, manage_api_port: int = 443): + def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: client.api_client.ApiClient, mas_version: str, coreapi_port: int = 443, admin_dashboard_port: int = 443, manage_api_port: int = 443): """ Initialize MASUserUtils for a specific MAS instance and workspace. @@ -57,6 +58,7 @@ def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: clie """ self.mas_instance_id = mas_instance_id self.mas_workspace_id = mas_workspace_id + self.mas_version = mas_version self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.mas_core_namespace = f"mas-{self.mas_instance_id}-core" @@ -938,7 +940,137 @@ def add_user_to_manage_group(self, user_id, group_name, manage_api_key): if response.status_code == 204: return None - raise Exception(f"{response.status_code} {response.text}") + def get_all_manage_groups(self): + """ + Get all security groups from Manage. + + Args: + manage_api_key (dict): API key record with 'apikey' field for authentication. + + Returns: + list: List of group names (strings). + + Raises: + Exception: If the API call fails. + """ + self.logger.debug("Getting all Manage security groups") + url = f"{self.manage_api_url_internal}/maximo/api/os/mxapigroup" + querystring = { + "ccm": 1, + "lean": 1, + "oslc.select": "groupname", + } + headers = { + "Accept": "application/json", + } + + response = requests.get( + url, + headers=headers, + params=querystring, + # verify=self.manage_internal_ca_pem_file_path, + cert=self.manage_internal_client_pem_file_path, + verify=False + ) + + if response.status_code != 200: + raise Exception(f"{response.status_code} {response.text}") + + json = response.json() + groups = [] + if "member" in json: + for member in json["member"]: + if "groupname" in member: + groups.append(member["groupname"]) + + self.logger.info(f"Found {len(groups)} security groups in Manage") + return groups + + # def grant_group_reassignment_auth(self, user_id, group_name, manage_api_key): + # """ + # Grant a user authorization to reassign users to/from a specific security group. + + # This adds an entry to the grpreassignauth collection for the user, allowing them + # to manage membership in the specified security group. + + # Args: + # user_id (str): The unique identifier of the user. + # group_name (str): The name of the security group to grant authorization for. + # manage_api_key (dict): API key record with 'apikey' field for authentication. + + # Returns: + # None: Returns None on success. + + # Raises: + # Exception: If the operation fails. + # """ + # self.logger.info(f"Granting user {user_id} authorization to reassign group {group_name}") + + # url = f"{self.manage_api_url_internal}/maximo/oslc/os/masperuser" + # querystring = { + # "lean": 1, + # "oslc.where": f"personid=\"{user_id}\"", + # } + # headers = { + # "Content-Type": "application/json", + # "Accept": "application/json", + # "x-method-override": "PATCH", + # "patchtype": "MERGE", + # "apikey": manage_api_key["apikey"], + # } + # payload = { + # "maxuser": [ + # { + # "userid": user_id, + # "grpreassignauth": [ + # { + # "groupname": group_name + # } + # ] + # } + # ] + # } + # response = requests.post( + # url, + # headers=headers, + # params=querystring, + # json=payload, + # verify=self.manage_internal_ca_pem_file_path, + # ) + # if response.status_code != 204: + # raise Exception(f"{response.status_code} {response.text}") + + # return None + + # def grant_all_group_reassignment_auth(self, user_id, manage_api_key): + # """ + # Grant a user authorization to reassign users to/from ALL security groups. + + # This method fetches all security groups and grants reassignment authorization + # for each one, allowing the user to fully manage security group memberships. + + # Args: + # user_id (str): The unique identifier of the user. + # manage_api_key (dict): API key record with 'apikey' field for authentication. + + # Returns: + # None: Returns None on success. + + # Raises: + # Exception: If the operation fails. + # """ + # self.logger.info(f"Granting user {user_id} authorization to reassign ALL security groups") + + # groups = self.get_all_manage_groups(manage_api_key) + + # for group_name in groups: + # try: + # self.grant_group_reassignment_auth(user_id, group_name, manage_api_key) + # except Exception as e: + # self.logger.warning(f"Failed to grant reassignment auth for group {group_name}: {str(e)}") + # # Continue with other groups even if one fails + + # self.logger.info(f"Completed granting group reassignment authorization for {len(groups)} groups") def get_mas_applications_in_workspace(self): """ @@ -1130,11 +1262,14 @@ def create_initial_users_for_saas(self, initial_users): completed = [] failed = [] + all_security_groups = self.get_all_manage_groups() + groupreassign = [{"groupname": group} for group in all_security_groups] + for primary_user in primary_users: self.logger.info("") try: self.logger.info(f"Syncing primary user with email {primary_user['email']}") - self.create_initial_user_for_saas(primary_user, "PRIMARY") + self.create_initial_user_for_saas(primary_user, "PRIMARY", groupreassign) completed.append(primary_user) self.logger.info(f"Completed sync of primary user {primary_user['email']}") except Exception as e: @@ -1159,7 +1294,7 @@ def create_initial_users_for_saas(self, initial_users): "failed": failed } - def create_initial_user_for_saas(self, user, user_type): + def create_initial_user_for_saas(self, user, user_type, groupreassign=None): """ Create and fully configure a single initial user for SaaS. @@ -1188,10 +1323,13 @@ def create_initial_user_for_saas(self, user, user_type): Note: PRIMARY users get: - userAdmin permission + - apikeyAdmin permission (API Key Management) + - idpAdmin permission (IDP Management) - PREMIUM application entitlement - - Workspace admin access + - Regular workspace access (not workspace admin) - ADMIN role for most apps, MANAGEUSER for Manage - - MAXADMIN security group membership + - USERMANAGEMENT security group membership + - Group reassignment authorization for ALL security groups SECONDARY users get: - No admin permissions @@ -1222,65 +1360,111 @@ def create_initial_user_for_saas(self, user, user_type): display_name = f"{user_given_name} {user_family_name}" # Set user permissions and entitlements based on requested user_type - if user_type == "PRIMARY": - permissions = { - "systemAdmin": False, - "userAdmin": True, - "apikeyAdmin": False - } - entitlement = { - "application": "PREMIUM", - "admin": "ADMIN_BASE", - "alwaysReserveLicense": True - } - is_workspace_admin = True - application_role = "ADMIN" - facilities_role = "PREMIUM" - manage_role = "MANAGEUSER" - # TODO: check which security groups primary users should be members of - manage_security_groups = ["MAXADMIN"] - elif user_type == "SECONDARY": - permissions = { - "systemAdmin": False, - "userAdmin": False, - "apikeyAdmin": False - } - entitlement = { - "application": "BASE", - "admin": "NONE", - "alwaysReserveLicense": True + if Version(self.mas_version) < Version('9.0'): + if user_type == "PRIMARY": + permissions = { + "systemAdmin": False, + "userAdmin": True, + "apikeyAdmin": False + } + entitlement = { + "application": "PREMIUM", + "admin": "ADMIN_BASE", + "alwaysReserveLicense": True + } + is_workspace_admin = True + application_role = "ADMIN" + facilities_role = "PREMIUM" + manage_role = "MANAGEUSER" + manage_security_groups = ["MAXADMIN"] + elif user_type == "SECONDARY": + permissions = { + "systemAdmin": False, + "userAdmin": False, + "apikeyAdmin": False + } + entitlement = { + "application": "BASE", + "admin": "NONE", + "alwaysReserveLicense": True + } + is_workspace_admin = False + application_role = "USER" + facilities_role = "BASE" + manage_role = "MANAGEUSER" + # TODO: check which security groups secondary users should be members of + manage_security_groups = [] + else: + raise Exception(f"Unsupported user_type: {user_type}") + + user_def = { + "id": user_id, + "status": {"active": True}, + "username": username, + "owner": "local", + "emails": [ + { + "value": user_email, + "type": "Work", + "primary": True + } + ], + "phoneNumbers": [], + "addresses": [], + "displayName": display_name, + "issuer": "local", + "permissions": permissions, + "entitlement": entitlement, + "givenName": user_given_name, + "familyName": user_family_name, + } - is_workspace_admin = False - application_role = "USER" - facilities_role = "BASE" - manage_role = "MANAGEUSER" - # TODO: check which security groups secondary users should be members of - manage_security_groups = [] else: - raise Exception(f"Unsupported user_type: {user_type}") - - user_def = { - "id": user_id, - "status": {"active": True}, - "username": username, - "owner": "local", - "emails": [ - { - "value": user_email, - "type": "Work", - "primary": True + if user_type == "PRIMARY": + maxuser_def = { + "userid": user_id, + "owner": "local", + "systemadmin": False, + "apikeyadmin": True, + "isauthorized": 1, + "idpadmin": True, + "groupuser": [ + { + "groupname": "USERMANAGEMENT" + } + ], + "grpreassignauth": [ + groupreassign + ] + } + elif user_type == "SECONDARY": + maxuser_def = { + "userid": user_id, + "owner": "local", + "systemadmin": False, + "apikeyadmin": False, + "isauthorized": 0, + "idpadmin": False } - ], - "phoneNumbers": [], - "addresses": [], - "displayName": display_name, - "issuer": "local", - "permissions": permissions, - "entitlement": entitlement, - "givenName": user_given_name, - "familyName": user_family_name - } + user_def = { + "id": user_id, + "status": {"active": True}, + "primaryemailtype": "Work", + "primaryemail": user_email, + # "username": username, + "primaryphone": "", + "addressline1": "", + "displayName": display_name, + "maxuser": maxuser_def + # "issuer": "local", + # "permissions": permissions, + # "entitlement": entitlement, + # "givenName": user_given_name, + # "familyName": user_family_name + } + + self.logger.info(f"User def - {user_def}") self.get_or_create_user(user_def) self.link_user_to_local_idp(user_id, email_password=True) self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) @@ -1303,3 +1487,7 @@ def create_initial_user_for_saas(self, user, user_type): maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) for manage_security_group in manage_security_groups: self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) + + # # Grant authorization to reassign users to/from ALL security groups (PRIMARY users only) + # if user_type == "PRIMARY": + # self.grant_all_group_reassignment_auth(user_id, maxadmin_manage_api_key) From e563c708af1194ecd76a6efe86a6738c99553a01 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Wed, 25 Mar 2026 20:23:49 +0530 Subject: [PATCH 02/35] default value for mas_version --- src/mas/devops/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 8f3f66c0..7148e4f9 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -44,7 +44,7 @@ class MASUserUtils(): MAXADMIN = "MAXADMIN" - def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: client.api_client.ApiClient, mas_version: str, coreapi_port: int = 443, admin_dashboard_port: int = 443, manage_api_port: int = 443): + def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: client.api_client.ApiClient, mas_version: str = '9.1', coreapi_port: int = 443, admin_dashboard_port: int = 443, manage_api_port: int = 443): """ Initialize MASUserUtils for a specific MAS instance and workspace. From 3da4faafdf4678f00e82dfbd0e2c8ef71fdee0bc Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Wed, 25 Mar 2026 20:42:42 +0530 Subject: [PATCH 03/35] default as 9.0 --- src/mas/devops/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 7148e4f9..ce078371 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -44,7 +44,7 @@ class MASUserUtils(): MAXADMIN = "MAXADMIN" - def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: client.api_client.ApiClient, mas_version: str = '9.1', coreapi_port: int = 443, admin_dashboard_port: int = 443, manage_api_port: int = 443): + def __init__(self, mas_instance_id: str, mas_workspace_id: str, k8s_client: client.api_client.ApiClient, mas_version: str = '9.0', coreapi_port: int = 443, admin_dashboard_port: int = 443, manage_api_port: int = 443): """ Initialize MASUserUtils for a specific MAS instance and workspace. From dc756e235d2434b9564c9bb3f848f024106dbde6 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Wed, 25 Mar 2026 23:33:38 +0530 Subject: [PATCH 04/35] Fix test cases --- src/mas/devops/users.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index ce078371..8df67847 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -940,6 +940,8 @@ def add_user_to_manage_group(self, user_id, group_name, manage_api_key): if response.status_code == 204: return None + raise Exception(f"{response.status_code} {response.text}") + def get_all_manage_groups(self): """ Get all security groups from Manage. @@ -1437,6 +1439,10 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): groupreassign ] } + is_workspace_admin = True + application_role = "ADMIN" + facilities_role = "PREMIUM" + manage_role = "MANAGEUSER" elif user_type == "SECONDARY": maxuser_def = { "userid": user_id, @@ -1446,6 +1452,12 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): "isauthorized": 0, "idpadmin": False } + is_workspace_admin = False + application_role = "USER" + facilities_role = "BASE" + manage_role = "MANAGEUSER" + else: + raise Exception(f"Unsupported user_type: {user_type}") user_def = { "id": user_id, From 32fdda0efc22aa72aea1aef62fe2679fd95c5276 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Wed, 25 Mar 2026 23:43:10 +0530 Subject: [PATCH 05/35] fix test case --- src/mas/devops/users.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 8df67847..e04a2282 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1443,6 +1443,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): application_role = "ADMIN" facilities_role = "PREMIUM" manage_role = "MANAGEUSER" + manage_security_groups = ["USERMANAGEMENT"] elif user_type == "SECONDARY": maxuser_def = { "userid": user_id, @@ -1456,6 +1457,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): application_role = "USER" facilities_role = "BASE" manage_role = "MANAGEUSER" + manage_security_groups = [] else: raise Exception(f"Unsupported user_type: {user_type}") From 25ca8cbb72060cd6364396c442cb1d900c000e53 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 00:24:22 +0530 Subject: [PATCH 06/35] Update test case --- src/mas/devops/users.py | 6 +- test/src/test_users.py | 136 +++++++++++++++++++++++++++++----------- 2 files changed, 101 insertions(+), 41 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index e04a2282..d235b6de 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1362,7 +1362,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): display_name = f"{user_given_name} {user_family_name}" # Set user permissions and entitlements based on requested user_type - if Version(self.mas_version) < Version('9.0'): + if Version(self.mas_version) < Version('9.1'): if user_type == "PRIMARY": permissions = { "systemAdmin": False, @@ -1435,9 +1435,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): "groupname": "USERMANAGEMENT" } ], - "grpreassignauth": [ - groupreassign - ] + "grpreassignauth": groupreassign } is_workspace_admin = True application_role = "ADMIN" diff --git a/test/src/test_users.py b/test/src/test_users.py index 8b973e00..ec87045c 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -115,13 +115,15 @@ def mock_logininitial_endpoint(requests_mock): ) -@fixture -def user_utils(mock_v1_secrets, mock_logininitial_endpoint, mock_named_temporary_file, mock_atexit): +@fixture(params=['9.0', '9.1']) +def user_utils(request, mock_v1_secrets, mock_logininitial_endpoint, mock_named_temporary_file, mock_atexit): k8s_client = MagicMock() # DynamicClient is mocked out, no methods will be called on the k8s_client + mas_version = request.param user_utils = MASUserUtils( MAS_INSTANCE_ID, MAS_WORKSPACE_ID, k8s_client, + mas_version=mas_version, coreapi_port=COREAPI_PORT, admin_dashboard_port=ADMIN_DASHBOARD_PORT, manage_api_port=MANAGE_API_PORT @@ -1652,60 +1654,70 @@ def test_create_initial_user_for_saas_unsupported_type(user_utils): # Assisted by watsonx Code Assistant -@pytest.mark.parametrize("user_type, user_id, user_email, permissions, entitlement, is_workspace_admin, application_role, manage_role, facilities_role, manage_security_groups", [ +@pytest.mark.parametrize("user_type, user_id, user_email, is_workspace_admin, application_role, manage_role, facilities_role, manage_security_groups_90, manage_security_groups_91", [ ( "PRIMARY", None, "bill.bob@acme.com", - {"systemAdmin": False, "userAdmin": True, "apikeyAdmin": False}, - {"application": "PREMIUM", "admin": "ADMIN_BASE", "alwaysReserveLicense": True}, True, "ADMIN", "MANAGEUSER", "PREMIUM", - ["MAXADMIN"] + ["MAXADMIN"], + ["USERMANAGEMENT"] ), ( "PRIMARY", "billbob", "bill.bob@acme.com", - {"systemAdmin": False, "userAdmin": True, "apikeyAdmin": False}, - {"application": "PREMIUM", "admin": "ADMIN_BASE", "alwaysReserveLicense": True}, True, "ADMIN", "MANAGEUSER", "PREMIUM", - ["MAXADMIN"] + ["MAXADMIN"], + ["USERMANAGEMENT"] ), ( "SECONDARY", None, "bab.bon@acme.com", - {"systemAdmin": False, "userAdmin": False, "apikeyAdmin": False}, - {"application": "BASE", "admin": "NONE", "alwaysReserveLicense": True}, False, "USER", "MANAGEUSER", "BASE", + [], [] ), ( "SECONDARY", "babbon", "bab.bon@acme.com", - {"systemAdmin": False, "userAdmin": False, "apikeyAdmin": False}, - {"application": "BASE", "admin": "NONE", "alwaysReserveLicense": True}, False, "USER", "MANAGEUSER", "BASE", + [], [] ) ]) def test_create_initial_user_for_saas( - user_type, user_id, user_email, permissions, entitlement, is_workspace_admin, application_role, manage_role, facilities_role, manage_security_groups, + user_type, user_id, user_email, is_workspace_admin, application_role, manage_role, facilities_role, manage_security_groups_90, manage_security_groups_91, user_utils, requests_mock ): + # Determine expected values based on MAS version + mas_version = user_utils.mas_version + if mas_version == '9.0': + manage_security_groups = manage_security_groups_90 + if user_type == "PRIMARY": + permissions = {"systemAdmin": False, "userAdmin": True, "apikeyAdmin": False} + entitlement = {"application": "PREMIUM", "admin": "ADMIN_BASE", "alwaysReserveLicense": True} + else: # SECONDARY + permissions = {"systemAdmin": False, "userAdmin": False, "apikeyAdmin": False} + entitlement = {"application": "BASE", "admin": "NONE", "alwaysReserveLicense": True} + else: # 9.1 + manage_security_groups = manage_security_groups_91 + permissions = None # Not used in 9.1 + entitlement = None # Not used in 9.1 user_utils.get_or_create_user = MagicMock() user_utils.link_user_to_local_idp = MagicMock() user_utils.add_user_to_workspace = MagicMock() @@ -1735,29 +1747,78 @@ def test_create_initial_user_for_saas( username = user_id - user_utils.create_initial_user_for_saas(initial_users, user_type) - - user_utils.get_or_create_user.assert_called_once_with({ - "id": user_id, - "status": {"active": True}, - "username": username, - "owner": "local", - "emails": [ - { - "value": user_email, - "type": "Work", - "primary": True + # For version 9.1 PRIMARY users, pass groupreassign parameter + if mas_version == '9.1' and user_type == "PRIMARY": + groupreassign = [{"groupname": "USERMANAGEMENT"}] + user_utils.create_initial_user_for_saas(initial_users, user_type, groupreassign) + else: + user_utils.create_initial_user_for_saas(initial_users, user_type) + + # Build expected user_def based on version + if mas_version == '9.0': + expected_user_def = { + "id": user_id, + "status": {"active": True}, + "username": username, + "owner": "local", + "emails": [ + { + "value": user_email, + "type": "Work", + "primary": True + } + ], + "phoneNumbers": [], + "addresses": [], + "displayName": display_name, + "issuer": "local", + "permissions": permissions, + "entitlement": entitlement, + "givenName": user_given_name, + "familyName": user_family_name + } + else: # 9.1 + if user_type == "PRIMARY": + maxuser_def = { + "userid": user_id, + "owner": "local", + "systemadmin": False, + "apikeyadmin": True, + "isauthorized": 1, + "idpadmin": True, + "groupuser": [ + { + "groupname": "USERMANAGEMENT" + } + ], + "grpreassignauth": [ + { + "groupname": "USERMANAGEMENT" + } + ] } - ], - "phoneNumbers": [], - "addresses": [], - "displayName": display_name, - "issuer": "local", - "permissions": permissions, - "entitlement": entitlement, - "givenName": user_given_name, - "familyName": user_family_name - }) + else: # SECONDARY + maxuser_def = { + "userid": user_id, + "owner": "local", + "systemadmin": False, + "apikeyadmin": False, + "isauthorized": 0, + "idpadmin": False + } + + expected_user_def = { + "id": user_id, + "status": {"active": True}, + "primaryemailtype": "Work", + "primaryemail": user_email, + "primaryphone": "", + "addressline1": "", + "displayName": display_name, + "maxuser": maxuser_def + } + + user_utils.get_or_create_user.assert_called_once_with(expected_user_def) user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True) user_utils.add_user_to_workspace.assert_called_once_with(user_id, is_workspace_admin=is_workspace_admin) user_utils.await_mas_application_availability.assert_has_calls([call("manage"), call("iot")]) @@ -1813,9 +1874,10 @@ def test_create_initial_users_for_saas(user_utils): mas_workspace_application_ids = ["manage", "iot"] user_utils.get_mas_applications_in_workspace = MagicMock(return_value=map(lambda x: {"id": x}, mas_workspace_application_ids)) user_utils.await_mas_application_availability = MagicMock() + user_utils.get_all_manage_groups = MagicMock(return_value=["MAXADMIN", "MAXUSER"]) user_utils.create_initial_user_for_saas = MagicMock() - def fail_for_users_b_and_e(user, user_type): + def fail_for_users_b_and_e(user, user_type, groupreassign=None): if user["email"] in ["b", "e"]: raise Exception(f"{user['email']} should fail") user_utils.create_initial_user_for_saas.side_effect = fail_for_users_b_and_e From cc1c0ce0a41dc0840ed6b3b2aceebbcd82293b6d Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 15:41:29 +0530 Subject: [PATCH 07/35] set groupreassignauth separately --- src/mas/devops/users.py | 65 +++++++++++++++++++++++++++++++++++++++-- test/src/test_users.py | 6 +--- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index d235b6de..23d196d5 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -273,6 +273,64 @@ def get_or_create_user(self, payload): raise Exception(f"{response.status_code} {response.text}") + def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_key): + """ + Set group reassignment authorization for a user via Manage API. + + This method updates the grpreassignauth field for a user's maxuser record, + which controls which security groups the user can reassign to other users. + + Args: + user_id (str): The unique identifier of the user. + groupreassign (list): List of group objects in format [{"groupname": "GROUP1"}, {"groupname": "GROUP2"}, ...] + manage_api_key (dict): API key record with 'apikey' field for authentication. + + Returns: + dict: Updated user record. + + Raises: + Exception: If the update fails. + """ + if not groupreassign or len(groupreassign) == 0: + self.logger.debug(f"No group reassignment authorization to set for user {user_id}") + return + + self.logger.info(f"Setting group reassignment authorization for user {user_id} with {len(groupreassign)} groups") + + # Use Manage API to update the user's grpreassignauth + url = f"{self.manage_api_url_internal}/maximo/api/os/masapiuser/{user_id}" + querystring = { + "lean": 1, + "ccm": 1 + } + headers = { + "Content-Type": "application/json", + "apikey": manage_api_key["apikey"] + } + + payload = { + "maxuser": [ + { + "grpreassignauth": groupreassign + } + ] + } + + response = requests.patch( + url, + json=payload, + headers=headers, + params=querystring, + cert=self.manage_internal_client_pem_file_path, + verify=self.manage_internal_ca_pem_file_path + ) + + if response.status_code == 200: + self.logger.info(f"Successfully set group reassignment authorization for user {user_id}") + return response.json() + + raise Exception(f"Failed to set group reassignment authorization: {response.status_code} {response.text}") + def update_user(self, payload): """ Update an existing user's details. @@ -972,7 +1030,7 @@ def get_all_manage_groups(self): params=querystring, # verify=self.manage_internal_ca_pem_file_path, cert=self.manage_internal_client_pem_file_path, - verify=False + verify=self.manage_internal_ca_pem_file_path ) if response.status_code != 200: @@ -1434,8 +1492,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): { "groupname": "USERMANAGEMENT" } - ], - "grpreassignauth": groupreassign + ] } is_workspace_admin = True application_role = "ADMIN" @@ -1499,6 +1556,8 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) for manage_security_group in manage_security_groups: self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) + if Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: + self.set_user_group_reassignment_auth(user_id, groupreassign, maxadmin_manage_api_key) # # Grant authorization to reassign users to/from ALL security groups (PRIMARY users only) # if user_type == "PRIMARY": diff --git a/test/src/test_users.py b/test/src/test_users.py index ec87045c..718bcc2d 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -1729,6 +1729,7 @@ def test_create_initial_user_for_saas( manage_api_key = "manage_api_key" # pragma: allowlist secret user_utils.create_or_get_manage_api_key_for_user = MagicMock(return_value=manage_api_key) user_utils.add_user_to_manage_group = MagicMock() + user_utils.set_user_group_reassignment_auth = MagicMock() user_given_name = "billy" user_family_name = "bobby" @@ -1790,11 +1791,6 @@ def test_create_initial_user_for_saas( { "groupname": "USERMANAGEMENT" } - ], - "grpreassignauth": [ - { - "groupname": "USERMANAGEMENT" - } ] } else: # SECONDARY From 76a754bc986dd3f464e6d7e623b89814711ad2b7 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 16:25:50 +0530 Subject: [PATCH 08/35] update user creation api for version >= 9.1 --- src/mas/devops/users.py | 58 +++++++++++++++++------- test/src/test_users.py | 97 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 128 insertions(+), 27 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 23d196d5..278b66af 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -232,6 +232,9 @@ def get_or_create_user(self, payload): their existing record is returned without modification. If the user doesn't exist, they are created with the provided payload. + For MAS version >= 9.1, this method uses the Manage API masapiuser endpoint. + For earlier versions, it uses the Core API v3/users endpoint. + Args: payload (dict): User definition dictionary containing user details. Must include "id" field as the unique identifier. @@ -250,21 +253,46 @@ def get_or_create_user(self, payload): self.logger.info(f"Creating new user {payload['id']}") - url = f"{self.mas_api_url_internal}/v3/users" - querystring = {} - headers = { - "Content-Type": "application/json", - "x-access-token": self.superuser_auth_token - } - response = requests.post( - url, - json=payload, - headers=headers, - params=querystring, - verify=self.core_internal_ca_pem_file_path - ) - if response.status_code == 201: - return response.json() + # For MAS version >= 9.1, use the Manage API masapiuser endpoint + if Version(self.mas_version) >= Version('9.1'): + # Get MAXADMIN API key for authentication + maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) + + url = f"{self.manage_api_url_internal}/maximo/api/os/masapiuser" + querystring = { + "lean": 1 + } + headers = { + "Content-Type": "application/json", + "apikey": maxadmin_manage_api_key["apikey"] + } + response = requests.post( + url, + json=payload, + headers=headers, + params=querystring, + cert=self.manage_internal_client_pem_file_path, + verify=self.manage_internal_ca_pem_file_path + ) + if response.status_code == 201: + return response.json() + else: + # For earlier versions, use the Core API v3/users endpoint + url = f"{self.mas_api_url_internal}/v3/users" + querystring = {} + headers = { + "Content-Type": "application/json", + "x-access-token": self.superuser_auth_token + } + response = requests.post( + url, + json=payload, + headers=headers, + params=querystring, + verify=self.core_internal_ca_pem_file_path + ) + if response.status_code == 201: + return response.json() # if response.status_code == 409: # json = response.json() diff --git a/test/src/test_users.py b/test/src/test_users.py index 718bcc2d..1dd46d4d 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -138,16 +138,36 @@ def mock_manage_api_key(requests_mock): Setup mock Manage APIs for setting up an API Key ''' user_id = "user1" - apikey = {"userid": user_id, "href": f"https://{MANAGE_API_URL}/maximo/api/os/mxapikey/theapikeyid"} + apikey = {"userid": user_id, "apikey": "test-api-key-12345", "href": f"https://{MANAGE_API_URL}/maximo/api/os/mxapikey/theapikeyid"} # pragma: allowlist secret + + # Also setup for MAXADMIN user + maxadmin_apikey = {"userid": "MAXADMIN", "apikey": "maxadmin-api-key-67890", "href": f"https://{MANAGE_API_URL}/maximo/api/os/mxapikey/maxadminapikeyid"} # pragma: allowlist secret + + def maxadmin_matcher(req): + return req.json().get("userid") == "MAXADMIN" and req.verify == PEM_PATH and req.cert == PEM_PATH + + def user1_matcher(req): + return req.json().get("userid") == user_id and req.verify == PEM_PATH and req.cert == PEM_PATH + + # Mock for MAXADMIN API key creation (returns 400 - key already exists) + requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/mxapiapikey?ccm=1&lean=1", + request_headers={"content-type": "application/json"}, + json={"Error": {"reasonCode": "BMXAA10051E", "message": "Only one API key allowed per user"}}, + status_code=400, + additional_matcher=maxadmin_matcher + ) + # Mock for user1 API key creation requests_mock.post( f"{MANAGE_API_URL}/maximo/api/os/mxapiapikey?ccm=1&lean=1", request_headers={"content-type": "application/json"}, json={"id": user_id}, status_code=201, - additional_matcher=lambda req: additional_matcher(req, json={"expiration": -1, "userid": user_id}, cert=PEM_PATH) + additional_matcher=user1_matcher ) + # Mock for user1 API key retrieval requests_mock.get( f"{MANAGE_API_URL}/maximo/api/os/mxapiapikey?ccm=1&lean=1&oslc.select=*&oslc.where=userid=\"{user_id}\"", request_headers={"accept": "application/json"}, @@ -156,7 +176,16 @@ def mock_manage_api_key(requests_mock): additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) ) - yield apikey + # Mock for MAXADMIN API key retrieval (returns existing key) + requests_mock.get( + f"{MANAGE_API_URL}/maximo/api/os/mxapiapikey?ccm=1&lean=1&oslc.select=*&oslc.where=userid=\"MAXADMIN\"", + request_headers={"accept": "application/json"}, + json={"member": [maxadmin_apikey]}, + status_code=200, + additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) + ) + + yield maxadmin_apikey def test_admin_internal_ca_pem_file_path(user_utils, mock_named_temporary_file, mock_atexit): @@ -319,11 +348,12 @@ def test_get_user_error(user_utils, requests_mock): assert get.call_count == 1 -def test_get_or_create_user_exists(user_utils, requests_mock): +def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" get = mock_get_user_200(requests_mock, user_id) - post = requests_mock.post( + # Mock Core API endpoint for version < 9.1 + post_core = requests_mock.post( f"{MAS_API_URL}/v3/users", request_headers={"x-access-token": TOKEN}, json={"id": user_id}, @@ -331,16 +361,27 @@ def test_get_or_create_user_exists(user_utils, requests_mock): additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}) ) + # Mock Manage API endpoint for version >= 9.1 + post_manage = requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/masapiuser?lean=1", + request_headers={"apikey": mock_manage_api_key["apikey"]}, + json={"id": user_id}, + status_code=201, + additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}, cert=PEM_PATH) + ) + assert user_utils.get_or_create_user({"id": user_id}) == {"id": user_id, "displayName": user_id} assert get.call_count == 1 - assert post.call_count == 0 + assert post_core.call_count == 0 + assert post_manage.call_count == 0 -def test_get_or_create_user_notfound(user_utils, requests_mock): +def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" get = mock_get_user_404(requests_mock, user_id) - post = requests_mock.post( + # Mock Core API endpoint for version < 9.1 + post_core = requests_mock.post( f"{MAS_API_URL}/v3/users", request_headers={"x-access-token": TOKEN}, json={"id": user_id, "displayName": user_id}, @@ -348,15 +389,32 @@ def test_get_or_create_user_notfound(user_utils, requests_mock): additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}) ) + # Mock Manage API endpoint for version >= 9.1 + post_manage = requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/masapiuser?lean=1", + request_headers={"apikey": mock_manage_api_key["apikey"]}, + json={"id": user_id, "displayName": user_id}, + status_code=201, + additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}, cert=PEM_PATH) + ) + assert user_utils.get_or_create_user({"id": user_id}) == {"id": user_id, "displayName": user_id} assert get.call_count == 1 - assert post.call_count == 1 + # Check that the correct endpoint was called based on version + if user_utils.mas_version >= '9.1': + assert post_core.call_count == 0 + assert post_manage.call_count == 1 + else: + assert post_core.call_count == 1 + assert post_manage.call_count == 0 -def test_get_or_create_user_error(user_utils, requests_mock): +def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" get = mock_get_user_404(requests_mock, user_id) - post = requests_mock.post( + + # Mock Core API endpoint for version < 9.1 + post_core = requests_mock.post( f"{MAS_API_URL}/v3/users", request_headers={"x-access-token": TOKEN}, json={"error": "unknown"}, @@ -364,10 +422,25 @@ def test_get_or_create_user_error(user_utils, requests_mock): additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}) ) + # Mock Manage API endpoint for version >= 9.1 + post_manage = requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/masapiuser?lean=1", + request_headers={"apikey": mock_manage_api_key["apikey"]}, + json={"error": "unknown"}, + status_code=500, + additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}, cert=PEM_PATH) + ) + with pytest.raises(Exception): user_utils.get_or_create_user({"id": user_id}) assert get.call_count == 1 - assert post.call_count == 1 + # Check that the correct endpoint was called based on version + if user_utils.mas_version >= '9.1': + assert post_core.call_count == 0 + assert post_manage.call_count == 1 + else: + assert post_core.call_count == 1 + assert post_manage.call_count == 0 def test_update_user(user_utils, requests_mock): From 1d11ab5281592d59c521fd16e49f0fc94d3126e4 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 18:29:10 +0530 Subject: [PATCH 09/35] add logger statements --- src/mas/devops/users.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 278b66af..33f7ed66 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -352,6 +352,8 @@ def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_ke cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) + self.logger.info(f"Response status code: {response.status_code}") + self.logger.info(f"Response text: {response.text}") if response.status_code == 200: self.logger.info(f"Successfully set group reassignment authorization for user {user_id}") @@ -790,6 +792,7 @@ def create_or_get_manage_api_key_for_user(self, user_id, temporary=False): # otherwise, retrieve the apikey (either it already existed, or we just created it) apikey = self.get_manage_api_key_for_user(user_id) + self.logger.info(f"Retrieved Manage API Key for user {user_id}: {apikey}") if apikey is None: # either create call reported that apikey already exists, or we created the api key # so we expect the get call to find it @@ -813,7 +816,7 @@ def get_manage_api_key_for_user(self, user_id): Raises: Exception: If the API call fails. """ - self.logger.debug(f"Getting Manage API Key for user {user_id}") + self.logger.info(f"Getting Manage API Key for user {user_id}") url = f"{self.manage_api_url_internal}/maximo/api/os/mxapiapikey" querystring = { "ccm": 1, @@ -832,6 +835,7 @@ def get_manage_api_key_for_user(self, user_id): verify=self.manage_internal_ca_pem_file_path, cert=self.manage_internal_client_pem_file_path ) + self.logger.info(f"Response: {response.status_code} {response.text}") if response.status_code == 200: json = response.json() @@ -1023,6 +1027,8 @@ def add_user_to_manage_group(self, user_id, group_name, manage_api_key): json=payload, verify=self.manage_internal_ca_pem_file_path, ) + self.logger.info(f"Response status code: {response.status_code}") + self.logger.info(f"Response text: {response.text}") if response.status_code == 204: return None @@ -1582,6 +1588,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): if len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids: maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) + self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}") for manage_security_group in manage_security_groups: self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) if Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: From 1b9a33c5c6e917ee6b739c8a209f4d1f9b603060 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 19:12:46 +0530 Subject: [PATCH 10/35] using masperuser --- src/mas/devops/users.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 33f7ed66..c5f814ef 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -258,7 +258,7 @@ def get_or_create_user(self, payload): # Get MAXADMIN API key for authentication maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) - url = f"{self.manage_api_url_internal}/maximo/api/os/masapiuser" + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser" querystring = { "lean": 1 } @@ -266,6 +266,7 @@ def get_or_create_user(self, payload): "Content-Type": "application/json", "apikey": maxadmin_manage_api_key["apikey"] } + self.logger.info(f"Creating new user {payload['id']} with Manage API with payload {payload}") response = requests.post( url, json=payload, @@ -274,6 +275,8 @@ def get_or_create_user(self, payload): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) + self.logger.info(f"Response status code: {response.status_code}") + self.logger.info(f"Response text: {response.text}") if response.status_code == 201: return response.json() else: From 2232491655990fb5c4a212ff23321eeea4d5195d Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 19:23:12 +0530 Subject: [PATCH 11/35] update api url --- test/src/test_users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/src/test_users.py b/test/src/test_users.py index 1dd46d4d..2f86a722 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -363,7 +363,7 @@ def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_ke # Mock Manage API endpoint for version >= 9.1 post_manage = requests_mock.post( - f"{MANAGE_API_URL}/maximo/api/os/masapiuser?lean=1", + f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1", request_headers={"apikey": mock_manage_api_key["apikey"]}, json={"id": user_id}, status_code=201, @@ -391,7 +391,7 @@ def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_ # Mock Manage API endpoint for version >= 9.1 post_manage = requests_mock.post( - f"{MANAGE_API_URL}/maximo/api/os/masapiuser?lean=1", + f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1", request_headers={"apikey": mock_manage_api_key["apikey"]}, json={"id": user_id, "displayName": user_id}, status_code=201, @@ -424,7 +424,7 @@ def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key # Mock Manage API endpoint for version >= 9.1 post_manage = requests_mock.post( - f"{MANAGE_API_URL}/maximo/api/os/masapiuser?lean=1", + f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1", request_headers={"apikey": mock_manage_api_key["apikey"]}, json={"error": "unknown"}, status_code=500, From a19d70873d75bb5818b1afbc8da2854e72b5e88f Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 20:07:01 +0530 Subject: [PATCH 12/35] include personid --- src/mas/devops/users.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index c5f814ef..8aafcc8a 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1520,6 +1520,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): if user_type == "PRIMARY": maxuser_def = { "userid": user_id, + "personid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": True, @@ -1539,6 +1540,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): elif user_type == "SECONDARY": maxuser_def = { "userid": user_id, + "personid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": False, From 56583dc75fbf38e4bce3077a96f0affd9bbf094b Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 20:12:23 +0530 Subject: [PATCH 13/35] update test case --- test/src/test_users.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/src/test_users.py b/test/src/test_users.py index 2f86a722..dedd953a 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -1855,6 +1855,7 @@ def test_create_initial_user_for_saas( if user_type == "PRIMARY": maxuser_def = { "userid": user_id, + "personid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": True, @@ -1869,6 +1870,7 @@ def test_create_initial_user_for_saas( else: # SECONDARY maxuser_def = { "userid": user_id, + "personid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": False, From 602a5b157829d65eeb27d2a867062df6dc68a837 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 20:44:32 +0530 Subject: [PATCH 14/35] update personid at root level --- src/mas/devops/users.py | 1 + test/src/test_users.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 8aafcc8a..6045ab3b 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1557,6 +1557,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): user_def = { "id": user_id, + "personid": user_id, "status": {"active": True}, "primaryemailtype": "Work", "primaryemail": user_email, diff --git a/test/src/test_users.py b/test/src/test_users.py index dedd953a..9d77251b 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -1880,6 +1880,7 @@ def test_create_initial_user_for_saas( expected_user_def = { "id": user_id, + "personid": user_id, "status": {"active": True}, "primaryemailtype": "Work", "primaryemail": user_email, From debfd6a97984c9e41be4ea6e4f39579ce4a49278 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 23:04:35 +0530 Subject: [PATCH 15/35] update payload --- src/mas/devops/users.py | 10 ++++++---- test/src/test_users.py | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 6045ab3b..5aff183f 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1521,11 +1521,13 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): maxuser_def = { "userid": user_id, "personid": user_id, + "loginid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": True, "isauthorized": 1, "idpadmin": True, + "status": "ACTIVE", "groupuser": [ { "groupname": "USERMANAGEMENT" @@ -1541,11 +1543,13 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): maxuser_def = { "userid": user_id, "personid": user_id, + "loginid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": False, "isauthorized": 0, - "idpadmin": False + "idpadmin": False, + "status": "ACTIVE" } is_workspace_admin = False application_role = "USER" @@ -1556,16 +1560,14 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): raise Exception(f"Unsupported user_type: {user_type}") user_def = { - "id": user_id, "personid": user_id, - "status": {"active": True}, "primaryemailtype": "Work", "primaryemail": user_email, # "username": username, "primaryphone": "", "addressline1": "", "displayName": display_name, - "maxuser": maxuser_def + "maxuser": maxuser_def, # "issuer": "local", # "permissions": permissions, # "entitlement": entitlement, diff --git a/test/src/test_users.py b/test/src/test_users.py index 9d77251b..3819545b 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -1856,11 +1856,13 @@ def test_create_initial_user_for_saas( maxuser_def = { "userid": user_id, "personid": user_id, + "loginid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": True, "isauthorized": 1, "idpadmin": True, + "status": "ACTIVE", "groupuser": [ { "groupname": "USERMANAGEMENT" @@ -1871,17 +1873,17 @@ def test_create_initial_user_for_saas( maxuser_def = { "userid": user_id, "personid": user_id, + "loginid": user_id, "owner": "local", "systemadmin": False, "apikeyadmin": False, "isauthorized": 0, - "idpadmin": False + "idpadmin": False, + "status": "ACTIVE" } expected_user_def = { - "id": user_id, "personid": user_id, - "status": {"active": True}, "primaryemailtype": "Work", "primaryemail": user_email, "primaryphone": "", From 765dc4c805703504688d90aee181cb7e8b7d201e Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Thu, 26 Mar 2026 23:45:24 +0530 Subject: [PATCH 16/35] update --- src/mas/devops/users.py | 20 +++++++++++++------- test/src/test_users.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 5aff183f..530ce869 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -228,16 +228,18 @@ def get_or_create_user(self, payload): """ Get an existing user or create a new one if not found. - This method is idempotent - if the user already exists (identified by payload["id"]), - their existing record is returned without modification. If the user doesn't exist, - they are created with the provided payload. + This method is idempotent - if the user already exists (identified by payload["id"] + for version < 9.1 or payload["personid"] for version >= 9.1), their existing record + is returned without modification. If the user doesn't exist, they are created with + the provided payload. For MAS version >= 9.1, this method uses the Manage API masapiuser endpoint. For earlier versions, it uses the Core API v3/users endpoint. Args: payload (dict): User definition dictionary containing user details. - Must include "id" field as the unique identifier. + Must include "id" field (version < 9.1) or "personid" field + (version >= 9.1) as the unique identifier. Returns: dict: The user record (either existing or newly created). @@ -245,13 +247,17 @@ def get_or_create_user(self, payload): Raises: Exception: If user creation fails with an unexpected status code. """ - existing_user = self.get_user(payload["id"]) + # Determine the user ID field based on version + user_id_field = "personid" if Version(self.mas_version) >= Version('9.1') else "id" + user_id = payload[user_id_field] + + existing_user = self.get_user(user_id) if existing_user is not None: self.logger.info(f"Existing user {existing_user['id']} found") return existing_user - self.logger.info(f"Creating new user {payload['id']}") + self.logger.info(f"Creating new user {user_id}") # For MAS version >= 9.1, use the Manage API masapiuser endpoint if Version(self.mas_version) >= Version('9.1'): @@ -266,7 +272,7 @@ def get_or_create_user(self, payload): "Content-Type": "application/json", "apikey": maxadmin_manage_api_key["apikey"] } - self.logger.info(f"Creating new user {payload['id']} with Manage API with payload {payload}") + self.logger.info(f"Creating new user {user_id} with Manage API with payload {payload}") response = requests.post( url, json=payload, diff --git a/test/src/test_users.py b/test/src/test_users.py index 3819545b..f205134f 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -367,10 +367,17 @@ def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_ke request_headers={"apikey": mock_manage_api_key["apikey"]}, json={"id": user_id}, status_code=201, - additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}, cert=PEM_PATH) + additional_matcher=lambda req: additional_matcher(req, json={"personid": user_id}, cert=PEM_PATH) ) - assert user_utils.get_or_create_user({"id": user_id}) == {"id": user_id, "displayName": user_id} + # Use correct payload structure based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + payload = {"personid": user_id} + else: + payload = {"id": user_id} + + assert user_utils.get_or_create_user(payload) == {"id": user_id, "displayName": user_id} assert get.call_count == 1 assert post_core.call_count == 0 assert post_manage.call_count == 0 @@ -395,10 +402,17 @@ def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_ request_headers={"apikey": mock_manage_api_key["apikey"]}, json={"id": user_id, "displayName": user_id}, status_code=201, - additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}, cert=PEM_PATH) + additional_matcher=lambda req: additional_matcher(req, json={"personid": user_id}, cert=PEM_PATH) ) - assert user_utils.get_or_create_user({"id": user_id}) == {"id": user_id, "displayName": user_id} + # Use correct payload structure based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + payload = {"personid": user_id} + else: + payload = {"id": user_id} + + assert user_utils.get_or_create_user(payload) == {"id": user_id, "displayName": user_id} assert get.call_count == 1 # Check that the correct endpoint was called based on version if user_utils.mas_version >= '9.1': @@ -428,11 +442,18 @@ def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key request_headers={"apikey": mock_manage_api_key["apikey"]}, json={"error": "unknown"}, status_code=500, - additional_matcher=lambda req: additional_matcher(req, json={"id": user_id}, cert=PEM_PATH) + additional_matcher=lambda req: additional_matcher(req, json={"personid": user_id}, cert=PEM_PATH) ) + # Use correct payload structure based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + payload = {"personid": user_id} + else: + payload = {"id": user_id} + with pytest.raises(Exception): - user_utils.get_or_create_user({"id": user_id}) + user_utils.get_or_create_user(payload) assert get.call_count == 1 # Check that the correct endpoint was called based on version if user_utils.mas_version >= '9.1': From 1bb218a7310ab81eff1f3cd3b0a6a5e740338b41 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 00:16:30 +0530 Subject: [PATCH 17/35] update --- src/mas/devops/users.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 530ce869..b459e63c 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -284,7 +284,12 @@ def get_or_create_user(self, payload): self.logger.info(f"Response status code: {response.status_code}") self.logger.info(f"Response text: {response.text}") if response.status_code == 201: - return response.json() + # Manage API returns empty response body on success, fetch the user + if response.text: + return response.json() + else: + # Fetch the newly created user from Core API + return self.get_user(user_id) else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users" From 1139736d6dd2352970cb5e45cf899c19ae4b781d Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 01:10:45 +0530 Subject: [PATCH 18/35] update --- src/mas/devops/users.py | 26 +++++++++++++------------ test/src/test_users.py | 43 ++++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index b459e63c..d43a53fd 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1591,16 +1591,17 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): self.link_user_to_local_idp(user_id, email_password=True) self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) - for mas_application_id in self.mas_workspace_application_ids: - self.await_mas_application_availability(mas_application_id) - if mas_application_id == "manage": - role = manage_role - elif mas_application_id == "facilities": - role = facilities_role - else: - # otherwise grant the user the appropriate role for their user_type - role = application_role - self.set_user_application_permission(user_id, mas_application_id, role) + if Version(self.mas_version) < Version('9.1'): + for mas_application_id in self.mas_workspace_application_ids: + self.await_mas_application_availability(mas_application_id) + if mas_application_id == "manage": + role = manage_role + elif mas_application_id == "facilities": + role = facilities_role + else: + # otherwise grant the user the appropriate role for their user_type + role = application_role + self.set_user_application_permission(user_id, mas_application_id, role) for mas_application_id in self.mas_workspace_application_ids: self.check_user_sync(user_id, mas_application_id) @@ -1608,8 +1609,9 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): if len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids: maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}") - for manage_security_group in manage_security_groups: - self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) + if Version(self.mas_version) < Version('9.1'): + for manage_security_group in manage_security_groups: + self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) if Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: self.set_user_group_reassignment_auth(user_id, groupreassign, maxadmin_manage_api_key) diff --git a/test/src/test_users.py b/test/src/test_users.py index f205134f..d274501f 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -1816,7 +1816,7 @@ def test_create_initial_user_for_saas( user_utils.link_user_to_local_idp = MagicMock() user_utils.add_user_to_workspace = MagicMock() mas_workspace_application_ids = ["manage", "iot", "facilities"] - user_utils.get_mas_applications_in_workspace = MagicMock(return_value=map(lambda x: {"id": x}, mas_workspace_application_ids)) + user_utils.get_mas_applications_in_workspace = MagicMock(return_value=list(map(lambda x: {"id": x}, mas_workspace_application_ids))) user_utils.await_mas_application_availability = MagicMock() user_utils.set_user_application_permission = MagicMock() user_utils.check_user_sync = MagicMock() @@ -1916,12 +1916,21 @@ def test_create_initial_user_for_saas( user_utils.get_or_create_user.assert_called_once_with(expected_user_def) user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True) user_utils.add_user_to_workspace.assert_called_once_with(user_id, is_workspace_admin=is_workspace_admin) - user_utils.await_mas_application_availability.assert_has_calls([call("manage"), call("iot")]) - user_utils.set_user_application_permission.assert_has_calls([ - call(user_id, "manage", manage_role), - call(user_id, "iot", application_role), - call(user_id, "facilities", facilities_role), - ]) + + # For version < 9.1, await_mas_application_availability and set_user_application_permission are called + # For version >= 9.1, they are NOT called + if mas_version == '9.0': + user_utils.await_mas_application_availability.assert_has_calls([call("manage"), call("iot")]) + user_utils.set_user_application_permission.assert_has_calls([ + call(user_id, "manage", manage_role), + call(user_id, "iot", application_role), + call(user_id, "facilities", facilities_role), + ]) + else: # 9.1 + user_utils.await_mas_application_availability.assert_not_called() + user_utils.set_user_application_permission.assert_not_called() + + # check_user_sync is called for all versions user_utils.check_user_sync.assert_has_calls([ call(user_id, "manage"), call(user_id, "iot"), @@ -1930,12 +1939,24 @@ def test_create_initial_user_for_saas( if len(manage_security_groups) > 0: user_utils.create_or_get_manage_api_key_for_user.assert_called_once_with("MAXADMIN", temporary=True) + + # For version < 9.1, add_user_to_manage_group is called + # For version >= 9.1, set_user_group_reassignment_auth is called for PRIMARY users + if mas_version == '9.0': + user_utils.add_user_to_manage_group.assert_has_calls( + list(map(lambda sg: call(user_id, sg, manage_api_key), manage_security_groups)) + ) + user_utils.set_user_group_reassignment_auth.assert_not_called() + else: # 9.1 + user_utils.add_user_to_manage_group.assert_not_called() + if user_type == "PRIMARY": + user_utils.set_user_group_reassignment_auth.assert_called_once_with(user_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) + else: + user_utils.set_user_group_reassignment_auth.assert_not_called() else: user_utils.create_or_get_manage_api_key_for_user.assert_not_called() - - user_utils.add_user_to_manage_group.assert_has_calls( - map(lambda sg: call(user_id, sg, manage_api_key), manage_security_groups) - ) + user_utils.add_user_to_manage_group.assert_not_called() + user_utils.set_user_group_reassignment_auth.assert_not_called() def test_create_initial_users_for_saas_invalid_inputs(user_utils): From dc647eae44846927acce3eecf93917ff6107e8d2 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 11:25:04 +0530 Subject: [PATCH 19/35] post instead of patch --- src/mas/devops/users.py | 53 ++++++--- test/src/test_users.py | 252 ++++++++++++++++++++++++++++++---------- 2 files changed, 230 insertions(+), 75 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index d43a53fd..8420e48e 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -193,7 +193,10 @@ def mas_workspace_application_ids(self): def get_user(self, user_id): """ - Retrieve a user's details from MAS Core API. + Retrieve a user's details from MAS API. + + For MAS version >= 9.1, this method uses the Manage API masperuser endpoint. + For earlier versions, it uses the Core API v3/users endpoint. Args: user_id (str): The unique identifier of the user to retrieve. @@ -205,16 +208,39 @@ def get_user(self, user_id): Exception: If the API returns an unexpected status code. """ self.logger.debug(f"Getting user {user_id}") - url = f"{self.mas_api_url_internal}/v3/users/{user_id}" - headers = { - "Accept": "application/json", - "x-access-token": self.superuser_auth_token - } - response = requests.get( - url, - headers=headers, - verify=self.core_internal_ca_pem_file_path - ) + + # For MAS version >= 9.1, use the Manage API masperuser endpoint + if Version(self.mas_version) >= Version('9.1'): + # Get MAXADMIN API key for authentication + maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) + + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{user_id}" + querystring = { + "lean": 1 + } + headers = { + "Accept": "application/json", + "apikey": maxadmin_manage_api_key["apikey"] + } + response = requests.get( + url, + headers=headers, + params=querystring, + cert=self.manage_internal_client_pem_file_path, + verify=self.manage_internal_ca_pem_file_path + ) + else: + # For earlier versions, use the Core API v3/users endpoint + url = f"{self.mas_api_url_internal}/v3/users/{user_id}" + headers = { + "Accept": "application/json", + "x-access-token": self.superuser_auth_token + } + response = requests.get( + url, + headers=headers, + verify=self.core_internal_ca_pem_file_path + ) if response.status_code == 404: return None @@ -358,7 +384,7 @@ def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_ke ] } - response = requests.patch( + response = requests.post( url, json=payload, headers=headers, @@ -1587,7 +1613,8 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): } self.logger.info(f"User def - {user_def}") - self.get_or_create_user(user_def) + user_info = self.get_or_create_user(user_def) + self.logger.info(f"User info - {user_info}") self.link_user_to_local_idp(user_id, email_password=True) self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) diff --git a/test/src/test_users.py b/test/src/test_users.py index d274501f..7a1ea429 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -199,8 +199,9 @@ def test_admin_internal_ca_pem_file_path(user_utils, mock_named_temporary_file, assert mock_atexit.mock_calls == [call(os.remove, PEM_PATH)] -def mock_get_user(requests_mock, user_id, json, status_code): - return requests_mock.get( +def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key): + # Mock Core API endpoint for version < 9.1 + core_mock = requests_mock.get( f"{MAS_API_URL}/v3/users/{user_id}", request_headers={"x-access-token": TOKEN}, json=json, @@ -208,22 +209,33 @@ def mock_get_user(requests_mock, user_id, json, status_code): additional_matcher=lambda req: additional_matcher(req) ) + # Mock Manage API endpoint for version >= 9.1 + manage_mock = requests_mock.get( + f"{MANAGE_API_URL}/maximo/api/os/masperuser/{user_id}?lean=1", + request_headers={"apikey": mock_manage_api_key["apikey"]}, + json=json, + status_code=status_code, + additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) + ) + + return core_mock, manage_mock + -def mock_get_user_200(requests_mock, user_id): +def mock_get_user_200(requests_mock, user_id, mock_manage_api_key): return mock_get_user( - requests_mock, user_id, {"id": user_id, "displayName": user_id}, 200 + requests_mock, user_id, {"id": user_id, "displayName": user_id}, 200, mock_manage_api_key ) -def mock_get_user_404(requests_mock, user_id): +def mock_get_user_404(requests_mock, user_id, mock_manage_api_key): return mock_get_user( - requests_mock, user_id, {"error": "notfound"}, 404 + requests_mock, user_id, {"error": "notfound"}, 404, mock_manage_api_key ) -def mock_get_user_500(requests_mock, user_id): +def mock_get_user_500(requests_mock, user_id, mock_manage_api_key): return mock_get_user( - requests_mock, user_id, {"error": "internal"}, 500 + requests_mock, user_id, {"error": "internal"}, 500, mock_manage_api_key ) @@ -326,31 +338,52 @@ def test_mas_workspace_application_ids(user_utils, requests_mock): assert get.call_count == 1 -def test_get_user_exists(user_utils, requests_mock): +def test_get_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_200(requests_mock, user_id) + get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) assert user_utils.get_user(user_id) == {"id": user_id, "displayName": user_id} - assert get.call_count == 1 + + # Check that the correct endpoint was called based on version + if user_utils.mas_version >= '9.1': + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 -def test_get_user_notfound(user_utils, requests_mock): +def test_get_user_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_404(requests_mock, user_id) + get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) assert user_utils.get_user(user_id) is None - assert get.call_count == 1 + + # Check that the correct endpoint was called based on version + if user_utils.mas_version >= '9.1': + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 -def test_get_user_error(user_utils, requests_mock): +def test_get_user_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_500(requests_mock, user_id) + get_core, get_manage = mock_get_user_500(requests_mock, user_id, mock_manage_api_key) with pytest.raises(Exception): user_utils.get_user(user_id) - assert get.call_count == 1 + + # Check that the correct endpoint was called based on version + if user_utils.mas_version >= '9.1': + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_200(requests_mock, user_id) + get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) # Mock Core API endpoint for version < 9.1 post_core = requests_mock.post( @@ -378,14 +411,21 @@ def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_ke payload = {"id": user_id} assert user_utils.get_or_create_user(payload) == {"id": user_id, "displayName": user_id} - assert get.call_count == 1 + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 assert post_core.call_count == 0 assert post_manage.call_count == 0 def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_404(requests_mock, user_id) + get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) # Mock Core API endpoint for version < 9.1 post_core = requests_mock.post( @@ -413,19 +453,23 @@ def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_ payload = {"id": user_id} assert user_utils.get_or_create_user(payload) == {"id": user_id, "displayName": user_id} - assert get.call_count == 1 # Check that the correct endpoint was called based on version - if user_utils.mas_version >= '9.1': + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 1 assert post_core.call_count == 0 assert post_manage.call_count == 1 else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 assert post_core.call_count == 1 assert post_manage.call_count == 0 def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_404(requests_mock, user_id) + get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) # Mock Core API endpoint for version < 9.1 post_core = requests_mock.post( @@ -454,12 +498,16 @@ def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key with pytest.raises(Exception): user_utils.get_or_create_user(payload) - assert get.call_count == 1 # Check that the correct endpoint was called based on version - if user_utils.mas_version >= '9.1': + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 1 assert post_core.call_count == 0 assert post_manage.call_count == 1 else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 assert post_core.call_count == 1 assert post_manage.call_count == 0 @@ -518,10 +566,10 @@ def test_update_user_display_name_error(user_utils, requests_mock): assert patche.call_count == 1 -def test_link_user_to_local_idp(user_utils, requests_mock): +def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" email_password = True - get = mock_get_user_200(requests_mock, user_id) + get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) put = requests_mock.put( f"{MAS_API_URL}/v3/users/{user_id}/idps/local?emailPassword={email_password}", @@ -533,13 +581,20 @@ def test_link_user_to_local_idp(user_utils, requests_mock): user_utils.link_user_to_local_idp(user_id, email_password=email_password) - assert get.call_count == 1 + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 assert put.call_count == 1 -def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock): +def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get = mock_get_user_404(requests_mock, user_id) + get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) put = requests_mock.put( f"{MAS_API_URL}/v3/users/{user_id}/idps/local", additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id}) @@ -548,15 +603,22 @@ def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock): with pytest.raises(Exception): user_utils.link_user_to_local_idp(user_id) - assert get.call_count == 1 + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 assert put.call_count == 0 -def test_link_user_to_local_idp_already_linked(user_utils, requests_mock): +def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" email_password = True - get = mock_get_user( - requests_mock, user_id, {"id": user_id, "identities": {"_local": {}}}, 200 + get_core, get_manage = mock_get_user( + requests_mock, user_id, {"id": user_id, "identities": {"_local": {}}}, 200, mock_manage_api_key ) put = requests_mock.put( @@ -569,7 +631,14 @@ def test_link_user_to_local_idp_already_linked(user_utils, requests_mock): user_utils.link_user_to_local_idp(user_id, email_password=email_password) - assert get.call_count == 1 + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 1 + else: + assert get_core.call_count == 1 + assert get_manage.call_count == 0 assert put.call_count == 0 @@ -778,13 +847,16 @@ def test_set_user_application_permissions_alreadyset(user_utils, requests_mock): assert put.call_count == 0 -def test_resync_users(user_utils, requests_mock): +def test_resync_users(user_utils, requests_mock, mock_manage_api_key): user_ids = ["user1", "user2"] - gets = [] + gets_core = [] + gets_manage = [] patches = [] for user_id in user_ids: - gets.append(mock_get_user_200(requests_mock, user_id)) + get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) + gets_core.append(get_core) + gets_manage.append(get_manage) patches.append( requests_mock.patch( @@ -799,14 +871,24 @@ def test_resync_users(user_utils, requests_mock): user_utils.resync_users(user_ids) - for get in gets: - assert get.call_count == 1 + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + for get_core in gets_core: + assert get_core.call_count == 0 + for get_manage in gets_manage: + assert get_manage.call_count == 1 + else: + for get_core in gets_core: + assert get_core.call_count == 1 + for get_manage in gets_manage: + assert get_manage.call_count == 0 for patche in patches: assert patche.call_count == 1 -def test_check_user_sync(user_utils, requests_mock): +def test_check_user_sync(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" application_id = "manage" @@ -836,22 +918,31 @@ def json_callback(request, context): } } - get = mock_get_user( + get_core, get_manage = mock_get_user( requests_mock, user_id, json_callback, - 200 + 200, + mock_manage_api_key ) user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) - assert get.call_count == 3 + + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 3 + else: + assert get_core.call_count == 3 + assert get_manage.call_count == 0 -def test_check_user_sync_timeout(user_utils, requests_mock): +def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" application_id = "manage" - get = mock_get_user( + get_core, get_manage = mock_get_user( requests_mock, user_id, { @@ -869,15 +960,24 @@ def test_check_user_sync_timeout(user_utils, requests_mock): } } }, - 200 + 200, + mock_manage_api_key ) with pytest.raises(Exception) as excinfo: user_utils.check_user_sync(user_id, application_id, timeout_secs=0.3, retry_interval_secs=0.05) assert str(excinfo.value) == f"User {user_id} sync failed to complete for app within {0.3} seconds" - assert get.call_count > 1 + + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count > 1 + else: + assert get_core.call_count > 1 + assert get_manage.call_count == 0 -def test_check_user_sync_appstate_notfound(user_utils, requests_mock): +def test_check_user_sync_appstate_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" application_id = "manage" @@ -927,21 +1027,30 @@ def json_callback(request, context): status_code=200 ) - get = mock_get_user( + get_core, get_manage = mock_get_user( requests_mock, user_id, json_callback, - 200 + 200, + mock_manage_api_key ) user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) - assert get.call_count == 3 + + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 3 + else: + assert get_core.call_count == 3 + assert get_manage.call_count == 0 # a single resync should have been triggered assert patche.call_count == 1 -def test_check_user_sync_appstate_transient_error(user_utils, requests_mock): +def test_check_user_sync_appstate_transient_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" application_id = "manage" @@ -986,21 +1095,30 @@ def json_callback(request, context): status_code=200 ) - get = mock_get_user( + get_core, get_manage = mock_get_user( requests_mock, user_id, json_callback, - 200 + 200, + mock_manage_api_key ) user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) - assert get.call_count == 3 + + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count == 3 + else: + assert get_core.call_count == 3 + assert get_manage.call_count == 0 # a single resync should have been triggered assert patche.call_count == 1 -def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock): +def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" application_id = "manage" @@ -1011,7 +1129,7 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock): status_code=200 ) - get = mock_get_user( + get_core, get_manage = mock_get_user( requests_mock, user_id, { @@ -1025,16 +1143,26 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock): } } }, - 200 + 200, + mock_manage_api_key ) with pytest.raises(Exception) as excinfo: user_utils.check_user_sync(user_id, application_id, timeout_secs=0.3, retry_interval_secs=0.05) assert str(excinfo.value) == f"User {user_id} sync failed to complete for app within {0.3} seconds" - assert get.call_count > 1 - # an "update_user_display_name" should have been triggered for every 2 get calls (1 call by check_user_sync, 1 by resync) - assert patche.call_count == get.call_count / 2 + # Check that the correct endpoint was called based on version + from packaging.version import Version + if Version(user_utils.mas_version) >= Version('9.1'): + assert get_core.call_count == 0 + assert get_manage.call_count > 1 + # an "update_user_display_name" should have been triggered for every 2 get calls (1 call by check_user_sync, 1 by resync) + assert patche.call_count == get_manage.call_count / 2 + else: + assert get_core.call_count > 1 + assert get_manage.call_count == 0 + # an "update_user_display_name" should have been triggered for every 2 get calls (1 call by check_user_sync, 1 by resync) + assert patche.call_count == get_core.call_count / 2 def test_get_manage_api_key_for_user_exists(user_utils, requests_mock): From c36d4d7a18877c8f77a76286f16024c8464a8900 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 11:33:08 +0530 Subject: [PATCH 20/35] update --- src/mas/devops/users.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 8420e48e..a85aea71 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -366,14 +366,16 @@ def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_ke self.logger.info(f"Setting group reassignment authorization for user {user_id} with {len(groupreassign)} groups") # Use Manage API to update the user's grpreassignauth - url = f"{self.manage_api_url_internal}/maximo/api/os/masapiuser/{user_id}" + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{user_id}" querystring = { "lean": 1, "ccm": 1 } headers = { "Content-Type": "application/json", - "apikey": manage_api_key["apikey"] + "apikey": manage_api_key["apikey"], + "x-method-override": "PATCH", + "patchtype": "MERGE" } payload = { From 292cd54c4c84152ca2d4f38b5a2601f70d60b6b4 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 12:36:22 +0530 Subject: [PATCH 21/35] Fetch resource id --- src/mas/devops/users.py | 37 ++++++++++++++++++++++++++++--------- test/src/test_users.py | 21 ++++++++++++++++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index a85aea71..075f3d5b 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -214,9 +214,10 @@ def get_user(self, user_id): # Get MAXADMIN API key for authentication maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) - url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{user_id}" + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser" querystring = { - "lean": 1 + "lean": 1, + "oslc.where": f"userid=\"{user_id}\"" } headers = { "Accept": "application/json", @@ -229,6 +230,9 @@ def get_user(self, user_id): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) + self.logger.info(f"GET {url} returned {response.status_code}") + self.logger.debug(f"Response: {response.text}") + self.logger.debug(f"Response json: {response.json}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" @@ -341,7 +345,7 @@ def get_or_create_user(self, payload): raise Exception(f"{response.status_code} {response.text}") - def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_key): + def set_user_group_reassignment_auth(self, resource_id, groupreassign, manage_api_key): """ Set group reassignment authorization for a user via Manage API. @@ -349,7 +353,7 @@ def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_ke which controls which security groups the user can reassign to other users. Args: - user_id (str): The unique identifier of the user. + resource_id (str): The resource identifier of the user (extracted from href). groupreassign (list): List of group objects in format [{"groupname": "GROUP1"}, {"groupname": "GROUP2"}, ...] manage_api_key (dict): API key record with 'apikey' field for authentication. @@ -360,13 +364,13 @@ def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_ke Exception: If the update fails. """ if not groupreassign or len(groupreassign) == 0: - self.logger.debug(f"No group reassignment authorization to set for user {user_id}") + self.logger.debug(f"No group reassignment authorization to set for resource {resource_id}") return - self.logger.info(f"Setting group reassignment authorization for user {user_id} with {len(groupreassign)} groups") + self.logger.info(f"Setting group reassignment authorization for resource {resource_id} with {len(groupreassign)} groups") # Use Manage API to update the user's grpreassignauth - url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{user_id}" + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{resource_id}" querystring = { "lean": 1, "ccm": 1 @@ -398,7 +402,7 @@ def set_user_group_reassignment_auth(self, user_id, groupreassign, manage_api_ke self.logger.info(f"Response text: {response.text}") if response.status_code == 200: - self.logger.info(f"Successfully set group reassignment authorization for user {user_id}") + self.logger.info(f"Successfully set group reassignment authorization for resource {resource_id}") return response.json() raise Exception(f"Failed to set group reassignment authorization: {response.status_code} {response.text}") @@ -1617,6 +1621,18 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): self.logger.info(f"User def - {user_def}") user_info = self.get_or_create_user(user_def) self.logger.info(f"User info - {user_info}") + + # Parse resource_id from user_info for version >= 9.1 + resource_id = None + if Version(self.mas_version) >= Version('9.1') and user_info: + # Check if user_info has member array with href + if "member" in user_info and len(user_info["member"]) > 0: + href = user_info["member"][0].get("href", "") + # Extract resource_id from href (e.g., "api/os/masperuser/") + if href and "/" in href: + resource_id = href.split("/")[-1] + self.logger.info(f"Extracted resource_id: {resource_id} from user_info") + self.link_user_to_local_idp(user_id, email_password=True) self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) @@ -1642,7 +1658,10 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): for manage_security_group in manage_security_groups: self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) if Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: - self.set_user_group_reassignment_auth(user_id, groupreassign, maxadmin_manage_api_key) + if resource_id: + self.set_user_group_reassignment_auth(resource_id, groupreassign, maxadmin_manage_api_key) + else: + self.logger.warning(f"Cannot set group reassignment auth: resource_id not found for user {user_id}") # # Grant authorization to reassign users to/from ALL security groups (PRIMARY users only) # if user_type == "PRIMARY": diff --git a/test/src/test_users.py b/test/src/test_users.py index 7a1ea429..d0b1e775 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -210,8 +210,9 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key ) # Mock Manage API endpoint for version >= 9.1 + # Uses query parameter oslc.where instead of path parameter manage_mock = requests_mock.get( - f"{MANAGE_API_URL}/maximo/api/os/masperuser/{user_id}?lean=1", + f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1&oslc.where=userid%3D%22{user_id}%22", request_headers={"apikey": mock_manage_api_key["apikey"]}, json=json, status_code=status_code, @@ -1940,7 +1941,18 @@ def test_create_initial_user_for_saas( manage_security_groups = manage_security_groups_91 permissions = None # Not used in 9.1 entitlement = None # Not used in 9.1 - user_utils.get_or_create_user = MagicMock() + # Mock get_or_create_user to return appropriate response based on version + # Note: user_id might be None at this point, it gets set to user_email later + actual_user_id = user_id if user_id is not None else user_email + if mas_version == '9.1': + # For 9.1, return response with member array containing href with resource_id + resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" + user_utils.get_or_create_user = MagicMock(return_value={ + "member": [{"href": f"api/os/masperuser/{resource_id}"}], + "id": actual_user_id + }) + else: + user_utils.get_or_create_user = MagicMock(return_value={"id": actual_user_id}) user_utils.link_user_to_local_idp = MagicMock() user_utils.add_user_to_workspace = MagicMock() mas_workspace_application_ids = ["manage", "iot", "facilities"] @@ -2078,7 +2090,10 @@ def test_create_initial_user_for_saas( else: # 9.1 user_utils.add_user_to_manage_group.assert_not_called() if user_type == "PRIMARY": - user_utils.set_user_group_reassignment_auth.assert_called_once_with(user_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) + # For 9.1, resource_id is passed instead of user_id + actual_user_id = user_id if user_id is not None else user_email + resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" + user_utils.set_user_group_reassignment_auth.assert_called_once_with(resource_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) else: user_utils.set_user_group_reassignment_auth.assert_not_called() else: From f44cb623aa14cd8fa5d1c67cf88f850769d48345 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 15:33:16 +0530 Subject: [PATCH 22/35] updated query param --- src/mas/devops/users.py | 2 +- test/src/test_users.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 075f3d5b..7b8d8228 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -217,7 +217,7 @@ def get_user(self, user_id): url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser" querystring = { "lean": 1, - "oslc.where": f"userid=\"{user_id}\"" + "oslc.where": f"user.userid=\"{user_id}\"" } headers = { "Accept": "application/json", diff --git a/test/src/test_users.py b/test/src/test_users.py index d0b1e775..7c982543 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -210,9 +210,9 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key ) # Mock Manage API endpoint for version >= 9.1 - # Uses query parameter oslc.where instead of path parameter + # Uses query parameter oslc.where with user.userid instead of path parameter manage_mock = requests_mock.get( - f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1&oslc.where=userid%3D%22{user_id}%22", + f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1&oslc.where=user.userid%3D%22{user_id}%22", request_headers={"apikey": mock_manage_api_key["apikey"]}, json=json, status_code=status_code, From 4082dfb3d27138a0831bd9e2219ac04f7f9f59f7 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 16:07:04 +0530 Subject: [PATCH 23/35] fix --- src/mas/devops/users.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 7b8d8228..1f604581 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -231,8 +231,8 @@ def get_user(self, user_id): verify=self.manage_internal_ca_pem_file_path ) self.logger.info(f"GET {url} returned {response.status_code}") - self.logger.debug(f"Response: {response.text}") - self.logger.debug(f"Response json: {response.json}") + self.logger.info(f"Response: {response.text}") + self.logger.info(f"Response json: {response.json}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" @@ -284,7 +284,9 @@ def get_or_create_user(self, payload): existing_user = self.get_user(user_id) if existing_user is not None: - self.logger.info(f"Existing user {existing_user['id']} found") + # Log using the appropriate field based on version + user_identifier = existing_user.get('personid') or existing_user.get('id') + self.logger.info(f"Existing user {user_identifier} found") return existing_user self.logger.info(f"Creating new user {user_id}") From 10234562da0956ee16565470a20c01f376cb79c5 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 19:50:28 +0530 Subject: [PATCH 24/35] update --- src/mas/devops/users.py | 79 ++++++++++++++++++++++++----------- test/src/test_users.py | 91 +++++++++++++++++++++++++++-------------- 2 files changed, 116 insertions(+), 54 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 1f604581..e80c0c80 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -202,12 +202,15 @@ def get_user(self, user_id): user_id (str): The unique identifier of the user to retrieve. Returns: - dict: User details dictionary if found, None if user doesn't exist (404). + tuple: (resource_id, user_data) where: + - resource_id (str|None): Resource ID extracted from href for version >= 9.1, None for version < 9.1 + - user_data (dict|None): User details dictionary if found, None if user doesn't exist (404) Raises: Exception: If the API returns an unexpected status code. """ self.logger.debug(f"Getting user {user_id}") + resource_id = None # For MAS version >= 9.1, use the Manage API masperuser endpoint if Version(self.mas_version) >= Version('9.1'): @@ -230,9 +233,36 @@ def get_user(self, user_id): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) + user_info = response.json() self.logger.info(f"GET {url} returned {response.status_code}") self.logger.info(f"Response: {response.text}") self.logger.info(f"Response json: {response.json}") + + # Parse resource_id from user_info for version >= 9.1 + if Version(self.mas_version) >= Version('9.1') and user_info: + # Check if user_info has member array with href + if "member" in user_info and len(user_info["member"]) > 0: + href = user_info["member"][0].get("href", "") + # Extract resource_id from href (e.g., "api/os/masperuser/") + if href and "/" in href: + resource_id = href.split("/")[-1] + self.logger.info(f"Extracted resource_id: {resource_id} from user_info") + + if resource_id is not None: + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{resource_id}" + headers = { + "Accept": "application/json", + "apikey": maxadmin_manage_api_key["apikey"] + } + response = requests.get( + url, + headers=headers, + cert=self.manage_internal_client_pem_file_path, + verify=self.manage_internal_ca_pem_file_path + ) + self.logger.info(f"GET {url} returned {response.status_code}") + self.logger.info(f"Response: {response.text}") + self.logger.info(f"Response json: {response.json}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" @@ -247,10 +277,10 @@ def get_user(self, user_id): ) if response.status_code == 404: - return None + return resource_id, None if response.status_code == 200: - return response.json() + return resource_id, response.json() raise Exception(f"{response.status_code} {response.text}") @@ -272,7 +302,9 @@ def get_or_create_user(self, payload): (version >= 9.1) as the unique identifier. Returns: - dict: The user record (either existing or newly created). + tuple: (resource_id, user_data) where: + - resource_id (str|None): Resource ID extracted from href for version >= 9.1, None for version < 9.1 + - user_data (dict): The user record (either existing or newly created) Raises: Exception: If user creation fails with an unexpected status code. @@ -281,13 +313,13 @@ def get_or_create_user(self, payload): user_id_field = "personid" if Version(self.mas_version) >= Version('9.1') else "id" user_id = payload[user_id_field] - existing_user = self.get_user(user_id) + resource_id, existing_user = self.get_user(user_id) if existing_user is not None: # Log using the appropriate field based on version user_identifier = existing_user.get('personid') or existing_user.get('id') self.logger.info(f"Existing user {user_identifier} found") - return existing_user + return resource_id, existing_user self.logger.info(f"Creating new user {user_id}") @@ -318,9 +350,17 @@ def get_or_create_user(self, payload): if response.status_code == 201: # Manage API returns empty response body on success, fetch the user if response.text: - return response.json() + response_data = response.json() + # Parse resource_id from response if available + resource_id = None + if "member" in response_data and len(response_data["member"]) > 0: + href = response_data["member"][0].get("href", "") + if href and "/" in href: + resource_id = href.split("/")[-1] + self.logger.info(f"Extracted resource_id: {resource_id} from create response") + return resource_id, response_data else: - # Fetch the newly created user from Core API + # Fetch the newly created user return self.get_user(user_id) else: # For earlier versions, use the Core API v3/users endpoint @@ -338,7 +378,8 @@ def get_or_create_user(self, payload): verify=self.core_internal_ca_pem_file_path ) if response.status_code == 201: - return response.json() + # For version < 9.1, resource_id is None + return None, response.json() # if response.status_code == 409: # json = response.json() @@ -504,7 +545,7 @@ def link_user_to_local_idp(self, user_id, email_password=False): """ # For the sake of idempotency, check if the user already has a local identity - user = self.get_user(user_id) + resource_id, user = self.get_user(user_id) if user is None: raise Exception(f"User {user_id} was not found") @@ -725,7 +766,7 @@ def check_user_sync(self, user_id, application_id, timeout_secs=60 * 10, retry_i t_end = time.time() + timeout_secs self.logger.info(f"Awaiting user {user_id} sync status \"SUCCESS\" for app {application_id}: {t_end - time.time():.2f} seconds remaining") while time.time() < t_end: - user = self.get_user(user_id) + resource_id, user = self.get_user(user_id) 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"]: self.logger.warning(f"User {user_id} does not have any sync state for application {application_id}, triggering resync") @@ -771,7 +812,7 @@ def resync_users(self, user_ids): # which reduces the impact of concurrent updates leading to race conditions) for user_id in user_ids: - user = self.get_user(user_id) + resource_id, user = self.get_user(user_id) self.update_user_display_name(user_id, user["displayName"]) def create_or_get_manage_api_key_for_user(self, user_id, temporary=False): @@ -1621,20 +1662,10 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): } self.logger.info(f"User def - {user_def}") - user_info = self.get_or_create_user(user_def) + resource_id, user_info = self.get_or_create_user(user_def) + self.logger.info(f"Resource ID - {resource_id}") self.logger.info(f"User info - {user_info}") - # Parse resource_id from user_info for version >= 9.1 - resource_id = None - if Version(self.mas_version) >= Version('9.1') and user_info: - # Check if user_info has member array with href - if "member" in user_info and len(user_info["member"]) > 0: - href = user_info["member"][0].get("href", "") - # Extract resource_id from href (e.g., "api/os/masperuser/") - if href and "/" in href: - resource_id = href.split("/")[-1] - self.logger.info(f"Extracted resource_id: {resource_id} from user_info") - self.link_user_to_local_idp(user_id, email_password=True) self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) diff --git a/test/src/test_users.py b/test/src/test_users.py index 7c982543..8ba820e2 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -12,7 +12,7 @@ import base64 from unittest.mock import MagicMock, patch, call from pytest import fixture - +from packaging.version import Version import os from mas.devops.users import MASUserUtils @@ -210,8 +210,8 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key ) # Mock Manage API endpoint for version >= 9.1 - # Uses query parameter oslc.where with user.userid instead of path parameter - manage_mock = requests_mock.get( + # First request: Uses query parameter oslc.where with user.userid to get resource_id + manage_query_mock = requests_mock.get( f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1&oslc.where=user.userid%3D%22{user_id}%22", request_headers={"apikey": mock_manage_api_key["apikey"]}, json=json, @@ -219,12 +219,32 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) ) - return core_mock, manage_mock + # Second request: If status is 200 and json is a dict (not a callback) with member array, mock the resource_id GET + if status_code == 200 and json and isinstance(json, dict) and "member" in json and len(json["member"]) > 0: + href = json["member"][0].get("href", "") + if href and "/" in href: + resource_id = href.split("/")[-1] + requests_mock.get( + f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}", + request_headers={"apikey": mock_manage_api_key["apikey"]}, + json=json, + status_code=status_code, + additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) + ) + + return core_mock, manage_query_mock def mock_get_user_200(requests_mock, user_id, mock_manage_api_key): + # For Manage API (9.1+), include member array with href containing resource_id + resource_id = f"{user_id}_resource_id" + json_response = { + "id": user_id, + "displayName": user_id, + "member": [{"href": f"api/os/masperuser/{resource_id}"}] + } return mock_get_user( - requests_mock, user_id, {"id": user_id, "displayName": user_id}, 200, mock_manage_api_key + requests_mock, user_id, json_response, 200, mock_manage_api_key ) @@ -342,7 +362,15 @@ def test_mas_workspace_application_ids(user_utils, requests_mock): def test_get_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) - assert user_utils.get_user(user_id) == {"id": user_id, "displayName": user_id} + resource_id, user_data = user_utils.get_user(user_id) + assert user_data["id"] == user_id + assert user_data["displayName"] == user_id + # For version >= 9.1, resource_id should be extracted; for < 9.1, it should be None + if Version(user_utils.mas_version) >= Version('9.1'): + assert resource_id is not None + assert resource_id == f"{user_id}_resource_id" + else: + assert resource_id is None # Check that the correct endpoint was called based on version if user_utils.mas_version >= '9.1': @@ -356,7 +384,9 @@ def test_get_user_exists(user_utils, requests_mock, mock_manage_api_key): def test_get_user_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) - assert user_utils.get_user(user_id) is None + resource_id, user_data = user_utils.get_user(user_id) + assert resource_id is None + assert user_data is None # Check that the correct endpoint was called based on version if user_utils.mas_version >= '9.1': @@ -405,15 +435,21 @@ def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_ke ) # Use correct payload structure based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): payload = {"personid": user_id} else: payload = {"id": user_id} - assert user_utils.get_or_create_user(payload) == {"id": user_id, "displayName": user_id} + resource_id, user_data = user_utils.get_or_create_user(payload) + assert user_data["id"] == user_id + assert user_data["displayName"] == user_id + # For version >= 9.1, resource_id should be extracted; for < 9.1, it should be None + if Version(user_utils.mas_version) >= Version('9.1'): + assert resource_id is not None + assert resource_id == f"{user_id}_resource_id" + else: + assert resource_id is None # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 @@ -447,15 +483,17 @@ def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_ ) # Use correct payload structure based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): payload = {"personid": user_id} else: payload = {"id": user_id} - assert user_utils.get_or_create_user(payload) == {"id": user_id, "displayName": user_id} + resource_id, user_data = user_utils.get_or_create_user(payload) + assert user_data == {"id": user_id, "displayName": user_id} + # For version >= 9.1, resource_id might be None if not in response; for < 9.1, it should be None + if Version(user_utils.mas_version) < Version('9.1'): + assert resource_id is None # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 @@ -491,7 +529,6 @@ def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key ) # Use correct payload structure based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): payload = {"personid": user_id} else: @@ -500,7 +537,6 @@ def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key with pytest.raises(Exception): user_utils.get_or_create_user(payload) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 @@ -583,7 +619,6 @@ def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key): user_utils.link_user_to_local_idp(user_id, email_password=email_password) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 @@ -605,7 +640,6 @@ def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_man user_utils.link_user_to_local_idp(user_id) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 @@ -633,7 +667,6 @@ def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_m user_utils.link_user_to_local_idp(user_id, email_password=email_password) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 @@ -873,7 +906,6 @@ def test_resync_users(user_utils, requests_mock, mock_manage_api_key): user_utils.resync_users(user_ids) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): for get_core in gets_core: assert get_core.call_count == 0 @@ -930,7 +962,6 @@ def json_callback(request, context): user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 3 @@ -969,7 +1000,6 @@ def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key) assert str(excinfo.value) == f"User {user_id} sync failed to complete for app within {0.3} seconds" # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count > 1 @@ -1039,7 +1069,6 @@ def json_callback(request, context): user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 3 @@ -1107,7 +1136,6 @@ def json_callback(request, context): user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 3 @@ -1153,7 +1181,6 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock, mo assert str(excinfo.value) == f"User {user_id} sync failed to complete for app within {0.3} seconds" # Check that the correct endpoint was called based on version - from packaging.version import Version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count > 1 @@ -1945,14 +1972,18 @@ def test_create_initial_user_for_saas( # Note: user_id might be None at this point, it gets set to user_email later actual_user_id = user_id if user_id is not None else user_email if mas_version == '9.1': - # For 9.1, return response with member array containing href with resource_id + # For 9.1, return tuple (resource_id, user_data) with member array containing href resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" - user_utils.get_or_create_user = MagicMock(return_value={ - "member": [{"href": f"api/os/masperuser/{resource_id}"}], - "id": actual_user_id - }) + user_utils.get_or_create_user = MagicMock(return_value=( + resource_id, + { + "member": [{"href": f"api/os/masperuser/{resource_id}"}], + "id": actual_user_id + } + )) else: - user_utils.get_or_create_user = MagicMock(return_value={"id": actual_user_id}) + # For version < 9.1, return tuple (None, user_data) + user_utils.get_or_create_user = MagicMock(return_value=(None, {"id": actual_user_id})) user_utils.link_user_to_local_idp = MagicMock() user_utils.add_user_to_workspace = MagicMock() mas_workspace_application_ids = ["manage", "iot", "facilities"] From 9f7f3a5bc53ab7e5fc883553993f975246f2f7b9 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 22:46:15 +0530 Subject: [PATCH 25/35] [skip test] update --- src/mas/devops/users.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index e80c0c80..90a98e91 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -248,21 +248,24 @@ def get_user(self, user_id): resource_id = href.split("/")[-1] self.logger.info(f"Extracted resource_id: {resource_id} from user_info") - if resource_id is not None: - url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{resource_id}" - headers = { - "Accept": "application/json", - "apikey": maxadmin_manage_api_key["apikey"] - } - response = requests.get( - url, - headers=headers, - cert=self.manage_internal_client_pem_file_path, - verify=self.manage_internal_ca_pem_file_path - ) - self.logger.info(f"GET {url} returned {response.status_code}") - self.logger.info(f"Response: {response.text}") - self.logger.info(f"Response json: {response.json}") + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser" + headers = { + "Accept": "application/json", + "apikey": maxadmin_manage_api_key["apikey"] + } + querystring = { + "lean": 1, + "oslc.where": f"userid=\"{user_id}\"" + } + response = requests.get( + url, + headers=headers, + cert=self.manage_internal_client_pem_file_path, + verify=self.manage_internal_ca_pem_file_path + ) + self.logger.info(f"GET {url} returned {response.status_code}") + self.logger.info(f"Response: {response.text}") + self.logger.info(f"Response json: {response.json}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" From 844e7bf4a81835b20bbedfa7be42b88dd899bb47 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Fri, 27 Mar 2026 23:56:03 +0530 Subject: [PATCH 26/35] update --- src/mas/devops/users.py | 38 ++++++++++--------- test/src/test_users.py | 84 ++++++++++++++++++++++++++--------------- 2 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 90a98e91..66e565ba 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -217,6 +217,7 @@ def get_user(self, user_id): # Get MAXADMIN API key for authentication maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) + # First request: Query to find user and get resource_id from href url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser" querystring = { "lean": 1, @@ -233,39 +234,40 @@ def get_user(self, user_id): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) - user_info = response.json() self.logger.info(f"GET {url} returned {response.status_code}") self.logger.info(f"Response: {response.text}") - self.logger.info(f"Response json: {response.json}") - - # Parse resource_id from user_info for version >= 9.1 - if Version(self.mas_version) >= Version('9.1') and user_info: - # Check if user_info has member array with href - if "member" in user_info and len(user_info["member"]) > 0: - href = user_info["member"][0].get("href", "") - # Extract resource_id from href (e.g., "api/os/masperuser/") - if href and "/" in href: - resource_id = href.split("/")[-1] - self.logger.info(f"Extracted resource_id: {resource_id} from user_info") - url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser" + user_info = response.json() + self.logger.info(f"Response json: {user_info}") + + # Parse resource_id from user_info + if user_info and "member" in user_info and len(user_info["member"]) > 0: + href = user_info["member"][0].get("href", "") + # Extract resource_id from href (e.g., "api/os/masperuser/") + if href and "/" in href: + resource_id = href.split("/")[-1] + self.logger.info(f"Extracted resource_id: {resource_id} from user_info") + + # Second request: Get full user details using resource_id + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/" + querystring = { + "lean": 1, + "oslc.where": f"personid=\"{user_id}\"", + "oslc.select": "personid,displayname" + } headers = { "Accept": "application/json", "apikey": maxadmin_manage_api_key["apikey"] } - querystring = { - "lean": 1, - "oslc.where": f"userid=\"{user_id}\"" - } response = requests.get( url, headers=headers, + params=querystring, cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) self.logger.info(f"GET {url} returned {response.status_code}") self.logger.info(f"Response: {response.text}") - self.logger.info(f"Response json: {response.json}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" diff --git a/test/src/test_users.py b/test/src/test_users.py index 8ba820e2..5fc585f5 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -219,20 +219,18 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) ) - # Second request: If status is 200 and json is a dict (not a callback) with member array, mock the resource_id GET - if status_code == 200 and json and isinstance(json, dict) and "member" in json and len(json["member"]) > 0: - href = json["member"][0].get("href", "") - if href and "/" in href: - resource_id = href.split("/")[-1] - requests_mock.get( - f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}", - request_headers={"apikey": mock_manage_api_key["apikey"]}, - json=json, - status_code=status_code, - additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) - ) + # Second request: Mock the query-based request with personid + # This matches the actual implementation at line 258-275 in users.py + # Always mock this for version >= 9.1, regardless of status_code + manage_personid_mock = requests_mock.get( + f"{MANAGE_API_URL}/maximo/api/os/masperuser/?lean=1&oslc.where=personid%3D%22{user_id}%22&oslc.select=personid%2Cdisplayname", + request_headers={"apikey": mock_manage_api_key["apikey"]}, + json=json, + status_code=status_code, + additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) + ) - return core_mock, manage_query_mock + return core_mock, manage_query_mock, manage_personid_mock def mock_get_user_200(requests_mock, user_id, mock_manage_api_key): @@ -361,7 +359,7 @@ def test_mas_workspace_application_ids(user_utils, requests_mock): def test_get_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) resource_id, user_data = user_utils.get_user(user_id) assert user_data["id"] == user_id assert user_data["displayName"] == user_id @@ -383,7 +381,7 @@ def test_get_user_exists(user_utils, requests_mock, mock_manage_api_key): def test_get_user_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) resource_id, user_data = user_utils.get_user(user_id) assert resource_id is None assert user_data is None @@ -399,7 +397,7 @@ def test_get_user_notfound(user_utils, requests_mock, mock_manage_api_key): def test_get_user_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_500(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_500(requests_mock, user_id, mock_manage_api_key) with pytest.raises(Exception): user_utils.get_user(user_id) @@ -414,7 +412,7 @@ def test_get_user_error(user_utils, requests_mock, mock_manage_api_key): def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) # Mock Core API endpoint for version < 9.1 post_core = requests_mock.post( @@ -462,7 +460,7 @@ def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_ke def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) # Mock Core API endpoint for version < 9.1 post_core = requests_mock.post( @@ -508,7 +506,7 @@ def test_get_or_create_user_notfound(user_utils, requests_mock, mock_manage_api_ def test_get_or_create_user_error(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) # Mock Core API endpoint for version < 9.1 post_core = requests_mock.post( @@ -606,7 +604,7 @@ def test_update_user_display_name_error(user_utils, requests_mock): def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" email_password = True - get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) put = requests_mock.put( f"{MAS_API_URL}/v3/users/{user_id}/idps/local?emailPassword={email_password}", @@ -630,7 +628,7 @@ def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key): def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" - get_core, get_manage = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) put = requests_mock.put( f"{MAS_API_URL}/v3/users/{user_id}/idps/local", additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id}) @@ -652,7 +650,7 @@ def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_man def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" email_password = True - get_core, get_manage = mock_get_user( + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, {"id": user_id, "identities": {"_local": {}}}, 200, mock_manage_api_key ) @@ -888,7 +886,7 @@ def test_resync_users(user_utils, requests_mock, mock_manage_api_key): gets_manage = [] patches = [] for user_id in user_ids: - get_core, get_manage = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) + get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) gets_core.append(get_core) gets_manage.append(get_manage) @@ -930,13 +928,18 @@ def test_check_user_sync(user_utils, requests_mock, mock_manage_api_key): def json_callback(request, context): nonlocal attempts - if attempts >= 2: + # For version >= 9.1, each get_user call makes 2 requests, so we need attempts >= 4 + # For version < 9.1, each get_user call makes 1 request, so we need attempts >= 2 + threshold = 4 if Version(user_utils.mas_version) >= Version('9.1') else 2 + if attempts >= threshold: state = "SUCCESS" else: state = "PENDING" attempts = attempts + 1 + resource_id = f"{user_id}_resource_id" return { "id": user_id, + "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { "other": { "sync": { @@ -951,7 +954,7 @@ def json_callback(request, context): } } - get_core, get_manage = mock_get_user( + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, json_callback, @@ -962,8 +965,11 @@ def json_callback(request, context): user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) # Check that the correct endpoint was called based on version + # Note: For version >= 9.1, get_user makes 2 requests (query + resource_id GET) + # but we only track the first query request in get_manage mock if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 + # Each get_user call makes 2 requests, but we only count the query request assert get_manage.call_count == 3 else: assert get_core.call_count == 3 @@ -974,7 +980,7 @@ def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key) user_id = "user1" application_id = "manage" - get_core, get_manage = mock_get_user( + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, { @@ -1019,10 +1025,15 @@ def test_check_user_sync_appstate_notfound(user_utils, requests_mock, mock_manag def json_callback(request, context): nonlocal attempts - if attempts >= 1: + resource_id = f"{user_id}_resource_id" + # For version >= 9.1, each get_user call makes 2 requests, so we need attempts >= 2 + # For version < 9.1, each get_user call makes 1 request, so we need attempts >= 1 + threshold = 2 if Version(user_utils.mas_version) >= Version('9.1') else 1 + if attempts >= threshold: ret = { "id": user_id, "displayName": user_id, + "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { "other": { "sync": { @@ -1040,6 +1051,7 @@ def json_callback(request, context): ret = { "id": user_id, "displayName": user_id, + "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { "other": { "sync": { @@ -1058,7 +1070,7 @@ def json_callback(request, context): status_code=200 ) - get_core, get_manage = mock_get_user( + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, json_callback, @@ -1071,7 +1083,9 @@ def json_callback(request, context): # Check that the correct endpoint was called based on version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 + # For version >= 9.1, each get_user call makes 2 requests assert get_manage.call_count == 3 + assert get_manage_personid.call_count == 3 else: assert get_core.call_count == 3 assert get_manage.call_count == 0 @@ -1091,10 +1105,15 @@ def test_check_user_sync_appstate_transient_error(user_utils, requests_mock, moc def json_callback(request, context): nonlocal attempts - if attempts >= 1: + resource_id = f"{user_id}_resource_id" + # For version >= 9.1, each get_user call makes 2 requests, so we need attempts >= 2 + # For version < 9.1, each get_user call makes 1 request, so we need attempts >= 1 + threshold = 2 if Version(user_utils.mas_version) >= Version('9.1') else 1 + if attempts >= threshold: ret = { "id": user_id, "displayName": user_id, + "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { application_id: { "sync": { @@ -1107,6 +1126,7 @@ def json_callback(request, context): ret = { "id": user_id, "displayName": user_id, + "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { application_id: { "sync": { @@ -1125,7 +1145,7 @@ def json_callback(request, context): status_code=200 ) - get_core, get_manage = mock_get_user( + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, json_callback, @@ -1138,7 +1158,9 @@ def json_callback(request, context): # Check that the correct endpoint was called based on version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 + # For version >= 9.1, each get_user call makes 2 requests assert get_manage.call_count == 3 + assert get_manage_personid.call_count == 3 else: assert get_core.call_count == 3 assert get_manage.call_count == 0 @@ -1158,7 +1180,7 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock, mo status_code=200 ) - get_core, get_manage = mock_get_user( + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, { From 5ea4af45777b65b766927f5071ebdce9349eefb8 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Sat, 28 Mar 2026 01:35:16 +0530 Subject: [PATCH 27/35] update --- src/mas/devops/users.py | 27 ++++- test/src/test_users.py | 256 +++++++++++++++++++++++++++++++++------- 2 files changed, 232 insertions(+), 51 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 66e565ba..464a2d61 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -209,7 +209,7 @@ def get_user(self, user_id): Raises: Exception: If the API returns an unexpected status code. """ - self.logger.debug(f"Getting user {user_id}") + self.logger.info(f"Getting user {user_id}") resource_id = None # For MAS version >= 9.1, use the Manage API masperuser endpoint @@ -267,7 +267,7 @@ def get_user(self, user_id): verify=self.manage_internal_ca_pem_file_path ) self.logger.info(f"GET {url} returned {response.status_code}") - self.logger.info(f"Response: {response.text}") + self.logger.info(f"Response: {response.json()}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" @@ -284,10 +284,21 @@ def get_user(self, user_id): if response.status_code == 404: return resource_id, None - if response.status_code == 200: - return resource_id, response.json() + if response.status_code != 200: + raise Exception(f"{response.status_code} {response.text}") - raise Exception(f"{response.status_code} {response.text}") + # Handle response based on version + if Version(self.mas_version) >= Version('9.1'): + # Manage API returns member array + user_data = response.json() + if "member" in user_data and len(user_data["member"]) > 0: + return resource_id, user_data["member"][0] + else: + # Empty member array means user not found + return resource_id, None + else: + # Core API returns user object directly + return resource_id, response.json() def get_or_create_user(self, payload): """ @@ -818,7 +829,11 @@ def resync_users(self, user_ids): for user_id in user_ids: resource_id, user = self.get_user(user_id) - self.update_user_display_name(user_id, user["displayName"]) + # For version >= 9.1, Manage API uses "displayname" (lowercase) + # For version < 9.1, Core API uses "displayName" (camelCase) + display_name = user.get("displayname") if Version(self.mas_version) >= Version('9.1') else user.get("displayName") + if display_name: + self.update_user_display_name(user_id, display_name) def create_or_get_manage_api_key_for_user(self, user_id, temporary=False): """ diff --git a/test/src/test_users.py b/test/src/test_users.py index 5fc585f5..efe4372f 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -199,7 +199,7 @@ def test_admin_internal_ca_pem_file_path(user_utils, mock_named_temporary_file, assert mock_atexit.mock_calls == [call(os.remove, PEM_PATH)] -def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key): +def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key, json_manage=None): # Mock Core API endpoint for version < 9.1 core_mock = requests_mock.get( f"{MAS_API_URL}/v3/users/{user_id}", @@ -209,12 +209,15 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key additional_matcher=lambda req: additional_matcher(req) ) + # Use separate JSON for Manage API if provided, otherwise use the same + manage_json = json_manage if json_manage is not None else json + # Mock Manage API endpoint for version >= 9.1 # First request: Uses query parameter oslc.where with user.userid to get resource_id manage_query_mock = requests_mock.get( f"{MANAGE_API_URL}/maximo/api/os/masperuser?lean=1&oslc.where=user.userid%3D%22{user_id}%22", request_headers={"apikey": mock_manage_api_key["apikey"]}, - json=json, + json=manage_json, status_code=status_code, additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) ) @@ -225,7 +228,7 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key manage_personid_mock = requests_mock.get( f"{MANAGE_API_URL}/maximo/api/os/masperuser/?lean=1&oslc.where=personid%3D%22{user_id}%22&oslc.select=personid%2Cdisplayname", request_headers={"apikey": mock_manage_api_key["apikey"]}, - json=json, + json=manage_json, status_code=status_code, additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) ) @@ -234,15 +237,25 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key def mock_get_user_200(requests_mock, user_id, mock_manage_api_key): - # For Manage API (9.1+), include member array with href containing resource_id - resource_id = f"{user_id}_resource_id" - json_response = { + # Core API response for version < 9.1 + core_json = { "id": user_id, - "displayName": user_id, - "member": [{"href": f"api/os/masperuser/{resource_id}"}] + "displayName": user_id } + + # Manage API response for version >= 9.1 + # Include member array with href containing resource_id + resource_id = f"{user_id}_resource_id" + manage_json = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id + }] + } + return mock_get_user( - requests_mock, user_id, json_response, 200, mock_manage_api_key + requests_mock, user_id, core_json, 200, mock_manage_api_key, json_manage=manage_json ) @@ -361,8 +374,14 @@ def test_get_user_exists(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) resource_id, user_data = user_utils.get_user(user_id) - assert user_data["id"] == user_id - assert user_data["displayName"] == user_id + # For version >= 9.1, Manage API uses "personid" and "displayname" + # For version < 9.1, Core API uses "id" and "displayName" + if Version(user_utils.mas_version) >= Version('9.1'): + assert user_data["personid"] == user_id + assert user_data["displayname"] == user_id + else: + assert user_data["id"] == user_id + assert user_data["displayName"] == user_id # For version >= 9.1, resource_id should be extracted; for < 9.1, it should be None if Version(user_utils.mas_version) >= Version('9.1'): assert resource_id is not None @@ -439,8 +458,14 @@ def test_get_or_create_user_exists(user_utils, requests_mock, mock_manage_api_ke payload = {"id": user_id} resource_id, user_data = user_utils.get_or_create_user(payload) - assert user_data["id"] == user_id - assert user_data["displayName"] == user_id + # For version >= 9.1, Manage API uses "personid" and "displayname" + # For version < 9.1, Core API uses "id" and "displayName" + if Version(user_utils.mas_version) >= Version('9.1'): + assert user_data["personid"] == user_id + assert user_data["displayname"] == user_id + else: + assert user_data["id"] == user_id + assert user_data["displayName"] == user_id # For version >= 9.1, resource_id should be extracted; for < 9.1, it should be None if Version(user_utils.mas_version) >= Version('9.1'): assert resource_id is not None @@ -650,8 +675,21 @@ def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_man def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" email_password = True + resource_id = f"{user_id}_resource_id" get_core, get_manage, get_manage_personid = mock_get_user( - requests_mock, user_id, {"id": user_id, "identities": {"_local": {}}}, 200, mock_manage_api_key + requests_mock, + user_id, + {"id": user_id, "identities": {"_local": {}}}, + 200, + mock_manage_api_key, + json_manage={ + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "identities": {"_local": {}} + }] + } ) put = requests_mock.put( @@ -926,20 +964,16 @@ def test_check_user_sync(user_utils, requests_mock, mock_manage_api_key): # transitions from PENDING -> SUCCESS on the third call attempts = 0 - def json_callback(request, context): + def json_callback_core(request, context): nonlocal attempts - # For version >= 9.1, each get_user call makes 2 requests, so we need attempts >= 4 - # For version < 9.1, each get_user call makes 1 request, so we need attempts >= 2 - threshold = 4 if Version(user_utils.mas_version) >= Version('9.1') else 2 - if attempts >= threshold: + # For version < 9.1, each get_user call makes 1 request + if attempts >= 2: state = "SUCCESS" else: state = "PENDING" attempts = attempts + 1 - resource_id = f"{user_id}_resource_id" return { "id": user_id, - "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { "other": { "sync": { @@ -954,12 +988,42 @@ def json_callback(request, context): } } + def json_callback_manage(request, context): + nonlocal attempts + # For version >= 9.1, each get_user call makes 2 requests + if attempts >= 4: + state = "SUCCESS" + else: + state = "PENDING" + attempts = attempts + 1 + resource_id = f"{user_id}_resource_id" + return { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + "other": { + "sync": { + "state": "ERROR" + } + }, + application_id: { + "sync": { + "state": state + } + } + } + }] + } + get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, - json_callback, + json_callback_core, 200, - mock_manage_api_key + mock_manage_api_key, + json_manage=json_callback_manage ) user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) @@ -980,6 +1044,7 @@ def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key) user_id = "user1" application_id = "manage" + resource_id = f"{user_id}_resource_id" get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, @@ -999,7 +1064,26 @@ def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key) } }, 200, - mock_manage_api_key + mock_manage_api_key, + json_manage={ + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + "other": { + "sync": { + "state": "ERROR" + } + }, + application_id: { + "sync": { + "state": "PENDING" + } + } + } + }] + } ) with pytest.raises(Exception) as excinfo: user_utils.check_user_sync(user_id, application_id, timeout_secs=0.3, retry_interval_secs=0.05) @@ -1023,17 +1107,12 @@ def test_check_user_sync_appstate_notfound(user_utils, requests_mock, mock_manag # a single resync should have been triggered attempts = 0 - def json_callback(request, context): + def json_callback_core(request, context): nonlocal attempts - resource_id = f"{user_id}_resource_id" - # For version >= 9.1, each get_user call makes 2 requests, so we need attempts >= 2 - # For version < 9.1, each get_user call makes 1 request, so we need attempts >= 1 - threshold = 2 if Version(user_utils.mas_version) >= Version('9.1') else 1 - if attempts >= threshold: + if attempts >= 1: ret = { "id": user_id, "displayName": user_id, - "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { "other": { "sync": { @@ -1051,7 +1130,6 @@ def json_callback(request, context): ret = { "id": user_id, "displayName": user_id, - "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { "other": { "sync": { @@ -1063,6 +1141,47 @@ def json_callback(request, context): attempts = attempts + 1 return ret + def json_callback_manage(request, context): + nonlocal attempts + resource_id = f"{user_id}_resource_id" + if attempts >= 2: + ret = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + "other": { + "sync": { + "state": "ERROR" + } + }, + application_id: { + "sync": { + "state": "SUCCESS" + } + } + } + }] + } + else: + ret = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + "other": { + "sync": { + "state": "ERROR" + } + }, + } + }] + } + attempts = attempts + 1 + return ret + patche = requests_mock.patch( f"{MAS_API_URL}/v3/users/{user_id}", request_headers={"x-access-token": TOKEN}, @@ -1073,9 +1192,10 @@ def json_callback(request, context): get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, - json_callback, + json_callback_core, 200, - mock_manage_api_key + mock_manage_api_key, + json_manage=json_callback_manage ) user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) @@ -1103,17 +1223,12 @@ def test_check_user_sync_appstate_transient_error(user_utils, requests_mock, moc # a single resync should have been triggered attempts = 0 - def json_callback(request, context): + def json_callback_core(request, context): nonlocal attempts - resource_id = f"{user_id}_resource_id" - # For version >= 9.1, each get_user call makes 2 requests, so we need attempts >= 2 - # For version < 9.1, each get_user call makes 1 request, so we need attempts >= 1 - threshold = 2 if Version(user_utils.mas_version) >= Version('9.1') else 1 - if attempts >= threshold: + if attempts >= 1: ret = { "id": user_id, "displayName": user_id, - "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { application_id: { "sync": { @@ -1126,7 +1241,6 @@ def json_callback(request, context): ret = { "id": user_id, "displayName": user_id, - "member": [{"href": f"api/os/masperuser/{resource_id}"}], "applications": { application_id: { "sync": { @@ -1138,6 +1252,42 @@ def json_callback(request, context): attempts = attempts + 1 return ret + def json_callback_manage(request, context): + nonlocal attempts + resource_id = f"{user_id}_resource_id" + if attempts >= 2: + ret = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + application_id: { + "sync": { + "state": "SUCCESS" + } + } + } + }] + } + else: + ret = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + application_id: { + "sync": { + "state": "ERROR" + } + } + } + }] + } + attempts = attempts + 1 + return ret + patche = requests_mock.patch( f"{MAS_API_URL}/v3/users/{user_id}", request_headers={"x-access-token": TOKEN}, @@ -1148,9 +1298,10 @@ def json_callback(request, context): get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, - json_callback, + json_callback_core, 200, - mock_manage_api_key + mock_manage_api_key, + json_manage=json_callback_manage ) user_utils.check_user_sync(user_id, application_id, timeout_secs=8, retry_interval_secs=0) @@ -1180,6 +1331,7 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock, mo status_code=200 ) + resource_id = f"{user_id}_resource_id" get_core, get_manage, get_manage_personid = mock_get_user( requests_mock, user_id, @@ -1195,7 +1347,21 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock, mo } }, 200, - mock_manage_api_key + mock_manage_api_key, + json_manage={ + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id, + "applications": { + application_id: { + "sync": { + "state": "ERROR" + } + } + } + }] + } ) with pytest.raises(Exception) as excinfo: From b7515a7f9a38f9fdac7fe8e71d7a957846da602e Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Sat, 28 Mar 2026 02:17:23 +0530 Subject: [PATCH 28/35] update --- src/mas/devops/users.py | 4 +- test/src/test_users.py | 155 ++++++++++++---------------------------- 2 files changed, 48 insertions(+), 111 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 464a2d61..98f1bc8f 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1701,8 +1701,8 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): role = application_role self.set_user_application_permission(user_id, mas_application_id, role) - for mas_application_id in self.mas_workspace_application_ids: - self.check_user_sync(user_id, mas_application_id) + for mas_application_id in self.mas_workspace_application_ids: + self.check_user_sync(user_id, mas_application_id) if len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids: maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) diff --git a/test/src/test_users.py b/test/src/test_users.py index efe4372f..9fca09c2 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -958,6 +958,10 @@ def test_resync_users(user_utils, requests_mock, mock_manage_api_key): def test_check_user_sync(user_utils, requests_mock, mock_manage_api_key): + # Skip for version >= 9.1 as Manage API doesn't return applications field + if Version(user_utils.mas_version) >= Version('9.1'): + pytest.skip("check_user_sync not applicable for version >= 9.1 (Manage API doesn't return applications field)") + user_id = "user1" application_id = "manage" @@ -991,29 +995,14 @@ def json_callback_core(request, context): def json_callback_manage(request, context): nonlocal attempts # For version >= 9.1, each get_user call makes 2 requests - if attempts >= 4: - state = "SUCCESS" - else: - state = "PENDING" attempts = attempts + 1 resource_id = f"{user_id}_resource_id" + # Manage API doesn't return applications field for version >= 9.1 return { "member": [{ "href": f"api/os/masperuser/{resource_id}", "personid": user_id, - "displayname": user_id, - "applications": { - "other": { - "sync": { - "state": "ERROR" - } - }, - application_id: { - "sync": { - "state": state - } - } - } + "displayname": user_id }] } @@ -1041,6 +1030,10 @@ def json_callback_manage(request, context): def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key): + # Skip for version >= 9.1 as Manage API doesn't return applications field + if Version(user_utils.mas_version) >= Version('9.1'): + pytest.skip("check_user_sync not applicable for version >= 9.1 (Manage API doesn't return applications field)") + user_id = "user1" application_id = "manage" @@ -1069,19 +1062,7 @@ def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key) "member": [{ "href": f"api/os/masperuser/{resource_id}", "personid": user_id, - "displayname": user_id, - "applications": { - "other": { - "sync": { - "state": "ERROR" - } - }, - application_id: { - "sync": { - "state": "PENDING" - } - } - } + "displayname": user_id }] } ) @@ -1099,6 +1080,10 @@ def test_check_user_sync_timeout(user_utils, requests_mock, mock_manage_api_key) def test_check_user_sync_appstate_notfound(user_utils, requests_mock, mock_manage_api_key): + # Skip for version >= 9.1 as Manage API doesn't return applications field + if Version(user_utils.mas_version) >= Version('9.1'): + pytest.skip("check_user_sync not applicable for version >= 9.1 (Manage API doesn't return applications field)") + user_id = "user1" application_id = "manage" @@ -1144,41 +1129,14 @@ def json_callback_core(request, context): def json_callback_manage(request, context): nonlocal attempts resource_id = f"{user_id}_resource_id" - if attempts >= 2: - ret = { - "member": [{ - "href": f"api/os/masperuser/{resource_id}", - "personid": user_id, - "displayname": user_id, - "applications": { - "other": { - "sync": { - "state": "ERROR" - } - }, - application_id: { - "sync": { - "state": "SUCCESS" - } - } - } - }] - } - else: - ret = { - "member": [{ - "href": f"api/os/masperuser/{resource_id}", - "personid": user_id, - "displayname": user_id, - "applications": { - "other": { - "sync": { - "state": "ERROR" - } - }, - } - }] - } + # Manage API doesn't return applications field for version >= 9.1 + ret = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id + }] + } attempts = attempts + 1 return ret @@ -1215,6 +1173,10 @@ def json_callback_manage(request, context): def test_check_user_sync_appstate_transient_error(user_utils, requests_mock, mock_manage_api_key): + # Skip for version >= 9.1 as Manage API doesn't return applications field + if Version(user_utils.mas_version) >= Version('9.1'): + pytest.skip("check_user_sync not applicable for version >= 9.1 (Manage API doesn't return applications field)") + user_id = "user1" application_id = "manage" @@ -1255,36 +1217,14 @@ def json_callback_core(request, context): def json_callback_manage(request, context): nonlocal attempts resource_id = f"{user_id}_resource_id" - if attempts >= 2: - ret = { - "member": [{ - "href": f"api/os/masperuser/{resource_id}", - "personid": user_id, - "displayname": user_id, - "applications": { - application_id: { - "sync": { - "state": "SUCCESS" - } - } - } - }] - } - else: - ret = { - "member": [{ - "href": f"api/os/masperuser/{resource_id}", - "personid": user_id, - "displayname": user_id, - "applications": { - application_id: { - "sync": { - "state": "ERROR" - } - } - } - }] - } + # Manage API doesn't return applications field for version >= 9.1 + ret = { + "member": [{ + "href": f"api/os/masperuser/{resource_id}", + "personid": user_id, + "displayname": user_id + }] + } attempts = attempts + 1 return ret @@ -1352,14 +1292,7 @@ def test_check_user_sync_appstate_persistent_error(user_utils, requests_mock, mo "member": [{ "href": f"api/os/masperuser/{resource_id}", "personid": user_id, - "displayname": user_id, - "applications": { - application_id: { - "sync": { - "state": "ERROR" - } - } - } + "displayname": user_id }] } ) @@ -2289,12 +2222,16 @@ def test_create_initial_user_for_saas( user_utils.await_mas_application_availability.assert_not_called() user_utils.set_user_application_permission.assert_not_called() - # check_user_sync is called for all versions - user_utils.check_user_sync.assert_has_calls([ - call(user_id, "manage"), - call(user_id, "iot"), - call(user_id, "facilities") - ]) + # check_user_sync is only called for version < 9.1 + # For version >= 9.1, Manage API doesn't return applications field, so sync check is not performed + if mas_version == '9.0': + user_utils.check_user_sync.assert_has_calls([ + call(user_id, "manage"), + call(user_id, "iot"), + call(user_id, "facilities") + ]) + else: # 9.1 + user_utils.check_user_sync.assert_not_called() if len(manage_security_groups) > 0: user_utils.create_or_get_manage_api_key_for_user.assert_called_once_with("MAXADMIN", temporary=True) From 6f64081f276e2494ae1e1058c95b1b1575f39830 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Sun, 29 Mar 2026 01:51:53 +0530 Subject: [PATCH 29/35] update --- src/mas/devops/users.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 98f1bc8f..a40a9c02 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -442,12 +442,11 @@ def set_user_group_reassignment_auth(self, resource_id, groupreassign, manage_ap } payload = { - "maxuser": [ - { - "grpreassignauth": groupreassign - } - ] + "maxuser": { + "grpreassignauth": groupreassign + } } + self.logger.info(f"Sending PATCH request to {url} with payload: {payload}") response = requests.post( url, @@ -1467,6 +1466,7 @@ def create_initial_users_for_saas(self, initial_users): all_security_groups = self.get_all_manage_groups() groupreassign = [{"groupname": group} for group in all_security_groups] + self.logger.info(f"Group reassign: {groupreassign}") for primary_user in primary_users: self.logger.info("") From f04b29f28ea54c95adf553f626be9445b9538fcb Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Sun, 29 Mar 2026 02:30:14 +0530 Subject: [PATCH 30/35] update --- src/mas/devops/users.py | 5 +++-- test/src/test_users.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index a40a9c02..a82049e1 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -404,7 +404,7 @@ def get_or_create_user(self, payload): raise Exception(f"{response.status_code} {response.text}") - def set_user_group_reassignment_auth(self, resource_id, groupreassign, manage_api_key): + def set_user_group_reassignment_auth(self, user_id, resource_id, groupreassign, manage_api_key): """ Set group reassignment authorization for a user via Manage API. @@ -443,6 +443,7 @@ def set_user_group_reassignment_auth(self, resource_id, groupreassign, manage_ap payload = { "maxuser": { + "userid": user_id, "grpreassignauth": groupreassign } } @@ -1712,7 +1713,7 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) if Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: if resource_id: - self.set_user_group_reassignment_auth(resource_id, groupreassign, maxadmin_manage_api_key) + self.set_user_group_reassignment_auth(user_id, resource_id, groupreassign, maxadmin_manage_api_key) else: self.logger.warning(f"Cannot set group reassignment auth: resource_id not found for user {user_id}") diff --git a/test/src/test_users.py b/test/src/test_users.py index 9fca09c2..9e8e3dfe 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -2246,10 +2246,10 @@ def test_create_initial_user_for_saas( else: # 9.1 user_utils.add_user_to_manage_group.assert_not_called() if user_type == "PRIMARY": - # For 9.1, resource_id is passed instead of user_id + # For 9.1, both user_id and resource_id are passed actual_user_id = user_id if user_id is not None else user_email resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" - user_utils.set_user_group_reassignment_auth.assert_called_once_with(resource_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) + user_utils.set_user_group_reassignment_auth.assert_called_once_with(actual_user_id, resource_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) else: user_utils.set_user_group_reassignment_auth.assert_not_called() else: From ef0ce3a209c7250b75dd249731615912216982a0 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Mon, 30 Mar 2026 10:50:46 +0530 Subject: [PATCH 31/35] update --- src/mas/devops/users.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index a82049e1..604e0f4c 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -460,9 +460,12 @@ def set_user_group_reassignment_auth(self, user_id, resource_id, groupreassign, self.logger.info(f"Response status code: {response.status_code}") self.logger.info(f"Response text: {response.text}") - if response.status_code == 200: + if response.status_code in [200, 204]: self.logger.info(f"Successfully set group reassignment authorization for resource {resource_id}") - return response.json() + # 204 No Content doesn't have a response body + if response.status_code == 200: + return response.json() + return None raise Exception(f"Failed to set group reassignment authorization: {response.status_code} {response.text}") From 12f1fb61b30d9eabad4fe54a9cfe191cdd96ae23 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Mon, 30 Mar 2026 15:15:05 +0530 Subject: [PATCH 32/35] update --- src/mas/devops/users.py | 157 ++++++++++++++++++++++++++++++---------- test/src/test_users.py | 88 +++++++++++++++++++--- 2 files changed, 197 insertions(+), 48 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 604e0f4c..00021b17 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -539,7 +539,7 @@ def update_user_display_name(self, user_id, display_name): raise Exception(f"{response.status_code} {response.text}") - def link_user_to_local_idp(self, user_id, email_password=False): + def link_user_to_local_idp(self, user_id, email_password=False, manage_api_key=None, resource_id=None): """ Link a user to the local identity provider (IDP). @@ -547,10 +547,17 @@ def link_user_to_local_idp(self, user_id, email_password=False): The method creates a local authentication identity for the user, enabling them to log in with username/password. + For MAS version < 9.1: Uses Core API PUT request + For MAS version >= 9.1: Uses Manage API PATCH request + Args: user_id (str): The unique identifier of the user to link. email_password (bool, optional): Whether to enable email/password authentication. Defaults to False. + manage_api_key (dict, optional): API key record with 'apikey' field for authentication. + Required for MAS version >= 9.1. + resource_id (str, optional): The resource identifier of the user (extracted from href). + Required for MAS version >= 9.1. Returns: None: Always returns None (authentication token is not exposed). @@ -563,40 +570,110 @@ def link_user_to_local_idp(self, user_id, email_password=False): or returned for security reasons. """ - # For the sake of idempotency, check if the user already has a local identity - resource_id, user = self.get_user(user_id) - if user is None: - raise Exception(f"User {user_id} was not found") + # Check MAS version to determine which API to use + current_version = Version(self.mas_version) + version_9_1 = Version("9.1") - if "identities" in user and "_local" in user["identities"]: - self.logger.info(f"User {user_id} already has a local identity") - return None + if current_version >= version_9_1: + # Version >= 9.1: Use Manage API PATCH request + if manage_api_key is None: + raise Exception("manage_api_key is required for MAS version >= 9.1") + if resource_id is None: + raise Exception("resource_id is required for MAS version >= 9.1") - self.logger.info(f"Linking user {user_id} to local IDP (email_password: {email_password})") - url = f"{self.mas_api_url_internal}/v3/users/{user_id}/idps/local" - querystring = { - "emailPassword": email_password - } - payload = { - "idpUserId": user_id, - } - headers = { - "Content-Type": "application/json", - "x-access-token": self.superuser_auth_token - } - response = requests.put( - url, - json=payload, - headers=headers, - params=querystring, - verify=self.core_internal_ca_pem_file_path - ) - if response.status_code != 200: - raise Exception(response.text) + # For the sake of idempotency, check if the user already has a local identity + _, user = self.get_user(user_id) + if user is None: + raise Exception(f"User {user_id} was not found") - # Important: HTTP 200 output will contain generated user token; DO NOT LOG + if "identities" in user and "_local" in user["identities"]: + self.logger.info(f"User {user_id} already has a local identity") + return None - return None + self.logger.info(f"Linking user {user_id} to local IDP using Manage API (version {self.mas_version})") + + url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{resource_id}" + querystring = { + "lean": 1, + "ccm": 1 + } + headers = { + "Content-Type": "application/json", + "apikey": manage_api_key["apikey"], + "x-method-override": "PATCH", + "patchtype": "MERGE" + } + + payload = { + "maxuser": { + "userid": user_id, + "masuseridp": [ + { + "emailpassword": email_password, + "idpid": "local", + "logintype": "0", + "idploginid": user_id, + "idptype": "local", + "enabled": True + } + ] + } + } + self.logger.info(f"Sending PATCH request to {url} with payload: {payload}") + + response = requests.post( + url, + json=payload, + headers=headers, + params=querystring, + cert=self.manage_internal_client_pem_file_path, + verify=self.manage_internal_ca_pem_file_path + ) + self.logger.info(f"Response status code: {response.status_code}") + self.logger.info(f"Response text: {response.text}") + + if response.status_code in [200, 204]: + self.logger.info(f"Successfully linked user {user_id} to local IDP") + return None + + raise Exception(f"Failed to link user to local IDP: {response.status_code} {response.text}") + + else: + # Version < 9.1: Use Core API PUT request (existing implementation) + # For the sake of idempotency, check if the user already has a local identity + _, user = self.get_user(user_id) + if user is None: + raise Exception(f"User {user_id} was not found") + + if "identities" in user and "_local" in user["identities"]: + self.logger.info(f"User {user_id} already has a local identity") + return None + + self.logger.info(f"Linking user {user_id} to local IDP using Core API (version {self.mas_version}, email_password: {email_password})") + url = f"{self.mas_api_url_internal}/v3/users/{user_id}/idps/local" + querystring = { + "emailPassword": email_password + } + payload = { + "idpUserId": user_id, + } + headers = { + "Content-Type": "application/json", + "x-access-token": self.superuser_auth_token + } + response = requests.put( + url, + json=payload, + headers=headers, + params=querystring, + verify=self.core_internal_ca_pem_file_path + ) + if response.status_code != 200: + raise Exception(response.text) + + # Important: HTTP 200 output will contain generated user token; DO NOT LOG + + return None def get_user_workspaces(self, user_id): """ @@ -1690,7 +1767,17 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): self.logger.info(f"Resource ID - {resource_id}") self.logger.info(f"User info - {user_info}") - self.link_user_to_local_idp(user_id, email_password=True) + # For version >= 9.1, we always need a Manage API key and resource_id to link user to local IDP + # For version < 9.1, we may need it later for manage_security_groups + if Version(self.mas_version) >= Version('9.1') or (len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids): + maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) + self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}") + + if Version(self.mas_version) >= Version('9.1'): + self.link_user_to_local_idp(user_id, email_password=True, manage_api_key=maxadmin_manage_api_key, resource_id=resource_id) + else: + self.link_user_to_local_idp(user_id, email_password=True) + self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) if Version(self.mas_version) < Version('9.1'): @@ -1709,8 +1796,6 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): self.check_user_sync(user_id, mas_application_id) if len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids: - maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) - self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}") if Version(self.mas_version) < Version('9.1'): for manage_security_group in manage_security_groups: self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) @@ -1719,7 +1804,3 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): self.set_user_group_reassignment_auth(user_id, resource_id, groupreassign, maxadmin_manage_api_key) else: self.logger.warning(f"Cannot set group reassignment auth: resource_id not found for user {user_id}") - - # # Grant authorization to reassign users to/from ALL security groups (PRIMARY users only) - # if user_type == "PRIMARY": - # self.grant_all_group_reassignment_auth(user_id, maxadmin_manage_api_key) diff --git a/test/src/test_users.py b/test/src/test_users.py index 9e8e3dfe..938df42b 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -629,8 +629,10 @@ def test_update_user_display_name_error(user_utils, requests_mock): def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" email_password = True + resource_id = f"{user_id}_resource_id" get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key) + # Mock Core API PUT request for version < 9.1 put = requests_mock.put( f"{MAS_API_URL}/v3/users/{user_id}/idps/local?emailPassword={email_password}", request_headers={"x-access-token": TOKEN}, @@ -639,28 +641,75 @@ def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key): additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id}) ) - user_utils.link_user_to_local_idp(user_id, email_password=email_password) + # Mock Manage API PATCH request for version >= 9.1 + patch = requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}?lean=1&ccm=1", + request_headers={ + "Content-Type": "application/json", + "apikey": mock_manage_api_key["apikey"], + "x-method-override": "PATCH", + "patchtype": "MERGE" + }, + json={"id": user_id}, + status_code=200, + additional_matcher=lambda req: additional_matcher( + req, + json={ + "maxuser": { + "userid": user_id, + "masuseridp": [{ + "emailpassword": True, + "idpid": "local", + "logintype": "0", + "idploginid": user_id, + "idptype": "local", + "enabled": True + }] + } + }, + cert=PEM_PATH + ) + ) + + # Call the function with appropriate parameters based on version + if Version(user_utils.mas_version) >= Version('9.1'): + user_utils.link_user_to_local_idp(user_id, email_password=email_password, manage_api_key=mock_manage_api_key, resource_id=resource_id) + else: + user_utils.link_user_to_local_idp(user_id, email_password=email_password) # Check that the correct endpoint was called based on version if Version(user_utils.mas_version) >= Version('9.1'): assert get_core.call_count == 0 assert get_manage.call_count == 1 + assert put.call_count == 0 + assert patch.call_count == 1 else: assert get_core.call_count == 1 assert get_manage.call_count == 0 - assert put.call_count == 1 + assert put.call_count == 1 + assert patch.call_count == 0 def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_manage_api_key): user_id = "user1" + resource_id = f"{user_id}_resource_id" get_core, get_manage, get_manage_personid = mock_get_user_404(requests_mock, user_id, mock_manage_api_key) + put = requests_mock.put( f"{MAS_API_URL}/v3/users/{user_id}/idps/local", additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id}) ) + patch = requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}?lean=1&ccm=1", + additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) + ) + with pytest.raises(Exception): - user_utils.link_user_to_local_idp(user_id) + if Version(user_utils.mas_version) >= Version('9.1'): + user_utils.link_user_to_local_idp(user_id, manage_api_key=mock_manage_api_key, resource_id=resource_id) + else: + user_utils.link_user_to_local_idp(user_id) # Check that the correct endpoint was called based on version if Version(user_utils.mas_version) >= Version('9.1'): @@ -670,6 +719,7 @@ def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_man assert get_core.call_count == 1 assert get_manage.call_count == 0 assert put.call_count == 0 + assert patch.call_count == 0 def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_manage_api_key): @@ -700,7 +750,16 @@ def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_m additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id}) ) - user_utils.link_user_to_local_idp(user_id, email_password=email_password) + patch = requests_mock.post( + f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}?lean=1&ccm=1", + additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH) + ) + + # Call the function with appropriate parameters based on version + if Version(user_utils.mas_version) >= Version('9.1'): + user_utils.link_user_to_local_idp(user_id, email_password=email_password, manage_api_key=mock_manage_api_key, resource_id=resource_id) + else: + user_utils.link_user_to_local_idp(user_id, email_password=email_password) # Check that the correct endpoint was called based on version if Version(user_utils.mas_version) >= Version('9.1'): @@ -710,6 +769,7 @@ def test_link_user_to_local_idp_already_linked(user_utils, requests_mock, mock_m assert get_core.call_count == 1 assert get_manage.call_count == 0 assert put.call_count == 0 + assert patch.call_count == 0 def test_get_user_workspaces(user_utils, requests_mock): @@ -2206,7 +2266,14 @@ def test_create_initial_user_for_saas( } user_utils.get_or_create_user.assert_called_once_with(expected_user_def) - user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True) + + # Check link_user_to_local_idp call based on version + if mas_version == '9.1': + resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" + user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True, manage_api_key=manage_api_key, resource_id=resource_id) + else: + user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True) + user_utils.add_user_to_workspace.assert_called_once_with(user_id, is_workspace_admin=is_workspace_admin) # For version < 9.1, await_mas_application_availability and set_user_application_permission are called @@ -2233,9 +2300,14 @@ def test_create_initial_user_for_saas( else: # 9.1 user_utils.check_user_sync.assert_not_called() - if len(manage_security_groups) > 0: + # For version >= 9.1, API key is always created (needed for link_user_to_local_idp) + # For version < 9.1, API key is only created if there are manage_security_groups + if mas_version == '9.1' or len(manage_security_groups) > 0: user_utils.create_or_get_manage_api_key_for_user.assert_called_once_with("MAXADMIN", temporary=True) + else: + user_utils.create_or_get_manage_api_key_for_user.assert_not_called() + if len(manage_security_groups) > 0: # For version < 9.1, add_user_to_manage_group is called # For version >= 9.1, set_user_group_reassignment_auth is called for PRIMARY users if mas_version == '9.0': @@ -2252,10 +2324,6 @@ def test_create_initial_user_for_saas( user_utils.set_user_group_reassignment_auth.assert_called_once_with(actual_user_id, resource_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) else: user_utils.set_user_group_reassignment_auth.assert_not_called() - else: - user_utils.create_or_get_manage_api_key_for_user.assert_not_called() - user_utils.add_user_to_manage_group.assert_not_called() - user_utils.set_user_group_reassignment_auth.assert_not_called() def test_create_initial_users_for_saas_invalid_inputs(user_utils): From e6a9826417a2e2aae1feda0c537b47f69481530a Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Mon, 30 Mar 2026 18:43:56 +0530 Subject: [PATCH 33/35] update --- src/mas/devops/users.py | 126 +++------------------------------------- test/src/test_users.py | 17 +++--- 2 files changed, 15 insertions(+), 128 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 00021b17..770217d4 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -209,7 +209,7 @@ def get_user(self, user_id): Raises: Exception: If the API returns an unexpected status code. """ - self.logger.info(f"Getting user {user_id}") + self.logger.debug(f"Getting user {user_id}") resource_id = None # For MAS version >= 9.1, use the Manage API masperuser endpoint @@ -234,11 +234,8 @@ def get_user(self, user_id): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) - self.logger.info(f"GET {url} returned {response.status_code}") - self.logger.info(f"Response: {response.text}") user_info = response.json() - self.logger.info(f"Response json: {user_info}") # Parse resource_id from user_info if user_info and "member" in user_info and len(user_info["member"]) > 0: @@ -248,7 +245,7 @@ def get_user(self, user_id): resource_id = href.split("/")[-1] self.logger.info(f"Extracted resource_id: {resource_id} from user_info") - # Second request: Get full user details using resource_id + # Second request: Get full user details url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/" querystring = { "lean": 1, @@ -266,8 +263,6 @@ def get_user(self, user_id): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) - self.logger.info(f"GET {url} returned {response.status_code}") - self.logger.info(f"Response: {response.json()}") else: # For earlier versions, use the Core API v3/users endpoint url = f"{self.mas_api_url_internal}/v3/users/{user_id}" @@ -352,7 +347,7 @@ def get_or_create_user(self, payload): "Content-Type": "application/json", "apikey": maxadmin_manage_api_key["apikey"] } - self.logger.info(f"Creating new user {user_id} with Manage API with payload {payload}") + self.logger.debug(f"Creating new user {user_id} with Manage API with payload {payload}") response = requests.post( url, json=payload, @@ -361,8 +356,6 @@ def get_or_create_user(self, payload): cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) - self.logger.info(f"Response status code: {response.status_code}") - self.logger.info(f"Response text: {response.text}") if response.status_code == 201: # Manage API returns empty response body on success, fetch the user if response.text: @@ -373,7 +366,7 @@ def get_or_create_user(self, payload): href = response_data["member"][0].get("href", "") if href and "/" in href: resource_id = href.split("/")[-1] - self.logger.info(f"Extracted resource_id: {resource_id} from create response") + self.logger.debug(f"Extracted resource_id: {resource_id} from create response") return resource_id, response_data else: # Fetch the newly created user @@ -447,7 +440,7 @@ def set_user_group_reassignment_auth(self, user_id, resource_id, groupreassign, "grpreassignauth": groupreassign } } - self.logger.info(f"Sending PATCH request to {url} with payload: {payload}") + self.logger.debug(f"Sending PATCH request to {url} with payload: {payload}") response = requests.post( url, @@ -457,8 +450,6 @@ def set_user_group_reassignment_auth(self, user_id, resource_id, groupreassign, cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) - self.logger.info(f"Response status code: {response.status_code}") - self.logger.info(f"Response text: {response.text}") if response.status_code in [200, 204]: self.logger.info(f"Successfully set group reassignment authorization for resource {resource_id}") @@ -619,7 +610,7 @@ def link_user_to_local_idp(self, user_id, email_password=False, manage_api_key=N ] } } - self.logger.info(f"Sending PATCH request to {url} with payload: {payload}") + self.logger.debug(f"Sending PATCH request to {url} with payload: {payload}") response = requests.post( url, @@ -629,8 +620,6 @@ def link_user_to_local_idp(self, user_id, email_password=False, manage_api_key=N cert=self.manage_internal_client_pem_file_path, verify=self.manage_internal_ca_pem_file_path ) - self.logger.info(f"Response status code: {response.status_code}") - self.logger.info(f"Response text: {response.text}") if response.status_code in [200, 204]: self.logger.info(f"Successfully linked user {user_id} to local IDP") @@ -1005,7 +994,6 @@ def get_manage_api_key_for_user(self, user_id): Raises: Exception: If the API call fails. """ - self.logger.info(f"Getting Manage API Key for user {user_id}") url = f"{self.manage_api_url_internal}/maximo/api/os/mxapiapikey" querystring = { "ccm": 1, @@ -1024,7 +1012,6 @@ def get_manage_api_key_for_user(self, user_id): verify=self.manage_internal_ca_pem_file_path, cert=self.manage_internal_client_pem_file_path ) - self.logger.info(f"Response: {response.status_code} {response.text}") if response.status_code == 200: json = response.json() @@ -1269,92 +1256,6 @@ def get_all_manage_groups(self): self.logger.info(f"Found {len(groups)} security groups in Manage") return groups - # def grant_group_reassignment_auth(self, user_id, group_name, manage_api_key): - # """ - # Grant a user authorization to reassign users to/from a specific security group. - - # This adds an entry to the grpreassignauth collection for the user, allowing them - # to manage membership in the specified security group. - - # Args: - # user_id (str): The unique identifier of the user. - # group_name (str): The name of the security group to grant authorization for. - # manage_api_key (dict): API key record with 'apikey' field for authentication. - - # Returns: - # None: Returns None on success. - - # Raises: - # Exception: If the operation fails. - # """ - # self.logger.info(f"Granting user {user_id} authorization to reassign group {group_name}") - - # url = f"{self.manage_api_url_internal}/maximo/oslc/os/masperuser" - # querystring = { - # "lean": 1, - # "oslc.where": f"personid=\"{user_id}\"", - # } - # headers = { - # "Content-Type": "application/json", - # "Accept": "application/json", - # "x-method-override": "PATCH", - # "patchtype": "MERGE", - # "apikey": manage_api_key["apikey"], - # } - # payload = { - # "maxuser": [ - # { - # "userid": user_id, - # "grpreassignauth": [ - # { - # "groupname": group_name - # } - # ] - # } - # ] - # } - # response = requests.post( - # url, - # headers=headers, - # params=querystring, - # json=payload, - # verify=self.manage_internal_ca_pem_file_path, - # ) - # if response.status_code != 204: - # raise Exception(f"{response.status_code} {response.text}") - - # return None - - # def grant_all_group_reassignment_auth(self, user_id, manage_api_key): - # """ - # Grant a user authorization to reassign users to/from ALL security groups. - - # This method fetches all security groups and grants reassignment authorization - # for each one, allowing the user to fully manage security group memberships. - - # Args: - # user_id (str): The unique identifier of the user. - # manage_api_key (dict): API key record with 'apikey' field for authentication. - - # Returns: - # None: Returns None on success. - - # Raises: - # Exception: If the operation fails. - # """ - # self.logger.info(f"Granting user {user_id} authorization to reassign ALL security groups") - - # groups = self.get_all_manage_groups(manage_api_key) - - # for group_name in groups: - # try: - # self.grant_group_reassignment_auth(user_id, group_name, manage_api_key) - # except Exception as e: - # self.logger.warning(f"Failed to grant reassignment auth for group {group_name}: {str(e)}") - # # Continue with other groups even if one fails - - # self.logger.info(f"Completed granting group reassignment authorization for {len(groups)} groups") - def get_mas_applications_in_workspace(self): """ Retrieve all MAS applications configured in the workspace. @@ -1547,7 +1448,6 @@ def create_initial_users_for_saas(self, initial_users): all_security_groups = self.get_all_manage_groups() groupreassign = [{"groupname": group} for group in all_security_groups] - self.logger.info(f"Group reassign: {groupreassign}") for primary_user in primary_users: self.logger.info("") @@ -1750,30 +1650,18 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): "personid": user_id, "primaryemailtype": "Work", "primaryemail": user_email, - # "username": username, "primaryphone": "", "addressline1": "", "displayName": display_name, "maxuser": maxuser_def, - # "issuer": "local", - # "permissions": permissions, - # "entitlement": entitlement, - # "givenName": user_given_name, - # "familyName": user_family_name } - self.logger.info(f"User def - {user_def}") - resource_id, user_info = self.get_or_create_user(user_def) - self.logger.info(f"Resource ID - {resource_id}") - self.logger.info(f"User info - {user_info}") + resource_id, _ = self.get_or_create_user(user_def) # For version >= 9.1, we always need a Manage API key and resource_id to link user to local IDP # For version < 9.1, we may need it later for manage_security_groups if Version(self.mas_version) >= Version('9.1') or (len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids): maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) - self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}") - - if Version(self.mas_version) >= Version('9.1'): self.link_user_to_local_idp(user_id, email_password=True, manage_api_key=maxadmin_manage_api_key, resource_id=resource_id) else: self.link_user_to_local_idp(user_id, email_password=True) diff --git a/test/src/test_users.py b/test/src/test_users.py index 938df42b..cf600834 100644 --- a/test/src/test_users.py +++ b/test/src/test_users.py @@ -223,7 +223,6 @@ def mock_get_user(requests_mock, user_id, json, status_code, mock_manage_api_key ) # Second request: Mock the query-based request with personid - # This matches the actual implementation at line 258-275 in users.py # Always mock this for version >= 9.1, regardless of status_code manage_personid_mock = requests_mock.get( f"{MANAGE_API_URL}/maximo/api/os/masperuser/?lean=1&oslc.where=personid%3D%22{user_id}%22&oslc.select=personid%2Cdisplayname", @@ -2152,7 +2151,7 @@ def test_create_initial_user_for_saas( # Mock get_or_create_user to return appropriate response based on version # Note: user_id might be None at this point, it gets set to user_email later actual_user_id = user_id if user_id is not None else user_email - if mas_version == '9.1': + if Version(mas_version) >= Version('9.1'): # For 9.1, return tuple (resource_id, user_data) with member array containing href resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" user_utils.get_or_create_user = MagicMock(return_value=( @@ -2195,7 +2194,7 @@ def test_create_initial_user_for_saas( username = user_id # For version 9.1 PRIMARY users, pass groupreassign parameter - if mas_version == '9.1' and user_type == "PRIMARY": + if Version(mas_version) >= Version('9.1') and user_type == "PRIMARY": groupreassign = [{"groupname": "USERMANAGEMENT"}] user_utils.create_initial_user_for_saas(initial_users, user_type, groupreassign) else: @@ -2224,7 +2223,7 @@ def test_create_initial_user_for_saas( "givenName": user_given_name, "familyName": user_family_name } - else: # 9.1 + else: # >=9.1 if user_type == "PRIMARY": maxuser_def = { "userid": user_id, @@ -2268,7 +2267,7 @@ def test_create_initial_user_for_saas( user_utils.get_or_create_user.assert_called_once_with(expected_user_def) # Check link_user_to_local_idp call based on version - if mas_version == '9.1': + if Version(mas_version) >= Version('9.1'): resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True, manage_api_key=manage_api_key, resource_id=resource_id) else: @@ -2285,7 +2284,7 @@ def test_create_initial_user_for_saas( call(user_id, "iot", application_role), call(user_id, "facilities", facilities_role), ]) - else: # 9.1 + else: # >=9.1 user_utils.await_mas_application_availability.assert_not_called() user_utils.set_user_application_permission.assert_not_called() @@ -2302,7 +2301,7 @@ def test_create_initial_user_for_saas( # For version >= 9.1, API key is always created (needed for link_user_to_local_idp) # For version < 9.1, API key is only created if there are manage_security_groups - if mas_version == '9.1' or len(manage_security_groups) > 0: + if Version(mas_version) >= Version('9.1') or len(manage_security_groups) > 0: user_utils.create_or_get_manage_api_key_for_user.assert_called_once_with("MAXADMIN", temporary=True) else: user_utils.create_or_get_manage_api_key_for_user.assert_not_called() @@ -2315,10 +2314,10 @@ def test_create_initial_user_for_saas( list(map(lambda sg: call(user_id, sg, manage_api_key), manage_security_groups)) ) user_utils.set_user_group_reassignment_auth.assert_not_called() - else: # 9.1 + else: # >=9.1 user_utils.add_user_to_manage_group.assert_not_called() if user_type == "PRIMARY": - # For 9.1, both user_id and resource_id are passed + # For versions >= 9.1, both user_id and resource_id are passed actual_user_id = user_id if user_id is not None else user_email resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id" user_utils.set_user_group_reassignment_auth.assert_called_once_with(actual_user_id, resource_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key) From bf5edaa4ad92f250437ca03b30e15a7ca005c226 Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Mon, 30 Mar 2026 19:12:26 +0530 Subject: [PATCH 34/35] update --- bin/mas-devops-create-initial-users-for-saas | 4 ++-- src/mas/devops/users.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/bin/mas-devops-create-initial-users-for-saas b/bin/mas-devops-create-initial-users-for-saas index 312003e8..1a05fbf2 100644 --- a/bin/mas-devops-create-initial-users-for-saas +++ b/bin/mas-devops-create-initial-users-for-saas @@ -34,7 +34,7 @@ if __name__ == "__main__": parser.add_argument("--coreapi-port", required=False, default=443) parser.add_argument("--admin-dashboard-port", required=False, default=443) parser.add_argument("--manage-api-port", required=False, default=443) - parser.add_argument("--mas_version", required=False, default="9.1") + parser.add_argument("--mas-version", required=False, default="9.0") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--initial-users-yaml-file") @@ -68,7 +68,7 @@ if __name__ == "__main__": logger.info("--------------") logger.info(f"mas_instance_id: {mas_instance_id}") logger.info(f"mas_workspace_id: {mas_workspace_id}") - logger.info(msg=f"mas_version: {mas_version}") + logger.info(f"mas_version: {mas_version}") logger.info(f"initial_users_yaml_file: {initial_users_yaml_file}") logger.info(f"initial_users_secret_name: {initial_users_secret_name}") logger.info(f"log_level: {log_level}") diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index 770217d4..f6c69fa1 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -970,7 +970,6 @@ def create_or_get_manage_api_key_for_user(self, user_id, temporary=False): # otherwise, retrieve the apikey (either it already existed, or we just created it) apikey = self.get_manage_api_key_for_user(user_id) - self.logger.info(f"Retrieved Manage API Key for user {user_id}: {apikey}") if apikey is None: # either create call reported that apikey already exists, or we created the api key # so we expect the get call to find it @@ -1203,8 +1202,6 @@ def add_user_to_manage_group(self, user_id, group_name, manage_api_key): json=payload, verify=self.manage_internal_ca_pem_file_path, ) - self.logger.info(f"Response status code: {response.status_code}") - self.logger.info(f"Response text: {response.text}") if response.status_code == 204: return None @@ -1505,13 +1502,19 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): Exception: If required fields are missing or user creation fails. Note: + For version < 9.1, PRIMARY users get: - userAdmin permission + - PREMIUM application entitlement + - Workspace admin access + - ADMIN role for most apps, MANAGEUSER for Manage + - MAXADMIN security group membership + + For version >= 9.1, + PRIMARY users get: - apikeyAdmin permission (API Key Management) - idpAdmin permission (IDP Management) - - PREMIUM application entitlement - Regular workspace access (not workspace admin) - - ADMIN role for most apps, MANAGEUSER for Manage - USERMANAGEMENT security group membership - Group reassignment authorization for ALL security groups From e4d2411505fdefc4c8f535f0a69bf944edd8de6f Mon Sep 17 00:00:00 2001 From: Nivedithaa Mahendran Date: Mon, 30 Mar 2026 19:40:50 +0530 Subject: [PATCH 35/35] update --- src/mas/devops/users.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/mas/devops/users.py b/src/mas/devops/users.py index f6c69fa1..f8275da6 100644 --- a/src/mas/devops/users.py +++ b/src/mas/devops/users.py @@ -1662,11 +1662,13 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): resource_id, _ = self.get_or_create_user(user_def) # For version >= 9.1, we always need a Manage API key and resource_id to link user to local IDP - # For version < 9.1, we may need it later for manage_security_groups - if Version(self.mas_version) >= Version('9.1') or (len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids): + # For version < 9.1, link user to local IDP first, then create API key only if needed for manage_security_groups + maxadmin_manage_api_key = None + if Version(self.mas_version) >= Version('9.1'): maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) self.link_user_to_local_idp(user_id, email_password=True, manage_api_key=maxadmin_manage_api_key, resource_id=resource_id) else: + # For version < 9.1, link user to local IDP without manage_api_key and resource_id self.link_user_to_local_idp(user_id, email_password=True) self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin) @@ -1688,10 +1690,11 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None): if len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids: if Version(self.mas_version) < Version('9.1'): + maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True) for manage_security_group in manage_security_groups: self.add_user_to_manage_group(user_id, manage_security_group, maxadmin_manage_api_key) - if Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: - if resource_id: + elif Version(self.mas_version) >= Version('9.1') and user_type == "PRIMARY" and groupreassign is not None: + if resource_id and maxadmin_manage_api_key: self.set_user_group_reassignment_auth(user_id, resource_id, groupreassign, maxadmin_manage_api_key) else: self.logger.warning(f"Cannot set group reassignment auth: resource_id not found for user {user_id}")