Skip to content

Commit 12f1fb6

Browse files
author
Nivedithaa Mahendran
committed
update
1 parent ef0ce3a commit 12f1fb6

2 files changed

Lines changed: 197 additions & 48 deletions

File tree

src/mas/devops/users.py

Lines changed: 119 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -539,18 +539,25 @@ def update_user_display_name(self, user_id, display_name):
539539

540540
raise Exception(f"{response.status_code} {response.text}")
541541

542-
def link_user_to_local_idp(self, user_id, email_password=False):
542+
def link_user_to_local_idp(self, user_id, email_password=False, manage_api_key=None, resource_id=None):
543543
"""
544544
Link a user to the local identity provider (IDP).
545545
546546
This method is idempotent - if the user already has a local identity, no action is taken.
547547
The method creates a local authentication identity for the user, enabling them to log in
548548
with username/password.
549549
550+
For MAS version < 9.1: Uses Core API PUT request
551+
For MAS version >= 9.1: Uses Manage API PATCH request
552+
550553
Args:
551554
user_id (str): The unique identifier of the user to link.
552555
email_password (bool, optional): Whether to enable email/password authentication.
553556
Defaults to False.
557+
manage_api_key (dict, optional): API key record with 'apikey' field for authentication.
558+
Required for MAS version >= 9.1.
559+
resource_id (str, optional): The resource identifier of the user (extracted from href).
560+
Required for MAS version >= 9.1.
554561
555562
Returns:
556563
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):
563570
or returned for security reasons.
564571
"""
565572

566-
# For the sake of idempotency, check if the user already has a local identity
567-
resource_id, user = self.get_user(user_id)
568-
if user is None:
569-
raise Exception(f"User {user_id} was not found")
573+
# Check MAS version to determine which API to use
574+
current_version = Version(self.mas_version)
575+
version_9_1 = Version("9.1")
570576

571-
if "identities" in user and "_local" in user["identities"]:
572-
self.logger.info(f"User {user_id} already has a local identity")
573-
return None
577+
if current_version >= version_9_1:
578+
# Version >= 9.1: Use Manage API PATCH request
579+
if manage_api_key is None:
580+
raise Exception("manage_api_key is required for MAS version >= 9.1")
581+
if resource_id is None:
582+
raise Exception("resource_id is required for MAS version >= 9.1")
574583

575-
self.logger.info(f"Linking user {user_id} to local IDP (email_password: {email_password})")
576-
url = f"{self.mas_api_url_internal}/v3/users/{user_id}/idps/local"
577-
querystring = {
578-
"emailPassword": email_password
579-
}
580-
payload = {
581-
"idpUserId": user_id,
582-
}
583-
headers = {
584-
"Content-Type": "application/json",
585-
"x-access-token": self.superuser_auth_token
586-
}
587-
response = requests.put(
588-
url,
589-
json=payload,
590-
headers=headers,
591-
params=querystring,
592-
verify=self.core_internal_ca_pem_file_path
593-
)
594-
if response.status_code != 200:
595-
raise Exception(response.text)
584+
# For the sake of idempotency, check if the user already has a local identity
585+
_, user = self.get_user(user_id)
586+
if user is None:
587+
raise Exception(f"User {user_id} was not found")
596588

597-
# Important: HTTP 200 output will contain generated user token; DO NOT LOG
589+
if "identities" in user and "_local" in user["identities"]:
590+
self.logger.info(f"User {user_id} already has a local identity")
591+
return None
598592

