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:
Christopher Hunt 2014-10-27 11:14:23 -05:00
parent 739ad6465c
commit f5754b9e5e
6 changed files with 219 additions and 16 deletions

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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))