Skip to content

Commit 4f0fb13

Browse files
Added in employee authentication
2 parents c5e3fcf + 6bad186 commit 4f0fb13

18 files changed

+560
-12
lines changed

README-internal.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
This document is for internal users wanting to use this library to interact with the internal API. It will not work for `api.softlayer.com`.
2+
3+
4+
## Certificate Example
5+
6+
For use with a utility certificate. In your config file (usually `~/.softlayer`), you need to set the following:
7+
8+
```
9+
[softlayer]
10+
endpoint_url = https://<internal api endpoint>/v3/internal/rest/
11+
timeout = 0
12+
theme = dark
13+
auth_cert = /etc/ssl/certs/my_utility_cert-dev.pem
14+
server_cert = /etc/ssl/certs/allCAbundle.pem
15+
```
16+
17+
`auth_cert`: is your utility user certificate
18+
`server_cert`: is the CA certificate bundle to validate the internal API ssl chain. Otherwise you get self-signed ssl errors without this.
19+
20+
21+
```
22+
import SoftLayer
23+
import logging
24+
import click
25+
26+
@click.command()
27+
def testAuthentication():
28+
client = SoftLayer.CertificateClient()
29+
result = client.call('SoftLayer_Account', 'getObject', id=12345, mask="mask[id,companyName]")
30+
print(result)
31+
32+
33+
if __name__ == "__main__":
34+
logger = logging.getLogger()
35+
logger.addHandler(logging.StreamHandler())
36+
logger.setLevel(logging.DEBUG)
37+
testAuthentication()
38+
```
39+
40+
## Employee Example

SoftLayer/API.py

+190
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
# pylint: disable=invalid-name
9+
import os
910
import time
1011
import warnings
1112

@@ -28,6 +29,7 @@
2829

