Skip to content

Commit

Permalink
softlayer#1315 basic IAM authentication support, and slcli login func…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
allmightyspiff committed Mar 22, 2021
1 parent 0ab9c0d commit 9f5abad
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 3 deletions.
163 changes: 160 additions & 3 deletions SoftLayer/API.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@
:license: MIT, see LICENSE for more details.
"""
# pylint: disable=invalid-name
import json
import logging
import requests
import warnings


from SoftLayer import auth as slauth
from SoftLayer import config
from SoftLayer import consts
from SoftLayer import exceptions
from SoftLayer import transports


LOGGER = logging.getLogger(__name__)
API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT
API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT
CONFIG_FILE = consts.CONFIG_FILE

__all__ = [
'create_client_from_env',
'Client',
Expand Down Expand Up @@ -80,6 +88,8 @@ def create_client_from_env(username=None,
'Your Company'
"""
if config_file is None:
config_file = CONFIG_FILE
settings = config.get_client_settings(username=username,
api_key=api_key,
endpoint_url=endpoint_url,
Expand Down Expand Up @@ -127,7 +137,7 @@ def create_client_from_env(username=None,
settings.get('api_key'),
)

return BaseClient(auth=auth, transport=transport)
return BaseClient(auth=auth, transport=transport, config_file=config_file)


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

_prefix = "SoftLayer_"

def __init__(self, auth=None, transport=None):
def __init__(self, auth=None, transport=None, config_file=None):
if config_file is None:
config_file = CONFIG_FILE
self.auth = auth
self.transport = transport
self.config_file = config_file
self.settings = config.get_config(self.config_file)

if transport is None:
url = self.settings['softlayer'].get('endpoint_url')
if url is not None and '/rest' in url:
# If this looks like a rest endpoint, use the rest transport
transport = transports.RestTransport(
endpoint_url=url,
proxy=self.settings['softlayer'].get('proxy'),
timeout=self.settings['softlayer'].getint('timeout'),
user_agent=consts.USER_AGENT,
verify=self.settings['softlayer'].getboolean('verify'),
)
else:
# Default the transport to use XMLRPC
transport = transports.XmlRpcTransport(
endpoint_url=url,
proxy=self.settings['softlayer'].get('proxy'),
timeout=self.settings['softlayer'].getint('timeout'),
user_agent=consts.USER_AGENT,
verify=self.settings['softlayer'].getboolean('verify'),
)

self.transport = transport

def authenticate_with_password(self, username, password,
security_question_id=None,
Expand Down Expand Up @@ -321,6 +357,127 @@ def __repr__(self):
def __len__(self):
return 0

class IAMClient(BaseClient):
"""IBM ID Client for using IAM authentication
:param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
"""


def authenticate_with_password(self, username, password):
"""Performs IBM IAM Username/Password Authentication
:param string username: your IBMid username
:param string password: your IBMid password
"""

iam_client = requests.Session()

headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': consts.USER_AGENT,
'Accept': 'application/json'
}
data = {
'grant_type': 'password',
'password': password,
'response_type': 'cloud_iam',
'username': username
}

response = iam_client.request(
'POST',
'https://iam.cloud.ibm.com/identity/token',
data=data,
headers=headers,
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
)
if response.status_code != 200:
LOGGER.error("Unable to login: {}".format(response.text))

response.raise_for_status()

tokens = json.loads(response.text)
self.settings['softlayer']['access_token'] = tokens['access_token']
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']

config.write_config(self.settings)
self.auth = slauth.BearerAuthentication('', tokens['access_token'], tokens['refresh_token'])
return tokens

def authenticate_with_iam_token(self, a_token, r_token):
"""Authenticates to the SL API with an IAM Token
:param string a_token: Access token
:param string r_token: Refresh Token, to be used if Access token is expired.
"""
self.auth = slauth.BearerAuthentication('', a_token)
user = None
try:
user = self.call('Account', 'getCurrentUser')
except exceptions.SoftLayerAPIError as ex:
if ex.faultCode == 401:
LOGGER.warning("Token has expired, trying to refresh.")
# self.refresh_iam_token(r_token)
else:
raise ex
return user

def refresh_iam_token(self, r_token):
iam_client = requests.Session()

headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': consts.USER_AGENT,
'Accept': 'application/json'
}
data = {
'grant_type': 'refresh_token',
'refresh_token': r_token,
'response_type': 'cloud_iam'
}

config = self.settings.get('softlayer')
if config.get('account', False):
data['account'] = account
if config.get('ims_account', False):
data['ims_account'] = ims_account

response = iam_client.request(
'POST',
'https://iam.cloud.ibm.com/identity/token',
data=data,
headers=headers,
auth=requests.auth.HTTPBasicAuth('bx', 'bx')
)
response.raise_for_status()

LOGGER.warning("Successfully refreshed Tokens, saving to config")
tokens = json.loads(response.text)
self.settings['softlayer']['access_token'] = tokens['access_token']
self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
config.write_config(self.settings)
return tokens



def call(self, service, method, *args, **kwargs):
"""Handles refreshing IAM tokens in case of a HTTP 401 error"""
try:
return super().call(service, method, *args, **kwargs)
except exceptions.SoftLayerAPIError as ex:
if ex.faultCode == 401:
LOGGER.warning("Token has expired, trying to refresh.")
self.refresh_iam_token(r_token)
return super().call(service, method, *args, **kwargs)
else:
raise ex


def __repr__(self):
return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth)


