Skip to content

Commit 3005872

Browse files
committed
Initial version
0 parents  commit 3005872

File tree

7 files changed

+755
-0
lines changed

7 files changed

+755
-0
lines changed

Dockerfile

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
FROM ubuntu:20.04
2+
3+
RUN \
4+
apt-get -y update && \
5+
export DEBIAN_FRONTEND=noninteractive && \
6+
export TZ=Etc/UTC && \
7+
apt-get -y install gcc make curl git openjdk-8-jre
8+
9+
# Copy in the SDK
10+
COPY --from=kbase/kb-sdk:20180808 /src /sdk
11+
RUN sed -i 's|/src|/sdk|g' /sdk/bin/*
12+
13+
RUN \
14+
V=py38_4.10.3 && \
15+
curl -o conda.sh -s https://repo.anaconda.com/miniconda/Miniconda3-${V}-Linux-x86_64.sh && \
16+
sh ./conda.sh -b -p /opt/conda3 && \
17+
rm conda.sh
18+
19+
ENV PATH=/opt/conda3/bin:$PATH:/sdk/bin
20+
21+
# Install packages including mamba
22+
RUN \
23+
conda install -c conda-forge mamba
24+
25+
ADD ./requirements.txt /tmp/
26+
RUN \
27+
pip install -r /tmp/requirements.txt
28+
29+
# Add in some legacy modules
30+
ADD biokbase /opt/conda3/lib/python3.8/site-packages/biokbase
31+

LICENSE.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright (c) 2022 The KBase Project and its Contributors
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SDK Base Python Image
2+
3+
This is a very minimal base python image for building KBase SDK apps.
4+
5+

biokbase/__init__.py

Whitespace-only changes.

biokbase/auth.py

+339
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
"""
2+
Kbase wrappers around Globus Online Nexus client libraries. We wrap the Nexus
3+
libraries to provide a similar API between the Perl Bio::KBase::Auth* libraries
4+
and the python version
5+
6+
In this module, we follow standard Python idioms of raising exceptions for
7+
various failure states ( Perl modules returned error states in error_msg field)
8+
"""
9+
from biokbase.nexus.client import NexusClient
10+
from ConfigParser import ConfigParser
11+
import os
12+
from urlparse import urlparse
13+
from pprint import pformat
14+
import requests
15+
import re
16+
17+
"""
18+
Package "globals"
19+
kb_config
20+
trust_token_signers
21+
attrs
22+
authdata
23+
config
24+
tokenend
25+
AuthSvcHost
26+
RoleSvcURL
27+
nexusconfig
28+
29+
"""
30+
__version__ = "0.9"
31+
32+
kb_config = os.environ.get('KB_DEPLOYMENT_CONFIG',os.environ['HOME']+"/.kbase_config")
33+
34+
trust_token_signers = [ 'https://nexus.api.globusonline.org/goauth/keys' ]
35+
attrs = [ 'user_id', 'token','client_secret', 'keyfile',
36+
'keyfile_passphrase','password','sshagent_keys',
37+
'sshagent_keyname']
38+
39+
# authdata stores the configuration key/values from any configuration file
40+
authdata = dict()
41+
if os.path.exists( kb_config):
42+
try:
43+
conf = ConfigParser()
44+
conf.read(kb_config)
45+
# strip down whatever we read to only what is legit
46+
for x in attrs:
47+
authdata[x] = conf.get('authentication',x) if conf.has_option('authentication',x) else None
48+
except Exception, e:
49+
print "Error while reading INI file %s: %s" % (kb_config, e)
50+
tokenenv = authdata.get( 'tokenvar', 'KB_AUTH_TOKEN')
51+
# Yes, some variables are camel cased and others are all lower. Trying to maintain
52+
# the attributes names from the perl version which was a mishmash too. regret.
53+
AuthSvcHost = authdata.get( 'servicehost', "https://nexus.api.globusonline.org/")
54+
# Copied from perl libs for reference, not used here
55+
#ProfilePath = authdata.get( 'authpath', "/goauth/token")
56+
RoleSvcURL = authdata.get( 'rolesvcurl', "https://kbase.us/services/authorization/Roles")
57+
nexusconfig = { 'cache' : { 'class': 'biokbase.nexus.token_utils.InMemoryCache',
58+
'args': [],
59+
},
60+
'server' : urlparse(AuthSvcHost).netloc,
61+
'verify_ssl' : False,
62+
'client' : None,
63+
'client_secret' : None}
64+
# Compile a regex for parsing out user_id's from tokens
65+
token_userid = re.compile( '(?<=^un=)\w+')
66+
67+
def LoadConfig():
68+
"""
69+
Method to load configuration from INI style files from the file in kb_config
70+
"""
71+
global kb_config,authdata,tokenenv,AuthSvcHost,RolesSvcHost
72+
global RoleSvcURL,nexusconfig,conf
73+
74+
kb_config = os.environ.get('KB_DEPLOYMENT_CONFIG',os.environ['HOME']+"/.kbase_config")
75+
76+
if os.path.exists( kb_config):
77+
try:
78+
conf = ConfigParser()
79+
conf.read(kb_config)
80+
# strip down whatever we read to only what is legit
81+
for x in attrs:
82+
authdata[x] = conf.get('authentication',x) if conf.has_option('authentication',x) else None
83+
except Exception, e:
84+
print "Error while reading INI file %s: %s" % (kb_config, e)
85+
tokenenv = authdata.get( 'tokenvar', 'KB_AUTH_TOKEN')
86+
# Yes, some variables are camel cased and others are all lower. Trying to maintain
87+
# the attributes names from the perl version which was a mishmash too. regret.
88+
AuthSvcHost = authdata.get( 'servicehost', "https://nexus.api.globusonline.org/")
89+
# Copied from perl libs for reference, not used here
90+
#ProfilePath = authdata.get( 'authpath', "/goauth/token")
91+
RoleSvcURL = authdata.get( 'rolesvcurl', "https://kbase.us/services/authorization/Roles")
92+
nexusconfig = { 'cache' : { 'class': 'biokbase.nexus.token_utils.InMemoryCache',
93+
'args': [],
94+
},
95+
'server' : urlparse(AuthSvcHost).netloc,
96+
'verify_ssl' : False,
97+
'client' : None,
98+
'client_secret' : None}
99+
100+
def SetConfigs( configs):
101+
"""
102+
Method used to set configuration directives in INI file kb_config
103+
Takes as a parameter a dictionary of config directives within the authentication section
104+
that need to be set/unset. If there is a dictionary entry where the value is None
105+
then the config setting will be deleted
106+
"""
107+
global kb_config,authdata,tokenenv,AuthSvcHost,RolesSvcHost
108+
global RoleSvcURL,nexusconfig,conf
109+
110+
conf = ConfigParser()
111+
if os.path.exists( kb_config):
112+
conf.read(kb_config)
113+
if not conf.has_section('authentication'):
114+
conf.add_section('authentication')
115+
for key in configs.keys():
116+
if configs[key] is not None:
117+
conf.set('authentication',key, configs[key])
118+
else:
119+
conf.remove_option('authentication',key)
120+
with open(kb_config, 'wb') as configfile:
121+
conf.write(configfile)
122+
LoadConfig()
123+
124+
class AuthCredentialsNeeded( Exception ):
125+
"""
126+
Simple wrapper around Exception class that flags the fact that we don't have
127+
enough credentials to authenticate, which is distinct from having bad or bogus
128+
credentials
129+
"""
130+
pass
131+
132+
class AuthFail( Exception ):
133+
"""
134+
Simple wrapper around Exception class that flags our credentials are bad or bogus
135+
"""
136+
pass
137+
138+
class Token:
139+
"""
140+
Class that handles token requests and validation. This is basically a wrapper
141+
around the biokbase.nexus.client.NexusClient class from GlobusOnline that provides a
142+
similar API to the perl Bio::KBase::AuthToken module. For KBase purposes
143+
we have modified the base Globus Online classes to support ssh agent based
144+
authentication as well.
145+
146+
In memory caching is provided by the underlying biokbase.nexus.client implementation.
147+
148+
Instance Attributes:
149+
user_id
150+
password
151+
token
152+
keyfile
153+
client_secret
154+
keyfile_passphrase
155+
sshagent_keyname
156+
"""
157+
158+
def __init__(self, **kwargs):
159+
"""
160+
Constructor for Token class will accept these optional parameters attributes in
161+
order to initialize the object:
162+
163+
user_id, password, token, keyfile, client_secret, keyfile_passphrase, sshagent_keyname
164+
165+
If user_id is provided among the initializers, the get() method will be called at the
166+
end of initialization to attempt to fetch a token from the service defined in
167+
AuthSvcHost. If there are not enough credentials to authenticate, we ignore the
168+
exception. However if there are enough credentials and they fail to authenticate,
169+
the exception will be reraised.
170+
171+
If there is a ~/kbase_config INI file, it will be used to fill in values when there
172+
are no initialization values given - this can be short circuited by setting
173+
ignore_kbase_config to true among the initialization params
174+
"""
175+
global nexusconfig
176+
attrs = [ 'keyfile','keyfile_passphrase','user_id','password','token','client_secret','sshagent_keyname']
177+
for attr in attrs:
178+
setattr( self, attr, kwargs.get(attr,None))
179+
self.nclient = NexusClient(nexusconfig)
180+
self.nclient.user_key_file = self.keyfile
181+
182+
if self.nclient.__dict__.has_key("agent_keys"):
183+
self.sshagent_keys = self.nclient.agent_keys
184+
else:
185+
self.sshagent_keys = dict()
186+
187+
# Flag to mark if we got default values from .kbase_config file
188+
defattr = reduce( lambda x,y: x or (authconf.get(y, None) is not None), attrs)
189+
# if we have a user_id defined, try to get a token with whatever else was given
190+
# if it fails due to not enough creds, try any values from ~/.kbase_config
191+
if (self.user_id):
192+
try:
193+
self.get()
194+
except AuthCredentialsNeeded:
195+
pass
196+
except Exception, e:
197+
raise e
198+
elif os.environ.get(tokenenv):
199+
self.token = os.environ[tokenenv]
200+
elif defattr and not kwargs.get('ignore_kbase_config'):
201+
for attr in attrs:
202+
if authdata.get(attr) is not None:
203+
setattr(self,attr,authdata[attr])
204+
try:
205+
self.get()
206+
except AuthCredentialsNeeded:
207+
pass
208+
except Exception, e:
209+
raise e
210+
if self.user_id is None and self.token:
211+
# parse out the user_id and set it
212+
self.user_id = token_userid.search( self.token).group(0)
213+
214+
def validate( self, token = None):
215+
"""
216+
Method that validates the contents of self.token against the authentication service backend
217+
This method caches results, so an initial validation will be high latency due to the
218+
network round trips, but subsequent validations will return very quickly
219+
220+
A successfully validated token will return user_id
221+
222+
Invalid tokens will generate a ValueError exception
223+
"""
224+
if token is not None:
225+
res = self.nclient.validate_token( token)
226+
else:
227+
res = self.nclient.validate_token( self.token)
228+
self.user_id = res[0]
229+
return self.user_id
230+
231+
def get(self, **kwargs):
232+
"""
233+
Use either explicit parameters or the current instance vars to authenticate and retrieve a
234+
token from GlobusOnline (or whoever else is defined in the AuthSvcHost class attribute).
235+
236+
The following parameters are optional, and will be assigned to the instance vars before
237+
attempting to fetch a token:
238+
keyfile, keyfile_passphrase, user_id, password, client_secret, sshagent_keyname
239+
240+
A user_id and any of the following will be enough to attempt authentication:
241+
keyfile, keyfile_passphrase, password, sshagent_keyname
242+
243+
If there are not enough credentials, then an AuthCredentialsNeeded exception will be raised
244+
If the underlying Globus libraries fail to authenticate, the exception will be passed up
245+
246+
Success returns self, but with the token attribute containing a good token, an AuthFail
247+
exception will be thrown if the credentials are rejected by Globus Online
248+
249+
Note: authentication with an explicit RSA client_secret is not currently supported
250+
"""
251+
# attributes that we would allow to be passed in via kwargs
252+
attrs = [ 'keyfile','keyfile_passphrase','user_id','password','token','client_secret','sshagent_keyname']
253+
for attr in attrs:
254+
if attr in kwargs:
255+
setattr( self, attr, kwargs[attr])
256+
# override the user_key_file default in the nclient object
257+
self.nclient.user_key_file = self.keyfile
258+
# in the perl libraries, if we have a user_id, no other credentials, and a single
259+
# available sshagent_keyname from ssh_agent, default to using that for auth
260+
if (self.user_id and not ( self.password or self.sshagent_keyname or self.keyfile)
261+
and (len(self.sshagent_keys.keys()) == 1)):
262+
self.sshagent_keyname = self.sshagent_keys.keys()[0]
263+
if not (self.user_id and ( self.password or self.sshagent_keyname or self.keyfile)):
264+
raise AuthCredentialsNeeded( "Need either (user_id, client_secret || password || sshagent_keyname) to be defined.")
265+
if self.keyfile:
266+
self.nclient.user_key_file = self.keyfile
267+
if (self.user_id and self.keyfile):
268+
passphrase = kwargs.get("keyfile_passphrase",self.keyfile_passphrase)
269+
res = self.nclient.request_client_credential( self.user_id, lambda : passphrase )
270+
elif (self.user_id and self.password):
271+
res = self.nclient.request_client_credential( self.user_id, self.password)
272+
elif (self.user_id and self.sshagent_keyname):
273+
res = self.nclient.request_client_credential_sshagent( self.user_id, self.sshagent_keyname)
274+
else:
275+
raise AuthCredentialsNeeded("Authentication with explicit client_secret not supported - please put key in file or sshagent")
276+
if 'access_token' in res:
277+
self.token = res['access_token']
278+
else:
279+
raise AuthFail('Could not authenticate with values: ' + pformat(self.__dict__))
280+
return self
281+
282+
def get_sessDB_token():
283+
pass
284+
285+
class User:
286+
top_attrs = { "user_id" : "username",
287+
"verified" : "email_validated",
288+
"opt_in" : "opt_in",
289+
"name" : "fullname",
290+
"email" : "email",
291+
"system_admin" : "system_admin" }
292+
293+
def __init__(self, **kwargs):
294+
"""
295+
Constructor for User class will accept these optional parameters attributes in
296+
order to initialize the object:
297+
298+
user_id, password, token, enabled, groups, name, email, verified
299+
300+
If a token is provided among the initializers, the get() method will be called at the
301+
end of initialization to attempt to fetch the user profile from Globus Online
302+
303+
The ~/.kbase_config file is only indirectly supported - use it to get a token, and then
304+
use that token as an initializer to this function to fetch a profile
305+
"""
306+
global nexusconfig
307+
attrs = [ 'user_id', 'enabled', 'groups', 'name', 'email', 'verified' ]
308+
for attr in attrs:
309+
setattr( self, attr, kwargs.get(attr,None))
310+
if kwargs['token']:
311+
self.authToken = Token( token = kwargs['token'])
312+
self.token = self.authToken.token
313+
self.get()
314+
return
315+
316+
def get(self, **kwargs):
317+
if 'token' in kwargs:
318+
self.authToken = Token( token = kwargs['token'])
319+
self.token = self.authToken.token
320+
if not self.token:
321+
raise AuthCredentialsNeeded( "Authentication token required")
322+
p = { 'custom_fields' : '*',
323+
'fields' : 'groups,username,email_validated,fullname,email'
324+
}
325+
headers = { 'Authorization' : 'Globus-Goauthtoken ' + self.token }
326+
resp = requests.get( AuthSvcHost+"users/" + self.authToken.user_id, params = p,
327+
headers = headers)
328+
profile = resp.json()
329+
for attr,go_attr in self.top_attrs.items():
330+
setattr( self, attr, profile.get( go_attr))
331+
# pull out the name field from the groups dict entries and put into groups
332+
setattr( self, 'groups', [ x['name'] for x in resp.json['groups']])
333+
if 'custom_fields' in profile:
334+
for attr in profile['custom_fields'].keys():
335+
setattr( self, attr, profile['custom_fields'][attr])
336+
return self
337+
338+
def update(self, **kwargs):
339+
pass

0 commit comments

Comments
 (0)