From 0bd486154c8a1dd5e341161c79e75faf9847f38b Mon Sep 17 00:00:00 2001 From: ntaken <71373110+ntaken@users.noreply.github.com> Date: Fri, 25 Sep 2020 11:05:16 +0200 Subject: [PATCH] Update authentication.py fix mixed indentation, import typo (son -> json), pep8/style fixes #592 by elegantandrogyne --- gateone/auth/authentication.py | 150 +++++++++++++++++---------------- 1 file changed, 76 insertions(+), 74 deletions(-) diff --git a/gateone/auth/authentication.py b/gateone/auth/authentication.py index 292580eb..b5e41389 100644 --- a/gateone/auth/authentication.py +++ b/gateone/auth/authentication.py @@ -2,19 +2,11 @@ # # Copyright 2013 Liftoff Software Corporation # - -# Meta -__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)" -__author__ = 'Dan McDougall ' - -__doc__ = """\ -.. _auth.py: - +""".. _auth.py: Authentication ============== This module contains Gate One's authentication classes. They map to Gate One's --auth configuration option like so: - =============== =================== --auth=none NullAuthHandler --auth=kerberos KerberosAuthHandler @@ -22,61 +14,53 @@ --auth=pam PAMAuthHandler --auth=api APIAuthHandler =============== =================== - .. note:: API authentication is handled inside of :ref:`gateone.py` - None or Anonymous ----------------- By default Gate One will not authenticate users. This means that user sessions will be tied to their browser cookie and users will not be able to resume their sessions from another computer/browser. Most useful for situations where session persistence and logging aren't important. - *All* users will show up as ANONYMOUS using this authentication type. - Kerberos -------- Kerberos authentication utilizes GSSAPI for Single Sign-on (SSO) but will fall back to HTTP Basic authentication if GSSAPI auth fails. This authentication type can be integrated into any Kerberos infrastructure including Windows Active Directory. - It is great for both transparent authentication and being able to tie sessions and logs to specific users within your organization (compliance). - .. note:: - The sso.py module itself has extensive documentation on this authentication type. - Google Authentication --------------------- If you want persistent user sessions but don't care to run your own authentication infrastructure this authentication type is for you. Assuming, of course, that your Gate One server and clients will have access to the Internet. - .. note:: - This authentication type is perfect if you're using Chromebooks (Chrome OS devices). - API Authentication ------------------ API-based authentication is actually handled in gateone.py but we still need *something* to exist at the /auth URL that will always return the 'unauthenticated' response. This ensures that no one can authenticate themselves by visiting that URL manually. - Docstrings ========== """ # Import stdlib stuff -import os, re, logging, json +import os +import re +import logging +import json try: from urllib import quote -except ImportError: # Python 3 +except ImportError: + # Python 3 from urllib.parse import quote # Import our own stuff @@ -94,23 +78,27 @@ import tornado.gen from tornado.options import options +# Meta +__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)" +__author__ = 'Dan McDougall ' + # Localization support _ = get_translation() # Globals -SETTINGS_CACHE = {} # Lists of settings files and their modification times +# Lists of settings files and their modification times # The security stuff below is a work-in-progress. Likely to change all around. +SETTINGS_CACHE = {} auth_log = go_logger('gateone.auth') + # Helper functions def additional_attributes(user, settings_dir=None): """ Given a *user* dict, return a dict containing any additional attributes defined in Gate One's attribute repositories. - .. note:: - This function doesn't actually work yet (support for attribute repos like LDAP is forthcoming). """ @@ -131,11 +119,11 @@ def get_current_user(self): expiration = self.settings.get('auth_timeout', "14d") # Need the expiration in days (which is a bit silly but whatever): expiration = ( - float(total_seconds(convert_to_timedelta(expiration))) - / float(86400)) + float(total_seconds(convert_to_timedelta(expiration))) / 86400.0) user_json = self.get_secure_cookie( "gateone_user", max_age_days=expiration) - if not user_json: return None + if not user_json: + return None user = tornado.escape.json_decode(user_json) # Add the IP attribute user['ip_address'] = self.request.remote_ip @@ -146,14 +134,15 @@ def user_login(self, user): Called immediately after a user authenticates successfully. Saves session information in the user's directory. Expects *user* to be a dict containing a 'upn' value representing the username or - userPrincipalName. e.g. 'user@REALM' or just 'someuser'. Any additional - values will be attached to the user object/cookie. + userPrincipalName. e.g. 'user@REALM' or just 'someuser'. + Any additional values will be attached to the user object/cookie. """ logging.debug("user_login(%s)" % user['upn']) user.update(additional_attributes(user)) # Make a directory to store this user's settings/files/logs/etc try: - # NOTE: These bytes checks are for Python 2 (not needed in Python 3) + # NOTE: These bytes checks are for Python 2 + # (not needed in Python 3) upn = user['upn'] if isinstance(user['upn'], bytes): upn = user['upn'].decode('utf-8') @@ -177,8 +166,9 @@ def user_login(self, user): session_data = open(session_file).read() try: session_info = tornado.escape.json_decode(session_data) - except ValueError: # Something wrong with the file - session_file_exists = False # Overwrite it below + except ValueError: + # Something wrong with the file, overwrite it below + session_file_exists = False if not session_file_exists: with open(session_file, 'w') as f: # Save it so we can keep track across multiple clients @@ -207,6 +197,7 @@ def user_logout(self, user, redirect=None): self.write(self.settings['url_prefix']) self.finish() + class NullAuthHandler(BaseAuthHandler): """ A handler for when no authentication method is chosen (i.e. --auth=none). @@ -263,10 +254,11 @@ def user_login(self, user): self.set_secure_cookie( "gateone_user", tornado.escape.json_encode(session_info)) + class APIAuthHandler(BaseAuthHandler): """ - A handler that always reports 'unauthenticated' since API-based auth doesn't - use auth handlers. + A handler that always reports 'unauthenticated' + since API-based auth doesn't use auth handlers. """ @tornado.web.asynchronous def get(self): @@ -341,9 +333,8 @@ def get(self): raise tornado.web.HTTPError(500, 'Google auth failed') access_token = str(user['access_token']) http_client = self.get_auth_http_client() - response = yield http_client.fetch( - 'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' - +access_token) + url = 'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' + response = yield http_client.fetch(url + access_token) if not response: self.clear_all_cookies() raise tornado.web.HTTPError(500, 'Google auth failed') @@ -376,7 +367,8 @@ def _on_auth(self, user): # 'id': '999999999999999999999', # 'family_name': 'Schmoe', # 'link': 'https://plus.google.com/999999999999999999999'} - user['upn'] = user['email'] # Use the email for the upn + # Use the email for the upn + user['upn'] = user['email'] self.user_login(user) next_url = self.get_argument("next", None) if next_url: @@ -384,6 +376,7 @@ def _on_auth(self, user): else: self.redirect(self.settings['url_prefix']) + class SSLAuthHandler(BaseAuthHandler): """ SSL Certificate-based authentication handler. Can only be used if the @@ -404,21 +397,26 @@ def _convert_certificate(self, cert): a format more suitable for a user dict. """ import re - # Can't have any of these in the upn because we name a directory with it + # Can't have any of these in the upn + # because we name a directory with it bad_chars = re.compile(r'[\/\\\$\;&`\!\*\?\|<>\n]') - user = {'notAfter': cert['notAfter']} # This one is the most direct + # This one is the most direct + user = {'notAfter': cert['notAfter']} for item in cert['subject']: for key, value in item: user.update({key: value}) - cn = user['commonName'] # Use the commonName as the UPN - cn = bad_chars.sub('.', cn) # Replace bad chars with dots + # Use the commonName as the UPN + cn = user['commonName'] + # Replace bad chars with dots + cn = bad_chars.sub('.', cn) # Try to use the 'issuer' to add more depth to the CN - if 'issuer' in cert: # This will only be there if you're using Python 3 + # This will only be there if you're using Python 3 + if 'issuer' in cert: for item in cert['issuer']: for key, value in item: if key == 'organizationName': - # Yeah this can get long but that's OK (it's better than - # conflicts) + # Yeah this can get long but that's OK + # (it's better than conflicts) cn = "%s@%s" % (cn, value) break # Should wind up as something like this: @@ -437,7 +435,7 @@ def get(self): """ check = self.get_argument("check", None) if check: - self.set_header ('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Origin', '*') user = self.get_current_user() if user: logging.debug('SSLAuthHandler: user is authenticated') @@ -470,6 +468,7 @@ def get(self): KerberosAuthHandler = None try: from gateone.auth.sso import KerberosAuthMixin + class KerberosAuthHandler(BaseAuthHandler, KerberosAuthMixin): """ Handles authenticating users via Kerberos/GSSAPI/SSO. @@ -478,8 +477,8 @@ class KerberosAuthHandler(BaseAuthHandler, KerberosAuthMixin): def get(self): """ Checks the user's request header for the proper Authorization data. - If it checks out the user will be logged in via _on_auth(). If not, - the browser will be redirected to login. + If it checks out the user will be logged in via _on_auth(). + If not, the browser will be redirected to login. """ check = self.get_argument("check", None) self.set_header('Access-Control-Allow-Origin', '*') @@ -489,7 +488,8 @@ def get(self): logging.debug('KerberosAuthHandler: user is authenticated') self.write('authenticated') else: - logging.debug('KerberosAuthHandler: user is NOT authenticated') + logging.debug('KerberosAuthHandler: ' + 'user is NOT authenticated') self.write('unauthenticated') self.finish() return @@ -512,19 +512,22 @@ def _on_auth(self, user): user = {'upn': user} # This takes care of the user's settings dir and their session info self.user_login(user) - # TODO: Add some LDAP or local DB lookups here to add more detail to user objects + # TODO: Add some LDAP or local DB lookups here + # to add more detail to user objects next_url = self.get_argument("next", None) if next_url: self.redirect(next_url) else: self.redirect(self.settings['url_prefix']) except ImportError: - pass # No SSO available. + # No SSO available. + pass # Add our PAMAuthHandler if it's available PAMAuthHandler = None try: from gateone.auth.pam import PAMAuthMixin + class PAMAuthHandler(BaseAuthHandler, PAMAuthMixin): """ Handles authenticating users via PAM. @@ -533,8 +536,8 @@ class PAMAuthHandler(BaseAuthHandler, PAMAuthMixin): def get(self): """ Checks the user's request header for the proper Authorization data. - If it checks out the user will be logged in via _on_auth(). If not, - the browser will be redirected to login. + If it checks out the user will be logged in via _on_auth(). + If not, the browser will be redirected to login. """ check = self.get_argument("check", None) self.set_header('Access-Control-Allow-Origin', '*') @@ -574,18 +577,20 @@ def _on_auth(self, user): else: self.redirect(self.settings['url_prefix']) except ImportError: - pass # No PAM auth available. + # No PAM auth available. + pass + class CASAuthHandler(BaseAuthHandler): """ CAS authentication handler. """ cas_user_regex = re.compile(r'(.*)') + def initialize(self): """ Print out helpful error messages if the requisite settings aren't configured. - NOTE: It won't hurt anything to override this method in your RequestHandler. """ @@ -631,10 +636,8 @@ def get(self): def authenticate_redirect(self, callback=None): """ Redirects to the authentication URL for this CAS service. - After authentication, the service will redirect back to the given callback URI with additional parameters. - We request the given attributes for the authenticated user by default (name, email, language, and username). If you don't need all those attributes for your app, you can request fewer with @@ -648,11 +651,13 @@ def authenticate_redirect(self, callback=None): next_param = "" if next_url: next_param = "?next=" + quote(next_url) - redirect_url = '%slogin?service=%s%s' % (cas_server, quote(service_url), quote(next_param)) - logging.debug("Redirecting to CAS URL: %s" % redirect_url) - self.redirect(redirect_url) - if callback: - callback() + redirect_url = ('%slogin?service=%s%s' + % (cas_server, quote(service_url), + quote(next_param))) + logging.debug("Redirecting to CAS URL: %s" % redirect_url) + self.redirect(redirect_url) + if callback: + callback() def get_authenticated_user(self, server_ticket): """ @@ -666,7 +671,7 @@ def get_authenticated_user(self, server_ticket): if not cas_server.endswith('/'): cas_server += '/' service_url = "%sauth" % self.base_url - #validate the ST + # validate the ST validate_suffix = 'proxyValidate' if cas_version == 1: validate_suffix = 'validate' @@ -674,15 +679,11 @@ def get_authenticated_user(self, server_ticket): next_param = "" if next_url: next_param = "?next=" + quote(next_url) - validate_url = ( - cas_server + - validate_suffix + - '?service=' + - quote(service_url) + - quote(next_param) + - '&ticket=' + - quote(server_ticket) - ) + validate_url = (cas_server + validate_suffix + + '?service=' + quote(service_url) + + quote(next_param) + + '&ticket=' + quote(server_ticket) + ) logging.debug("Fetching CAS URL: %s" % validate_url) validate_cert = False if ca_certs: @@ -711,3 +712,4 @@ def _on_auth(self, response): self.redirect(next_url) else: self.redirect(self.settings['url_prefix']) +© 2020 GitHub, Inc.