|
| 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