Fix session authentication issues
Session authentication issues can occur when the client has been initialized and some intermediate failure has occurred such as a transport failure to the remote BMC or even with setting changes on the remote BMC. Previously, these sorts of issues could result in cases where the client does not properly respond. Either by falling back to "basic" authentication, or getting stuck in a loop where the client was unable to re-negotiate a new session with the BMC. In the event of the BMC no longer being able to support Session authentication, after a session initialization, the client will raise an AccessError exception to signify to the consumer that something has occurred with credentials preventing further client use without potentially re-initializing the client. Should the re-authentication fail, the exception will be directly raised such that the client can no longer fall into a recursive loop. Should the state in the client be unable to be determined, and an AccessError is raised, the client will now attempt to re-authenticate. Ultimately moves from using the SessionService to using a new base class method ``create_session``. The SessionService class ``create_session`` method is still usable as well, however new authentication attempts will not utilize it in order to bypass potential failure cases. Functionally, this change requires change I9eb238d1bbaf522d03ee3fce12fc0ab80c1b69b4 to be present as it removes X-Auth-Token headers which can lead to spurious authentication failures when sessions expire. Prior to any authentication action, this patch does *also* do the needful to prevent a poisoned client which would have potentially resulted in an AccessError previously. In backporting, also minor conflict with https://review.opendev.org/c/openstack/sushy/+/799504 resulted in a minor change, specifically around the argument being passed which was added in that changeset. Additional minor changes around ConnectionError exceptions which previously did not include the ability to pass a message back as part of the error. Change-Id: Id9a32ebcc04f4f3fecb4e10cf157a97322707c94 Story: 2009719 Task: 44106 (cherry picked from commit8d55d51284
) (cherry picked from commit38e62dd16a
) (cherry picked from commit078707439a
) (cherry picked from commit8427417e20
)
This commit is contained in:
parent
ef9bc98f08
commit
1acff06080
|
@ -248,9 +248,9 @@ Creating and using a sushy manager object
|
||||||
virtmedia_inst.eject_media()
|
virtmedia_inst.eject_media()
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------------------
|
-----------------------------------------------
|
||||||
Creating and using a sushy session service object
|
Creating and using a sushy client with Sessions
|
||||||
-------------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -266,33 +266,27 @@ Creating and using a sushy session service object
|
||||||
s = sushy.Sushy('http://localhost:8000/redfish/v1',
|
s = sushy.Sushy('http://localhost:8000/redfish/v1',
|
||||||
username='foo', password='bar')
|
username='foo', password='bar')
|
||||||
|
|
||||||
# Instantiate a SessionService object
|
# Get the ComputerSystem object (if there is only one), otherwise
|
||||||
sess_serv = s.get_session_service()
|
# the identity must be provided as a path to the system.
|
||||||
|
system = s.get_system()
|
||||||
|
|
||||||
# Get SessionCollection
|
# A session is created automatically for you.
|
||||||
sess_col = sess_serv.sessions
|
# Print the boot field in the ComputerSystem.
|
||||||
|
print(system.boot)
|
||||||
|
|
||||||
# Print the ID of the sessions available in the collection
|
# Upon session timeout, Sushy recreates the session based upon
|
||||||
print(sess_col.members_identities)
|
# provided credentials. If this fails, an exception is raised.
|
||||||
|
|
||||||
# Get a list of systems objects available in the collection
|
# Explicitly request a session_key and session_uri.
|
||||||
sess_col_insts = sess_col.get_members()
|
# This is not stored, but may be useful.
|
||||||
|
session_key, session_uri = s.create_session(username='foo',
|
||||||
|
password='bar')
|
||||||
|
|
||||||
# Instantiate a session object, same as getting it directly
|
# Retrieve the session
|
||||||
sess_inst = sess_col.get_member(sess_col.members_identities[0])
|
session = s.get_session(session_uri)
|
||||||
# Getting it directly
|
|
||||||
sess_inst = s.get_session(sess_col.members_identities[0])
|
|
||||||
|
|
||||||
# Delete the session
|
# Delete the session
|
||||||
sess_inst.delete()
|
session.delete()
|
||||||
|
|
||||||
# Create a new session
|
|
||||||
session_key, session_id = sess_serv.create_session(
|
|
||||||
username='foo', password='bar')
|
|
||||||
|
|
||||||
# Delete a session
|
|
||||||
sess_serv.close_session(sess_col.members_identities[0])
|
|
||||||
|
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
Using OEM extensions
|
Using OEM extensions
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Fixes issues with the refresh of ``Session`` based authentication where
|
||||||
|
a previous refresh attempt failing could result in a fallback
|
||||||
|
to ``Basic`` authentication and would silently fail. The client library
|
||||||
|
now attempts to re-authenticate.
|
||||||
|
- |
|
||||||
|
Fixes silent failures when a refresh of an authentication ``Session``
|
||||||
|
fails and was unable to be re-established due to an ``AccessError``.
|
||||||
|
Should this occur, now the ``AccessError`` exception is explicitly raised
|
||||||
|
as opposed to attempting to fall back to ``Basic`` authentication.
|
||||||
|
- |
|
||||||
|
Fixes issues where the ``Session`` and ``Basic`` auth interface would
|
||||||
|
fallback to ``Basic`` authentication should a ``ConnectionError``
|
||||||
|
exception occur while attempting to perform an authentication action.
|
||||||
|
``ConnectionError`` exceptions are signs of networking transport issues,
|
||||||
|
and should be investigated. A ``ConnectionError`` exception is now raised.
|
||||||
|
- |
|
||||||
|
Prevents the combined ``Session`` and ``Basic`` authentication support
|
||||||
|
from falling back to ``Basic`` authentication once ``Session`` based
|
||||||
|
authentication has been established. This should be considered a potential
|
||||||
|
security issue or an environmental change requiring potential client
|
||||||
|
re-initialization. This is exposed as an ``AccessError`` exception.
|
||||||
|
Continued operations against the Sushy library will attempt to
|
||||||
|
reauthenticate, if possible.
|
|
@ -40,6 +40,8 @@ class AuthBase(object, metaclass=abc.ABCMeta):
|
||||||
:param root_resource: Root sushy object
|
:param root_resource: Root sushy object
|
||||||
:param connector: Connector for http connections
|
:param connector: Connector for http connections
|
||||||
"""
|
"""
|
||||||
|
# Set the root resource, and connector to use
|
||||||
|
# for normal opreations.
|
||||||
self._root_resource = root_resource
|
self._root_resource = root_resource
|
||||||
self._connector = connector
|
self._connector = connector
|
||||||
self._connector.set_auth(self)
|
self._connector.set_auth(self)
|
||||||
|
@ -123,6 +125,8 @@ class SessionAuth(AuthBase):
|
||||||
"""Our Sessions Key"""
|
"""Our Sessions Key"""
|
||||||
self._session_resource_id = None
|
self._session_resource_id = None
|
||||||
"""Our Sessions Unique Resource ID or URL"""
|
"""Our Sessions Unique Resource ID or URL"""
|
||||||
|
self._session_auth_previously_successful = False
|
||||||
|
"""Our reminder for tracking if session auth has previously worked."""
|
||||||
|
|
||||||
super(SessionAuth, self).__init__(username,
|
super(SessionAuth, self).__init__(username,
|
||||||
password)
|
password)
|
||||||
|
@ -149,21 +153,18 @@ class SessionAuth(AuthBase):
|
||||||
:raises: AccessError
|
:raises: AccessError
|
||||||
:raises: HTTPError
|
:raises: HTTPError
|
||||||
"""
|
"""
|
||||||
target_uri = None
|
auth_token = None
|
||||||
try:
|
|
||||||
target_uri = self._root_resource.get_sessions_path()
|
auth_token, session_uri = self._root_resource.create_session(
|
||||||
except exceptions.MissingAttributeError:
|
self._username, self._password)
|
||||||
LOG.debug('Missing Sessions attribute under Links in Root '
|
# Record the session authentication data.
|
||||||
'Service, we\'ll try to determine it from Session '
|
self._session_key = auth_token
|
||||||
'Service')
|
|
||||||
session_service = self._root_resource.get_session_service()
|
|
||||||
session_auth_token, session_uri = (
|
|
||||||
session_service.create_session(self._username,
|
|
||||||
self._password,
|
|
||||||
target_uri=target_uri))
|
|
||||||
self._session_key = session_auth_token
|
|
||||||
self._session_resource_id = session_uri
|
self._session_resource_id = session_uri
|
||||||
self._connector.set_http_session_auth(session_auth_token)
|
# Set flag so we know we've previously successfully achieved
|
||||||
|
# session authentication in order to lockout possible fallback during
|
||||||
|
# session refresh/renegotiation.
|
||||||
|
self._session_auth_previously_successful = True
|
||||||
|
self._connector.set_http_session_auth(auth_token)
|
||||||
|
|
||||||
def can_refresh_session(self):
|
def can_refresh_session(self):
|
||||||
"""Method to assert if session based refresh can be done."""
|
"""Method to assert if session based refresh can be done."""
|
||||||
|
@ -225,6 +226,12 @@ class SessionOrBasicAuth(SessionAuth):
|
||||||
super(SessionOrBasicAuth, self).__init__(username, password)
|
super(SessionOrBasicAuth, self).__init__(username, password)
|
||||||
self.basic_auth = BasicAuth(username=username, password=password)
|
self.basic_auth = BasicAuth(username=username, password=password)
|
||||||
|
|
||||||
|
def _fallback_to_basic_authentication(self):
|
||||||
|
"""Fallback to basic authentication."""
|
||||||
|
self.reset_session_attrs()
|
||||||
|
self.basic_auth.set_context(self._root_resource, self._connector)
|
||||||
|
self.basic_auth.authenticate()
|
||||||
|
|
||||||
def _do_authenticate(self):
|
def _do_authenticate(self):
|
||||||
"""Establish a RedfishSession.
|
"""Establish a RedfishSession.
|
||||||
|
|
||||||
|
@ -234,16 +241,50 @@ class SessionOrBasicAuth(SessionAuth):
|
||||||
try:
|
try:
|
||||||
# Attempt session based authentication
|
# Attempt session based authentication
|
||||||
super(SessionOrBasicAuth, self)._do_authenticate()
|
super(SessionOrBasicAuth, self)._do_authenticate()
|
||||||
|
except exceptions.AccessError as e:
|
||||||
|
if (not self.can_refresh_session()
|
||||||
|
and not self._session_auth_previously_successful):
|
||||||
|
# We should only try and fallback if we've not been able
|
||||||
|
# to establish a session. If we had a session previously
|
||||||
|
# and we're trying to fallback, something fishy is afoot.
|
||||||
|
LOG.warning('Falling back to "Basic" authentication as '
|
||||||
|
'we have been unable to authenticate to the '
|
||||||
|
'BMC. Exception: %(exception)s.',
|
||||||
|
{'exception': e})
|
||||||
|
self._fallback_to_basic_authentication()
|
||||||
|
else:
|
||||||
|
# We previously had session authentication, something
|
||||||
|
# has changed which is far out of the ordinary.
|
||||||
|
LOG.debug('Received exception "%(exception)s" while '
|
||||||
|
'attempting to re-establish a session. '
|
||||||
|
'Raising AccessError, as something fishy '
|
||||||
|
'has occurred and the BMC has suddenly '
|
||||||
|
'ceased to support Session based '
|
||||||
|
'authentication.',
|
||||||
|
{'exception': e})
|
||||||
|
raise
|
||||||
|
except exceptions.ConnectionError as e:
|
||||||
|
# The reason to explicitly catch a connectivity failure is the
|
||||||
|
# case where transitory connectivity failures can occur while
|
||||||
|
# working to authenticate.
|
||||||
|
LOG.debug('Encountered a connectivity failure while attempting '
|
||||||
|
'to authenticate. We will not explicitly fallback. '
|
||||||
|
'Error: %(exception)s',
|
||||||
|
{'exception': e})
|
||||||
|
# Previously we would silently eat the failure as SushyError
|
||||||
|
# and fallback as it is a general fault. Callers on direct
|
||||||
|
# invocations through a connector _op method call can still
|
||||||
|
# receieve these exceptions, and applicaitons like Ironic do
|
||||||
|
# consider a client re-use disqualifier if there has been
|
||||||
|
# a connection failure, so it is okay for us to fix the behavior
|
||||||
|
# here.
|
||||||
|
raise
|
||||||
except exceptions.SushyError as e:
|
except exceptions.SushyError as e:
|
||||||
LOG.debug('Received exception "%(exception)s" while '
|
LOG.debug('Received exception "%(exception)s" while '
|
||||||
'attempting to establish a session. '
|
'attempting to establish a session. '
|
||||||
'Falling back to basic authentication.',
|
'Falling back to basic authentication.',
|
||||||
{'exception': e})
|
{'exception': e})
|
||||||
|
self._fallback_to_basic_authentication()
|
||||||
# Fall back to basic authentication
|
|
||||||
self.reset_session_attrs()
|
|
||||||
self.basic_auth.set_context(self._root_resource, self._connector)
|
|
||||||
self.basic_auth.authenticate()
|
|
||||||
|
|
||||||
def refresh_session(self):
|
def refresh_session(self):
|
||||||
"""Method to refresh a session to a Redfish controller.
|
"""Method to refresh a session to a Redfish controller.
|
||||||
|
|
|
@ -42,6 +42,9 @@ class Connector(object):
|
||||||
self._response_callback = response_callback
|
self._response_callback = response_callback
|
||||||
self._auth = None
|
self._auth = None
|
||||||
|
|
||||||
|
# NOTE(TheJulia): In order to help prevent recursive post operations
|
||||||
|
# by allowing us to understand that we should stop authentication.
|
||||||
|
self._sessions_uri = None
|
||||||
# NOTE(etingof): field studies reveal that some BMCs choke at
|
# NOTE(etingof): field studies reveal that some BMCs choke at
|
||||||
# long-running persistent HTTP connections (or TCP connections).
|
# long-running persistent HTTP connections (or TCP connections).
|
||||||
# By default, we ask HTTP server to shut down HTTP connection we've
|
# By default, we ask HTTP server to shut down HTTP connection we've
|
||||||
|
@ -126,9 +129,23 @@ class Connector(object):
|
||||||
try:
|
try:
|
||||||
exceptions.raise_for_response(method, url, response)
|
exceptions.raise_for_response(method, url, response)
|
||||||
except exceptions.AccessError as e:
|
except exceptions.AccessError as e:
|
||||||
if self._auth is not None and self._auth.can_refresh_session():
|
if (method == 'POST'
|
||||||
|
and self._sessions_uri is not None
|
||||||
|
and self._sessions_uri in url):
|
||||||
|
LOG.error('Authentication to the session service failed. '
|
||||||
|
'Please check credentials and try again.')
|
||||||
|
raise
|
||||||
|
if self._auth is not None:
|
||||||
try:
|
try:
|
||||||
|
if self._auth.can_refresh_session():
|
||||||
self._auth.refresh_session()
|
self._auth.refresh_session()
|
||||||
|
else:
|
||||||
|
LOG.warning('Session authentication appears to have '
|
||||||
|
'been lost at some point in time. '
|
||||||
|
'Connectivity may have been lost during '
|
||||||
|
'a prior session refresh. Attempting to '
|
||||||
|
're-authenticate.')
|
||||||
|
self._auth.authenticate()
|
||||||
except exceptions.AccessError as refresh_exc:
|
except exceptions.AccessError as refresh_exc:
|
||||||
LOG.error("A failure occured while attempting to refresh "
|
LOG.error("A failure occured while attempting to refresh "
|
||||||
"the session. Error: %s", refresh_exc.message)
|
"the session. Error: %s", refresh_exc.message)
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import requests
|
import requests
|
||||||
|
@ -367,10 +368,15 @@ class Sushy(base.ResourceBase):
|
||||||
|
|
||||||
def get_sessions_path(self):
|
def get_sessions_path(self):
|
||||||
"""Returns the Sessions url"""
|
"""Returns the Sessions url"""
|
||||||
|
# NOTE(TheJulia): This method is the standard discovery method
|
||||||
|
# advised by the DMTF DSP0266 standard to find the session service.
|
||||||
try:
|
try:
|
||||||
links_url = self.json.get('Links')
|
links_url = self.json.get('Links')
|
||||||
return links_url['Sessions']['@odata.id']
|
sessions_uri = links_url['Sessions']['@odata.id']
|
||||||
|
# Save the session URL for post detection and prevention
|
||||||
|
# of recursive autentication attempts.
|
||||||
|
self._conn._sessions_uri = sessions_uri
|
||||||
|
return sessions_uri
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
raise exceptions.MissingAttributeError(
|
raise exceptions.MissingAttributeError(
|
||||||
attribute='Links/Sessions/@data.id', resource=self.path)
|
attribute='Links/Sessions/@data.id', resource=self.path)
|
||||||
|
@ -386,6 +392,63 @@ class Sushy(base.ResourceBase):
|
||||||
redfish_version=self.redfish_version,
|
redfish_version=self.redfish_version,
|
||||||
registries=self.lazy_registries)
|
registries=self.lazy_registries)
|
||||||
|
|
||||||
|
def create_session(self, username=None, password=None):
|
||||||
|
"""Creates a session without invoking SessionService.
|
||||||
|
|
||||||
|
For use when a new connection is to be established. Removes prior
|
||||||
|
Session and authentication data before making the request.
|
||||||
|
|
||||||
|
:param username: The username to utilize to create a session with the
|
||||||
|
remote endpoint.
|
||||||
|
:param password: The password to utilize to create a session with the
|
||||||
|
remote endpoint.
|
||||||
|
:returns: A session key and uri in the form of a tuple
|
||||||
|
:raises: MissingXAuthToken
|
||||||
|
:raises: ConnectionError
|
||||||
|
:raises: AccessError
|
||||||
|
:raises: HTTPError
|
||||||
|
:raises: MissingAttributeError
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Explicitly removes in-client session data to proceed. This prevents
|
||||||
|
# AccessErrors as prior authentication shouldn't be submitted with a
|
||||||
|
# new authentication attempt, and doing so with old/invalid session
|
||||||
|
# data can result in an AccessError being raised.
|
||||||
|
self._conn._session.auth = None
|
||||||
|
if 'X-Auth-Token' in self._conn._session.headers:
|
||||||
|
# Delete the token value that was saved to the session
|
||||||
|
# as otherwise we would end up with a dictionary containing
|
||||||
|
# a {'X-Auth-Token': null} being sent across to the remote
|
||||||
|
# bmc.
|
||||||
|
del self._conn._session.headers['X-Auth-Token']
|
||||||
|
try:
|
||||||
|
session_service_path = self.get_sessions_path()
|
||||||
|
except (exceptions.MissingAttributeError, exceptions.AccessError):
|
||||||
|
# Guesses path based upon DMTF standard, in the event
|
||||||
|
# the links resource on the service root is incorrect.
|
||||||
|
session_service_path = os.path.join(self._path,
|
||||||
|
'SessionService/Sessions')
|
||||||
|
LOG.warning('Could not discover the Session service path, '
|
||||||
|
'falling back to %s.',
|
||||||
|
session_service_path)
|
||||||
|
session_url = self._root_prefix + 'SessionService/Sessions'
|
||||||
|
self._conn._sessions_uri = session_url
|
||||||
|
|
||||||
|
data = {'UserName': username, 'Password': password}
|
||||||
|
LOG.debug("Requesting new session from %s.",
|
||||||
|
session_service_path)
|
||||||
|
rsp = self._conn.post(session_service_path, data=data)
|
||||||
|
session_key = rsp.headers.get('X-Auth-Token')
|
||||||
|
if session_key is None:
|
||||||
|
raise exceptions.MissingXAuthToken(
|
||||||
|
method='POST', url=session_service_path, response=rsp)
|
||||||
|
|
||||||
|
session_uri = rsp.headers.get('Location')
|
||||||
|
if session_uri is None:
|
||||||
|
LOG.warning("Received X-Auth-Token but NO session uri.")
|
||||||
|
|
||||||
|
return session_key, session_uri
|
||||||
|
|
||||||
def get_update_service(self):
|
def get_update_service(self):
|
||||||
"""Get the UpdateService object
|
"""Get the UpdateService object
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,9 @@ class SessionService(base.ResourceBase):
|
||||||
registries=None):
|
registries=None):
|
||||||
"""A class representing a SessionService
|
"""A class representing a SessionService
|
||||||
|
|
||||||
|
Warning: This class should only be invoked with a connector which
|
||||||
|
has already established authentication.
|
||||||
|
|
||||||
:param connector: A Connector instance
|
:param connector: A Connector instance
|
||||||
:param identity: The identity of the SessionService resource
|
:param identity: The identity of the SessionService resource
|
||||||
:param redfish_version: The version of RedFish. Used to construct
|
:param redfish_version: The version of RedFish. Used to construct
|
||||||
|
@ -54,15 +57,11 @@ class SessionService(base.ResourceBase):
|
||||||
:param registries: Dict of Redfish Message Registry objects to be
|
:param registries: Dict of Redfish Message Registry objects to be
|
||||||
used in any resource that needs registries to parse messages
|
used in any resource that needs registries to parse messages
|
||||||
"""
|
"""
|
||||||
try:
|
# Populating the base resource so session interactions can
|
||||||
|
# occur based on the contents of it.
|
||||||
super(SessionService, self).__init__(
|
super(SessionService, self).__init__(
|
||||||
connector, identity, redfish_version, registries)
|
connector, identity, redfish_version=redfish_version,
|
||||||
|
registries=registries)
|
||||||
except exceptions.AccessError as ae:
|
|
||||||
LOG.debug('Received access error "%s" when trying to refresh the '
|
|
||||||
'SessionService. If this happens before '
|
|
||||||
'authentication, we\'ll have to guess the Sessions URL.',
|
|
||||||
ae)
|
|
||||||
|
|
||||||
def _get_sessions_collection_path(self):
|
def _get_sessions_collection_path(self):
|
||||||
"""Helper function to find the SessionCollections path"""
|
"""Helper function to find the SessionCollections path"""
|
||||||
|
@ -94,6 +93,8 @@ class SessionService(base.ResourceBase):
|
||||||
def create_session(self, username, password, target_uri=None):
|
def create_session(self, username, password, target_uri=None):
|
||||||
"""This function will try to create a session.
|
"""This function will try to create a session.
|
||||||
|
|
||||||
|
Create a session and return the associated key and URI.
|
||||||
|
|
||||||
:param username: the username of the user requesting a new session
|
:param username: the username of the user requesting a new session
|
||||||
:param password: the password associated to the user requesting
|
:param password: the password associated to the user requesting
|
||||||
a new session
|
a new session
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"Content-Security-Policy": "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self'; img-src 'self'; frame-src 'self'; font-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'",
|
||||||
|
"ETag": "'W/\"7dc5e2b9\"'",
|
||||||
|
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||||
|
"Connection": "Keep-Alive",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"X-Auth-Token": "adc530e2016a0ea98c76c087f0e4b76f",
|
||||||
|
"Expires": "0",
|
||||||
|
"X-Frame-Options": "SAMEORIGIN",
|
||||||
|
"Content-Length": "392",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"Content-Type": "application/json;charset=utf-8",
|
||||||
|
"OData-Version": "4.0",
|
||||||
|
"Keep-Alive": "timeout=1, max=32",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||||
|
"Date": "Tue, 06 Jun 2017 17:07:48 GMT"
|
||||||
|
}
|
|
@ -37,15 +37,17 @@ class SessionServiceTestCase(base.TestCase):
|
||||||
self.conn, '/redfish/v1/SessionService',
|
self.conn, '/redfish/v1/SessionService',
|
||||||
redfish_version='1.0.2')
|
redfish_version='1.0.2')
|
||||||
|
|
||||||
@mock.patch.object(sessionservice, 'LOG', autospec=True)
|
def test__init_throws_exception(self):
|
||||||
def test__init_throws_exception(self, mock_LOG):
|
|
||||||
self.conn.get.return_value.json.reset_mock()
|
self.conn.get.return_value.json.reset_mock()
|
||||||
self.conn.get.return_value.json.side_effect = (
|
self.conn.get.return_value.json.side_effect = (
|
||||||
exceptions.AccessError(
|
exceptions.AccessError(
|
||||||
'GET', 'any_url', mock.MagicMock()))
|
'GET', 'any_url', mock.MagicMock()))
|
||||||
sessionservice.SessionService(
|
# Previously sushy would just mask these, but now we raise
|
||||||
self.conn, '/redfish/v1/SessionService', redfish_version='1.0.2')
|
# the access error to the user.
|
||||||
self.assertTrue(mock_LOG.debug.called)
|
self.assertRaises(exceptions.AccessError,
|
||||||
|
sessionservice.SessionService,
|
||||||
|
self.conn, '/redfish/v1/SessionService',
|
||||||
|
redfish_version='1.0.2')
|
||||||
|
|
||||||
def test__parse_attributes(self):
|
def test__parse_attributes(self):
|
||||||
self.sess_serv_inst._parse_attributes(self.json_doc)
|
self.sess_serv_inst._parse_attributes(self.json_doc)
|
||||||
|
|
|
@ -140,24 +140,21 @@ class SessionAuthTestCase(base.TestCase):
|
||||||
def test__do_authenticate(self):
|
def test__do_authenticate(self):
|
||||||
self.assertIsNone(self.sess_auth.get_session_resource_id())
|
self.assertIsNone(self.sess_auth.get_session_resource_id())
|
||||||
self.assertIsNone(self.sess_auth.get_session_key())
|
self.assertIsNone(self.sess_auth.get_session_key())
|
||||||
mock_sess_serv = mock.Mock()
|
self.assertFalse(self.sess_auth._session_auth_previously_successful)
|
||||||
mock_sess_serv.create_session.return_value = (self.sess_key,
|
self.root.create_session.return_value = (self.sess_key,
|
||||||
self.sess_uri)
|
self.sess_uri)
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
self.sess_auth.set_context(self.root, self.conn)
|
self.sess_auth.set_context(self.root, self.conn)
|
||||||
self.sess_auth.authenticate()
|
self.sess_auth.authenticate()
|
||||||
self.assertEqual(self.sess_uri,
|
self.assertEqual(self.sess_uri,
|
||||||
self.sess_auth.get_session_resource_id())
|
self.sess_auth.get_session_resource_id())
|
||||||
self.assertEqual(self.sess_key,
|
self.assertEqual(self.sess_key,
|
||||||
self.sess_auth.get_session_key())
|
self.sess_auth.get_session_key())
|
||||||
|
self.assertTrue(self.sess_auth._session_auth_previously_successful)
|
||||||
self.conn.set_http_session_auth.assert_called_once_with(self.sess_key)
|
self.conn.set_http_session_auth.assert_called_once_with(self.sess_key)
|
||||||
|
|
||||||
def test_can_refresh_session(self):
|
def test_can_refresh_session(self):
|
||||||
mock_sess_serv = mock.Mock()
|
self.root.create_session.return_value = (self.sess_key,
|
||||||
mock_sess_serv.create_session.return_value = (self.sess_key,
|
|
||||||
self.sess_uri)
|
self.sess_uri)
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
|
|
||||||
self.sess_auth.set_context(self.root, self.conn)
|
self.sess_auth.set_context(self.root, self.conn)
|
||||||
self.sess_auth.authenticate()
|
self.sess_auth.authenticate()
|
||||||
|
|
||||||
|
@ -166,10 +163,8 @@ class SessionAuthTestCase(base.TestCase):
|
||||||
def test_refresh(self):
|
def test_refresh(self):
|
||||||
self.assertIsNone(self.sess_auth.get_session_resource_id())
|
self.assertIsNone(self.sess_auth.get_session_resource_id())
|
||||||
self.assertIsNone(self.sess_auth.get_session_key())
|
self.assertIsNone(self.sess_auth.get_session_key())
|
||||||
mock_sess_serv = mock.Mock()
|
self.root.create_session.return_value = (self.sess_key,
|
||||||
mock_sess_serv.create_session.return_value = (self.sess_key,
|
|
||||||
self.sess_uri)
|
self.sess_uri)
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
self._session = mock.Mock(spec=requests.Session)
|
self._session = mock.Mock(spec=requests.Session)
|
||||||
self.sess_auth.set_context(self.root, self.conn)
|
self.sess_auth.set_context(self.root, self.conn)
|
||||||
self.sess_auth.refresh_session()
|
self.sess_auth.refresh_session()
|
||||||
|
@ -287,10 +282,8 @@ class SessionOrBasicAuthTestCase(base.TestCase):
|
||||||
def test__do_authenticate(self):
|
def test__do_authenticate(self):
|
||||||
self.assertIsNone(self.sess_basic_auth.get_session_resource_id())
|
self.assertIsNone(self.sess_basic_auth.get_session_resource_id())
|
||||||
self.assertIsNone(self.sess_basic_auth.get_session_key())
|
self.assertIsNone(self.sess_basic_auth.get_session_key())
|
||||||
mock_sess_serv = mock.Mock()
|
self.root.create_session.return_value = (self.sess_key,
|
||||||
mock_sess_serv.create_session.return_value = (self.sess_key,
|
|
||||||
self.sess_uri)
|
self.sess_uri)
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
self.sess_basic_auth.set_context(self.root, self.conn)
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
self.sess_basic_auth.authenticate()
|
self.sess_basic_auth.authenticate()
|
||||||
self.assertEqual(self.sess_uri,
|
self.assertEqual(self.sess_uri,
|
||||||
|
@ -302,10 +295,7 @@ class SessionOrBasicAuthTestCase(base.TestCase):
|
||||||
def test__do_authenticate_for_basic_auth(self):
|
def test__do_authenticate_for_basic_auth(self):
|
||||||
self.assertIsNone(self.sess_basic_auth.get_session_resource_id())
|
self.assertIsNone(self.sess_basic_auth.get_session_resource_id())
|
||||||
self.assertIsNone(self.sess_basic_auth.get_session_key())
|
self.assertIsNone(self.sess_basic_auth.get_session_key())
|
||||||
mock_sess_serv = mock.Mock()
|
self.root.create_session.side_effect = exceptions.SushyError
|
||||||
mock_sess_serv.create_session.side_effect = exceptions.SushyError
|
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
|
|
||||||
self.sess_basic_auth.set_context(self.root, self.conn)
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
self.sess_basic_auth.authenticate()
|
self.sess_basic_auth.authenticate()
|
||||||
|
|
||||||
|
@ -315,11 +305,8 @@ class SessionOrBasicAuthTestCase(base.TestCase):
|
||||||
self.username, self.password)
|
self.username, self.password)
|
||||||
|
|
||||||
def test_can_refresh_session(self):
|
def test_can_refresh_session(self):
|
||||||
mock_sess_serv = mock.Mock()
|
self.root.create_session.return_value = (self.sess_key,
|
||||||
mock_sess_serv.create_session.return_value = (self.sess_key,
|
|
||||||
self.sess_uri)
|
self.sess_uri)
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
|
|
||||||
self.sess_basic_auth.set_context(self.root, self.conn)
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
self.sess_basic_auth.authenticate()
|
self.sess_basic_auth.authenticate()
|
||||||
|
|
||||||
|
@ -340,10 +327,8 @@ class SessionOrBasicAuthTestCase(base.TestCase):
|
||||||
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
||||||
'/Sessions/testingfirst')
|
'/Sessions/testingfirst')
|
||||||
self.sess_basic_auth._session_resource_id = test_url
|
self.sess_basic_auth._session_resource_id = test_url
|
||||||
mock_sess_serv = mock.Mock()
|
self.root.create_session.return_value = (self.sess_key,
|
||||||
mock_sess_serv.create_session.return_value = (self.sess_key,
|
|
||||||
self.sess_uri)
|
self.sess_uri)
|
||||||
self.root.get_session_service.return_value = mock_sess_serv
|
|
||||||
self.sess_basic_auth.set_context(self.root, self.conn)
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
self.sess_basic_auth.refresh_session()
|
self.sess_basic_auth.refresh_session()
|
||||||
self.assertEqual(self.sess_uri,
|
self.assertEqual(self.sess_uri,
|
||||||
|
@ -352,6 +337,121 @@ class SessionOrBasicAuthTestCase(base.TestCase):
|
||||||
self.sess_basic_auth.get_session_key())
|
self.sess_basic_auth.get_session_key())
|
||||||
self.conn.set_http_session_auth.assert_called_once_with(self.sess_key)
|
self.conn.set_http_session_auth.assert_called_once_with(self.sess_key)
|
||||||
|
|
||||||
|
@mock.patch.object(auth.SessionOrBasicAuth,
|
||||||
|
'_fallback_to_basic_authentication',
|
||||||
|
autospec=True)
|
||||||
|
def test_refresh_refresh_connection_error(self, mock_activate_basic_auth):
|
||||||
|
self.sess_basic_auth._session_key = 'ThisisFirstKey'
|
||||||
|
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
||||||
|
'/Sessions/testingfirst')
|
||||||
|
self.sess_basic_auth._session_auth_previously_successful = True
|
||||||
|
self.sess_basic_auth._session_resource_id = test_url
|
||||||
|
self.root.create_session.side_effect = \
|
||||||
|
exceptions.ConnectionError()
|
||||||
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
|
self.assertRaises(exceptions.ConnectionError,
|
||||||
|
self.sess_basic_auth.refresh_session)
|
||||||
|
self.assertIsNone(self.sess_basic_auth.get_session_resource_id())
|
||||||
|
self.assertIsNone(self.sess_basic_auth.get_session_key())
|
||||||
|
self.conn.set_http_session_auth.assert_not_called()
|
||||||
|
self.assertIsNone(self.sess_basic_auth.basic_auth._root_resource)
|
||||||
|
self.assertIsNone(self.sess_basic_auth.basic_auth._connector)
|
||||||
|
self.assertFalse(mock_activate_basic_auth.called)
|
||||||
|
|
||||||
|
@mock.patch.object(auth.SessionOrBasicAuth,
|
||||||
|
'_fallback_to_basic_authentication',
|
||||||
|
autospec=True)
|
||||||
|
def test_refresh_refresh_connection_error_clears(
|
||||||
|
self, mock_activate_basic_auth):
|
||||||
|
self.sess_basic_auth._session_key = 'ThisisFirstKey'
|
||||||
|
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
||||||
|
'/Sessions/testingfirst')
|
||||||
|
self.sess_basic_auth._session_auth_previously_successful = True
|
||||||
|
self.sess_basic_auth._session_resource_id = test_url
|
||||||
|
self.root.create_session.side_effect = [
|
||||||
|
exceptions.ConnectionError(),
|
||||||
|
(self.sess_key, self.sess_uri)
|
||||||
|
]
|
||||||
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
|
self.assertRaises(exceptions.ConnectionError,
|
||||||
|
self.sess_basic_auth.refresh_session)
|
||||||
|
self.assertIsNone(self.sess_basic_auth.get_session_resource_id())
|
||||||
|
self.assertIsNone(self.sess_basic_auth.get_session_key())
|
||||||
|
self.conn.set_http_session_auth.assert_not_called()
|
||||||
|
self.assertIsNone(self.sess_basic_auth.basic_auth._root_resource)
|
||||||
|
self.assertIsNone(self.sess_basic_auth.basic_auth._connector)
|
||||||
|
self.assertFalse(mock_activate_basic_auth.called)
|
||||||
|
# Refresh no longer works, explicit authentication is now required.
|
||||||
|
self.sess_basic_auth.refresh_session()
|
||||||
|
self.conn.set_http_session_auth.assert_not_called()
|
||||||
|
|
||||||
|
self.sess_basic_auth.authenticate()
|
||||||
|
self.conn.set_http_session_auth.assert_called_once_with(self.sess_key)
|
||||||
|
|
||||||
|
@mock.patch.object(auth.SessionOrBasicAuth,
|
||||||
|
'_fallback_to_basic_authentication',
|
||||||
|
autospec=True)
|
||||||
|
def test_authenticate_session_fails(self, mock_activate_basic_auth):
|
||||||
|
self.sess_basic_auth._session_key = None
|
||||||
|
self.sess_basic_auth._session_auth_previously_successful = False
|
||||||
|
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
||||||
|
'/Sessions/testingfirst')
|
||||||
|
self.sess_basic_auth._session_resource_id = test_url
|
||||||
|
ae_exc = exceptions.AccessError(
|
||||||
|
'GET', 'any_url', mock.MagicMock())
|
||||||
|
|
||||||
|
self.root.create_session.side_effect = ae_exc
|
||||||
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
|
self.sess_basic_auth.authenticate()
|
||||||
|
# We fall back to basic auth as we failed to authenticate.
|
||||||
|
self.assertTrue(mock_activate_basic_auth.called)
|
||||||
|
|
||||||
|
@mock.patch.object(auth, 'LOG', autospec=True)
|
||||||
|
@mock.patch.object(auth.SessionOrBasicAuth,
|
||||||
|
'_fallback_to_basic_authentication',
|
||||||
|
autospec=True)
|
||||||
|
def test_authenticate_session_fails_reauth_raises_exception(
|
||||||
|
self, mock_activate_basic_auth, mock_log):
|
||||||
|
self.sess_basic_auth._session_key = None
|
||||||
|
self.sess_basic_auth._session_auth_previously_successful = True
|
||||||
|
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
||||||
|
'/Sessions/testingfirst')
|
||||||
|
self.sess_basic_auth._session_resource_id = test_url
|
||||||
|
ae_exc = exceptions.AccessError(
|
||||||
|
'GET', 'any_url', mock.MagicMock())
|
||||||
|
|
||||||
|
self.root.create_session.side_effect = ae_exc
|
||||||
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
|
self.assertRaises(exceptions.AccessError,
|
||||||
|
self.sess_basic_auth.authenticate)
|
||||||
|
self.assertTrue(mock_log.debug.called)
|
||||||
|
# We do not fallback to basic auth if we have already been
|
||||||
|
# authenticated.
|
||||||
|
self.assertFalse(mock_activate_basic_auth.called)
|
||||||
|
|
||||||
|
@mock.patch.object(auth.SessionOrBasicAuth,
|
||||||
|
'_fallback_to_basic_authentication',
|
||||||
|
autospec=True)
|
||||||
|
def test_authenticate_session_fails_connection_error(
|
||||||
|
self, mock_activate_basic_auth):
|
||||||
|
self.sess_basic_auth._session_key = None
|
||||||
|
self.sess_basic_auth._session_auth_previously_successful = False
|
||||||
|
test_url = ('https://testing:8000/redfish/v1/SessionService'
|
||||||
|
'/Sessions/testingfirst')
|
||||||
|
self.sess_basic_auth._session_resource_id = test_url
|
||||||
|
mock_sess_serv = mock.Mock()
|
||||||
|
|
||||||
|
ae_exc = exceptions.ConnectionError()
|
||||||
|
|
||||||
|
self.root.create_session.side_effect = ae_exc
|
||||||
|
self.root.get_session_service.return_value = mock_sess_serv
|
||||||
|
self.sess_basic_auth.set_context(self.root, self.conn)
|
||||||
|
self.assertRaises(exceptions.ConnectionError,
|
||||||
|
self.sess_basic_auth.authenticate)
|
||||||
|
# We don't fall back to basic auth if we've never connected
|
||||||
|
# before
|
||||||
|
self.assertFalse(mock_activate_basic_auth.called)
|
||||||
|
|
||||||
def test_close_do_nothing(self):
|
def test_close_do_nothing(self):
|
||||||
self.conn.delete.assert_not_called()
|
self.conn.delete.assert_not_called()
|
||||||
|
|
||||||
|
|
|
@ -263,6 +263,11 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
self.request = self.session.request
|
self.request = self.session.request
|
||||||
self.request.return_value.status_code = http_client.FORBIDDEN
|
self.request.return_value.status_code = http_client.FORBIDDEN
|
||||||
self.request.return_value.json.side_effect = ValueError('no json')
|
self.request.return_value.json.side_effect = ValueError('no json')
|
||||||
|
mock_response = mock.Mock()
|
||||||
|
mock_response.json.side_effect = ValueError('no json')
|
||||||
|
mock_response.status_code = http_client.FORBIDDEN
|
||||||
|
self.conn._auth.authenticate.side_effect = \
|
||||||
|
exceptions.AccessError('POST', 'fake/path', mock_response)
|
||||||
with self.assertRaisesRegex(exceptions.AccessError,
|
with self.assertRaisesRegex(exceptions.AccessError,
|
||||||
'unknown error') as ae:
|
'unknown error') as ae:
|
||||||
self.conn._op('POST', path='fake/path', data=self.data,
|
self.conn._op('POST', path='fake/path', data=self.data,
|
||||||
|
@ -289,6 +294,42 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
self.auth.can_refresh_session.assert_called_with()
|
self.auth.can_refresh_session.assert_called_with()
|
||||||
self.assertEqual(response.json, second_response.json)
|
self.assertEqual(response.json, second_response.json)
|
||||||
|
|
||||||
|
def test_timed_out_session_timed_out_refresh(self):
|
||||||
|
self.auth._session_key = 'asdf1234'
|
||||||
|
self.auth.get_session_key.return_value = 'asdf1234'
|
||||||
|
self.conn._auth = self.auth
|
||||||
|
self.session = mock.Mock(spec=requests.Session)
|
||||||
|
self.conn._session = self.session
|
||||||
|
self.request = self.session.request
|
||||||
|
first_response = mock.MagicMock()
|
||||||
|
first_response.status_code = http_client.FORBIDDEN
|
||||||
|
second_response = first_response
|
||||||
|
self.auth.refresh_session.side_effect = \
|
||||||
|
exceptions.ConnectionError()
|
||||||
|
third_response = mock.MagicMock()
|
||||||
|
third_response.status_code = http_client.OK
|
||||||
|
third_response.json = {'Test': 'Testing'}
|
||||||
|
self.auth.can_refresh_session.return_value = True
|
||||||
|
|
||||||
|
self.request.side_effect = [first_response, second_response,
|
||||||
|
third_response]
|
||||||
|
self.assertRaises(exceptions.ConnectionError, self.conn._op, 'POST',
|
||||||
|
path='fake/path', data=self.data,
|
||||||
|
headers=self.headers)
|
||||||
|
self.auth.refresh_session.assert_called_with()
|
||||||
|
self.auth.refresh_session.reset_mock()
|
||||||
|
# Normally, this would be reset by refresh_session, but given
|
||||||
|
# the heavy mocking, we need to do it for this test.
|
||||||
|
self.auth._session_key = None
|
||||||
|
self.auth.get_session_key.return_value = None
|
||||||
|
self.auth.can_refresh_session.return_value = False
|
||||||
|
|
||||||
|
response = self.conn._op('POST', path='fake/path', data=self.data,
|
||||||
|
headers=self.headers)
|
||||||
|
self.auth.refresh_session.assert_not_called()
|
||||||
|
self.auth.authenticate.assert_called_once()
|
||||||
|
self.assertEqual(response.json, third_response.json)
|
||||||
|
|
||||||
def test_connection_error(self):
|
def test_connection_error(self):
|
||||||
self.request.side_effect = requests.exceptions.ConnectionError
|
self.request.side_effect = requests.exceptions.ConnectionError
|
||||||
self.assertRaises(exceptions.ConnectionError, self.conn._op, 'GET')
|
self.assertRaises(exceptions.ConnectionError, self.conn._op, 'GET')
|
||||||
|
@ -361,7 +402,7 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
self.assertEqual(6, self.request.call_count)
|
self.assertEqual(6, self.request.call_count)
|
||||||
|
|
||||||
def test_access_error(self):
|
def test_access_error(self):
|
||||||
self.conn._auth.can_refresh_session.return_value = False
|
self.conn._auth = None
|
||||||
|
|
||||||
self.request.return_value.status_code = http_client.FORBIDDEN
|
self.request.return_value.status_code = http_client.FORBIDDEN
|
||||||
self.request.return_value.json.side_effect = ValueError('no json')
|
self.request.return_value.json.side_effect = ValueError('no json')
|
||||||
|
@ -372,6 +413,42 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
exc = cm.exception
|
exc = cm.exception
|
||||||
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
||||||
|
|
||||||
|
def test_access_error_on_session_post_does_not_retry(self):
|
||||||
|
self.request.return_value.status_code = http_client.FORBIDDEN
|
||||||
|
self.request.return_value.json.side_effect = ValueError('no json')
|
||||||
|
|
||||||
|
self.conn._sessions_uri = '/redfish/v1/SessionService/Sessions'
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(exceptions.AccessError,
|
||||||
|
'unknown error') as cm:
|
||||||
|
self.conn._op('POST', 'http://foo.bar/redfish/v1/Session'
|
||||||
|
'Service/Sessions',
|
||||||
|
data={'foo': 'bar'})
|
||||||
|
exc = cm.exception
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
||||||
|
self.request.assert_called_once()
|
||||||
|
|
||||||
|
def test_access_error_triggers_auth_attempt(self):
|
||||||
|
self.conn._auth.can_refresh_session.return_value = False
|
||||||
|
value_error = ValueError('no json')
|
||||||
|
mock_response = mock.Mock()
|
||||||
|
mock_response.json.side_effect = value_error
|
||||||
|
mock_response.status_code = http_client.FORBIDDEN
|
||||||
|
# This doesn't test/wire all way back through auth -> main
|
||||||
|
# and ultimately we want to make sure we get an error all
|
||||||
|
# the way back.
|
||||||
|
self.conn._auth.authenticate.side_effect = \
|
||||||
|
exceptions.AccessError('GET', '/', mock_response)
|
||||||
|
self.request.return_value.status_code = http_client.FORBIDDEN
|
||||||
|
self.request.return_value.json.side_effect = value_error
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(exceptions.AccessError,
|
||||||
|
'unknown error') as cm:
|
||||||
|
self.conn._op('GET', 'http://foo.bar')
|
||||||
|
exc = cm.exception
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
||||||
|
self.conn._auth.authenticate.assert_called_once()
|
||||||
|
|
||||||
def test_access_error_without_auth(self):
|
def test_access_error_without_auth(self):
|
||||||
self.conn._auth = None
|
self.conn._auth = None
|
||||||
|
|
||||||
|
@ -385,9 +462,28 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
||||||
|
|
||||||
@mock.patch.object(connector.LOG, 'debug', autospec=True)
|
@mock.patch.object(connector.LOG, 'debug', autospec=True)
|
||||||
def test_access_error_service_session(self, mock_log):
|
def test_access_error_service_session_reauth(self, mock_log):
|
||||||
self.conn._auth.can_refresh_session.return_value = False
|
self.conn._auth.can_refresh_session.return_value = False
|
||||||
|
|
||||||
|
self.request.return_value.status_code = http_client.FORBIDDEN
|
||||||
|
mock_response = mock.Mock()
|
||||||
|
mock_response.json.side_effect = ValueError('no json')
|
||||||
|
mock_response.status_code = http_client.FORBIDDEN
|
||||||
|
self.conn._auth.authenticate.side_effect = \
|
||||||
|
exceptions.AccessError('POST', 'fake/path', mock_response)
|
||||||
|
self.request.return_value.json.side_effect = ValueError('no json')
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(exceptions.AccessError,
|
||||||
|
'unknown error') as cm:
|
||||||
|
self.conn._op('GET', 'http://redfish/v1/SessionService')
|
||||||
|
exc = cm.exception
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, exc.status_code)
|
||||||
|
self.conn._auth.authenticate.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(connector.LOG, 'debug', autospec=True)
|
||||||
|
def test_access_error_service_session_no_auth(self, mock_log):
|
||||||
|
self.conn._auth = None
|
||||||
|
|
||||||
self.request.return_value.status_code = http_client.FORBIDDEN
|
self.request.return_value.status_code = http_client.FORBIDDEN
|
||||||
self.request.return_value.json.side_effect = ValueError('no json')
|
self.request.return_value.json.side_effect = ValueError('no json')
|
||||||
|
|
||||||
|
|
|
@ -280,6 +280,110 @@ class MainTestCase(base.TestCase):
|
||||||
self.root._conn, 'asdf',
|
self.root._conn, 'asdf',
|
||||||
self.root.redfish_version, self.root.lazy_registries)
|
self.root.redfish_version, self.root.lazy_registries)
|
||||||
|
|
||||||
|
def test_create_session(self):
|
||||||
|
self.root._conn._session.headers = []
|
||||||
|
self.root._conn._sessions_uri = None
|
||||||
|
with open('sushy/tests/unit/json_samples/'
|
||||||
|
'session_creation_headers.json') as f:
|
||||||
|
self.conn.post.return_value.headers = json.load(f)
|
||||||
|
|
||||||
|
session_key, session_uri = (
|
||||||
|
self.root.create_session('foo', 'secret'))
|
||||||
|
self.assertEqual('adc530e2016a0ea98c76c087f0e4b76f', session_key)
|
||||||
|
self.assertEqual(
|
||||||
|
'/redfish/v1/SessionService/Sessions/151edd65d41c0b89',
|
||||||
|
session_uri)
|
||||||
|
self.assertEqual('/redfish/v1/SessionService/Sessions',
|
||||||
|
self.root._conn._sessions_uri)
|
||||||
|
|
||||||
|
def test_create_session_removes_auth_data(self):
|
||||||
|
self.root._conn._session.headers = {'X-Auth-Token': 'meow'}
|
||||||
|
self.root._conn._session.auth = 'meow'
|
||||||
|
with open('sushy/tests/unit/json_samples/'
|
||||||
|
'session_creation_headers.json') as f:
|
||||||
|
self.conn.post.return_value.headers = json.load(f)
|
||||||
|
|
||||||
|
session_key, session_uri = (
|
||||||
|
self.root.create_session('foo', 'secret'))
|
||||||
|
self.assertEqual('adc530e2016a0ea98c76c087f0e4b76f', session_key)
|
||||||
|
self.assertEqual(
|
||||||
|
'/redfish/v1/SessionService/Sessions/151edd65d41c0b89',
|
||||||
|
session_uri)
|
||||||
|
self.assertIsNone(self.root._conn._session.auth)
|
||||||
|
self.assertNotIn('X-Auth-Token', self.root._conn._session.headers)
|
||||||
|
|
||||||
|
def test_create_session_no_session_path(self):
|
||||||
|
self.root._conn._session.headers = []
|
||||||
|
mock_get_session_path = mock.Mock()
|
||||||
|
mock_get_session_path.side_effect = exceptions.MissingAttributeError()
|
||||||
|
self.root.get_sessions_path = mock_get_session_path
|
||||||
|
with open('sushy/tests/unit/json_samples/'
|
||||||
|
'session_creation_headers.json') as f:
|
||||||
|
self.conn.post.return_value.headers = json.load(f)
|
||||||
|
|
||||||
|
session_key, session_uri = (
|
||||||
|
self.root.create_session('foo', 'secret'))
|
||||||
|
self.assertEqual('adc530e2016a0ea98c76c087f0e4b76f', session_key)
|
||||||
|
self.assertEqual(
|
||||||
|
'/redfish/v1/SessionService/Sessions/151edd65d41c0b89',
|
||||||
|
session_uri)
|
||||||
|
self.conn.post.assert_called_once_with(
|
||||||
|
'/redfish/v1/SessionService/Sessions',
|
||||||
|
data={'UserName': 'foo', 'Password': 'secret'})
|
||||||
|
|
||||||
|
@mock.patch.object(main, 'LOG', autospec=True)
|
||||||
|
def test_create_session_no_session_path_access_error(self, mock_log):
|
||||||
|
self.root._conn._session.headers = []
|
||||||
|
mock_res = mock.Mock()
|
||||||
|
mock_res.status_code = 403
|
||||||
|
mock_res.json.side_effect = ValueError('no json')
|
||||||
|
mock_get_session_path = mock.Mock()
|
||||||
|
mock_get_session_path.side_effect = exceptions.AccessError(
|
||||||
|
'GET', 'redfish/v1', mock_res)
|
||||||
|
self.root.get_sessions_path = mock_get_session_path
|
||||||
|
with open('sushy/tests/unit/json_samples/'
|
||||||
|
'session_creation_headers_no_location.json') as f:
|
||||||
|
self.conn.post.return_value.headers = json.load(f)
|
||||||
|
|
||||||
|
session_key, session_uri = (
|
||||||
|
self.root.create_session('foo', 'secret'))
|
||||||
|
self.assertEqual('adc530e2016a0ea98c76c087f0e4b76f', session_key)
|
||||||
|
self.assertIsNone(session_uri)
|
||||||
|
self.conn.post.assert_called_once_with(
|
||||||
|
'/redfish/v1/SessionService/Sessions',
|
||||||
|
data={'UserName': 'foo', 'Password': 'secret'})
|
||||||
|
self.assertTrue(mock_log.warning.called)
|
||||||
|
|
||||||
|
def test_create_session_path_discovery(self):
|
||||||
|
self.root._conn._session.headers = []
|
||||||
|
with open('sushy/tests/unit/json_samples/root.json') as f:
|
||||||
|
self.conn.get.json.return_value = json.load(f)
|
||||||
|
with open('sushy/tests/unit/json_samples/'
|
||||||
|
'session_creation_headers.json') as f:
|
||||||
|
self.conn.post.return_value.headers = json.load(f)
|
||||||
|
|
||||||
|
session_key, session_uri = (
|
||||||
|
self.root.create_session('foo', 'secret'))
|
||||||
|
self.assertEqual('adc530e2016a0ea98c76c087f0e4b76f', session_key)
|
||||||
|
self.assertEqual(
|
||||||
|
'/redfish/v1/SessionService/Sessions/151edd65d41c0b89',
|
||||||
|
session_uri)
|
||||||
|
self.conn.get.assert_called_once_with(path='/redfish/v1/')
|
||||||
|
self.conn.post.assert_called_once_with(
|
||||||
|
'/redfish/v1/SessionService/Sessions',
|
||||||
|
data={'UserName': 'foo', 'Password': 'secret'})
|
||||||
|
|
||||||
|
def test_create_session_missing_x_auth_token(self):
|
||||||
|
self.root._conn._session.headers = []
|
||||||
|
with open('sushy/tests/unit/json_samples/'
|
||||||
|
'session_creation_headers.json') as f:
|
||||||
|
self.conn.post.return_value.headers = json.load(f)
|
||||||
|
|
||||||
|
self.conn.post.return_value.headers.pop('X-Auth-Token')
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
exceptions.MissingXAuthToken, 'No X-Auth-Token returned',
|
||||||
|
self.root.create_session, 'foo', 'bar')
|
||||||
|
|
||||||
@mock.patch.object(updateservice, 'UpdateService', autospec=True)
|
@mock.patch.object(updateservice, 'UpdateService', autospec=True)
|
||||||
def test_get_update_service(self, mock_upd_serv):
|
def test_get_update_service(self, mock_upd_serv):
|
||||||
self.root.get_update_service()
|
self.root.get_update_service()
|
||||||
|
@ -395,8 +499,10 @@ class MainTestCase(base.TestCase):
|
||||||
self.assertEqual(1, mock_registries.__getitem__.call_count)
|
self.assertEqual(1, mock_registries.__getitem__.call_count)
|
||||||
|
|
||||||
def test_get_sessions_path(self):
|
def test_get_sessions_path(self):
|
||||||
|
self.root._conn._sessions_uri = None
|
||||||
expected = '/redfish/v1/SessionService/Sessions'
|
expected = '/redfish/v1/SessionService/Sessions'
|
||||||
self.assertEqual(expected, self.root.get_sessions_path())
|
self.assertEqual(expected, self.root.get_sessions_path())
|
||||||
|
self.assertEqual(expected, self.root._conn._sessions_uri)
|
||||||
|
|
||||||
|
|
||||||
class BareMinimumMainTestCase(base.TestCase):
|
class BareMinimumMainTestCase(base.TestCase):
|
||||||
|
|
Loading…
Reference in New Issue