Skip to content

Commit 607ec97

Browse files
committed
Ticket #24280 - Add support to authenticate with a login/password.
1 parent fd6a80e commit 607ec97

File tree

4 files changed

+182
-55
lines changed

4 files changed

+182
-55
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
tests/config
1414
.coverage
1515
.cache
16+
htmlcov
1617

1718
# setup related
1819
build

shotgun_api3/shotgun.py

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -215,22 +215,26 @@ class Shotgun(object):
215215

216216
def __init__(self,
217217
base_url,
218-
script_name,
219-
api_key,
218+
script_name=None,
219+
api_key=None,
220220
convert_datetimes_to_utc=True,
221221
http_proxy=None,
222222
ensure_ascii=True,
223223
connect=True,
224-
ca_certs=None):
224+
ca_certs=None,
225+
login=None,
226+
password=None):
225227
"""Initialises a new instance of the Shotgun client.
226228
227229
:param base_url: http or https url to the shotgun server.
228230
229231
:param script_name: name of the client script, used to authenticate
230-
to the server.
232+
to the server. If script_name is provided, then api_key must be as
233+
well and neither login nor password can be provided.
231234
232235
:param api_key: key assigned to the client script, used to
233-
authenticate to the server.
236+
authenticate to the server. If api_key is provided, then script_name
237+
must be as well and neither login nor password can be provided.
234238
235239
:param convert_datetimes_to_utc: If True date time values are
236240
converted from local time to UTC time before been sent to the server.
@@ -245,11 +249,42 @@ def __init__(self,
245249
246250
:param ca_certs: The path to the SSL certificate file. Useful for users
247251
who would like to package their application into an executable.
252+
253+
:param login: The login to use to authenticate to the server. If login
254+
is provided, then password must be as well and neither script_name nor
255+
api_key can be provided.
256+
257+
:param password: The password for the login to use to authenticate to
258+
the server. If password is provided, then login must be as well and
259+
neither script_name nor api_key can be provided.
248260
"""
249261