599-
return None
593+
self.logger.info(f"Linking user {user_id} to local IDP using Manage API (version {self.mas_version})")
594+
595+
url = f"{self.manage_api_url_internal}/maximo/api/os/masperuser/{resource_id}"
596+
querystring = {
597+
"lean": 1,
598+
"ccm": 1
599+
}
600+
headers = {
601+
"Content-Type": "application/json",
602+
"apikey": manage_api_key["apikey"],
603+
"x-method-override": "PATCH",
604+
"patchtype": "MERGE"
605+
}
606+
607+
payload = {
608+
"maxuser": {
609+
"userid": user_id,
610+
"masuseridp": [
611+
{
612+
"emailpassword": email_password,
613+
"idpid": "local",
614+
"logintype": "0",
615+
"idploginid": user_id,
616+
"idptype": "local",
617+
"enabled": True
618+
}
619+
]
620+
}
621+
}
622+
self.logger.info(f"Sending PATCH request to {url} with payload: {payload}")
623+
624+
response = requests.post(
625+
url,
626+
json=payload,
627+
headers=headers,
628+
params=querystring,
629+
cert=self.manage_internal_client_pem_file_path,
630+
verify=self.manage_internal_ca_pem_file_path
631+
)
632+
self.logger.info(f"Response status code: {response.status_code}")
633+
self.logger.info(f"Response text: {response.text}")
634+
635+
if response.status_code in [200, 204]:
636+
self.logger.info(f"Successfully linked user {user_id} to local IDP")
637+
return None
638+
639+
raise Exception(f"Failed to link user to local IDP: {response.status_code} {response.text}")
640+
641+
else:
642+
# Version < 9.1: Use Core API PUT request (existing implementation)
643+
# For the sake of idempotency, check if the user already has a local identity
644+
_, user = self.get_user(user_id)
645+
if user is None:
646+
raise Exception(f"User {user_id} was not found")
647+
648+
if "identities" in user and "_local" in user["identities"]:
649+
self.logger.info(f"User {user_id} already has a local identity")
650+
return None
651+
652+
self.logger.info(f"Linking user {user_id} to local IDP using Core API (version {self.mas_version}, email_password: {email_password})")
653+
url = f"{self.mas_api_url_internal}/v3/users/{user_id}/idps/local"
654+
querystring = {
655+
"emailPassword": email_password
656+
}
657+
payload = {
658+
"idpUserId": user_id,
659+
}
660+
headers = {
661+
"Content-Type": "application/json",
662+
"x-access-token": self.superuser_auth_token
663+
}
664+
response = requests.put(
665+
url,
666+
json=payload,
667+
headers=headers,
668+
params=querystring,
669+
verify=self.core_internal_ca_pem_file_path
670+
)
671+
if response.status_code != 200:
672+
raise Exception(response.text)
673+
674+
# Important: HTTP 200 output will contain generated user token; DO NOT LOG
675+
676+
return None
600677

