@@ -97,6 +97,12 @@ class AuthenticationFault(Fault):
9797 """Exception when the server side reports an error related to authentication"""
9898 pass
9999
100+ class MissingTwoFactorAuthenticationFault (Fault ):
101+ """Exception when the server side reports an error related to missing
102+ two factor authentication credentials
103+ """
104+ pass
105+
100106# ----------------------------------------------------------------------------
101107# API
102108
@@ -231,6 +237,7 @@ def __init__(self):
231237 self .script_name = None
232238 self .user_login = None
233239 self .user_password = None
240+ self .auth_token = None
234241 self .sudo_as_login = None
235242 # uuid as a string
236243 self .session_uuid = None
@@ -279,7 +286,8 @@ def __init__(self,
279286 login = None ,
280287 password = None ,
281288 sudo_as_login = None ,
282- session_token = None ):
289+ session_token = None ,
290+ auth_token = None ):
283291 """Initialises a new instance of the Shotgun client.
284292
285293 :param base_url: http or https url to the shotgun server.
@@ -329,6 +337,13 @@ def __init__(self,
329337 :param session_token: The session token to use to authenticate to the server. This
330338 can be used as an alternative to authenticating with a script user or regular user.
331339 You retrieve the session token by running the get_session_token() method.
340+
341+ :param auth_token: The authentication token required to authenticate to
342+ a server with two factor authentication turned on. If auth_token is provided,
343+ then login and password must be as well and neither script_name nor api_key
344+ can be provided. Note that these tokens can be short lived so a session is
345+ established right away if an auth_token is provided. A
346+ MissingTwoFactorAuthenticationFault will be raised if the auth_token is invalid.
332347 """
333348
334349 # verify authentication arguments
@@ -355,6 +370,13 @@ def __init__(self,
355370 if api_key is None :
356371 raise ValueError ("script_name provided without api_key" )
357372
373+ if auth_token is not None :
374+ if login is None or password is None :
375+ raise ValueError ("must provide a user login and password with an auth_token" )
376+
377+ if script_name is not None or api_key is not None :
378+ raise ValueError ("cannot provide an auth_code with script_name/api_key" )
379+
358380 # Can't use 'all' with python 2.4
359381 if len ([x for x in [session_token , script_name , api_key , login , password ] if x ]) == 0 :
360382 if connect :
@@ -365,6 +387,7 @@ def __init__(self,
365387 self .config .script_name = script_name
366388 self .config .user_login = login
367389 self .config .user_password = password
390+ self .config .auth_token = auth_token
368391 self .config .session_token = session_token
369392 self .config .sudo_as_login = sudo_as_login
370393 self .config .convert_datetimes_to_utc = convert_datetimes_to_utc
@@ -423,8 +446,6 @@ def __init__(self,
423446 proxy_addr = "http://%s%s:%d" % (auth_string , self .config .proxy_server , self .config .proxy_port )
424447 self .config .proxy_handler = urllib2 .ProxyHandler ({self .config .scheme : proxy_addr })
425448
426-
427-
428449 if ensure_ascii :
429450 self ._json_loads = self ._json_loads_ascii
430451
@@ -438,6 +459,15 @@ def __init__(self,
438459 if connect :
439460 self .server_caps
440461
462+ # When using auth_token in a 2FA scenario we need to switch to session-based
463+ # authentication because the auth token will no longer be valid after a first use.
464+ if self .config .auth_token != None :
465+ self .config .session_token = self .get_session_token ()
466+ self .config .user_login = None
467+ self .config .user_password = None
468+ self .config .auth_token = None
469+
470+
441471 # ========================================================================
442472 # API Functions
443473
@@ -1519,11 +1549,17 @@ def get_attachment_download_url(self, attachment):
15191549 None , None , None ))
15201550 return url
15211551
1522- def authenticate_human_user (self , user_login , user_password ):
1552+ def authenticate_human_user (self , user_login , user_password , auth_token = None ):
15231553 '''Authenticate Shotgun HumanUser. HumanUser must be an active account.
1524- @param user_login: Login name of Shotgun HumanUser
1525- @param user_password: Password for Shotgun HumanUser
1526- @return: Dictionary of HumanUser including ID if authenticated, None is unauthorized.
1554+ :param user_login: Login name of Shotgun HumanUser
1555+
1556+ :param user_password: Password for Shotgun HumanUser
1557+
1558+ :param auth_token: One-time token required to authenticate Shotgun HumanUser
1559+ when two factor authentication is turned on.
1560+
1561+ :return: Dictionary of HumanUser including ID if authenticated, None if unauthorized.
1562+ """
15271563 '''
15281564 if not user_login :
15291565 raise ValueError ('Please supply a username to authenticate.' )
@@ -1534,24 +1570,29 @@ def authenticate_human_user(self, user_login, user_password):
15341570 # Override permissions on Config obj
15351571 original_login = self .config .user_login
15361572 original_password = self .config .user_password
1573+ original_auth_token = self .config .auth_token
15371574
15381575 self .config .user_login = user_login
15391576 self .config .user_password = user_password
1577+ self .config .auth_token = auth_token
15401578
15411579 try :
15421580 data = self .find_one ('HumanUser' , [['sg_status_list' , 'is' , 'act' ], ['login' , 'is' , user_login ]], ['id' , 'login' ], '' , 'all' )
15431581 # Set back to default - There finally and except cannot be used together in python2.4
15441582 self .config .user_login = original_login
15451583 self .config .user_password = original_password
1584+ self .config .auth_token = original_auth_token
15461585 return data
15471586 except Fault :
15481587 # Set back to default - There finally and except cannot be used together in python2.4
15491588 self .config .user_login = original_login
15501589 self .config .user_password = original_password
1590+ self .config .auth_token = original_auth_token
15511591 except :
15521592 # Set back to default - There finally and except cannot be used together in python2.4
15531593 self .config .user_login = original_login
15541594 self .config .user_password = original_password
1595+ self .config .auth_token = original_auth_token
15551596 raise
15561597
15571598
@@ -1597,7 +1638,8 @@ def get_session_token(self):
15971638 session_token = (rv or {}).get ("session_id" )
15981639 if not session_token :
15991640 raise RuntimeError ("Could not extract session_id from %s" , rv )
1600- self .config .session_token = session_token
1641+ self .config .session_token = session_token
1642+
16011643 return session_token
16021644
16031645 def _build_opener (self , handler ):
@@ -1669,6 +1711,8 @@ def _auth_params(self):
16691711 "user_login" : str (self .config .user_login ),
16701712 "user_password" : str (self .config .user_password ),
16711713 }
1714+ if self .config .auth_token :
1715+ auth_params ["auth_token" ] = str (self .config .auth_token )
16721716
16731717 # Use script name instead
16741718 elif self .config .script_name and self .config .api_key :
@@ -1867,14 +1911,15 @@ def _response_errors(self, sg_response):
18671911
18681912 :raises ShotgunError: If the server response contains an exception.
18691913 """
1870-
1914+
18711915 ERR_AUTH = 102 # error code for authentication related problems
1916+ ERR_2FA = 106 # error code when 2FA authentication is required but no 23FA token provided.
18721917
18731918 if isinstance (sg_response , dict ) and sg_response .get ("exception" ):
1874-
18751919 if sg_response .get ("error_code" ) == ERR_AUTH :
18761920 raise AuthenticationFault (sg_response .get ("message" , "Unknown Authentication Error" ))
1877-
1921+ elif sg_response .get ("error_code" ) == ERR_2FA :
1922+ raise MissingTwoFactorAuthenticationFault (sg_response .get ("message" , "Unknown 2FA Authentication Error" ))
18781923 else :
18791924 # raise general Fault
18801925 raise Fault (sg_response .get ("message" , "Unknown Error" ))
0 commit comments