262+
# verify authentication arguments
263+
if login is not None or password is not None:
264+
if script_name is not None or api_key is not None:
265+
raise ValueError("cannot provide both login/password "
266+
"and script_name/api_key")
267+
if login is None:
268+
raise ValueError("password provided without login")
269+
if password is None:
270+
raise ValueError("login provided without password")
271+
272+
if script_name is not None or api_key is not None:
273+
if script_name is None:
274+
raise ValueError("api_key provided without script_name")
275+
if api_key is None:
276+
raise ValueError("script_name provided without api_key")
277+
278+
if all(v is None for v in [script_name, api_key, login, password]):
279+
if connect:
280+
raise ValueError("must provide either login/password "
281+
"or script_name/api_key")
282+
250283
self.config = _Config()
251284
self.config.api_key = api_key
252285
self.config.script_name = script_name
286+
self.config.user_login = login
287+
self.config.user_password = password
253288
self.config.convert_datetimes_to_utc = convert_datetimes_to_utc
254289
self.config.no_ssl_validation = NO_SSL_VALIDATION
255290
self._connection = None
@@ -350,7 +385,7 @@ def info(self):
350385
351386
:returns: dict of the server meta data.
352387
"""
353-
return self._call_rpc("info", None, include_script_name=False)
388+
return self._call_rpc("info", None, include_auth_params=False)
354389

355390
def find_one(self, entity_type, filters, fields=None, order=None,
356391
filter_operator=None, retired_only=False):
@@ -1046,11 +1081,9 @@ def share_thumbnail(self, entities, thumbnail_path=None, source_entity=None,
10461081
"entities" : ','.join(entities_str),
10471082
"source_entity": "%s_%s" % (source_entity['type'], source_entity['id']),
10481083
"filmstrip_thumbnail" : filmstrip_thumbnail,
1049-
"script_name" : self.config.script_name,
1050-
"script_key" : self.config.api_key,
10511084
}
1052-
if self.config.session_uuid:
1053-
params["session_uuid"] = self.config.session_uuid
1085+
1086+
params.update(self._auth_params())
10541087

10551088
# Create opener with extended form post support
10561089
opener = self._build_opener(FormPostHandler)
@@ -1126,11 +1159,9 @@ def upload(self, entity_type, entity_id, path, field_name=None,
11261159
params = {
11271160
"entity_type" : entity_type,
11281161
"entity_id" : entity_id,
1129-
"script_name" : self.config.script_name,
1130-
"script_key" : self.config.api_key,
11311162
}
1132-
if self.config.session_uuid:
1133-
params["session_uuid"] = self.config.session_uuid
1163+
1164+
params.update(self._auth_params())
11341165

11351166
if is_thumbnail:
11361167
url = urlparse.urlunparse((self.config.scheme, self.config.server,
@@ -1331,25 +1362,28 @@ def authenticate_human_user(self, user_login, user_password):
13311362

13321363
if not user_password:
13331364
raise ValueError('Please supply a password for the user.')
1334-
1365+
13351366
# Override permissions on Config obj
1367+
original_login = self.config.user_login
1368+
original_password = self.config.user_password
1369+
13361370
self.config.user_login = user_login
13371371
self.config.user_password = user_password
13381372

13391373
try:
13401374
data = self.find_one('HumanUser', [['sg_status_list', 'is', 'act'], ['login', 'is', user_login]], ['id', 'login'], '', 'all')
13411375
# Set back to default - There finally and except cannot be used together in python2.4
1342-
self.config.user_login = None
1343-
self.config.user_password = None
1376+
self.config.user_login = original_login
1377+
self.config.user_password = original_password
13441378
return data
13451379
except Fault:
13461380
# Set back to default - There finally and except cannot be used together in python2.4
1347-
self.config.user_login = None
1348-
self.config.user_password = None
1381+
self.config.user_login = original_login
1382+
self.config.user_password = original_password
13491383
except:
13501384
# Set back to default - There finally and except cannot be used together in python2.4
1351-
self.config.user_login = None
1352-
self.config.user_password = None
1385+
self.config.user_login = original_login
1386+
self.config.user_password = original_password
13531387
raise
13541388

13551389

@@ -1396,7 +1430,7 @@ def entity_types(self):
13961430
# ========================================================================
13971431
# RPC Functions
13981432

1399-
def _call_rpc(self, method, params, include_script_name=True, first=False):
1433+
def _call_rpc(self, method, params, include_auth_params=True, first=False):
14001434
"""Calls the specified method on the Shotgun Server sending the
14011435
supplied payload.
14021436
@@ -1407,7 +1441,7 @@ def _call_rpc(self, method, params, include_script_name=True, first=False):
14071441

14081442
params = self._transform_outbound(params)
14091443
payload = self._build_payload(method, params,
1410-
include_script_name=include_script_name)
1444+
include_auth_params=include_auth_params)
14111445
encoded_payload = self._encode_payload(payload)
14121446