601678
def get_user_workspaces(self, user_id):
602679
"""
@@ -1690,7 +1767,17 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None):
16901767
self.logger.info(f"Resource ID - {resource_id}")
16911768
self.logger.info(f"User info - {user_info}")
16921769

1693-
self.link_user_to_local_idp(user_id, email_password=True)
1770+
# For version >= 9.1, we always need a Manage API key and resource_id to link user to local IDP
1771+
# For version < 9.1, we may need it later for manage_security_groups
1772+
if Version(self.mas_version) >= Version('9.1') or (len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids):
1773+
maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True)
1774+
self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}")
1775+
1776+
if Version(self.mas_version) >= Version('9.1'):
1777+
self.link_user_to_local_idp(user_id, email_password=True, manage_api_key=maxadmin_manage_api_key, resource_id=resource_id)
1778+
else:
1779+
self.link_user_to_local_idp(user_id, email_password=True)
1780+
16941781
self.add_user_to_workspace(user_id, is_workspace_admin=is_workspace_admin)
16951782

16961783
if Version(self.mas_version) < Version('9.1'):
@@ -1709,8 +1796,6 @@ def create_initial_user_for_saas(self, user, user_type, groupreassign=None):
17091796
self.check_user_sync(user_id, mas_application_id)
17101797

17111798
if len(manage_security_groups) > 0 and "manage" in self.mas_workspace_application_ids:
1712-
maxadmin_manage_api_key = self.create_or_get_manage_api_key_for_user(MASUserUtils.MAXADMIN, temporary=True)
1713-
self.logger.info(f"Maxadmin manage api key - {maxadmin_manage_api_key}")
17141799
if Version(self.mas_version) < Version('9.1'):
17151800
for manage_security_group in manage_security_groups:
17161801
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):
17191804
self.set_user_group_reassignment_auth(user_id, resource_id, groupreassign, maxadmin_manage_api_key)
17201805
else:
17211806
self.logger.warning(f"Cannot set group reassignment auth: resource_id not found for user {user_id}")
1722-
1723-
# # Grant authorization to reassign users to/from ALL security groups (PRIMARY users only)
1724-
# if user_type == "PRIMARY":
1725-
# self.grant_all_group_reassignment_auth(user_id, maxadmin_manage_api_key)

test/src/test_users.py

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -629,8 +629,10 @@ def test_update_user_display_name_error(user_utils, requests_mock):
629629
def test_link_user_to_local_idp(user_utils, requests_mock, mock_manage_api_key):
630630
user_id = "user1"
631631
email_password = True
632+
resource_id = f"{user_id}_resource_id"
632633
get_core, get_manage, get_manage_personid = mock_get_user_200(requests_mock, user_id, mock_manage_api_key)
633634

635+
# Mock Core API PUT request for version < 9.1
634636
put = requests_mock.put(
635637
f"{MAS_API_URL}/v3/users/{user_id}/idps/local?emailPassword={email_password}",
636638
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):
639641
additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id})
640642
)
641643

642-
user_utils.link_user_to_local_idp(user_id, email_password=email_password)
644+
# Mock Manage API PATCH request for version >= 9.1
645+
patch = requests_mock.post(
646+
f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}?lean=1&ccm=1",
647+
request_headers={
648+
"Content-Type": "application/json",
649+
"apikey": mock_manage_api_key["apikey"],
650+
"x-method-override": "PATCH",
651+
"patchtype": "MERGE"
652+
},
653+
json={"id": user_id},
654+
status_code=200,
655+
additional_matcher=lambda req: additional_matcher(
656+
req,
657+
json={
658+
"maxuser": {
659+
"userid": user_id,
660+
"masuseridp": [{
661+
"emailpassword": True,
662+
"idpid": "local",
663+
"logintype": "0",
664+
"idploginid": user_id,
665+
"idptype": "local",
666+
"enabled": True
667+
}]
668+
}
669+
},
670+
cert=PEM_PATH
671+
)
672+
)
673+
674+
# Call the function with appropriate parameters based on version
675+
if Version(user_utils.mas_version) >= Version('9.1'):
676+
user_utils.link_user_to_local_idp(user_id, email_password=email_password, manage_api_key=mock_manage_api_key, resource_id=resource_id)
677+
else:
678+
user_utils.link_user_to_local_idp(user_id, email_password=email_password)
643679

644680
# Check that the correct endpoint was called based on version
645681
if Version(user_utils.mas_version) >= Version('9.1'):
646682
assert get_core.call_count == 0
647683
assert get_manage.call_count == 1
684+
assert put.call_count == 0
685+
assert patch.call_count == 1
648686
else:
649687
assert get_core.call_count == 1
650688
assert get_manage.call_count == 0
651-
assert put.call_count == 1
689+
assert put.call_count == 1
690+
assert patch.call_count == 0
652691

653692

654693
def test_link_user_to_local_idp_usernotfound(user_utils, requests_mock, mock_manage_api_key):
655694
user_id = "user1"
695+
resource_id = f"{user_id}_resource_id"
656696
get_core, get_manage, get_manage_personid = mock_get_user_404(requests_mock, user_id, mock_manage_api_key)
697+
657698
put = requests_mock.put(
658699
f"{MAS_API_URL}/v3/users/{user_id}/idps/local",
659700
additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id})
660701
)
661702

703+
patch = requests_mock.post(
704+
f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}?lean=1&ccm=1",
705+
additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH)
706+
)
707+
662708
with pytest.raises(Exception):
663-
user_utils.link_user_to_local_idp(user_id)
709+
if Version(user_utils.mas_version) >= Version('9.1'):
710+
user_utils.link_user_to_local_idp(user_id, manage_api_key=mock_manage_api_key, resource_id=resource_id)
711+
else:
712+
user_utils.link_user_to_local_idp(user_id)
664713

665714
# Check that the correct endpoint was called based on version
666715
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
670719
assert get_core.call_count == 1
671720
assert get_manage.call_count == 0
672721
assert put.call_count == 0
722+
assert patch.call_count == 0
673723

674724

675725
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
700750
additional_matcher=lambda req: additional_matcher(req, json={"idpUserId": user_id})
701751
)
702752

703-
user_utils.link_user_to_local_idp(user_id, email_password=email_password)
753+
patch = requests_mock.post(
754+
f"{MANAGE_API_URL}/maximo/api/os/masperuser/{resource_id}?lean=1&ccm=1",
755+
additional_matcher=lambda req: additional_matcher(req, cert=PEM_PATH)
756+
)
757+
758+
# Call the function with appropriate parameters based on version
759+
if Version(user_utils.mas_version) >= Version('9.1'):
760+
user_utils.link_user_to_local_idp(user_id, email_password=email_password, manage_api_key=mock_manage_api_key, resource_id=resource_id)
761+
else:
762+
user_utils.link_user_to_local_idp(user_id, email_password=email_password)
704763

705764
# Check that the correct endpoint was called based on version
706765
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
710769
assert get_core.call_count == 1
711770
assert get_manage.call_count == 0
712771
assert put.call_count == 0
772+
assert patch.call_count == 0
713773

714774

715775
def test_get_user_workspaces(user_utils, requests_mock):
@@ -2206,7 +2266,14 @@ def test_create_initial_user_for_saas(
22062266
}
22072267

22082268
user_utils.get_or_create_user.assert_called_once_with(expected_user_def)
2209-
user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True)
2269+
2270+
# Check link_user_to_local_idp call based on version
2271+
if mas_version == '9.1':
2272+
resource_id = f"_{actual_user_id.replace('@', '_').replace('.', '_')}_resource_id"
2273+
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)
2274+
else:
2275+
user_utils.link_user_to_local_idp.assert_called_once_with(user_id, email_password=True)
2276+
22102277
user_utils.add_user_to_workspace.assert_called_once_with(user_id, is_workspace_admin=is_workspace_admin)
22112278

22122279
# 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(
22332300
else: # 9.1
22342301
user_utils.check_user_sync.assert_not_called()
22352302

2236-
if len(manage_security_groups) > 0:
2303+
# For version >= 9.1, API key is always created (needed for link_user_to_local_idp)
2304+
# For version < 9.1, API key is only created if there are manage_security_groups
2305+
if mas_version == '9.1' or len(manage_security_groups) > 0:
22372306
user_utils.create_or_get_manage_api_key_for_user.assert_called_once_with("MAXADMIN", temporary=True)
2307+
else:
2308+
user_utils.create_or_get_manage_api_key_for_user.assert_not_called()
22382309

2310+
if len(manage_security_groups) > 0:
22392311
# For version < 9.1, add_user_to_manage_group is called
22402312
# For version >= 9.1, set_user_group_reassignment_auth is called for PRIMARY users
22412313
if mas_version == '9.0':
@@ -2252,10 +2324,6 @@ def test_create_initial_user_for_saas(
22522324
user_utils.set_user_group_reassignment_auth.assert_called_once_with(actual_user_id, resource_id, [{"groupname": "USERMANAGEMENT"}], manage_api_key)
22532325
else:
22542326
user_utils.set_user_group_reassignment_auth.assert_not_called()
2255-
else:
2256-
user_utils.create_or_get_manage_api_key_for_user.assert_not_called()
2257-
user_utils.add_user_to_manage_group.assert_not_called()
2258-
user_utils.set_user_group_reassignment_auth.assert_not_called()
22592327

22602328

22612329
def test_create_initial_users_for_saas_invalid_inputs(user_utils):

0 commit comments

Comments
 (0)