77"""
88# pylint: disable=invalid-name
99import time
10- import warnings
1110
1211import concurrent .futures as cf
1312import json
2827
2928__all__ = [
3029 'create_client_from_env' ,
30+ 'employee_client' ,
3131 'Client' ,
3232 'BaseClient' ,
3333 'API_PUBLIC_ENDPOINT' ,
3434 'API_PRIVATE_ENDPOINT' ,
3535 'IAMClient' ,
36+ 'CertificateClient'
3637]
3738
3839VALID_CALL_ARGS = set ((
@@ -143,33 +144,112 @@ def create_client_from_env(username=None,
143144 return BaseClient (auth = auth , transport = transport , config_file = config_file )
144145
145146
146- def Client (** kwargs ):
147- """Get a SoftLayer API Client using environmental settings.
147+ def employee_client (username = None ,
148+ access_token = None ,
149+ endpoint_url = None ,
150+ timeout = None ,
151+ auth = None ,
152+ config_file = None ,
153+ proxy = None ,
154+ user_agent = None ,
155+ transport = None ,
156+ verify = True ):
157+ """Creates an INTERNAL SoftLayer API client using your environment.
158+
159+ Settings are loaded via keyword arguments, environemtal variables and config file.
148160
149- Deprecated in favor of create_client_from_env()
161+ :param username: your user ID
162+ :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token)
163+ :param password: password to use for employee authentication
164+ :param endpoint_url: the API endpoint base URL you wish to connect to.
165+ Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network.
166+ :param proxy: proxy to be used to make API calls
167+ :param integer timeout: timeout for API requests
168+ :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers.
169+ Example: `BasicAuthentication`
170+ :param config_file: A path to a configuration file used to load settings
171+ :param user_agent: an optional User Agent to report when making API
172+ calls if you wish to bypass the packages built in User Agent string
173+ :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
174+ :param bool verify: decide to verify the server's SSL/TLS cert.
150175 """
151- warnings .warn ("use SoftLayer.create_client_from_env() instead" ,
152- DeprecationWarning )
176+ settings = config .get_client_settings (username = username ,
177+ api_key = None ,
178+ endpoint_url = endpoint_url ,
179+ timeout = timeout ,
180+ proxy = proxy ,
181+ verify = None ,
182+ config_file = config_file )
183+
184+ url = settings .get ('endpoint_url' )
185+ verify = settings .get ('verify' , True )
186+
187+ if 'internal' not in url :
188+ raise exceptions .SoftLayerError (f"{ url } does not look like an Internal Employee url." )
189+
190+ if transport is None :
191+ if url is not None and '/rest' in url :
192+ # If this looks like a rest endpoint, use the rest transport
193+ transport = transports .RestTransport (
194+ endpoint_url = settings .get ('endpoint_url' ),
195+ proxy = settings .get ('proxy' ),
196+ timeout = settings .get ('timeout' ),
197+ user_agent = user_agent ,
198+ verify = verify ,
199+ )
200+ else :
201+ # Default the transport to use XMLRPC
202+ transport = transports .XmlRpcTransport (
203+ endpoint_url = settings .get ('endpoint_url' ),
204+ proxy = settings .get ('proxy' ),
205+ timeout = settings .get ('timeout' ),
206+ user_agent = user_agent ,
207+ verify = verify ,
208+ )
209+
210+ if access_token is None :
211+ access_token = settings .get ('access_token' )
212+
213+ user_id = settings .get ('userid' )
214+
215+ # Assume access_token is valid for now, user has logged in before at least.
216+ if access_token and user_id :
217+ auth = slauth .EmployeeAuthentication (user_id , access_token )
218+ return EmployeeClient (auth = auth , transport = transport )
219+ else :
220+ # This is for logging in mostly.
221+ LOGGER .info ("No access_token or userid found in settings, creating a No Auth client for now." )
222+ return EmployeeClient (auth = None , transport = transport )
223+
224+
225+ def Client (** kwargs ):
226+ """Get a SoftLayer API Client using environmental settings."""
153227 return create_client_from_env (** kwargs )
154228
155229
156230class BaseClient (object ):
157231 """Base SoftLayer API client.
158232
159233 :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
160- :param transport: An object that's callable with this signature:
161- transport(SoftLayer.transports.Request)
234+ :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
162235 """
163-
164236 _prefix = "SoftLayer_"
237+ auth : slauth .AuthenticationBase
165238
166239 def __init__ (self , auth = None , transport = None , config_file = None ):
167240 if config_file is None :
168241 config_file = CONFIG_FILE
169- self .auth = auth
170242 self .config_file = config_file
171243 self .settings = config .get_config (self .config_file )
244+ self .__setAuth (auth )
245+ self .__setTransport (transport )
172246
247+ def __setAuth (self , auth = None ):
248+ """Prepares the authentication property"""
249+ self .auth = auth
250+
251+ def __setTransport (self , transport = None ):
252+ """Prepares the transport property"""
173253 if transport is None :
174254 url = self .settings ['softlayer' ].get ('endpoint_url' )
175255 if url is not None and '/rest' in url :
@@ -194,9 +274,7 @@ def __init__(self, auth=None, transport=None, config_file=None):
194274
195275 self .transport = transport
196276
197- def authenticate_with_password (self , username , password ,
198- security_question_id = None ,
199- security_question_answer = None ):
277+ def authenticate_with_password (self , username , password , security_question_id = None , security_question_answer = None ):
200278 """Performs Username/Password Authentication
201279
202280 :param string username: your SoftLayer username
@@ -259,8 +337,7 @@ def call(self, service, method, *args, **kwargs):
259337
260338 invalid_kwargs = set (kwargs .keys ()) - VALID_CALL_ARGS
261339 if invalid_kwargs :
262- raise TypeError (
263- 'Invalid keyword arguments: %s' % ',' .join (invalid_kwargs ))
340+ raise TypeError ('Invalid keyword arguments: %s' % ',' .join (invalid_kwargs ))
264341
265342 prefixes = (self ._prefix , 'BluePages_Search' , 'IntegratedOfferingTeam_Region' )
266343 if self ._prefix and not service .startswith (prefixes ):
@@ -286,6 +363,7 @@ def call(self, service, method, *args, **kwargs):
286363 request .filter = kwargs .get ('filter' )
287364 request .limit = kwargs .get ('limit' )
288365 request .offset = kwargs .get ('offset' )
366+ request .url = self .settings ['softlayer' ].get ('endpoint_url' )
289367 if kwargs .get ('verify' ) is not None :
290368 request .verify = kwargs .get ('verify' )
291369
@@ -391,6 +469,31 @@ def __len__(self):
391469 return 0
392470
393471
472+ class CertificateClient (BaseClient ):
473+ """Client that works with a X509 Certificate for authentication.
474+
475+ Will read the certificate file from the config file (~/.softlayer usually).
476+ > auth_cert = /path/to/authentication/cert.pm
477+ > server_cert = /path/to/CAcert.pem
478+ Set auth to a SoftLayer.auth.Authentication class to manually set authentication
479+ """
480+
481+ def __init__ (self , auth = None , transport = None , config_file = None ):
482+ BaseClient .__init__ (self , auth , transport , config_file )
483+ self .__setAuth (auth )
484+
485+ def __setAuth (self , auth = None ):
486+ """Prepares the authentication property"""
487+ if auth is None :
488+ auth_cert = self .settings ['softlayer' ].get ('auth_cert' )
489+ serv_cert = self .settings ['softlayer' ].get ('server_cert' , None )
490+ auth = slauth .X509Authentication (auth_cert , serv_cert )
491+ self .auth = auth
492+
493+ def __repr__ (self ):
494+ return "CertificateClient(transport=%r, auth=%r)" % (self .transport , self .auth )
495+
496+
394497class IAMClient (BaseClient ):
395498 """IBM ID Client for using IAM authentication
396499
@@ -575,6 +678,94 @@ def __repr__(self):
575678 return "IAMClient(transport=%r, auth=%r)" % (self .transport , self .auth )
576679
577680
681+ class EmployeeClient (BaseClient ):
682+ """Internal SoftLayer Client
683+
684+ :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
685+ :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
686+ """
687+
688+ def __init__ (self , auth = None , transport = None , config_file = None , account_id = None ):
689+ BaseClient .__init__ (self , auth , transport , config_file )
690+ self .account_id = account_id
691+
692+ def authenticate_with_internal (self , username , password , security_token = None ):
693+ """Performs internal authentication
694+
695+ :param string username: your softlayer username
696+ :param string password: your softlayer password
697+ :param int security_token: your 2FA token, prompt if None
698+ """
699+
700+ self .auth = None
701+ if security_token is None :
702+ security_token = input ("Enter your 2FA Token now: " )
703+ if len (security_token ) != 6 :
704+ raise exceptions .SoftLayerAPIError ("Invalid security token: {}" .format (security_token ))
705+
706+ auth_result = self .call ('SoftLayer_User_Employee' , 'performExternalAuthentication' ,
707+ username , password , security_token )
708+
709+ self .settings ['softlayer' ]['access_token' ] = auth_result ['hash' ]
710+ self .settings ['softlayer' ]['userid' ] = str (auth_result ['userId' ])
711+ # self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
712+
713+ config .write_config (self .settings , self .config_file )
714+ self .auth = slauth .EmployeeAuthentication (auth_result ['userId' ], auth_result ['hash' ])
715+
716+ return auth_result
717+
718+ def authenticate_with_hash (self , userId , access_token ):
719+ """Authenticates to the Internal SL API with an employee userid + token
720+
721+ :param string userId: Employee UserId
722+ :param string access_token: Employee Hash Token
723+ """
724+ self .auth = slauth .EmployeeAuthentication (userId , access_token )
725+
726+ def refresh_token (self , userId , auth_token ):
727+ """Refreshes the login token"""
728+
729+ # Go directly to base client, to avoid infite loop if the token is super expired.
730+ auth_result = BaseClient .call (self , 'SoftLayer_User_Employee' , 'refreshEncryptedToken' , auth_token , id = userId )
731+ if len (auth_result ) > 1 :
732+ for returned_data in auth_result :
733+ # Access tokens should be 188 characters, but just incase its longer or something.
734+ if len (returned_data ) > 180 :
735+ self .settings ['softlayer' ]['access_token' ] = returned_data
736+ else :
737+ message = "Excepted 2 properties from refreshEncryptedToken, got {}|" .format (auth_result )
738+ raise exceptions .SoftLayerAPIError (message )
739+
740+ config .write_config (self .settings , self .config_file )
741+ self .auth = slauth .EmployeeAuthentication (userId , auth_result [0 ])
742+ return auth_result
743+
744+ def call (self , service , method , * args , ** kwargs ):
745+ """Handles refreshing Employee tokens in case of a HTTP 401 error"""
746+ if (service == 'SoftLayer_Account' or service == 'Account' ) and not kwargs .get ('id' ):
747+ if not self .account_id :
748+ raise exceptions .SoftLayerError ("SoftLayer_Account service requires an ID" )
749+ kwargs ['id' ] = self .account_id
750+
751+ try :
752+ return BaseClient .call (self , service , method , * args , ** kwargs )
753+ except exceptions .SoftLayerAPIError as ex :
754+ if ex .faultCode == "SoftLayer_Exception_EncryptedToken_Expired" :
755+ userId = self .settings ['softlayer' ].get ('userid' )
756+ access_token = self .settings ['softlayer' ].get ('access_token' )
757+ LOGGER .warning ("Token has expired, trying to refresh. %s" , ex .faultString )
758+ self .refresh_token (userId , access_token )
759+ # Try the Call again this time....
760+ return BaseClient .call (self , service , method , * args , ** kwargs )
761+
762+ else :
763+ raise ex
764+
765+ def __repr__ (self ):
766+ return "EmployeeClient(transport=%r, auth=%r)" % (self .transport , self .auth )
767+
768+
578769class Service (object ):
579770 """A SoftLayer Service.
580771
0 commit comments