Skip to content

Commit 9f5abad

Browse files
#1315 basic IAM authentication support, and slcli login function
1 parent 0ab9c0d commit 9f5abad

File tree

7 files changed

+306
-3
lines changed

7 files changed

+306
-3
lines changed

SoftLayer/API.py

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
# pylint: disable=invalid-name
9+
import json
10+
import logging
11+
import requests
912
import warnings
1013

1114

1215
from SoftLayer import auth as slauth
1316
from SoftLayer import config
1417
from SoftLayer import consts
18+
from SoftLayer import exceptions
1519
from SoftLayer import transports
1620

21+
22+
LOGGER = logging.getLogger(__name__)
1723
API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT
1824
API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT
25+
CONFIG_FILE = consts.CONFIG_FILE
26+
1927
__all__ = [
2028
'create_client_from_env',
2129
'Client',
@@ -80,6 +88,8 @@ def create_client_from_env(username=None,
8088
'Your Company'
8189
8290
"""
91+
if config_file is None:
92+
config_file = CONFIG_FILE
8393
settings = config.get_client_settings(username=username,
8494
api_key=api_key,
8595
endpoint_url=endpoint_url,
@@ -127,7 +137,7 @@ def create_client_from_env(username=None,
127137
settings.get('api_key'),
128138
)
129139

130-
return BaseClient(auth=auth, transport=transport)
140+
return BaseClient(auth=auth, transport=transport, config_file=config_file)
131141

132142

133143
def Client(**kwargs):
@@ -150,9 +160,35 @@ class BaseClient(object):
150160

151161
_prefix = "SoftLayer_"
152162

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

157193
def authenticate_with_password(self, username, password,
158194
security_question_id=None,
@@ -321,6 +357,127 @@ def __repr__(self):
321357
def __len__(self):
322358
return 0
323359

360+
class IAMClient(BaseClient):
361+
"""IBM ID Client for using IAM authentication
362+
363+
:param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
364+
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
365+
"""
366+
367+
368+
def authenticate_with_password(self, username, password):
369+
"""Performs IBM IAM Username/Password Authentication
370+
371+
:param string username: your IBMid username
372+
:param string password: your IBMid password
373+
"""
374+
375+
iam_client = requests.Session()
376+
377+
headers = {
378+
'Content-Type': 'application/x-www-form-urlencoded',
379+
'User-Agent': consts.USER_AGENT,
380+
'Accept': 'application/json'
381+
}
382+
data = {
383+
'grant_type': 'password',
384+
'password': password,
385+
'response_type': 'cloud_iam',
386+
'username': username
387+
}
388+
389+
response = iam_client.request(
390+
'POST',
391+
'https://iam.cloud.ibm.com/identity/token',
392+
data=data,
393+
headers=headers,
394+
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
395+
)
396+
if response.status_code != 200:
397+
LOGGER.error("Unable to login: {}".format(response.text))
398+
399+
response.raise_for_status()
400+
401+
tokens = json.loads(response.text)
402+
self.settings['softlayer']['access_token'] = tokens['access_token']
403+
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
404+
405+
config.write_config(self.settings)
406+
self.auth = slauth.BearerAuthentication('', tokens['access_token'], tokens['refresh_token'])
407+
return tokens
408+
409+
def authenticate_with_iam_token(self, a_token, r_token):
410+
"""Authenticates to the SL API with an IAM Token
411+
412+
:param string a_token: Access token
413+
:param string r_token: Refresh Token, to be used if Access token is expired.
414+
"""
415+
self.auth = slauth.BearerAuthentication('', a_token)
416+
user = None
417+
try:
418+
user = self.call('Account', 'getCurrentUser')
419+
except exceptions.SoftLayerAPIError as ex:
420+
if ex.faultCode == 401:
421+
LOGGER.warning("Token has expired, trying to refresh.")
422+
# self.refresh_iam_token(r_token)
423+
else:
424+
raise ex
425+
return user
426+
427+
def refresh_iam_token(self, r_token):
428+
iam_client = requests.Session()
429+
430+
headers = {
431+
'Content-Type': 'application/x-www-form-urlencoded',
432+
'User-Agent': consts.USER_AGENT,
433+
'Accept': 'application/json'
434+
}
435+
data = {
436+
'grant_type': 'refresh_token',
437+
'refresh_token': r_token,
438+
'response_type': 'cloud_iam'
439+
}
440+
441+
config = self.settings.get('softlayer')
442+
if config.get('account', False):
443+
data['account'] = account
444+
if config.get('ims_account', False):
445+
data['ims_account'] = ims_account
446+
447+
response = iam_client.request(
448+
'POST',
449+
'https://iam.cloud.ibm.com/identity/token',
450+
data=data,
451+
headers=headers,
452+
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
453+
)
454+
response.raise_for_status()
455+
456+
LOGGER.warning("Successfully refreshed Tokens, saving to config")
457+
tokens = json.loads(response.text)
458+
self.settings['softlayer']['access_token'] = tokens['access_token']
459+
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
460+
config.write_config(self.settings)
461+
return tokens
462+
463+
464+
465+
def call(self, service, method, *args, **kwargs):
466+
"""Handles refreshing IAM tokens in case of a HTTP 401 error"""
467+
try:
468+
return super().call(service, method, *args, **kwargs)
469+
except exceptions.SoftLayerAPIError as ex:
470+
if ex.faultCode == 401:
471+
LOGGER.warning("Token has expired, trying to refresh.")
472+
self.refresh_iam_token(r_token)
473+
return super().call(service, method, *args, **kwargs)
474+
else:
475+
raise ex
476+
477+
478+
def __repr__(self):
479+
return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth)
480+
324481

325482
class Service(object):
326483
"""A SoftLayer Service.

SoftLayer/CLI/config/login.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Gets a temporary token for a user"""
2+
# :license: MIT, see LICENSE for more details.
3+
import configparser
4+
import os.path
5+
6+
7+
import click
8+
import json
9+
import requests
10+
11+
import SoftLayer
12+
from SoftLayer import config
13+
from SoftLayer.CLI import environment
14+
from SoftLayer.CLI import exceptions
15+
from SoftLayer.CLI import formatting
16+
from SoftLayer.consts import USER_AGENT
17+
from SoftLayer import utils
18+
19+
20+
@click.command()
21+
@environment.pass_env
22+
def cli(env):
23+
24+
email = env.input("Email:")
25+
password = env.getpass("Password:")
26+
27+
account_id = ''
28+
ims_id = ''
29+
print("ENV CONFIG FILE IS {}".format(env.config_file))
30+
sl_config = config.get_config(env.config_file)
31+
tokens = {'access_token': sl_config['softlayer']['access_token'], 'refresh_token': sl_config['softlayer']['refresh_token']}
32+
client = SoftLayer.API.IAMClient(config_file=env.config_file)
33+
user = client.authenticate_with_iam_token(tokens['access_token'], tokens['refresh_token'])
34+
print(user)
35+
# tokens = client.authenticate_with_password(email, password)
36+
37+
# tokens = login(email, password)
38+
# print(tokens)
39+
40+
41+
42+
accounts = get_accounts(tokens['access_token'])
43+
print(accounts)
44+
45+
# if accounts.get('total_results', 0) == 1:
46+
# selected = accounts['resources'][0]
47+
# account_id = utils.lookup(selected, 'metadata', 'guid')
48+
# ims_id = None
49+
# for links in utils.lookup(selected, 'metadata', 'linked_accounts'):
50+
# if links.get('origin') == "IMS":
51+
# ims_id = links.get('id')
52+
53+
# print("Using account {}".format(utils.lookup(selected, 'entity', 'name')))
54+
# tokens = refresh_token(tokens['refresh_token'], account_id, ims_id)
55+
# print(tokens)
56+
57+
# print("Saving Tokens...")
58+
59+
60+
for key in sl_config['softlayer']:
61+
print("{} = {} ".format(key, sl_config['softlayer'][key]))
62+
63+
# sl_config['softlayer']['access_token'] = tokens['access_token']
64+
# sl_config['softlayer']['refresh_token'] = tokens['refresh_token']
65+
# sl_config['softlayer']['ims_account'] = ims_id
66+
# sl_config['softlayer']['account_id'] = account_id
67+
# config.write_config(sl_config, env.config_file)
68+
# print(sl_config)
69+
70+
# print("Email: {}, Password: {}".format(email, password))
71+
72+
print("Checking for an API key")
73+
74+
user = client.call('SoftLayer_Account', 'getCurrentUser')
75+
print(user)
76+
77+
78+
79+
def get_accounts(a_token):
80+
"""Gets account list from accounts.cloud.ibm.com/v1/accounts"""
81+
iam_client = requests.Session()
82+
83+
headers = {
84+
'Content-Type': 'application/x-www-form-urlencoded',
85+
'User-Agent': USER_AGENT,
86+
'Accept': 'application/json'
87+
}
88+
headers['Authorization'] = 'Bearer {}'.format(a_token)
89+
response = iam_client.request(
90+
'GET',
91+
'https://accounts.cloud.ibm.com/v1/accounts',
92+
headers=headers
93+
)
94+
95+
response.raise_for_status()
96+
return json.loads(response.text)

SoftLayer/CLI/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
('config:setup', 'SoftLayer.CLI.config.setup:cli'),
7171
('config:show', 'SoftLayer.CLI.config.show:cli'),
7272
('setup', 'SoftLayer.CLI.config.setup:cli'),
73+
('login', 'SoftLayer.CLI.config.login:cli'),
7374

7475
('dns', 'SoftLayer.CLI.dns'),
7576
('dns:import', 'SoftLayer.CLI.dns.zone_import:cli'),

SoftLayer/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
__copyright__ = 'Copyright 2016 SoftLayer Technologies, Inc.'
3232
__all__ = [ # noqa: F405
3333
'BaseClient',
34+
'IAMClient',
3435
'create_client_from_env',
3536
'Client',
3637
'BasicAuthentication',

SoftLayer/auth.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"""
88
# pylint: disable=no-self-use
99

10+
from SoftLayer import config
11+
1012
__all__ = [
1113
'BasicAuthentication',
1214
'TokenAuthentication',
@@ -109,3 +111,30 @@ def get_request(self, request):
109111

110112
def __repr__(self):
111113
return "BasicHTTPAuthentication(username=%r)" % self.username
114+
115+
class BearerAuthentication(AuthenticationBase):
116+
"""Bearer Token authentication class.
117+
118+
:param username str: a user's username, not really needed but all the others use it.
119+
:param api_key str: a user's IAM Token
120+
"""
121+
122+
def __init__(self, username, token, r_token=None):
123+
"""For using IBM IAM authentication
124+
125+
:param username str: Not really needed, will be set to their current username though for logging
126+
:param token str: the IAM Token
127+
:param r_token str: The refresh Token, optional
128+
"""
129+
self.username = username
130+
self.api_key = token
131+
self.r_token = r_token
132+
133+
def get_request(self, request):
134+
"""Sets token-based auth headers."""
135+
request.transport_headers['Authorization'] = 'Bearer {}'.format(self.api_key)
136+
request.transport_user = self.username
137+
return request
138+
139+
def __repr__(self):
140+
return "BearerAuthentication(username={}, token={})".format(self.username, self.api_key)

SoftLayer/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
import configparser
9+
import logging
910
import os
1011
import os.path
1112

13+
LOGGER = logging.getLogger(__name__)
1214

1315
def get_client_settings_args(**kwargs):
1416
"""Retrieve client settings from user-supplied arguments.
@@ -91,3 +93,19 @@ def get_client_settings(**kwargs):
9193
all_settings = settings
9294

9395
return all_settings
96+
97+
98+
def get_config(config_file=None):
99+
if config_file is None:
100+
config_file = '~/.softlayer'
101+
config = configparser.ConfigParser()
102+
config.read(os.path.expanduser(config_file))
103+
return config
104+
105+
def write_config(configuration, config_file=None):
106+
if config_file is None:
107+
config_file = '~/.softlayer'
108+
config_file = os.path.expanduser(config_file)
109+
LOGGER.warning("Updating config file {} with new access tokens".format(config_file))
110+
with open(config_file, 'w') as file:
111+
configuration.write(file)

0 commit comments

Comments
 (0)