2930
__all__ = [
3031
'create_client_from_env',
32+
'employee_client',
3133
'Client',
3234
'BaseClient',
3335
'API_PUBLIC_ENDPOINT',
@@ -144,6 +146,102 @@ def create_client_from_env(username=None,
144146
return BaseClient(auth=auth, transport=transport, config_file=config_file)
145147

146148

149+
def employee_client(username=None,
150+
access_token=None,
151+
password=None,
152+
endpoint_url=None,
153+
timeout=None,
154+
auth=None,
155+
config_file=None,
156+
proxy=None,
157+
user_agent=None,
158+
transport=None,
159+
verify=False):
160+
"""Creates an INTERNAL SoftLayer API client using your environment.
161+
162+
Settings are loaded via keyword arguments, environemtal variables and
163+
config file.
164+
165+
:param username: your user ID
166+
:param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, 2fa_string)
167+
:param password: password to use for employee authentication
168+
:param endpoint_url: the API endpoint base URL you wish to connect to.
169+
Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private
170+
network.
171+
:param proxy: proxy to be used to make API calls
172+
:param integer timeout: timeout for API requests
173+
:param auth: an object which responds to get_headers() to be inserted into
174+
the xml-rpc headers. Example: `BasicAuthentication`
175+
:param config_file: A path to a configuration file used to load settings
176+
:param user_agent: an optional User Agent to report when making API
177+
calls if you wish to bypass the packages built in User Agent string
178+
:param transport: An object that's callable with this signature:
179+
transport(SoftLayer.transports.Request)
180+
:param bool verify: decide to verify the server's SSL/TLS cert. DO NOT SET
181+
TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS.
182+
183+
Usage:
184+
185+
>>> import SoftLayer
186+
>>> client = SoftLayer.create_client_from_env()
187+
>>> resp = client.call('Account', 'getObject')
188+
>>> resp['companyName']
189+
'Your Company'
190+
191+
"""
192+
# SSL verification is OFF because internal api uses a self signed cert
193+
settings = config.get_client_settings(username=username,
194+
api_key=None,
195+
endpoint_url=endpoint_url,
196+
timeout=timeout,
197+
proxy=proxy,
198+
verify=verify,
199+
config_file=config_file)
200+
201+
url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT
202+
203+
if 'internal' not in url:
204+
raise exceptions.SoftLayerError("{} does not look like an Internal Employee url. Try {}".format(
205+
url, consts.API_EMPLOYEE_ENDPOINT))
206+
207+
if transport is None:
208+
209+
if url is not None and '/rest' in url:
210+
# If this looks like a rest endpoint, use the rest transport
211+
transport = transports.RestTransport(
212+
endpoint_url=settings.get('endpoint_url'),
213+
proxy=settings.get('proxy'),
214+
timeout=settings.get('timeout'),
215+
user_agent=user_agent,
216+
verify=verify,
217+
)
218+
else:
219+
# Default the transport to use XMLRPC
220+
transport = transports.XmlRpcTransport(
221+
endpoint_url=settings.get('endpoint_url'),
222+
proxy=settings.get('proxy'),
223+
timeout=settings.get('timeout'),
224+
user_agent=user_agent,
225+
verify=verify,
226+
)
227+
228+
229+
if access_token is None:
230+
access_token = settings.get('access_token')
231+
232+
user_id = settings.get('userid')
233+
234+
# Assume access_token is valid for now, user has logged in before at least.
235+
if access_token and user_id:
236+
auth = slauth.EmployeeAuthentication(user_id, access_token)
237+
return EmployeeClient(auth=auth, transport=transport)
238+
else:
239+
# This is for logging in mostly.
240+
LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.")
241+
return EmployeeClient(auth=None, transport=transport)
242+
243+
244+
147245
def Client(**kwargs):
148246
"""Get a SoftLayer API Client using environmental settings."""
149247
return create_client_from_env(**kwargs)
@@ -282,6 +380,7 @@ def call(self, service, method, *args, **kwargs):
282380
request.filter = kwargs.get('filter')
283381
request.limit = kwargs.get('limit')
284382
request.offset = kwargs.get('offset')
383+
request.url = self.settings['softlayer'].get('endpoint_url')
285384
if kwargs.get('verify') is not None:
286385
request.verify = kwargs.get('verify')
287386

@@ -617,6 +716,97 @@ def __repr__(self):
617716
return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth)
618717

619718

719+
class EmployeeClient(BaseClient):
720+
"""Internal SoftLayer Client
721+
722+
:param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
723+
:param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
724+
"""
725+
726+
def __init__(self, auth=None, transport=None, config_file=None, account_id=None):
727+
BaseClient.__init__(self, auth, transport, config_file)
728+
self.account_id = account_id
729+
730+
731+
def authenticate_with_password(self, username, password, security_token=None):
732+
"""Performs IBM IAM Username/Password Authentication
733+
734+
:param string username: your softlayer username
735+
:param string password: your softlayer password
736+
:param int security_token: your 2FA token, prompt if None
737+
"""
738+
739+
self.auth = None
740+
if security_token is None:
741+
security_token = input("Enter your 2FA Token now: ")
742+
if len(security_token) != 6:
743+
raise Exception("Invalid security token: {}".format(security_token))
744+
745+
auth_result = self.call('SoftLayer_User_Employee', 'performExternalAuthentication',
746+
username, password, security_token)
747+
748+
749+
self.settings['softlayer']['access_token'] = auth_result['hash']
750+
self.settings['softlayer']['userid'] = str(auth_result['userId'])
751+
# self.settings['softlayer']['refresh_token'] = tokens['refresh_token']
752+
753+
config.write_config(self.settings, self.config_file)
754+
self.auth = slauth.EmployeeAuthentication(auth_result['userId'], auth_result['hash'])
755+
756+
return auth_result
757+
758+
759+
760+
def authenticate_with_hash(self, userId, access_token):
761+
"""Authenticates to the Internal SL API with an employee userid + token
762+
763+
:param string userId: Employee UserId
764+
:param string access_token: Employee Hash Token
765+
"""
766+
self.auth = slauth.EmployeeAuthentication(userId, access_token)
767+
768+
def refresh_token(self, userId, auth_token):
769+
"""Refreshes the login token"""
770+
771+
# Go directly to base client, to avoid infite loop if the token is super expired.
772+
auth_result = BaseClient.call(self, 'SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId)
773+
if len(auth_result) > 1:
774+
for returned_data in auth_result:
775+
# Access tokens should be 188 characters, but just incase its longer or something.
776+
if len(returned_data) > 180:
777+
self.settings['softlayer']['access_token'] = returned_data
778+
else:
779+
message = "Excepted 2 properties from refreshEncryptedToken, got {}|".format(auth_result)
780+
raise exceptions.SoftLayerAPIError(message)
781+
782+
config.write_config(self.settings, self.config_file)
783+
self.auth = slauth.EmployeeAuthentication(userId, auth_result[0])
784+
return auth_result
785+
786+
def call(self, service, method, *args, **kwargs):
787+
"""Handles refreshing Employee tokens in case of a HTTP 401 error"""
788+
if (service == 'SoftLayer_Account' or service == 'Account') and not kwargs.get('id'):
789+
if not self.account_id:
790+
raise exceptions.SoftLayerError("SoftLayer_Account service requires an ID")
791+
kwargs['id'] = self.account_id
792+
793+
try:
794+
return BaseClient.call(self, service, method, *args, **kwargs)
795+
except exceptions.SoftLayerAPIError as ex:
796+
if ex.faultCode == "SoftLayer_Exception_EncryptedToken_Expired":
797+
userId = self.settings['softlayer'].get('userid')
798+
access_token = self.settings['softlayer'].get('access_token')
799+
LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString)
800+
self.refresh_token(userId, access_token)
801+
# Try the Call again this time....
802+
return BaseClient.call(self, service, method, *args, **kwargs)
803+
804+
else:
805+
raise ex
806+
807+
def __repr__(self):
808+
return "EmployeeClient(transport=%r, auth=%r)" % (self.transport, self.auth)
809+
620810
class Service(object):
621811
"""A SoftLayer Service.
622812

SoftLayer/CLI/core.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,8 @@ def get_version_message(ctx, param, value):
6969
ctx.exit()
7070

7171

72-
@click.group(help="SoftLayer Command-line Client",
73-
epilog="""To use most commands your SoftLayer username and api_key need to be configured.
74-
The easiest way to do that is to use: 'slcli setup'""",
72+
@click.group(help="SoftLayer Employee Command-line Client",
73+
epilog="""Run 'islcli login' to authenticate""",
7574
cls=CommandLoader,
7675
context_settings=CONTEXT_SETTINGS)
7776
@click.option('--format',
@@ -102,6 +101,7 @@ def get_version_message(ctx, param, value):
102101
help="Use demo data instead of actually making API calls")
103102
@click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=get_version_message,
104103
help="Show version information.", allow_from_autoenv=False,)
104+
@click.option('--account', '-a', help="Account Id")
105105
@environment.pass_env
106106
def cli(env,
107107
format='table',
@@ -110,6 +110,7 @@ def cli(env,
110110
proxy=None,
111111
really=False,
112112
demo=False,
113+
account=None,
113114
**kwargs):
114115
"""Main click CLI entry-point."""
115116

@@ -133,6 +134,8 @@ def cli(env,
133134
env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport)
134135
env.vars['verbose'] = verbose
135136
env.client.transport = env.vars['_timings']
137+
print("Account ID is now: {}".format(account))
138+
env.client.account_id = account
136139

137140

138141
@cli.result_callback()

SoftLayer/CLI/environment.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def ensure_client(self, config_file=None, is_demo=False, proxy=None):
189189
)
190190
else:
191191
# Create SL Client
192-
client = SoftLayer.create_client_from_env(
192+
client = SoftLayer.employee_client(
193193
proxy=proxy,
194194
config_file=config_file,
195195
)

SoftLayer/CLI/login.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Login with your employee username, password, 2fa token"""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import click
5+
import os
6+
7+
8+
from SoftLayer.API import EmployeeClient
9+
from SoftLayer.CLI.command import SLCommand as SLCommand
10+
from SoftLayer import config
11+
from SoftLayer import consts
12+
from SoftLayer.CLI import environment
13+
14+
15+
# def get_username(env):
16+
# """Gets the username from config or env"""
17+
# settings =
18+
19+
def censor_password(value):
20+
if value:
21+
value = '*' * len(value)
22+
return value
23+
24+
@click.command(cls=SLCommand)
25+
@environment.pass_env
26+
def cli(env):
27+
"""Logs you into the internal SoftLayer Network.
28+
29+
username: Set this in either the softlayer config, or SL_USER ENV variable
30+
password: Set this in SL_PASSWORD env variable. You will be prompted for them otherwise.
31+
"""
32+
config_settings = config.get_config(config_file=env.config_file)
33+
settings = config_settings['softlayer']
34+
username = settings.get('username') or os.environ.get('SLCLI_USER', None)
35+
password = os.environ.get('SLCLI_PASSWORD', '')
36+
yubi = None
37+
client = env.client
38+
39+
# Might already be logged in, try and refresh token
40+
if settings.get('access_token') and settings.get('userid'):
41+
client.authenticate_with_hash(settings.get('userid'), settings.get('access_token'))
42+
try:
43+
employee = client.call('SoftLayer_User_Employee', 'getObject', id=settings.get('userid'), mask="mask[id,username]")
44+
print(employee)
45+
client.refresh_token(settings.get('userid'), settings.get('access_token'))
46+
refresh = client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=settings.get('userid'))
47+
48+
config_settings['softlayer'] = settings
49+
config.write_config(config_settings, env.config_file)
50+
return
51+
except Exception as ex:
52+
print("Error with Hash Authentication, try with password: {}".format(ex))
53+
54+
55+
url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT
56+
click.echo("URL: {}".format(url))
57+
if username is None:
58+
username = input("Username: ")
59+
click.echo("Username: {}".format(username))
60+
if not password:
61+
password = env.getpass("Password: ")
62+
click.echo("Password: {}".format(censor_password(password)))
63+
yubi = input("Yubi: ")
64+
65+
66+
try:
67+
result = client.authenticate_with_password(username, password, str(yubi))
68+
print(result)
69+
except Exception as e:
70+
click.echo("EXCEPTION: {}".format(e))
71+
72+
print("OK")

SoftLayer/CLI/routes.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
ALL_ROUTES = [
1010
('shell', 'SoftLayer.shell.core:cli'),
11+
('login', 'SoftLayer.CLI.login:cli'),
1112

1213
('call-api', 'SoftLayer.CLI.call_api:cli'),
1314

0 commit comments

Comments
 (0)