From 9f5abad79d7b5de168cc8a0c67bb439d47ff2384 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 22 Mar 2021 17:16:27 -0500 Subject: [PATCH] #1315 basic IAM authentication support, and slcli login function --- SoftLayer/API.py | 163 +++++++++++++++++++++++++++++++++- SoftLayer/CLI/config/login.py | 96 ++++++++++++++++++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/__init__.py | 1 + SoftLayer/auth.py | 29 ++++++ SoftLayer/config.py | 18 ++++ SoftLayer/consts.py | 1 + 7 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 SoftLayer/CLI/config/login.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 430ed2d14..81e667817 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -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', @@ -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, @@ -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): @@ -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, @@ -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. diff --git a/SoftLayer/CLI/config/login.py b/SoftLayer/CLI/config/login.py new file mode 100644 index 000000000..546ce6fdb --- /dev/null +++ b/SoftLayer/CLI/config/login.py @@ -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) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index c223bfae2..070f32c42 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -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'), diff --git a/SoftLayer/__init__.py b/SoftLayer/__init__.py index 9e14ea38e..30dd65774 100644 --- a/SoftLayer/__init__.py +++ b/SoftLayer/__init__.py @@ -31,6 +31,7 @@ __copyright__ = 'Copyright 2016 SoftLayer Technologies, Inc.' __all__ = [ # noqa: F405 'BaseClient', + 'IAMClient', 'create_client_from_env', 'Client', 'BasicAuthentication', diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 4046937e6..c2d22168e 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -7,6 +7,8 @@ """ # pylint: disable=no-self-use +from SoftLayer import config + __all__ = [ 'BasicAuthentication', 'TokenAuthentication', @@ -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) \ No newline at end of file diff --git a/SoftLayer/config.py b/SoftLayer/config.py index d008893f0..3caebde0f 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -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. @@ -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) \ No newline at end of file diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index b10b164d0..71c52be75 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -11,3 +11,4 @@ API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' API_PRIVATE_ENDPOINT_REST = 'https://api.service.softlayer.com/rest/v3.1/' USER_AGENT = "softlayer-python/%s" % VERSION +CONFIG_FILE = "~/.softlayer"