Source code for dl.authClient

#!/usr/bin/env python
#
# AUTHCLIENT -- Client methods for the Data Lab Authentication Manager service
#

from __future__ import print_function

__authors__ = 'Mike Fitzpatrick <mike.fitzpatrick@noirlab.edu>, Data Lab <datalab@noirlab.edu>'
try:
    from authmanager.__version__ import __version__
except ImportError as e:
    from dl.__version__ import __version__


'''
    Client methods for the Data Lab Authentication Manager Service.

    Auth Manager Client Interface
    -----------------------------

                 login  (user, password=None, debug=False, verbose=False)
                logout  (token=None)

                whoAmI  ()
               isAlive  (svc_url=DEF_SERVICE_URL)
          isValidToken  (token)
           isValidUser  (user)
       isValidPassword  (user, password)
             hasAccess  (user, resource)
        isUserLoggedIn  (user)
       isTokenLoggedIn  (token)
         passwordReset  (token, username, password)

           set_svc_url  (svc_url)
           get_svc_url  ()
           set_profile  (profile)
           get_profile  ()
             getClient  ()


Import via

.. code-block:: python

    from dl import authClient
'''

import requests
import socket
import os
import sys
from time import gmtime, strftime

try:
    from urllib import urlencode                # Python 2
    import ConfigParser                         # Python 2
except ImportError:
    from urllib.parse import urlencode          # Python 3
    import configparser as ConfigParser         # Python 3

#if os.path.isfile('./Util.py'):                # use local dev copy
#    from Util import def_token
#else:                                           # use distribution copy
#    from dl.Util import def_token
from dl.Util import def_token, split_auth_token

is_py3 = sys.version_info.major == 3



# Pre-defined authentication tokens. These are fixed strings that provide
# limited access to Data Lab services, this access is controlled on the
# server-side so we don't need strict security here.

ANON_TOKEN = "anonymous.0.0.anon_access"
DEMO_TOKEN = "dldemo.99999.99999.demo_access"
TEST_TOKEN = "dltest.99998.99998.test_access"

# Set the default user accounts for the Authentication Manager service.  We don't
# include privileged users so that account can remain secure.

DEF_USERS = {'anonymous': ANON_TOKEN,
             'dldemo': DEMO_TOKEN,
             'dltest': TEST_TOKEN}


# The URL of the Authentication Manager service to contact.  This may be changed by
# passing a new URL into the set_svc_url() method before beginning.

DEF_SERVICE_ROOT = "https://datalab.noirlab.edu"

# Allow the service URL for dev/test systems to override the default.
THIS_HOST = socket.gethostname()
if THIS_HOST[:5] == 'dldev':
    DEF_SERVICE_ROOT = "https://dldev.datalab.noirlab.edu"
elif THIS_HOST[:6] == 'dltest':
    DEF_SERVICE_ROOT = "https://dltest.datalab.noirlab.edu"

DEF_SERVICE_URL = DEF_SERVICE_ROOT + "/auth"
SM_SERVICE_URL  = DEF_SERVICE_ROOT + "/storage"
QM_SERVICE_URL  = DEF_SERVICE_ROOT + "/query"


# Check for a file to override the default service URL.
if os.path.exists('/tmp/AM_SVC_URL'):
    with open('/tmp/AM_SVC_URL') as fd:
        DEF_SERVICE_URL = fd.read().strip()
if os.path.exists('/tmp/SM_SVC_URL'):
    with open('/tmp/SM_SVC_URL') as fd:
        SM_SERVICE_URL = fd.read().strip()
if os.path.exists('/tmp/QM_SVC_URL'):
    with open('/tmp/QM_SVC_URL') as fd:
        QM_SERVICE_URL = fd.read().strip()


# The requested authentication "profile".  A profile refers to the specific
# machines and services used by the Authentication Manager on the server. [Note that
# profiles are not currently used.]

DEF_SERVICE_PROFILE = "default"

# Use a /tmp/AM_DEBUG file as a way to turn on debugging in the client code.
DEBUG = os.path.isfile('/tmp/AM_DEBUG')


# ######################################################################
#
#  Authentication Client Interface
#
#  This API provides convenience methods that allow an application to
#  import the Client class without having to explicitly instantiate a
#  class object.  The parameter descriptions and example usage is given
#  in the comments for the class methods.  Module methods have their
#  docstrings patched below.
#
# ######################################################################


