diff --git a/changelogs/fragments/8652-Redfish-Password-Change-Required.yml b/changelogs/fragments/8652-Redfish-Password-Change-Required.yml new file mode 100644 index 00000000000..44cfd41430e --- /dev/null +++ b/changelogs/fragments/8652-Redfish-Password-Change-Required.yml @@ -0,0 +1,2 @@ +minor_changes: + - redfish_command - add handling of the ``PasswordChangeRequired`` message from services in the ``UpdateUserPassword`` command to directly modify the user's password if the requested user is the one invoking the operation (https://github.com/ansible-collections/community.general/issues/8652, https://github.com/ansible-collections/community.general/pull/8653). diff --git a/plugins/module_utils/redfish_utils.py b/plugins/module_utils/redfish_utils.py index b7fdeb3a526..c1efd00b704 100644 --- a/plugins/module_utils/redfish_utils.py +++ b/plugins/module_utils/redfish_utils.py @@ -165,11 +165,11 @@ def get_request(self, uri, override_headers=None, allow_no_resp=False, timeout=N if not allow_no_resp: raise except HTTPError as e: - msg = self._get_extended_message(e) + msg, data = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on GET request to '%s', extended message: '%s'" % (e.code, uri, msg), - 'status': e.code} + 'status': e.code, 'data': data} except URLError as e: return {'ret': False, 'msg': "URL Error on GET request to '%s': '%s'" % (uri, e.reason)} @@ -208,11 +208,11 @@ def post_request(self, uri, pyld, multipart=False): data = None headers = {k.lower(): v for (k, v) in resp.info().items()} except HTTPError as e: - msg = self._get_extended_message(e) + msg, data = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on POST request to '%s', extended message: '%s'" % (e.code, uri, msg), - 'status': e.code} + 'status': e.code, 'data': data} except URLError as e: return {'ret': False, 'msg': "URL Error on POST request to '%s': '%s'" % (uri, e.reason)} @@ -256,11 +256,11 @@ def patch_request(self, uri, pyld, check_pyld=False): follow_redirects='all', use_proxy=True, timeout=self.timeout, ciphers=self.ciphers) except HTTPError as e: - msg = self._get_extended_message(e) + msg, data = self._get_extended_message(e) return {'ret': False, 'changed': False, 'msg': "HTTP Error %s on PATCH request to '%s', extended message: '%s'" % (e.code, uri, msg), - 'status': e.code} + 'status': e.code, 'data': data} except URLError as e: return {'ret': False, 'changed': False, 'msg': "URL Error on PATCH request to '%s': '%s'" % (uri, e.reason)} @@ -291,11 +291,11 @@ def put_request(self, uri, pyld): follow_redirects='all', use_proxy=True, timeout=self.timeout, ciphers=self.ciphers) except HTTPError as e: - msg = self._get_extended_message(e) + msg, data = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on PUT request to '%s', extended message: '%s'" % (e.code, uri, msg), - 'status': e.code} + 'status': e.code, 'data': data} except URLError as e: return {'ret': False, 'msg': "URL Error on PUT request to '%s': '%s'" % (uri, e.reason)} @@ -317,11 +317,11 @@ def delete_request(self, uri, pyld=None): follow_redirects='all', use_proxy=True, timeout=self.timeout, ciphers=self.ciphers) except HTTPError as e: - msg = self._get_extended_message(e) + msg, data = self._get_extended_message(e) return {'ret': False, 'msg': "HTTP Error %s on DELETE request to '%s', extended message: '%s'" % (e.code, uri, msg), - 'status': e.code} + 'status': e.code, 'data': data} except URLError as e: return {'ret': False, 'msg': "URL Error on DELETE request to '%s': '%s'" % (uri, e.reason)} @@ -391,8 +391,10 @@ def _get_extended_message(error): :param error: an HTTPError exception :type error: HTTPError :return: the ExtendedInfo message if present, else standard HTTP error + :return: the JSON data of the response if present """ msg = http_client.responses.get(error.code, '') + data = None if error.code >= 400: try: body = error.read().decode('utf-8') @@ -406,7 +408,7 @@ def _get_extended_message(error): msg = str(data['error']['@Message.ExtendedInfo']) except Exception: pass - return msg + return msg, data def _init_session(self): pass @@ -1245,32 +1247,49 @@ def reset_to_defaults(self, command, resource_uri, action_name): return response return {'ret': True, 'changed': True} - def _find_account_uri(self, username=None, acct_id=None): + def _find_account_uri(self, username=None, acct_id=None, password_change_uri=None): if not any((username, acct_id)): return {'ret': False, 'msg': 'Must provide either account_id or account_username'} - response = self.get_request(self.root_uri + self.accounts_uri) - if response['ret'] is False: - return response - data = response['data'] - - uris = [a.get('@odata.id') for a in data.get('Members', []) if - a.get('@odata.id')] - for uri in uris: - response = self.get_request(self.root_uri + uri) + if password_change_uri: + # Password change required; go directly to the specified URI + response = self.get_request(self.root_uri + password_change_uri) if response['ret'] is False: - continue + return response data = response['data'] headers = response['headers'] if username: if username == data.get('UserName'): return {'ret': True, 'data': data, - 'headers': headers, 'uri': uri} + 'headers': headers, 'uri': password_change_uri} if acct_id: if acct_id == data.get('Id'): return {'ret': True, 'data': data, - 'headers': headers, 'uri': uri} + 'headers': headers, 'uri': password_change_uri} + else: + # Walk the accounts collection to find the desired user + response = self.get_request(self.root_uri + self.accounts_uri) + if response['ret'] is False: + return response + data = response['data'] + + uris = [a.get('@odata.id') for a in data.get('Members', []) if + a.get('@odata.id')] + for uri in uris: + response = self.get_request(self.root_uri + uri) + if response['ret'] is False: + continue + data = response['data'] + headers = response['headers'] + if username: + if username == data.get('UserName'): + return {'ret': True, 'data': data, + 'headers': headers, 'uri': uri} + if acct_id: + if acct_id == data.get('Id'): + return {'ret': True, 'data': data, + 'headers': headers, 'uri': uri} return {'ret': False, 'no_match': True, 'msg': 'No account with the given account_id or account_username found'} @@ -1491,7 +1510,8 @@ def update_user_password(self, user): 'Must provide account_password for UpdateUserPassword command'} response = self._find_account_uri(username=user.get('account_username'), - acct_id=user.get('account_id')) + acct_id=user.get('account_id'), + password_change_uri=user.get('account_passwordchangerequired')) if not response['ret']: return response @@ -1534,6 +1554,31 @@ def update_accountservice_properties(self, user): resp['msg'] = 'Modified account service' return resp + def check_password_change_required(self, return_data): + """ + Checks a response if a user needs to change their password + + :param return_data: The return data for a failed request + :return: None or the URI of the account to update + """ + uri = None + if 'data' in return_data: + # Find the extended messages in the response payload + extended_messages = return_data['data'].get('error', {}).get('@Message.ExtendedInfo', []) + if len(extended_messages) == 0: + extended_messages = return_data['data'].get('@Message.ExtendedInfo', []) + # Go through each message and look for Base.1.X.PasswordChangeRequired + for message in extended_messages: + message_id = message.get('MessageId') + if message_id is None: + # While this is invalid, treat the lack of a MessageId as "no message" + continue + if message_id.startswith('Base.1.') and message_id.endswith('.PasswordChangeRequired'): + # Password change required; get the URI of the user account + uri = message['MessageArgs'][0] + break + return uri + def get_sessions(self): result = {} # listing all users has always been slower than other operations, why? diff --git a/plugins/modules/redfish_command.py b/plugins/modules/redfish_command.py index f9b0c8bd3b9..df541a1bd3c 100644 --- a/plugins/modules/redfish_command.py +++ b/plugins/modules/redfish_command.py @@ -911,6 +911,7 @@ def main(): 'account_oemaccounttypes': module.params['oem_account_types'], 'account_updatename': module.params['update_username'], 'account_properties': module.params['account_properties'], + 'account_passwordchangerequired': None, } # timeout @@ -983,10 +984,16 @@ def main(): # execute only if we find an Account service resource result = rf_utils._find_accountservice_resource() if result['ret'] is False: - module.fail_json(msg=to_native(result['msg'])) - - for command in command_list: - result = ACCOUNTS_COMMANDS[command](user) + # If a password change is required and the user is attempting to + # modify their password, try to proceed. + user['account_passwordchangerequired'] = rf_utils.check_password_change_required(result) + if len(command_list) == 1 and command_list[0] == "UpdateUserPassword" and user['account_passwordchangerequired']: + result = rf_utils.update_user_password(user) + else: + module.fail_json(msg=to_native(result['msg'])) + else: + for command in command_list: + result = ACCOUNTS_COMMANDS[command](user) elif category == "Systems": # execute only if we find a System resource