From 8e6e366aa352fe2038c1f6d6a4b1684ad9e2e842 Mon Sep 17 00:00:00 2001 From: Christos Triantafyllidis Date: Feb 26 2017 15:52:59 +0000 Subject: Added PAM authentication support --- diff --git a/hub/hub.conf b/hub/hub.conf index 8b5dd55..7178aff 100644 --- a/hub/hub.conf +++ b/hub/hub.conf @@ -37,6 +37,9 @@ KojiDir = /mnt/koji ## end SSL client certificate auth configuration +## PAM auth configuration ## +# PAMService = koji +## end PAM auth configuration ## ## Other options ## LoginCreatesUser = On diff --git a/hub/kojixmlrpc.py b/hub/kojixmlrpc.py index 47c1284..7e484b8 100644 --- a/hub/kojixmlrpc.py +++ b/hub/kojixmlrpc.py @@ -432,6 +432,8 @@ def load_config(environ): ['CheckClientIP', 'boolean', True], + ['PAMService', 'string', None], + ['LoginCreatesUser', 'boolean', True], ['KojiWebURL', 'string', 'http://localhost.localdomain/koji'], ['EmailDomain', 'string', None], diff --git a/koji/__init__.py b/koji/__init__.py index a8730e6..148d803 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -78,6 +78,7 @@ import xmlrpclib import xml.sax import xml.sax.handler from xmlrpclib import loads, dumps, Fault +from getpass import getpass PROFILE_MODULES = {} # {module_name: module_instance} @@ -1590,7 +1591,8 @@ def read_config(profile_name, user_config=None): 'ca': '', # FIXME: remove in next major release 'serverca': None, 'no_ssl_verify': False, - 'authtype': None + 'authtype': None, + 'user': None } result = config_defaults.copy() @@ -2023,6 +2025,10 @@ class ClientSession(object): self.sinfo = sinfo def login(self, opts=None): + if not 'user' in self.opts: + self.opts['user'] = raw_input('Username: ') + if not 'password' in self.opts: + self.opts['password'] = getpass('Password: ') sinfo = self.callMethod('login', self.opts['user'], self.opts['password'], opts) if not sinfo: return False diff --git a/koji/auth.py b/koji/auth.py index ef7635f..64e1cf1 100644 --- a/koji/auth.py +++ b/koji/auth.py @@ -27,6 +27,10 @@ import krbV import koji import cgi #for parse_qs from context import context +try: + import pam +except ImportError: # pragma: no cover + pam = None # 1 - load session if provided # - check uri for session id @@ -272,13 +276,28 @@ class Session(object): # check passwd c = context.cnx.cursor() - q = """SELECT id FROM users - WHERE name = %(user)s AND password = %(password)s""" - c.execute(q, locals()) - r = c.fetchone() - if not r: - raise koji.AuthError, 'invalid username or password' - user_id = r[0] + if context.opts.get('PAMService') and pam is not None: + if not pam.authenticate(user, password, context.opts.get('PAMService')): + raise koji.AuthError, 'invalid username or password' + q = """SELECT id FROM users + WHERE name = %(user)s""" + c.execute(q, locals()) + r = c.fetchone() + if r: + user_id = r[0] + else: + if context.opts.get('LoginCreatesUser'): + user_id = self.createUser(user) + else: + raise koji.AuthError, 'Unknown user: %s' % user + else: + q = """SELECT id FROM users + WHERE name = %(user)s AND password = %(password)s""" + c.execute(q, locals()) + r = c.fetchone() + if not r: + raise koji.AuthError, 'invalid username or password' + user_id = r[0] self.checkLoginAllowed(user_id) diff --git a/koji/server.py b/koji/server.py index 60b29a9..5bb236a 100644 --- a/koji/server.py +++ b/koji/server.py @@ -35,6 +35,8 @@ class ServerError(Exception): class ServerRedirect(ServerError): """Used to handle redirects""" +class NotAuthorized(ServerError): + """Used to handle unauthorized""" class WSGIWrapper(object): """A very thin wsgi compat layer for mod_python diff --git a/www/conf/kojiweb.conf b/www/conf/kojiweb.conf index 807ef23..c6000a9 100644 --- a/www/conf/kojiweb.conf +++ b/www/conf/kojiweb.conf @@ -54,6 +54,11 @@ Alias /koji "/usr/share/koji-web/scripts/wsgi_publisher.py" # SSLOptions +StdEnvVars # +# uncomment this to enable authentication via BasicAuth +# +# WSGIPassAuthorization On +# + Alias /koji-static/ "/usr/share/koji-web/static/" diff --git a/www/conf/web.conf b/www/conf/web.conf index 2ec0959..9fca15d 100644 --- a/www/conf/web.conf +++ b/www/conf/web.conf @@ -17,6 +17,9 @@ KojiFilesURL = http://server.example.com/kojifiles # WebCert = /etc/kojiweb/kojiweb.crt # KojiHubCA = /etc/kojiweb/kojihubca.crt +# BasicAuth authentication options +# BasicAuthRealm = Koji + LoginTimeout = 72 # This must be changed and uncommented before deployment diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index 7f4e4dd..60eb4be 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -31,7 +31,7 @@ import logging import time import koji import kojiweb.util -from koji.server import ServerRedirect +from koji.server import ServerRedirect, NotAuthorized from kojiweb.util import _initValues from kojiweb.util import _genHTML from kojiweb.util import _getValidTokens @@ -251,6 +251,22 @@ def login(environ, page=None): username = principal authlogger.info('Successful Kerberos authentication by %s', username) + elif options['BasicAuthRealm']: + if environ['wsgi.url_scheme'] != 'https': + dest = 'login' + if page: + dest = dest + '?page=' + page + _redirectBack(environ, dest, forceSSL=True) + return + + http_authorization = environ.get('HTTP_AUTHORIZATION') + if not http_authorization: + raise NotAuthorized + session.opts['user'], session.opts['password'] = http_authorization.split(' ')[1].decode('base64').split(':') + if not session.login(): + raise koji.AuthError, 'could not login %s using those credentials' % http_username + username = session.opts['user'] + authlogger.info('Successful BasicAuth authentication by %s', username) else: raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator' diff --git a/www/kojiweb/wsgi_publisher.py b/www/kojiweb/wsgi_publisher.py index 6fd7f04..3dc77b2 100644 --- a/www/kojiweb/wsgi_publisher.py +++ b/www/kojiweb/wsgi_publisher.py @@ -30,7 +30,7 @@ import sys import traceback from ConfigParser import RawConfigParser -from koji.server import WSGIWrapper, ServerError, ServerRedirect +from koji.server import WSGIWrapper, ServerError, ServerRedirect, NotAuthorized from koji.util import dslice @@ -80,6 +80,8 @@ class Dispatcher(object): ['WebCert', 'string', None], ['KojiHubCA', 'string', '/etc/kojiweb/kojihubca.crt'], + ['BasicAuthRealm', 'string', None], + ['PythonDebug', 'boolean', False], ['LoginTimeout', 'integer', 72], @@ -403,6 +405,10 @@ class Dispatcher(object): result, headers = self.error_page(environ, message=msg, err=False) start_response(status, headers) return result + except NotAuthorized: + status = "401 Not Authorized" + start_response(status, [('WWW-Authenticate', 'Basic realm="%s"' % self.options['BasicAuthRealm'])]) + return '401 Not Authorized' except Exception: tb_str = ''.join(traceback.format_exception(*sys.exc_info())) self.logger.error(tb_str)