# ###################################
#  Authentication error class
# ###################################

[docs] class dlAuthError(Exception): '''A throwable error class. ''' def __init__(self, message): self.message = message def __str__(self): return self.message
# User methods -- All methods except login() return either a 'True' string # or an error of the form 'ERR <message>'. On success, the login() method # will return the user-id token.
[docs] def login(user, password=None, debug=False, verbose=False): response = 'OK' if user in list(DEF_USERS.keys()): return DEF_USERS[user] else: try: response = ac_client.login(user, password, debug=debug, verbose=verbose) except Exception as e: response = str(e) return response
[docs] def whoAmI(): user = 'anonymous' try: token = def_token(None) user, uid, gid, hash = split_auth_token(token.strip()) except: return 'anonymous' else: return user
[docs] def isAlive(svc_url=DEF_SERVICE_URL): try: response = ac_client.isAlive(svc_url.strip('/')) except Exception as e: response = str(e) return False return response
[docs] def isValidToken(token): try: user, uid, gid, hash = split_auth_token(token.strip()) except Exception: return False if user in list(DEF_USERS.keys()) and token in list(DEF_USERS.values()): return True else: try: response = ac_client.isValidToken(token) except Exception: return False return (True if response.lower() == 'true' else False)
[docs] def isValidUser(user): if user in list(DEF_USERS.keys()): return True else: try: response = ac_client.isValidUser(user) except Exception as e: response = str(e) return (True if response.lower() == 'true' else False)
[docs] def isValidPassword(user, password): if (user == password) and (user in list(DEF_USERS.keys())): return True else: try: response = ac_client.isValidPassword(user, password) except Exception as e: response = str(e) return (True if response.lower() == 'true' else False)
[docs] def hasAccess(user, resource): try: response = ac_client.hasAccess(user, resource) except Exception as e: response = str(e) return (True if response.lower() == 'true' else False)
[docs] def isUserLoggedIn(user): try: response = ac_client.isUserLoggedIn(user) except Exception as e: response = str(e) return (True if response.lower() == 'true' else False)
[docs] def isTokenLoggedIn(token): try: response = ac_client.isTokenLoggedIn(token) except Exception as e: response = str(e) return (True if response.lower() == 'true' else False)
[docs] def logout(token=None): try: response = ac_client.logout(def_token(token)) except Exception as e: response = str(e) return response
[docs] def passwordReset(token, username, password): try: response = ac_client.passwordReset(token, username, password) except Exception as e: response = str(e) return response
# Standard Service Methods
[docs] def set_svc_url(svc_url): if svc_url is not None and svc_url != '': return ac_client.set_svc_url(svc_url.strip('/'))
[docs] def get_svc_url(): return ac_client.get_svc_url()
[docs] def set_profile(profile): return ac_client.set_profile(profile)
[docs] def get_profile(): return ac_client.get_profile()
[docs] def list_profiles(token, profile=None, format='text'): return "None"
##################################### # Authentication client procedures #####################################
[docs] class authClient(object): ''' AUTHCLIENT -- Client-side methods to access the Data Lab Authentication Manager service. ''' def __init__(self): '''Initialize the authClient class. ''' self.svc_url = DEF_SERVICE_URL # service URL self.svc_profile = DEF_SERVICE_PROFILE # service prfile self.username = "" # default client logn user self.auth_token = None # default client logn token # Check the $HOME/.datalab directory for a valid token. If that dir # doesn't already exist, create it so we can store the new token. self.home = '%s/.datalab' % os.path.expanduser('~') if not os.path.exists(self.home): os.makedirs(self.home) self.loadConfig() # load config file self.debug = DEBUG # interface debug flag
[docs] def loadConfig(self): '''Read the $HOME/.datalab/dl.conf file. ''' self.config = ConfigParser.RawConfigParser(allow_no_value=True) if os.path.exists('%s/dl.conf' % self.home): self.config.read('%s/dl.conf' % self.home) else: self.config.add_section('datalab') self.config.set('datalab', 'created', strftime( '%Y-%m-%d %H:%M:%S', gmtime())) self.config.add_section('login') self.config.set('login', 'status', 'loggedout') self.config.set('login', 'user', '') self.config.set('login', 'authtoken', '') self.config.add_section('auth') self.config.set('auth', 'profile', 'default') self.config.set('auth', 'svc_url', DEF_SERVICE_URL) self.config.add_section('query') self.config.set('query', 'profile', 'default') self.config.set('query', 'svc_url', QM_SERVICE_URL) self.config.add_section('storage') self.config.set('storage', 'profile', 'default') self.config.set('storage', 'svc_url', SM_SERVICE_URL) self.config.add_section('vospace') self.config.set('vospace', 'mount', '') self.writeConfig()
[docs] def setConfig(self, section, param, value): '''Set a value and save the configuration file. ''' if not self.config.has_section(section): self.config.add_section(section) self.config.set(section, param, value) self.writeConfig()
[docs] def getConfig(self, section, param): '''Get a value from the configuration file. ''' return self.config.get(section, param)
[docs] def writeConfig(self): '''Write out the configuration file to disk. ''' with open('%s/dl.conf' % self.home, 'w') as configfile: self.config.write(configfile)
[docs] def whoAmI(self): '''Return the currently logged in user identity. Parameters ---------- None Returns ------- name : str Currently logged in user name, or 'anonymous' if not logged in. Example ------- >>> from dl import authClient >>> name = authClient.whoAmI() ''' user = 'anonymous' try: token = def_token(None) user, uid, gid, hash = split_auth_token(token.strip()) except: return 'anonymous' else: return user
# Standard Data Lab service methods. #
[docs] def set_svc_url(self, svc_url): '''Set the URL of the Authentication Manager service to be used. Parameters ---------- svc_url : str Authentication Manager service base URL to call. Returns ------- Nothing Example ------- >>> from dl import authClient >>> authClient.set_svc_url("http://localhost:7001/") ''' if svc_url is not None and svc_url != '': self.svc_url = acToString(svc_url.strip('/'))
[docs] def get_svc_url(self): '''Return the currently-used Authentication Manager service URL. Parameters ---------- None Returns ------- service_url : str The currently-used Authentication Manager service URL. Example ------- >>> from dl import authClient >>> service_url = authClient.get_svc_url() ''' return acToString(self.svc_url)
[docs] def set_profile(self, profile): '''Set the requested service profile. Parameters ---------- profile : str Requested service profile. Returns ------- Nothing Example ------- >>> from dl import authClient >>> authClient.set_profile("dev") ''' if profile is not None and profile != '': self.svc_profile = acToString(profile)
[docs] def get_profile(self): '''Get the requested service profile. Parameters ---------- None Returns ------- profile : str The currently requested service profile. Example ------- >>> from dl import authClient >>> profile = authClient.get_profile() ''' return acToString(self.svc_profile)
[docs] def list_profiles(self, token, profile=None, format='text'): '''List the service profiles which can be accessed by the user. Parameters ---------- token : str Valid auth service token. Returns ------- profiles : JSON string Example ------- >>> from dl import authClient >>> profiles = authClient.list_profiles(token, profile, format) ''' pass
[docs] def isAlive(self, svc_url=DEF_SERVICE_URL): '''Check whether the Authentication Manager service at the given URL is alive and responding. This is a simple call to the root service URL or ``ping()`` method. Parameters ---------- service_url : str The Query Service URL to ping. Returns ------- result : bool ``True`` if service responds properly, ``False`` otherwise. Example ------- >>> from dl import authClient >>> authClient.isAlive() ''' url = svc_url try: r = requests.get(url, timeout=2) resp = r.text if r.status_code != 200: return False elif resp is not None and r.text.lower()[:11] != "hello world": return False except Exception: return False return True
################################################### # SESSION MANAGEMENT ###################################################
[docs] def login(self, username, password, debug=False, verbose=False): '''Authenticate the user with the Authentication Manager service. We first check for a valid login token in the user's ``$HOME/.datalab/`` directory and simply return that rather than make a service call to get a new token. If a token exists but is invalid, we remove it and get a new token. In either case, we save the token for later use. Parameters ---------- username : str User login name. password : str User password. If not given, a valid ID token will be searched for in the ``$HOME/.datalab`` directory. debug : bool Method debug flag. verbose : bool Initialize session to print verbose output messages. Returns ------- token : str One-time security token for valid user (identified via ``username`` and ``password``). Example ------- >>> from dl import authClient >>> token = authClient.login('dldemo', 'dldemo') # get security token ''' # Check the $HOME/.datalab directory for a valid token. If that dir # doesn't already exist, create it so we can store the new token. if not os.path.exists(self.home): os.makedirs(self.home) # See if a datalab token file exists for the requested user. tok_file = ('%s/id_token.%s' % (self.home, username)) if debug or self.debug: print("top of login: tok_file = '" + tok_file + "'") print("top of login: self.auth_token = '%s'" % str(self.auth_token)) print("top of login: token = ") os.system("cat " + tok_file) if password is None: if os.path.exists(tok_file) and os.stat(tok_file).st_size > 0: with open(tok_file, "r") as tok_fd: o_tok = acToString(tok_fd.read(128)) # read the old token # Return a valid token, otherwise remove the file and obtain a # new one. if o_tok.startswith(username+'.') and self.isValidToken(o_tok): self.username = username self.auth_token = o_tok if debug or self.debug: print("using old token for '%s'" % username) return acToString(o_tok) else: if debug or self.debug: print("removing invalid token file '%s'" % tok_file) os.remove(tok_file) # Either the user is not logged in or the token is invalid, so # make a service call to get a new token. url = self.svc_url + "/login?" query_args = {"username": username, "profile": self.svc_profile, "debug": (debug or self.debug)} # Add the auth token and password to the reauest header. headers = {'X-DL-Password': password} response = 'None' try: r = requests.get(url, params=query_args, headers=headers) response = acToString(r.content) if debug or self.debug: print("%s: resp = '%s'" % (str(r.status_code),response)) if r.status_code != 200: raise Exception(response) except Exception as e: if debug or self.debug: print("Raw exception msg = '%s'" % acToString(r.content)) if self.isAlive(self.svc_url) == False: raise dlAuthError("AuthManager Service not responding.") if self.isValidUser(username): if password is None: if not os.path.exists(tok_file): raise dlAuthError("No password or token supplied") else: raise dlAuthError("No password supplied") elif not self.isValidPassword(username, password): raise dlAuthError("Invalid password in login()") else: raise dlAuthError(str(e)) else: raise dlAuthError("Invalid username in login()") else: self.auth_token = response self.username = username # Save the token and config file. if os.access(self.home, os.W_OK): tok_file = '%s/id_token.%s' % (self.home, username) with open(tok_file, 'w') as tok_fd: if debug or self.debug: print("login: writing new token for '%s'" % username) print("login: self.auth_token = '%s'" % str(self.auth_token)) print("login: token = ") os.system('cat ' + tok_file) tok_fd.write(self.auth_token) self.config.set('login', 'status', 'loggedin') self.config.set('login', 'user', username) self.config.set('login', 'authtoken', self.auth_token) self.writeConfig() return acToString(self.auth_token)
[docs] def logout(self, token=None): '''Log the user out of Data Lab. Parameters ---------- token : str [Optional] User login token. Returns ------- 'OK' string on success, or exception message. Example ------- >>> from dl import authClient >>> status = authClient.logout() ''' url = self.svc_url + "/logout?" args = urlencode({"token": token, "debug": self.debug}) url = url + args if self.debug: print("logout: token = '%s'" % token) print("logout: auth_token = '%s'" % self.auth_token) print("logout: url = '%s'" % url) print("logout: logged in = " + self.isTokenLoggedIn(token)) try: user, uid, gid, hash = split_auth_token(token.strip()) except Exception: raise dlAuthError('Error: Invalid user token') if not isValidToken(token): raise dlAuthError("Error: Invalid user token") if not isTokenLoggedIn(token): raise dlAuthError("Error: User token is not logged in") try: # Add the auth token to the reauest header. headers = {'X-DL-AuthToken': token} r = requests.get(url, params=args, headers=headers) response = acToString(r.content) if r.status_code != 200: raise Exception(response) except Exception as e: raise dlAuthError(str(e)) else: self.auth_token = None tok_file = self.home + '/id_token.' + user if os.path.exists(tok_file): os.remove(tok_file) self.config.set('login', 'status', 'loggedout') self.config.set('login', 'user', '') self.config.set('login', 'authtoken', '') self.writeConfig() return response
[docs] def passwordReset(self, token, username, password): '''Reset a user password. We require that the user provide either a valid 'root' token or the token for the account being reset. Parameters ---------- token : str User login token. username : str User login name. password : str User password. Returns ------- 'OK' string on success, or exception message. Example ------- >>> from dl import authClient >>> authClient.passwordReset(token, 'monty', 'python') ''' url = self.svc_url + "/passwordReset?" args = urlencode({"token": token, "username": username, "debug": self.debug}) if self.debug: print("passwdReset: token = '%s'" % token) print("passwdReset: auth_token = '%s'" % self.auth_token) print("passwdReset: url = '%s'" % url) if not self.isValidToken(token): if self.debug: print("passwdReset: Invalid user token") raise Exception("Error: Invalid user token") # Reset the auth_token to the one passed in by the service call. self.auth_token = token user, uid, gid, hash = split_auth_token(self.auth_token.strip()) if user != 'root' and user != username: raise Exception("Error: Invalid user or non-root token") try: # Add the auth token and password to the reauest header. headers = {'X-DL-AuthToken': token, 'X-DL-Password': password} r = requests.get(url, params=args, headers=headers) response = acToString(r.content) if r.status_code != 200: raise Exception(r.content) except Exception: raise dlAuthError(acToString(r.content)) else: # Update the saved user token. if response is not None: self.auth_token = response tok_file = self.home + '/id_token.' + self.username if os.path.exists(tok_file): os.remove(tok_file) with open(tok_file, 'wb') as tok_fd: if self.debug: print("pwreset: writing new token for '%s'" + username) print("pwreset: response = '%s'" + response) print("pwreset: token = '%s'" + self.auth_token) tok_fd.write(acToString(self.auth_token)) tok_fd.close() else: print('pwReset response is None') return response
[docs] def hasAccess(self, token, resource): '''See whether the given token has access to the named Resource. Parameters ---------- token : str User login token. resource : str Resource identifier to check. Returns ------- status : bool ``True`` if user owns or has access, ``False`` otherwise. Example ------- >>> from dl import authClient >>> status = authClient.hasAccess(token, 'vos://test.dat') ''' # Either the user is not logged in or the token is invalid, so # make a service call to get a new token. url = self.svc_url + "/hasAccess?" args = urlencode({"user": token, "resource": resource, "profile": self.svc_profile}) url = url + args if self.debug: print("hasAccess: url = '%s'" % url) return self.retBoolValue(url)
[docs] def isValidToken(self, token): '''See whether the current token is valid. Parameters ---------- token : str User login token. Returns ------- status : bool ``True`` if token is valid, ``False`` otherwise. Example ------- >>> from dl import authClient >>> authClient.isValidToken(token) ''' url = self.svc_url + "/isValidToken?" args = urlencode({"token": token, "profile": self.svc_profile}) url = url + args if self.debug: print("isValidToken: url = '%s'" % url) # Save the value before returning so we can print it in debug mode. isValid = self.retBoolValue(url) if self.debug: print("isValidToken: valid = " + str(isValid)) return isValid
[docs] def isValidPassword(self, user, password): '''See whether the password is valid for the user. Parameters ---------- user : str User login name. password : str Password for named user. Returns ------- status : bool ``True`` if password is valid for the user, ``False`` otherwise. Example ------- >>> from dl import authClient >>> authClient.isValidPassword('monty', 'python') ''' url = self.svc_url + "/isValidPassword?" args = urlencode({"user": user, "password": password, "profile": self.svc_profile}) url = url + args if self.debug: print("isValidPassword: url = '%s'" % url) try: val = self.retBoolValue(url) except Exception: val = "False" return val
[docs] def isValidUser(self, user): '''See whether the specified user is valid. Parameters ---------- user : str User login name. Returns ------- status : bool ``True`` if ``user`` is a valid user name, ``False`` otherwise. Example ------- >>> from dl import authClient >>> authClient.isValidUser('monty') ''' url = self.svc_url + "/isValidUser?" args = urlencode({"user": user, "profile": self.svc_profile}) url = url + args if self.debug: print("isValidUser: url = '%s'" % url) try: val = self.retBoolValue(url) except Exception: val = "False" return val
[docs] def isUserLoggedIn(self, user): '''See whether the user identified by the username is currently logged in. Parameters ---------- user : str User login name. Returns ------- status : bool ``True`` if user is currently logged in, ``False`` otherwise. Example ------- >>> from dl import authClient >>> authClient.isUserLoggedIn('monty') ''' url = self.svc_url + "/isUserLoggedIn?" args = urlencode({"user": user, "profile": self.svc_profile}) url = url + args if self.debug: print("isUserLoggedIn: url = '%s'" % url) try: val = self.retBoolValue(url) except Exception: val = "False" return val
[docs] def isTokenLoggedIn(self, token): '''See whether the user identified by the token is currently logged in. Parameters ---------- token : str User login token. Returns ------- status : bool ``True`` if token is marked as logged in, ``False`` otherwise. Example ------- >>> from dl import authClient >>> authClient.isTokenLoggedIn(token) ''' url = self.svc_url + "/isTokenLoggedIn?" args = urlencode({"token": token, "profile": self.svc_profile}) url = url + args if self.debug: print("isTokenLoggedIn: tok = '%s'" % token) print("isTokenLoggedIn: url = '%s'" % url) try: val = self.retBoolValue(url) except Exception: val = "False" return val
################################################### # PRIVATE UTILITY METHODS ###################################################
[docs] def debug(self, debug_val): '''Toggle debug flag. ''' self.debug = debug_val
[docs] def retBoolValue(self, url): '''Utility method to call a boolean service at the given URL. ''' response = "" try: # Add the auth token to the reauest header. if self.auth_token != None: headers = {'X-DL-AuthToken': self.auth_token} r = requests.get(url, headers=headers) else: r = requests.get(url) response = acToString(r.content) if r.status_code != 200: raise Exception(r.content) except Exception: return acToString(r.content) else: return response
[docs] def getHeaders(self, token): '''Get default tracking headers. ''' tok = def_token(token) user, uid, gid, hash = split_auth_token(tok.strip()) hdrs = {'Content-Type': 'text/ascii', 'X-DL-ClientVersion': __version__, 'X-DL-OriginIP': self.hostip, 'X-DL-OriginHost': self.hostname, 'X-DL-User': user, 'X-DL-AuthToken': tok} # application/x-sql return hdrs
[docs] def getFromURL(self, svc_url, path, token): '''Get something from a URL. Return a 'response' object. ''' try: hdrs = self.getHeaders(token) resp = requests.get("%s%s" % (svc_url, path), headers=hdrs) except Exception as e: raise dlAuthError(str(e)) return resp
# ################################### # Authentication Client Handles # ###################################
[docs] def getClient(): '''Get a new instance of the authClient client. Parameters ---------- None Returns ------- client : authClient An authClient object. Example ------- >>> from dl import authClient >>> new_client = authClient.getClient() ''' return authClient()
ac_client = getClient() # ########################################## # Patch the docstrings for module functions # ########################################## login.__doc__ = ac_client.login.__doc__ logout.__doc__ = ac_client.logout.__doc__ passwordReset.__doc__ = ac_client.passwordReset.__doc__ whoAmI.__doc__ = ac_client.whoAmI.__doc__ isAlive.__doc__ = ac_client.isAlive.__doc__ isValidToken.__doc__ = ac_client.isValidToken.__doc__ isValidUser.__doc__ = ac_client.isValidUser.__doc__ isValidPassword.__doc__ = ac_client.isValidPassword.__doc__ hasAccess.__doc__ = ac_client.hasAccess.__doc__ isUserLoggedIn.__doc__ = ac_client.isUserLoggedIn.__doc__ isTokenLoggedIn.__doc__ = ac_client.isTokenLoggedIn.__doc__ set_svc_url.__doc__ = ac_client.set_svc_url.__doc__ get_svc_url.__doc__ = ac_client.get_svc_url.__doc__ set_profile.__doc__ = ac_client.set_profile.__doc__ get_profile.__doc__ = ac_client.get_profile.__doc__ # #################################################################### # Py2/Py3 Compatability Utilities # ####################################################################
[docs] def acToString(s): '''acToString -- Force a return value to be type 'string' for all Python versions. ''' if is_py3: if isinstance(s,bytes): strval = str(s.decode()) elif isinstance(s,str): strval = s else: if isinstance(s,bytes) or isinstance(s,unicode): strval = str(s) else: strval = s return strval