Skip to content

Commit 8f8b51a

Browse files
Updating internal login to support session token login directly
1 parent b2bb7c9 commit 8f8b51a

File tree

3 files changed

+121
-65
lines changed

3 files changed

+121
-65
lines changed

SoftLayer/API.py

Lines changed: 78 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,38 @@
5050
))
5151

5252

53+
def _build_transport(url, proxy, timeout, user_agent, verify):
54+
"""Construct the appropriate transport based on the endpoint URL.
55+
56+
Selects RestTransport when the URL contains '/rest', otherwise falls back
57+
to XmlRpcTransport. Extracted to avoid duplicating this logic across
58+
``create_client_from_env``, ``employee_client``, and ``BaseClient``.
59+
60+
:param str url: The API endpoint URL.
61+
:param str proxy: Optional proxy URL.
62+
:param timeout: Request timeout in seconds (``None`` means no timeout).
63+
:param str user_agent: Optional User-Agent string override.
64+
:param verify: SSL verification — ``True``, ``False``, or a path to a CA bundle.
65+
:returns: A :class:`~SoftLayer.transports.RestTransport` or
66+
:class:`~SoftLayer.transports.XmlRpcTransport` instance.
67+
"""
68+
if url is not None and '/rest' in url:
69+
return transports.RestTransport(
70+
endpoint_url=url,
71+
proxy=proxy,
72+
timeout=timeout,
73+
user_agent=user_agent,
74+
verify=verify,
75+
)
76+
return transports.XmlRpcTransport(
77+
endpoint_url=url,
78+
proxy=proxy,
79+
timeout=timeout,
80+
user_agent=user_agent,
81+
verify=verify,
82+
)
83+
84+
5385
def create_client_from_env(username=None,
5486
api_key=None,
5587
endpoint_url=None,
@@ -62,7 +94,7 @@ def create_client_from_env(username=None,
6294
verify=True):
6395
"""Creates a SoftLayer API client using your environment.
6496
65-
Settings are loaded via keyword arguments, environemtal variables and
97+
Settings are loaded via keyword arguments, environmental variables and
6698
config file.
6799
68100
:param username: an optional API username if you wish to bypass the
@@ -104,25 +136,13 @@ def create_client_from_env(username=None,
104136
config_file=config_file)
105137

106138
if transport is None:
107-
url = settings.get('endpoint_url')
108-
if url is not None and '/rest' in url:
109-
# If this looks like a rest endpoint, use the rest transport
110-
transport = transports.RestTransport(
111-
endpoint_url=settings.get('endpoint_url'),
112-
proxy=settings.get('proxy'),
113-
timeout=settings.get('timeout'),
114-
user_agent=user_agent,
115-
verify=verify,
116-
)
117-
else:
118-
# Default the transport to use XMLRPC
119-
transport = transports.XmlRpcTransport(
120-
endpoint_url=settings.get('endpoint_url'),
121-
proxy=settings.get('proxy'),
122-
timeout=settings.get('timeout'),
123-
user_agent=user_agent,
124-
verify=verify,
125-
)
139+
transport = _build_transport(
140+
url=settings.get('endpoint_url'),
141+
proxy=settings.get('proxy'),
142+
timeout=settings.get('timeout'),
143+
user_agent=user_agent,
144+
verify=verify,
145+
)
126146

127147
# If we have enough information to make an auth driver, let's do it
128148
if auth is None and settings.get('username') and settings.get('api_key'):
@@ -157,13 +177,13 @@ def employee_client(username=None,
157177
verify=True):
158178
"""Creates an INTERNAL SoftLayer API client using your environment.
159179
160-
Settings are loaded via keyword arguments, environemtal variables and config file.
180+
Settings are loaded via keyword arguments, environmental variables and config file.
161181
162182
:param username: your user ID
163-
:param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token)
164-
:param password: password to use for employee authentication
183+
:param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication
165184
:param endpoint_url: the API endpoint base URL you wish to connect to.
166-
Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network.
185+
Must contain 'internal'. Set this to API_PRIVATE_ENDPOINT to connect
186+
via SoftLayer's private network.
167187
:param proxy: proxy to be used to make API calls
168188
:param integer timeout: timeout for API requests
169189
:param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers.
@@ -173,56 +193,54 @@ def employee_client(username=None,
173193
calls if you wish to bypass the packages built in User Agent string
174194
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
175195
:param bool verify: decide to verify the server's SSL/TLS cert.
196+
DO NOT SET TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS.
176197
"""
198+
# Pass caller-supplied verify so it is not silently discarded; the config
199+
# file value will take precedence if present (via get_client_settings).
177200
settings = config.get_client_settings(username=username,
178201
api_key=None,
179202
endpoint_url=endpoint_url,
180203
timeout=timeout,
181204
proxy=proxy,
182-
verify=None,
205+
verify=verify,
183206
config_file=config_file)
184207

185208
url = settings.get('endpoint_url', '')
186-
verify = settings.get('verify', True)
209+
# Honour the config-file value; fall back to the caller-supplied default.
210+
verify = settings.get('verify', verify)
187211

188212
if 'internal' not in url:
189213
raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.")
190214

215+
# url is guaranteed non-empty here (the guard above ensures it contains
216+
# 'internal'), so no additional None-check is needed.
191217
if transport is None:
192-
if url is not None and '/rest' in url:
193-
# If this looks like a rest endpoint, use the rest transport
194-
transport = transports.RestTransport(
195-
endpoint_url=url,
196-
proxy=settings.get('proxy'),
197-
timeout=settings.get('timeout'),
198-
user_agent=user_agent,
199-
verify=verify,
200-
)
201-
else:
202-
# Default the transport to use XMLRPC
203-
transport = transports.XmlRpcTransport(
204-
endpoint_url=url,
205-
proxy=settings.get('proxy'),
206-
timeout=settings.get('timeout'),
207-
user_agent=user_agent,
208-
verify=verify,
209-
)
210-
218+
transport = _build_transport(
219+
url=url,
220+
proxy=settings.get('proxy'),
221+
timeout=settings.get('timeout'),
222+
user_agent=user_agent,
223+
verify=verify,
224+
)
225+
226+
# Resolve all settings-derived credentials together before auth selection.
211227
if access_token is None:
212228
access_token = settings.get('access_token')
213-
214229
user_id = settings.get('userid')
215-
# Assume access_token is valid for now, user has logged in before at least.
216-
if settings.get('auth_cert', False):
217-
auth = slauth.X509Authentication(settings.get('auth_cert'), verify)
218-
return EmployeeClient(auth=auth, transport=transport, config_file=config_file)
219-
elif access_token and user_id:
220-
auth = slauth.EmployeeAuthentication(user_id, access_token)
221-
return EmployeeClient(auth=auth, transport=transport, config_file=config_file)
222-
else:
223-
# This is for logging in mostly.
224-
LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.")
225-
return EmployeeClient(auth=None, transport=transport, config_file=config_file)
230+
231+
# Select the appropriate auth driver only when the caller has not already
232+
# supplied one. A single return keeps construction separate from selection.
233+
if auth is None:
234+
if settings.get('auth_cert'):
235+
auth = slauth.X509Authentication(settings.get('auth_cert'), verify)
236+
elif access_token and user_id:
237+
auth = slauth.EmployeeAuthentication(user_id, access_token)
238+
else:
239+
# No credentials available — caller must authenticate explicitly
240+
# (e.g. via EmployeeClient.authenticate_with_internal).
241+
LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.")
242+
243+
return EmployeeClient(auth=auth, transport=transport, config_file=config_file)
226244

227245

228246
def Client(**kwargs):
@@ -237,7 +255,7 @@ class BaseClient(object):
237255
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
238256
"""
239257
_prefix = "SoftLayer_"
240-
auth: slauth.AuthenticationBase
258+
auth: slauth.AuthenticationBase | None
241259

242260
def __init__(self, auth=None, transport=None, config_file=None):
243261
if config_file is None:
@@ -247,7 +265,7 @@ def __init__(self, auth=None, transport=None, config_file=None):
247265
self.__setAuth(auth)
248266
self.__setTransport(transport)
249267

250-
def __setAuth(self, auth=None):
268+
def __setAuth(self, auth: slauth.AuthenticationBase | None = None):
251269
"""Prepares the authentication property"""
252270
self.auth = auth
253271

@@ -751,7 +769,7 @@ def refresh_token(self, userId, auth_token):
751769

752770
def call(self, service, method, *args, **kwargs):
753771
"""Handles refreshing Employee tokens in case of a HTTP 401 error"""
754-
if self.account_id:
772+
if self.account_id and not kwargs.get('id', False):
755773
kwargs['id'] = self.account_id
756774

757775
try:

SoftLayer/CLI/login.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,56 @@ def censor_password(value):
1717

1818

1919
@click.command(cls=SLCommand)
20+
@click.option('--session-token',
21+
default=None,
22+
help='An existing employee session token (hash). Click the "Copy Session Token" in the internal portal to get this value.'
23+
'Can also be set via the SLCLI_SESSION_TOKEN environment variable.',
24+
envvar='SLCLI_SESSION_TOKEN')
25+
@click.option('--user-id',
26+
default=None,
27+
type=int,
28+
help='Employee IMS user ID. This is the number in the url when you click your username in the internal portal, under "user information". '
29+
'Can also be set via the SLCLI_USER_ID environment variable. Or read from the configuration file.',
30+
envvar='SLCLI_USER_ID')
31+
@click.option('--legacy',
32+
default=False,
33+
type=bool,
34+
is_flag=True,
35+
help='Login with username, password, yubi key combination. Only valid if ISV is not required. If using ISV, use your session token.')
2036
@environment.pass_env
21-
def cli(env):
37+
def cli(env, session_token: str | None, user_id: int | None, legacy: bool):
2238
"""Logs you into the internal SoftLayer Network.
2339
2440
username: Set this in either the softlayer config, or SL_USER ENV variable
2541
password: Set this in SL_PASSWORD env variable. You will be prompted for them otherwise.
42+
43+
To log in with an existing session token instead of username/password/2FA:
44+
45+
slcli login --session-token <token> --user-id <id>
46+
47+
Or via environment variables:
48+
49+
SLCLI_SESSION_TOKEN=<token> SLCLI_USER_ID=<id> slcli login
2650
"""
2751
config_settings = config.get_config(config_file=env.config_file)
2852
settings = config_settings['softlayer']
53+
54+
if not user_id:
55+
user_id = int(settings.get('userid', 0))
56+
# --session-token supplied on the CLI (or via SLCLI_SESSION_TOKEN env var):
57+
# authenticate directly, persist to config, and return immediately.
58+
if session_token and not legacy:
59+
if not user_id:
60+
user_id = int(input("User ID (number): "))
61+
env.client.authenticate_with_hash(user_id, session_token)
62+
settings['access_token'] = session_token
63+
settings['userid'] = str(user_id)
64+
config_settings['softlayer'] = settings
65+
config.write_config(config_settings, env.config_file)
66+
click.echo("Logged in with session token for user ID {}.".format(user_id))
67+
return
68+
69+
2970
username = settings.get('username') or os.environ.get('SLCLI_USER', None)
3071
password = os.environ.get('SLCLI_PASSWORD', '')
3172
yubi = None

tests/api_tests.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,10 +389,7 @@ def test_expired_token_is_really_expired(self, api_response):
389389
@mock.patch('SoftLayer.API.BaseClient.call')
390390
def test_account_check(self, _call):
391391
self.client.transport = self.mocks
392-
exception = self.assertRaises(
393-
exceptions.SoftLayerError,
394-
self.client.call, "SoftLayer_Account", "getObject")
395-
self.assertEqual(str(exception), "SoftLayer_Account service requires an ID")
392+
396393
self.client.account_id = 1234
397394
self.client.call("SoftLayer_Account", "getObject")
398395
self.client.call("SoftLayer_Account", "getObject1", id=9999)

0 commit comments

Comments
 (0)