14131447
req_headers = {
@@ -1438,7 +1472,31 @@ def _call_rpc(self, method, params, include_script_name=True, first=False):
14381472
return results[0]
14391473
return results
14401474

1441-
def _build_payload(self, method, params, include_script_name=True):
1475+
def _auth_params(self):
1476+
""" return a dictionary of the authentication parameters being used. """
1477+
# Used to authenticate HumanUser credentials
1478+
if self.config.user_login and self.config.user_password:
1479+
auth_params = {
1480+
"user_login" : str(self.config.user_login),
1481+
"user_password" : str(self.config.user_password),
1482+
}
1483+
1484+
# Use script name instead
1485+
elif self.config.script_name and self.config.api_key:
1486+
auth_params = {
1487+
"script_name" : str(self.config.script_name),
1488+
"script_key" : str(self.config.api_key),
1489+
}
1490+
1491+
else:
1492+
raise ValueError("invalid auth params")
1493+
1494+
if self.config.session_uuid:
1495+
auth_params["session_uuid"] = self.config.session_uuid
1496+
1497+
return auth_params
1498+
1499+
def _build_payload(self, method, params, include_auth_params=True):
14421500
"""Builds the payload to be send to the rpc endpoint.
14431501
14441502
"""
@@ -1447,28 +1505,8 @@ def _build_payload(self, method, params, include_script_name=True):
14471505

14481506
call_params = []
14491507

1450-
if include_script_name:
1451-
if not self.config.script_name:
1452-
raise ValueError("script_name is empty")
1453-
if not self.config.api_key:
1454-
raise ValueError("api_key is empty")
1455-
1456-
# Used to authenticate HumanUser credentials
1457-
if self.config.user_login and self.config.user_password:
1458-
auth_params = {
1459-
"user_login" : str(self.config.user_login),
1460-
"user_password" : str(self.config.user_password),
1461-
}
1462-
1463-
# Use script name instead
1464-
else:
1465-
auth_params = {
1466-
"script_name" : str(self.config.script_name),
1467-
"script_key" : str(self.config.api_key),
1468-
}
1469-
1470-
if self.config.session_uuid:
1471-
auth_params["session_uuid"] = self.config.session_uuid
1508+
if include_auth_params:
1509+
auth_params = self._auth_params()
14721510
call_params.append(auth_params)
14731511

14741512
if params:

tests/base.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class TestBase(unittest.TestCase):
1919
'''Base class for tests.
2020
2121
Sets up mocking and database test data.'''
22+
2223
def __init__(self, *args, **kws):
2324
unittest.TestCase.__init__(self, *args, **kws)
2425
self.human_user = None
@@ -35,22 +36,31 @@ def __init__(self, *args, **kws):
3536
self.connect = False
3637

3738

38-
def setUp(self):
39+
def setUp(self, auth_mode='ApiUser'):
3940
self.config = SgTestConfig()
4041
self.config.read_config(CONFIG_PATH)
42+
self.human_login = self.config.human_login
4143
self.human_password = self.config.human_password
4244
self.server_url = self.config.server_url
4345
self.script_name = self.config.script_name
4446
self.api_key = self.config.api_key
4547
self.http_proxy = self.config.http_proxy
4648
self.session_uuid = self.config.session_uuid
4749

48-
49-
self.sg = api.Shotgun(self.config.server_url,
50-
self.config.script_name,
51-
self.config.api_key,
52-
http_proxy=self.config.http_proxy,
53-
connect=self.connect)
50+
if auth_mode == 'ApiUser':
51+
self.sg = api.Shotgun(self.config.server_url,
52+
self.config.script_name,
53+
self.config.api_key,
54+
http_proxy=self.config.http_proxy,
55+
connect=self.connect)
56+
elif auth_mode == 'HumanUser':
57+
self.sg = api.Shotgun(self.config.server_url,
58+
login=self.human_login,
59+
password=self.human_password,
60+
http_proxy=self.config.http_proxy,
61+
connect=self.connect)
62+
else:
63+
raise ValueError("Unknown value for auth_mode: %s" % auth_mode)
5464

5565
if self.config.session_uuid:
5666
self.sg.set_session_uuid(self.config.session_uuid)
@@ -164,8 +174,8 @@ def _setup_mock_data(self):
164174

165175
class LiveTestBase(TestBase):
166176
'''Test base for tests relying on connection to server.'''
167-
def setUp(self):
168-
super(LiveTestBase, self).setUp()
177+
def setUp(self, auth_mode='ApiUser'):
178+
super(LiveTestBase, self).setUp(auth_mode)
169179
self.sg_version = self.sg.info()['version'][:3]
170180
self._setup_db(self.config)
171181
if self.sg.server_caps.version and \
@@ -244,6 +254,15 @@ def _setup_db(self, config):
244254
self.local_storage = _find_or_create_entity(self.sg, 'LocalStorage', data, keys)
245255

246256

257+
class HumanUserAuthLiveTestBase(LiveTestBase):
258+
'''
259+
Test base for relying on a Shotgun connection authenticate through the
260+
configured login/password pair.
261+
'''
262+
def setUp(self):
263+
super(HumanUserAuthLiveTestBase, self).setUp('HumanUser')
264+
265+
247266
class SgTestConfig(object):
248267
'''Reads test config and holds values'''
249268
def __init__(self):

0 commit comments

Comments
 (0)