Skip to content

Commit

Permalink
Redfish: Added steps to allow a user to change their password when th…
Browse files Browse the repository at this point in the history
…eir account requires a password change (#8653)

* Redfish: Added steps to allow a user to change their password when their account requires a password change

Signed-off-by: Mike Raineri <[email protected]>

* Bug fix

Signed-off-by: Mike Raineri <[email protected]>

* Bug fix

Signed-off-by: Mike Raineri <[email protected]>

* Bug fixes with return data handling

Signed-off-by: Mike Raineri <[email protected]>

* Added changelog fragment

Signed-off-by: Mike Raineri <[email protected]>

* Update changelogs/fragments/8652-Redfish-Password-Change-Required.yml

Co-authored-by: Felix Fontein <[email protected]>

---------

Signed-off-by: Mike Raineri <[email protected]>
Co-authored-by: Felix Fontein <[email protected]>
  • Loading branch information
mraineri and felixfontein authored Sep 18, 2024
1 parent f93883a commit 80f48cc
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -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).
95 changes: 70 additions & 25 deletions plugins/module_utils/redfish_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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)}
Expand Down Expand Up @@ -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)}
Expand Down Expand Up @@ -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)}
Expand All @@ -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)}
Expand Down Expand Up @@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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'}
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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?
Expand Down
15 changes: 11 additions & 4 deletions plugins/modules/redfish_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 80f48cc

Please sign in to comment.