Added new Auth Strategy - RAX_AUTH_MFA
* Added new strategy - RAX_AUTH_MFA * Added new config option in user section: passcode * Added new client for MFA authentication * Updated behaviors to use correct client calls to authenticate based on strategy * Updated auth request model to support second api call * Added Passcode Credentials model for request model on second API call Change-Id: I0b4b75dfd21c32eecf04a8f08569297e947b58b1
This commit is contained in:
parent
739ad6465c
commit
f5754b9e5e
|
@ -32,12 +32,11 @@ class UserAuthConfig(ConfigSectionInterface):
|
|||
@property
|
||||
def strategy(self):
|
||||
"""The type of authentication exposed by the auth_endpoint. Currently,
|
||||
supported values are 'keystone', 'rax_auth' or 'saio_tempauth'.
|
||||
supported values are 'keystone', 'rax_auth', 'rax_auth_mfa', or
|
||||
'saio_tempauth'.
|
||||
"""
|
||||
|
||||
return self.get("strategy")
|
||||
|
||||
|
||||
class UserConfig(ConfigSectionInterface):
|
||||
|
||||
SECTION_NAME = 'user'
|
||||
|
@ -76,3 +75,8 @@ class UserConfig(ConfigSectionInterface):
|
|||
def project_id(self):
|
||||
"""The users's project_id, if applicable"""
|
||||
return self.get("project_id")
|
||||
|
||||
@property
|
||||
def passcode(self):
|
||||
"""The auth MFA's secondary password/passcode"""
|
||||
return self.get("passcode", 'MFA_PASSCODE_NOT_SET')
|
||||
|
|
|
@ -16,9 +16,13 @@ limitations under the License.
|
|||
from cafe.drivers.unittest.decorators import memoized
|
||||
from cloudcafe.auth.config import UserAuthConfig, UserConfig
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.client import \
|
||||
TokenAPI_Client as RaxTokenAPI_Client
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.behaviors import \
|
||||
TokenAPI_Behaviors as RaxTokenAPI_Behaviors
|
||||
TokenAPI_Client as RaxTokenAPI_Client, \
|
||||
MFA_TokenAPI_Client as RaxToken_MFA_API_Client
|
||||
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.behaviors \
|
||||
import TokenAPI_Behaviors as RaxTokenAPI_Behaviors, \
|
||||
MFA_TokenAPI_Behaviors as RaxToken_MFA_API_Behaviors
|
||||
|
||||
from cloudcafe.extensions.saio_tempauth.v1_0.client import \
|
||||
TempauthAPI_Client as SaioAuthAPI_Client
|
||||
from cloudcafe.extensions.saio_tempauth.v1_0.behaviors import \
|
||||
|
@ -49,6 +53,20 @@ class MemoizedAuthServiceComposite(object):
|
|||
behaviors = RaxTokenAPI_Behaviors(client)
|
||||
return behaviors.get_access_data(username, api_key, tenant_id)
|
||||
|
||||
@classmethod
|
||||
@memoized
|
||||
def get_rackspace_mfa_access_data(cls, username, password, tenant_id,
|
||||
auth_endpoint, passcode):
|
||||
if passcode is None:
|
||||
# TODO: This is a place holder for adding the functionality to
|
||||
# use an external service (e.g. - SMS) to provide the passcode
|
||||
# Also add this to get_access_data() in the AuthProvider class
|
||||
pass
|
||||
token_client = RaxToken_MFA_API_Client(
|
||||
auth_endpoint, 'json', 'json', passcode)
|
||||
token_behaviors = RaxToken_MFA_API_Behaviors(token_client)
|
||||
return token_behaviors.get_access_data(username, password, tenant_id)
|
||||
|
||||
@classmethod
|
||||
@memoized
|
||||
def get_keystone_access_data(
|
||||
|
@ -77,6 +95,12 @@ class MemoizedAuthServiceComposite(object):
|
|||
self.user_config.username, self.user_config.api_key,
|
||||
self.user_config.tenant_id, self.endpoint_config.auth_endpoint)
|
||||
|
||||
elif self.auth_strategy == 'rax_auth_mfa':
|
||||
return self.get_rackspace_mfa_access_data(
|
||||
self.user_config.username, self.user_config.password,
|
||||
self.user_config.tenant_id, self.endpoint_config.auth_endpoint,
|
||||
self.user_config.passcode)
|
||||
|
||||
elif self.auth_strategy == 'saio_tempauth':
|
||||
return self.get_saio_tempauth_access_data(
|
||||
self.user_config.username, self.user_config.password,
|
||||
|
@ -138,6 +162,19 @@ class AuthProvider(object):
|
|||
user_config.api_key,
|
||||
user_config.tenant_id)
|
||||
|
||||
elif endpoint_config.strategy.lower() == 'rax_auth_mfa':
|
||||
passcode = user_config.passcode
|
||||
if passcode is None:
|
||||
# TODO: This is a place holder for adding the functionality to
|
||||
# use an external service (e.g. - SMS) to provide the passcode
|
||||
pass
|
||||
token_client = RaxToken_MFA_API_Client(
|
||||
endpoint_config.auth_endpoint, 'json', 'json', passcode)
|
||||
token_behaviors = RaxToken_MFA_API_Behaviors(token_client)
|
||||
return token_behaviors.get_access_data(user_config.username,
|
||||
user_config.api_key,
|
||||
user_config.tenant_id)
|
||||
|
||||
elif endpoint_config.strategy.lower() == 'saio_tempauth':
|
||||
auth_client = SaioAuthAPI_Client(endpoint_config.auth_endpoint)
|
||||
auth_behaviors = SaioAuthAPI_Behaviors(auth_client)
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
from cafe.engine.behaviors import BaseBehavior, behavior
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.client \
|
||||
import TokenAPI_Client
|
||||
import TokenAPI_Client, MFA_TokenAPI_Client
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.config \
|
||||
import TokenAPI_Config
|
||||
|
||||
|
@ -24,6 +24,7 @@ from cloudcafe.extensions.rax_auth.v2_0.tokens_api.config \
|
|||
class TokenAPI_Behaviors(BaseBehavior):
|
||||
|
||||
def __init__(self, identity_user_api_client=None):
|
||||
super(TokenAPI_Behaviors, self).__init__()
|
||||
self._client = identity_user_api_client
|
||||
self.config = TokenAPI_Config()
|
||||
|
||||
|
@ -43,3 +44,50 @@ class TokenAPI_Behaviors(BaseBehavior):
|
|||
access_data = response.entity
|
||||
|
||||
return access_data
|
||||
|
||||
|
||||
class MFA_TokenAPI_Behaviors(BaseBehavior):
|
||||
|
||||
def __init__(self, identity_user_api_client=None):
|
||||
super(MFA_TokenAPI_Behaviors, self).__init__()
|
||||
self._client = identity_user_api_client
|
||||
self.config = TokenAPI_Config()
|
||||
|
||||
@behavior(MFA_TokenAPI_Client)
|
||||
def get_access_data(self, username=None, password=None,
|
||||
tenant_id=None, passcode=None):
|
||||
username = username or self.config.username
|
||||
password = password or self.config.password
|
||||
tenant_id = tenant_id or self.config.tenant_id
|
||||
passcode = passcode or 'bypassed_passcode'
|
||||
|
||||
access_data = None
|
||||
if username is not None and password is not None:
|
||||
# Make first call to get session ID
|
||||
response = self._client.authenticate(
|
||||
username=username, password=password, tenant_id=tenant_id)
|
||||
session_id = self._get_session_id(response)
|
||||
|
||||
# Make 2nd call with passcode + session ID to get token and catalog
|
||||
response = self._client.authenticate_passcode(
|
||||
session_id=session_id, passcode=passcode)
|
||||
access_data = response.entity
|
||||
return access_data
|
||||
|
||||
@classmethod
|
||||
def _get_session_id(cls, response):
|
||||
"""
|
||||
Finds specific response header, and parses the session id out of the
|
||||
header value
|
||||
|
||||
:param response: raw request response
|
||||
:return: session_id (String)
|
||||
"""
|
||||
|
||||
session_header = 'www-authenticate'
|
||||
session_id = None
|
||||
if session_header in response.headers:
|
||||
session_id = response.headers[session_header].split("=")[1]
|
||||
session_id = session_id[
|
||||
session_id.find("'") + 1: session_id.rfind("'")]
|
||||
return session_id
|
||||
|
|
|
@ -21,6 +21,9 @@ from cloudcafe.extensions.rax_auth.v2_0.tokens_api.models.responses. \
|
|||
access import Access as AuthResponse
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.models.requests. \
|
||||
credentials import ApiKeyCredentials
|
||||
from cloudcafe.identity.v2_0.models import requests
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.models.requests.passcode \
|
||||
import PasscodeCredentials
|
||||
|
||||
_version = 'v2.0'
|
||||
_tokens = 'tokens'
|
||||
|
@ -63,15 +66,17 @@ class TokenAPI_Client(BaseTokenAPI_Client):
|
|||
def authenticate(self, username, api_key, tenant_id,
|
||||
requestslib_kwargs=None):
|
||||
|
||||
'''
|
||||
"""
|
||||
@summary: Creates authentication using Username and password.
|
||||
@param username: The username of the customer.
|
||||
@type username: String
|
||||
@param api_key: The user password.
|
||||
@type api_key: String
|
||||
@param tenant_id: The id of the tenant
|
||||
@type tenant_id: String
|
||||
@return: Response Object containing auth response
|
||||
@rtype: Response Object
|
||||
'''
|
||||
"""
|
||||
|
||||
credentials = ApiKeyCredentials(
|
||||
username=username,
|
||||
|
@ -84,3 +89,60 @@ class TokenAPI_Client(BaseTokenAPI_Client):
|
|||
request_entity=auth_request_entity,
|
||||
requestslib_kwargs=requestslib_kwargs)
|
||||
return response
|
||||
|
||||
|
||||
class MFA_TokenAPI_Client(BaseTokenAPI_Client):
|
||||
def __init__(self, url, serialize_format, deserialize_format=None,
|
||||
auth_token=None):
|
||||
super(MFA_TokenAPI_Client, self).__init__(
|
||||
serialize_format, deserialize_format)
|
||||
self.base_url = '{0}/{1}'.format(url, _version)
|
||||
self.default_headers['Content-Type'] = 'application/{0}'.format(
|
||||
serialize_format)
|
||||
self.default_headers['Accept'] = 'application/{0}'.format(
|
||||
serialize_format)
|
||||
|
||||
if auth_token is not None:
|
||||
self.default_headers['X-Auth-Token'] = auth_token
|
||||
|
||||
def authenticate(self, username, password, tenant_id,
|
||||
requestslib_kwargs=None):
|
||||
"""
|
||||
@summary: Authenticates using username and password.
|
||||
@param username: The username of the customer.
|
||||
@type username: String
|
||||
@param password: The user password.
|
||||
@type password: String
|
||||
@param tenant_id: The id of the tenant
|
||||
@type tenant_id: String
|
||||
@return: Response Object containing auth response
|
||||
@rtype: Response Object
|
||||
"""
|
||||
|
||||
request_entity = requests.Auth(
|
||||
username=username, password=password, tenant_id=tenant_id)
|
||||
url = '{0}/{1}'.format(self.base_url, _tokens)
|
||||
return self.post(url, request_entity=request_entity,
|
||||
requestslib_kwargs=requestslib_kwargs)
|
||||
|
||||
def authenticate_passcode(self, session_id, passcode,
|
||||
requestslib_kwargs=None):
|
||||
"""
|
||||
@summary: Authenticates using session id and passcode.
|
||||
@param session_id: The session id from the first auth response.
|
||||
@type session_id: string
|
||||
@param passcode: The passcode that is sent to duo for validation.
|
||||
@type passcode: string
|
||||
@return: Response Object containing auth response
|
||||
@rtype: Response Object
|
||||
"""
|
||||
|
||||
credentials = PasscodeCredentials(passcode=passcode)
|
||||
auth_request_entity = AuthRequest(passcodeCredentials=credentials)
|
||||
headers = {'X-SessionId': session_id}
|
||||
url = '{0}/{1}'.format(self.base_url, _tokens)
|
||||
return self.post(url, headers=headers,
|
||||
response_entity_type=AuthResponse,
|
||||
request_entity=auth_request_entity,
|
||||
requestslib_kwargs=requestslib_kwargs)
|
||||
|
||||
|
|
|
@ -23,28 +23,36 @@ from cloudcafe.extensions.rax_auth.v2_0.tokens_api.models.constants \
|
|||
import V2_0Constants
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.models.requests. \
|
||||
credentials import ApiKeyCredentials
|
||||
from cloudcafe.extensions.rax_auth.v2_0.tokens_api.models.requests. \
|
||||
passcode import PasscodeCredentials
|
||||
|
||||
|
||||
class Auth(BaseIdentityModel):
|
||||
|
||||
ROOT_TAG = 'auth'
|
||||
|
||||
def __init__(self, apiKeyCredentials=None,
|
||||
def __init__(self, apiKeyCredentials=None, passcodeCredentials=None,
|
||||
tenantId=None, token=None):
|
||||
super(Auth, self).__init__()
|
||||
self.apiKeyCredentials = apiKeyCredentials
|
||||
self.passcode_credentials = passcodeCredentials
|
||||
self.token = token
|
||||
self.tenantId = tenantId
|
||||
|
||||
def _obj_to_json(self):
|
||||
ret = {}
|
||||
|
||||
ret[ApiKeyCredentials.JSON_ROOT_TAG] = \
|
||||
self.apiKeyCredentials._obj_to_dict()
|
||||
if self.token is not None:
|
||||
ret[Token.ROOT_TAG] = self.token._obj_to_dict()
|
||||
if self.tenantId is not None:
|
||||
ret['tenantId'] = self.tenantId
|
||||
if self.apiKeyCredentials is not None:
|
||||
ret[ApiKeyCredentials.JSON_ROOT_TAG] = \
|
||||
self.apiKeyCredentials._obj_to_dict()
|
||||
if self.token is not None:
|
||||
ret[Token.ROOT_TAG] = self.token._obj_to_dict()
|
||||
if self.tenantId is not None:
|
||||
ret['tenantId'] = self.tenantId
|
||||
else:
|
||||
ret[PasscodeCredentials.RAW_NAME] = \
|
||||
self.passcode_credentials._obj_to_dict()
|
||||
|
||||
ret = {self.ROOT_TAG: ret}
|
||||
return json.dumps(ret)
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from cloudcafe.identity.v2_0.common.models.base import BaseIdentityModel
|
||||
from cloudcafe.identity.v2_0.common.models.constants import V2_0Constants
|
||||
|
||||
|
||||
class PasscodeCredentials(BaseIdentityModel):
|
||||
PREFIX = 'RAX-AUTH:'
|
||||
ROOT_TAG = 'passcodeCredentials'
|
||||
PASSCODE_KEY = 'passcode'
|
||||
RAW_NAME = '{0}{1}'.format(PREFIX, ROOT_TAG)
|
||||
|
||||
rax_auth_xmlns = V2_0Constants.XML_NS_RAX_AUTH
|
||||
|
||||
def __init__(self, passcode=''):
|
||||
super(PasscodeCredentials, self).__init__()
|
||||
self.passcode = '' if passcode is None else str(passcode)
|
||||
|
||||
def _obj_to_dict(self, remove_root=True):
|
||||
attrs = {self.PASSCODE_KEY: self.passcode}
|
||||
return attrs if remove_root else {self.RAW_NAME: attrs}
|
||||
|
||||
def _obj_to_xml_ele(self):
|
||||
element = ET.Element(
|
||||
"{{{0}}}{1}".format(self.rax_auth_xmlns, self.ROOT_TAG))
|
||||
return self._set_xml_etree_element(element,
|
||||
{self.PASSCODE_KEY: self.passcode})
|
||||
|
||||
@classmethod
|
||||
def _xml_ele_to_obj(cls, element):
|
||||
if element is None:
|
||||
return None
|
||||
return cls(passcode=element.attrib.get(cls.PASSCODE_KEY))
|
||||
|
||||
@classmethod
|
||||
def _dict_to_obj(cls, data):
|
||||
if data is None:
|
||||
return None
|
||||
return cls(passcode=data.get(cls.PASSCODE_KEY))
|
||||
|
||||
@classmethod
|
||||
def _json_to_obj(cls, serialized_str):
|
||||
data = json.loads(serialized_str)
|
||||
return cls._dict_to_obj(data.get(cls.PREFIX + cls.ROOT_TAG))
|
Loading…
Reference in New Issue