class Service(object):
"""A SoftLayer Service.
Expand Down
96 changes: 96 additions & 0 deletions SoftLayer/CLI/config/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Gets a temporary token for a user"""
# :license: MIT, see LICENSE for more details.
import configparser
import os.path


import click
import json
import requests

import SoftLayer
from SoftLayer import config
from SoftLayer.CLI import environment
from SoftLayer.CLI import exceptions
from SoftLayer.CLI import formatting
from SoftLayer.consts import USER_AGENT
from SoftLayer import utils


@click.command()
@environment.pass_env
def cli(env):

email = env.input("Email:")
password = env.getpass("Password:")

account_id = ''
ims_id = ''
print("ENV CONFIG FILE IS {}".format(env.config_file))
sl_config = config.get_config(env.config_file)
tokens = {'access_token': sl_config['softlayer']['access_token'], 'refresh_token': sl_config['softlayer']['refresh_token']}
client = SoftLayer.API.IAMClient(config_file=env.config_file)
user = client.authenticate_with_iam_token(tokens['access_token'], tokens['refresh_token'])
print(user)
# tokens = client.authenticate_with_password(email, password)

# tokens = login(email, password)
# print(tokens)



accounts = get_accounts(tokens['access_token'])
print(accounts)

# if accounts.get('total_results', 0) == 1:
# selected = accounts['resources'][0]
# account_id = utils.lookup(selected, 'metadata', 'guid')
# ims_id = None
# for links in utils.lookup(selected, 'metadata', 'linked_accounts'):
# if links.get('origin') == "IMS":
# ims_id = links.get('id')

# print("Using account {}".format(utils.lookup(selected, 'entity', 'name')))
# tokens = refresh_token(tokens['refresh_token'], account_id, ims_id)
# print(tokens)

# print("Saving Tokens...")


for key in sl_config['softlayer']:
print("{} = {} ".format(key, sl_config['softlayer'][key]))

# sl_config['softlayer']['access_token'] = tokens['access_token']
# sl_config['softlayer']['refresh_token'] = tokens['refresh_token']
# sl_config['softlayer']['ims_account'] = ims_id
# sl_config['softlayer']['account_id'] = account_id
# config.write_config(sl_config, env.config_file)
# print(sl_config)

# print("Email: {}, Password: {}".format(email, password))

print("Checking for an API key")

user = client.call('SoftLayer_Account', 'getCurrentUser')
print(user)



def get_accounts(a_token):
"""Gets account list from accounts.cloud.ibm.com/v1/accounts"""
iam_client = requests.Session()

headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'Accept': 'application/json'
}
headers['Authorization'] = 'Bearer {}'.format(a_token)
response = iam_client.request(
'GET',
'https://accounts.cloud.ibm.com/v1/accounts',
headers=headers
)

response.raise_for_status()
return json.loads(response.text)
1 change: 1 addition & 0 deletions SoftLayer/CLI/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
('config:setup', 'SoftLayer.CLI.config.setup:cli'),
('config:show', 'SoftLayer.CLI.config.show:cli'),
('setup', 'SoftLayer.CLI.config.setup:cli'),
('login', 'SoftLayer.CLI.config.login:cli'),

('dns', 'SoftLayer.CLI.dns'),
('dns:import', 'SoftLayer.CLI.dns.zone_import:cli'),
Expand Down
1 change: 1 addition & 0 deletions SoftLayer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
__copyright__ = 'Copyright 2016 SoftLayer Technologies, Inc.'
__all__ = [ # noqa: F405
'BaseClient',
'IAMClient',
'create_client_from_env',
'Client',
'BasicAuthentication',
Expand Down
29 changes: 29 additions & 0 deletions SoftLayer/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"""
# pylint: disable=no-self-use

from SoftLayer import config

__all__ = [
'BasicAuthentication',
'TokenAuthentication',
Expand Down Expand Up @@ -109,3 +111,30 @@ def get_request(self, request):

def __repr__(self):
return "BasicHTTPAuthentication(username=%r)" % self.username

class BearerAuthentication(AuthenticationBase):
"""Bearer Token authentication class.
:param username str: a user's username, not really needed but all the others use it.
:param api_key str: a user's IAM Token
"""

def __init__(self, username, token, r_token=None):
"""For using IBM IAM authentication
:param username str: Not really needed, will be set to their current username though for logging
:param token str: the IAM Token
:param r_token str: The refresh Token, optional
"""
self.username = username
self.api_key = token
self.r_token = r_token

def get_request(self, request):
"""Sets token-based auth headers."""
request.transport_headers['Authorization'] = 'Bearer {}'.format(self.api_key)
request.transport_user = self.username
return request

def __repr__(self):
return "BearerAuthentication(username={}, token={})".format(self.username, self.api_key)
18 changes: 18 additions & 0 deletions SoftLayer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
:license: MIT, see LICENSE for more details.
"""
import configparser
import logging
import os
import os.path

LOGGER = logging.getLogger(__name__)

def get_client_settings_args(**kwargs):
"""Retrieve client settings from user-supplied arguments.
Expand Down Expand Up @@ -91,3 +93,19 @@ def get_client_settings(**kwargs):
all_settings = settings

return all_settings


def get_config(config_file=None):
if config_file is None:
config_file = '~/.softlayer'
config = configparser.ConfigParser()
config.read(os.path.expanduser(config_file))
return config

def write_config(configuration, config_file=None):
if config_file is None:
config_file = '~/.softlayer'
config_file = os.path.expanduser(config_file)
LOGGER.warning("Updating config file {} with new access tokens".format(config_file))
with open(config_file, 'w') as file:
configuration.write(file)

0 comments on commit 9f5abad

Please sign in to comment.