Skip to content

Commit 170418c

Browse files
Merge pull request #1447 from allmightyspiff/issues1315
IBMid Authentication support
2 parents ab1ede5 + e20ceea commit 170418c

File tree

8 files changed

+561
-74
lines changed

8 files changed

+561
-74
lines changed

SoftLayer/API.py

Lines changed: 224 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,25 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
# pylint: disable=invalid-name
9+
import time
910
import warnings
1011

12+
import json
13+
import logging
14+
import requests
15+
1116

1217
from SoftLayer import auth as slauth
1318
from SoftLayer import config
1419
from SoftLayer import consts
20+
from SoftLayer import exceptions
1521
from SoftLayer import transports
1622

23+
LOGGER = logging.getLogger(__name__)
1724
API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT
1825
API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT
26+
CONFIG_FILE = consts.CONFIG_FILE
27+
1928
__all__ = [
2029
'create_client_from_env',
2130
'Client',
@@ -80,6 +89,8 @@ def create_client_from_env(username=None,
8089
'Your Company'
8190
8291
"""
92+
if config_file is None:
93+
config_file = CONFIG_FILE
8394
settings = config.get_client_settings(username=username,
8495
api_key=api_key,
8596
endpoint_url=endpoint_url,
@@ -127,7 +138,7 @@ def create_client_from_env(username=None,
127138
settings.get('api_key'),
128139
)
129140

130-
return BaseClient(auth=auth, transport=transport)
141+
return BaseClient(auth=auth, transport=transport, config_file=config_file)
131142

132143

133144
def Client(**kwargs):
@@ -150,8 +161,35 @@ class BaseClient(object):
150161

151162
_prefix = "SoftLayer_"
152163

153-
def __init__(self, auth=None, transport=None):
164+
def __init__(self, auth=None, transport=None, config_file=None):
165+
if config_file is None:
166+
config_file = CONFIG_FILE
154167
self.auth = auth
168+
self.config_file = config_file
169+
self.settings = config.get_config(self.config_file)
170+
171+
if transport is None:
172+
url = self.settings['softlayer'].get('endpoint_url')
173+
if url is not None and '/rest' in url:
174+
# If this looks like a rest endpoint, use the rest transport
175+
transport = transports.RestTransport(
176+
endpoint_url=url,
177+
proxy=self.settings['softlayer'].get('proxy'),
178+
# prevents an exception incase timeout is a float number.
179+
timeout=int(self.settings['softlayer'].getfloat('timeout')),
180+
user_agent=consts.USER_AGENT,
181+
verify=self.settings['softlayer'].getboolean('verify'),
182+
)
183+
else:
184+
# Default the transport to use XMLRPC
185+
transport = transports.XmlRpcTransport(
186+
endpoint_url=url,
187+
proxy=self.settings['softlayer'].get('proxy'),
188+
timeout=int(self.settings['softlayer'].getfloat('timeout')),
189+
user_agent=consts.USER_AGENT,
190+
verify=self.settings['softlayer'].getboolean('verify'),
191+
)
192+
155193
self.transport = transport
156194

157195
def authenticate_with_password(self, username, password,
@@ -322,6 +360,190 @@ def __len__(self):
322360
return 0
323361

324362

363+
class IAMClient(BaseClient):
364+
"""IBM ID Client for using IAM authentication
365+
366+
:param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
367+
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
368+
"""
369+
370+
def authenticate_with_password(self, username, password, security_question_id=None, security_question_answer=None):
371+
"""Performs IBM IAM Username/Password Authentication
372+
373+
:param string username: your IBMid username
374+
:param string password: your IBMid password
375+
"""
376+
377+
iam_client = requests.Session()
378+
379+
headers = {
380+
'Content-Type': 'application/x-www-form-urlencoded',
381+
'User-Agent': consts.USER_AGENT,
382+
'Accept': 'application/json'
383+
}
384+
data = {
385+
'grant_type': 'password',
386+
'password': password,
387+
'response_type': 'cloud_iam',
388+
'username': username
389+
}
390+
391+
try:
392+
response = iam_client.request(
393+
'POST',
394+
'https://iam.cloud.ibm.com/identity/token',
395+
data=data,
396+
headers=headers,
397+
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
398+
)
399+
if response.status_code != 200:
400+
LOGGER.error("Unable to login: %s", response.text)
401+
402+
response.raise_for_status()
403+
tokens = json.loads(response.text)
404+
except requests.HTTPError as ex:
405+
error = json.loads(response.text)
406+
raise exceptions.IAMError(response.status_code,
407+
error.get('errorMessage'),
408+
'https://iam.cloud.ibm.com/identity/token') from ex
409+
410+
self.settings['softlayer']['access_token'] = tokens['access_token']
411+
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
412+
413+
config.write_config(self.settings, self.config_file)
414+
self.auth = slauth.BearerAuthentication('', tokens['access_token'], tokens['refresh_token'])
415+
416+
return tokens
417+
418+
def authenticate_with_passcode(self, passcode):
419+
"""Performs IBM IAM SSO Authentication
420+
421+
:param string passcode: your IBMid password
422+
"""
423+
424+
iam_client = requests.Session()
425+
426+
headers = {
427+
'Content-Type': 'application/x-www-form-urlencoded',
428+
'User-Agent': consts.USER_AGENT,
429+
'Accept': 'application/json'
430+
}
431+
data = {
432+
'grant_type': 'urn:ibm:params:oauth:grant-type:passcode',
433+
'passcode': passcode,
434+
'response_type': 'cloud_iam'
435+
}
436+
437+
try:
438+
response = iam_client.request(
439+
'POST',
440+
'https://iam.cloud.ibm.com/identity/token',
441+
data=data,
442+
headers=headers,
443+
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
444+
)
445+
if response.status_code != 200:
446+
LOGGER.error("Unable to login: %s", response.text)
447+
448+
response.raise_for_status()
449+
tokens = json.loads(response.text)
450+
451+
except requests.HTTPError as ex:
452+
error = json.loads(response.text)
453+
raise exceptions.IAMError(response.status_code,
454+
error.get('errorMessage'),
455+
'https://iam.cloud.ibm.com/identity/token') from ex
456+
457+
self.settings['softlayer']['access_token'] = tokens['access_token']
458+
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
459+
a_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['expiration']))
460+
r_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['refresh_token_expiration']))
461+
LOGGER.warning("Tokens retrieved, expires at %s, Refresh expires at %s", a_expire, r_expire)
462+
config.write_config(self.settings, self.config_file)
463+
self.auth = slauth.BearerAuthentication('', tokens['access_token'], tokens['refresh_token'])
464+
465+
return tokens
466+
467+
def authenticate_with_iam_token(self, a_token, r_token=None):
468+
"""Authenticates to the SL API with an IAM Token
469+
470+
:param string a_token: Access token
471+
:param string r_token: Refresh Token, to be used if Access token is expired.
472+
"""
473+
self.auth = slauth.BearerAuthentication('', a_token, r_token)
474+
475+
def refresh_iam_token(self, r_token, account_id=None, ims_account=None):
476+
"""Refreshes the IAM Token, will default to values in the config file"""
477+
iam_client = requests.Session()
478+
479+
headers = {
480+
'Content-Type': 'application/x-www-form-urlencoded',
481+
'User-Agent': consts.USER_AGENT,
482+
'Accept': 'application/json'
483+
}
484+
data = {
485+
'grant_type': 'refresh_token',
486+
'refresh_token': r_token,
487+
'response_type': 'cloud_iam'
488+
}
489+
490+
sl_config = self.settings['softlayer']
491+
492+
if account_id is None and sl_config.get('account_id', False):
493+
account_id = sl_config.get('account_id')
494+
if ims_account is None and sl_config.get('ims_account', False):
495+
ims_account = sl_config.get('ims_account')
496+
497+
data['account'] = account_id
498+
data['ims_account'] = ims_account
499+
500+
try:
501+
response = iam_client.request(
502+
'POST',
503+
'https://iam.cloud.ibm.com/identity/token',
504+
data=data,
505+
headers=headers,
506+
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
507+
)
508+
509+
if response.status_code != 200:
510+
LOGGER.warning("Unable to refresh IAM Token. %s", response.text)
511+
512+
response.raise_for_status()
513+
tokens = json.loads(response.text)
514+
515+
except requests.HTTPError as ex:
516+
error = json.loads(response.text)
517+
raise exceptions.IAMError(response.status_code,
518+
error.get('errorMessage'),
519+
'https://iam.cloud.ibm.com/identity/token') from ex
520+
521+
a_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['expiration']))
522+
r_expire = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(tokens['refresh_token_expiration']))
523+
LOGGER.warning("Tokens retrieved, expires at %s, Refresh expires at %s", a_expire, r_expire)
524+
525+
self.settings['softlayer']['access_token'] = tokens['access_token']
526+
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
527+
config.write_config(self.settings, self.config_file)
528+
self.auth = slauth.BearerAuthentication('', tokens['access_token'])
529+
return tokens
530+
531+
def call(self, service, method, *args, **kwargs):
532+
"""Handles refreshing IAM tokens in case of a HTTP 401 error"""
533+
try:
534+
return super().call(service, method, *args, **kwargs)
535+
except exceptions.SoftLayerAPIError as ex:
536+
537+
if ex.faultCode == 401:
538+
LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString)
539+
return ex
540+
else:
541+
raise ex
542+
543+
def __repr__(self):
544+
return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth)
545+
546+
325547
class Service(object):
326548
"""A SoftLayer Service.
327549

0 commit comments

Comments
 (0)