[PR #8653/80f48cce backport][stable-9] Redfish: Added steps to allow a user to change their password when their account requires a password change (#8881)
Redfish: Added steps to allow a user to change their password when their 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 <michael.raineri@dell.com>
* Bug fix
Signed-off-by: Mike Raineri <michael.raineri@dell.com>
* Bug fix
Signed-off-by: Mike Raineri <michael.raineri@dell.com>
* Bug fixes with return data handling
Signed-off-by: Mike Raineri <michael.raineri@dell.com>
* Added changelog fragment
Signed-off-by: Mike Raineri <michael.raineri@dell.com>
* Update changelogs/fragments/8652-Redfish-Password-Change-Required.yml
Co-authored-by: Felix Fontein <felix@fontein.de>
---------
Signed-off-by: Mike Raineri <michael.raineri@dell.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 80f48cceb4
)
Co-authored-by: Mike Raineri <mraineri@gmail.com>
pull/8890/head
parent
5550ba1946
commit
e6edf9cdea
|
@ -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).
|
|
@ -165,11 +165,11 @@ class RedfishUtils(object):
|
||||||
if not allow_no_resp:
|
if not allow_no_resp:
|
||||||
raise
|
raise
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
msg = self._get_extended_message(e)
|
msg, data = self._get_extended_message(e)
|
||||||
return {'ret': False,
|
return {'ret': False,
|
||||||
'msg': "HTTP Error %s on GET request to '%s', extended message: '%s'"
|
'msg': "HTTP Error %s on GET request to '%s', extended message: '%s'"
|
||||||
% (e.code, uri, msg),
|
% (e.code, uri, msg),
|
||||||
'status': e.code}
|
'status': e.code, 'data': data}
|
||||||
except URLError as e:
|
except URLError as e:
|
||||||
return {'ret': False, 'msg': "URL Error on GET request to '%s': '%s'"
|
return {'ret': False, 'msg': "URL Error on GET request to '%s': '%s'"
|
||||||
% (uri, e.reason)}
|
% (uri, e.reason)}
|
||||||
|
@ -208,11 +208,11 @@ class RedfishUtils(object):
|
||||||
data = None
|
data = None
|
||||||
headers = {k.lower(): v for (k, v) in resp.info().items()}
|
headers = {k.lower(): v for (k, v) in resp.info().items()}
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
msg = self._get_extended_message(e)
|
msg, data = self._get_extended_message(e)
|
||||||
return {'ret': False,
|
return {'ret': False,
|
||||||
'msg': "HTTP Error %s on POST request to '%s', extended message: '%s'"
|
'msg': "HTTP Error %s on POST request to '%s', extended message: '%s'"
|
||||||
% (e.code, uri, msg),
|
% (e.code, uri, msg),
|
||||||
'status': e.code}
|
'status': e.code, 'data': data}
|
||||||
except URLError as e:
|
except URLError as e:
|
||||||
return {'ret': False, 'msg': "URL Error on POST request to '%s': '%s'"
|
return {'ret': False, 'msg': "URL Error on POST request to '%s': '%s'"
|
||||||
% (uri, e.reason)}
|
% (uri, e.reason)}
|
||||||
|
@ -256,11 +256,11 @@ class RedfishUtils(object):
|
||||||
follow_redirects='all',
|
follow_redirects='all',
|
||||||
use_proxy=True, timeout=self.timeout, ciphers=self.ciphers)
|
use_proxy=True, timeout=self.timeout, ciphers=self.ciphers)
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
msg = self._get_extended_message(e)
|
msg, data = self._get_extended_message(e)
|
||||||
return {'ret': False, 'changed': False,
|
return {'ret': False, 'changed': False,
|
||||||
'msg': "HTTP Error %s on PATCH request to '%s', extended message: '%s'"
|
'msg': "HTTP Error %s on PATCH request to '%s', extended message: '%s'"
|
||||||
% (e.code, uri, msg),
|
% (e.code, uri, msg),
|
||||||
'status': e.code}
|
'status': e.code, 'data': data}
|
||||||
except URLError as e:
|
except URLError as e:
|
||||||
return {'ret': False, 'changed': False,
|
return {'ret': False, 'changed': False,
|
||||||
'msg': "URL Error on PATCH request to '%s': '%s'" % (uri, e.reason)}
|
'msg': "URL Error on PATCH request to '%s': '%s'" % (uri, e.reason)}
|
||||||
|
@ -291,11 +291,11 @@ class RedfishUtils(object):
|
||||||
follow_redirects='all',
|
follow_redirects='all',
|
||||||
use_proxy=True, timeout=self.timeout, ciphers=self.ciphers)
|
use_proxy=True, timeout=self.timeout, ciphers=self.ciphers)
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
msg = self._get_extended_message(e)
|
msg, data = self._get_extended_message(e)
|
||||||
return {'ret': False,
|
return {'ret': False,
|
||||||
'msg': "HTTP Error %s on PUT request to '%s', extended message: '%s'"
|
'msg': "HTTP Error %s on PUT request to '%s', extended message: '%s'"
|
||||||
% (e.code, uri, msg),
|
% (e.code, uri, msg),
|
||||||
'status': e.code}
|
'status': e.code, 'data': data}
|
||||||
except URLError as e:
|
except URLError as e:
|
||||||
return {'ret': False, 'msg': "URL Error on PUT request to '%s': '%s'"
|
return {'ret': False, 'msg': "URL Error on PUT request to '%s': '%s'"
|
||||||
% (uri, e.reason)}
|
% (uri, e.reason)}
|
||||||
|
@ -317,11 +317,11 @@ class RedfishUtils(object):
|
||||||
follow_redirects='all',
|
follow_redirects='all',
|
||||||
use_proxy=True, timeout=self.timeout, ciphers=self.ciphers)
|
use_proxy=True, timeout=self.timeout, ciphers=self.ciphers)
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
msg = self._get_extended_message(e)
|
msg, data = self._get_extended_message(e)
|
||||||
return {'ret': False,
|
return {'ret': False,
|
||||||
'msg': "HTTP Error %s on DELETE request to '%s', extended message: '%s'"
|
'msg': "HTTP Error %s on DELETE request to '%s', extended message: '%s'"
|
||||||
% (e.code, uri, msg),
|
% (e.code, uri, msg),
|
||||||
'status': e.code}
|
'status': e.code, 'data': data}
|
||||||
except URLError as e:
|
except URLError as e:
|
||||||
return {'ret': False, 'msg': "URL Error on DELETE request to '%s': '%s'"
|
return {'ret': False, 'msg': "URL Error on DELETE request to '%s': '%s'"
|
||||||
% (uri, e.reason)}
|
% (uri, e.reason)}
|
||||||
|
@ -391,8 +391,10 @@ class RedfishUtils(object):
|
||||||
:param error: an HTTPError exception
|
:param error: an HTTPError exception
|
||||||
:type error: HTTPError
|
:type error: HTTPError
|
||||||
:return: the ExtendedInfo message if present, else standard HTTP error
|
: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, '')
|
msg = http_client.responses.get(error.code, '')
|
||||||
|
data = None
|
||||||
if error.code >= 400:
|
if error.code >= 400:
|
||||||
try:
|
try:
|
||||||
body = error.read().decode('utf-8')
|
body = error.read().decode('utf-8')
|
||||||
|
@ -406,7 +408,7 @@ class RedfishUtils(object):
|
||||||
msg = str(data['error']['@Message.ExtendedInfo'])
|
msg = str(data['error']['@Message.ExtendedInfo'])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return msg
|
return msg, data
|
||||||
|
|
||||||
def _init_session(self):
|
def _init_session(self):
|
||||||
pass
|
pass
|
||||||
|
@ -1245,32 +1247,49 @@ class RedfishUtils(object):
|
||||||
return response
|
return response
|
||||||
return {'ret': True, 'changed': True}
|
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)):
|
if not any((username, acct_id)):
|
||||||
return {'ret': False, 'msg':
|
return {'ret': False, 'msg':
|
||||||
'Must provide either account_id or account_username'}
|
'Must provide either account_id or account_username'}
|
||||||
|
|
||||||
response = self.get_request(self.root_uri + self.accounts_uri)
|
if password_change_uri:
|
||||||
if response['ret'] is False:
|
# Password change required; go directly to the specified URI
|
||||||
return response
|
response = self.get_request(self.root_uri + password_change_uri)
|
||||||
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:
|
if response['ret'] is False:
|
||||||
continue
|
return response
|
||||||
data = response['data']
|
data = response['data']
|
||||||
headers = response['headers']
|
headers = response['headers']
|
||||||
if username:
|
if username:
|
||||||
if username == data.get('UserName'):
|
if username == data.get('UserName'):
|
||||||
return {'ret': True, 'data': data,
|
return {'ret': True, 'data': data,
|
||||||
'headers': headers, 'uri': uri}
|
'headers': headers, 'uri': password_change_uri}
|
||||||
if acct_id:
|
if acct_id:
|
||||||
if acct_id == data.get('Id'):
|
if acct_id == data.get('Id'):
|
||||||
return {'ret': True, 'data': data,
|
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':
|
return {'ret': False, 'no_match': True, 'msg':
|
||||||
'No account with the given account_id or account_username found'}
|
'No account with the given account_id or account_username found'}
|
||||||
|
@ -1491,7 +1510,8 @@ class RedfishUtils(object):
|
||||||
'Must provide account_password for UpdateUserPassword command'}
|
'Must provide account_password for UpdateUserPassword command'}
|
||||||
|
|
||||||
response = self._find_account_uri(username=user.get('account_username'),
|
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']:
|
if not response['ret']:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -1534,6 +1554,31 @@ class RedfishUtils(object):
|
||||||
resp['msg'] = 'Modified account service'
|
resp['msg'] = 'Modified account service'
|
||||||
return resp
|
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):
|
def get_sessions(self):
|
||||||
result = {}
|
result = {}
|
||||||
# listing all users has always been slower than other operations, why?
|
# listing all users has always been slower than other operations, why?
|
||||||
|
|
|
@ -911,6 +911,7 @@ def main():
|
||||||
'account_oemaccounttypes': module.params['oem_account_types'],
|
'account_oemaccounttypes': module.params['oem_account_types'],
|
||||||
'account_updatename': module.params['update_username'],
|
'account_updatename': module.params['update_username'],
|
||||||
'account_properties': module.params['account_properties'],
|
'account_properties': module.params['account_properties'],
|
||||||
|
'account_passwordchangerequired': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# timeout
|
# timeout
|
||||||
|
@ -983,10 +984,16 @@ def main():
|
||||||
# execute only if we find an Account service resource
|
# execute only if we find an Account service resource
|
||||||
result = rf_utils._find_accountservice_resource()
|
result = rf_utils._find_accountservice_resource()
|
||||||
if result['ret'] is False:
|
if result['ret'] is False:
|
||||||
module.fail_json(msg=to_native(result['msg']))
|
# If a password change is required and the user is attempting to
|
||||||
|
# modify their password, try to proceed.
|
||||||
for command in command_list:
|
user['account_passwordchangerequired'] = rf_utils.check_password_change_required(result)
|
||||||
result = ACCOUNTS_COMMANDS[command](user)
|
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":
|
elif category == "Systems":
|
||||||
# execute only if we find a System resource
|
# execute only if we find a System resource
|
||||||
|
|
Loading…
Reference in New Issue