---
hub/hub.conf | 4 +++-
hub/kojixmlrpc.py | 2 ++
koji.spec | 1 +
koji/auth.py | 33 +++++++++++++++++++++++++--------
koji/server.py | 2 ++
www/conf/kojiweb.conf | 5 +++++
www/conf/web.conf | 3 +++
www/kojiweb/index.py | 18 +++++++++++++++++-
www/kojiweb/wsgi_publisher.py | 9 +++++++--
9 files changed, 65 insertions(+), 12 deletions(-)
diff --git a/hub/hub.conf b/hub/hub.conf
index f1e40c1..e4fd77a 100644
--- a/hub/hub.conf
+++ b/hub/hub.conf
@@ -36,7 +36,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 efb99a6..2ecc4d1 100644
--- a/hub/kojixmlrpc.py
+++ b/hub/kojixmlrpc.py
@@ -426,6 +426,8 @@ def load_config(environ):
['DNUsernameComponent', 'string', 'CN'],
['ProxyDNs', 'string', ''],
+ ['PAMService', 'string', None],
+
['LoginCreatesUser', 'boolean', True],
['KojiWebURL', 'string',
'http://localhost.localdomain/koji'],
['EmailDomain', 'string', None],
diff --git a/koji.spec b/koji.spec
index 8a65b6f..73c4132 100644
--- a/koji.spec
+++ b/koji.spec
@@ -47,6 +47,7 @@ License: LGPLv2 and GPLv2
Requires: httpd
Requires: mod_wsgi
Requires: postgresql-python
+Requires: python-pam
Requires: %{name} = %{version}-%{release}
%description hub
diff --git a/koji/auth.py b/koji/auth.py
index d419d77..f7971ed 100644
--- a/koji/auth.py
+++ b/koji/auth.py
@@ -27,6 +27,7 @@ import krbV
import koji
import cgi #for parse_qs
from context import context
+import pam
# 1 - load session if provided
# - check uri for session id
@@ -267,14 +268,30 @@ class Session(object):
hostip = socket.gethostbyname(socket.gethostname())
# 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'):
+ if not
pam.authenticate(user,password,context.opts.get('PAMService'):
+ raise koji.AuthError, 'invalid username or password'
+ cursor = context.cnx.cursor()
+ query = """SELECT id FROM users
+ WHERE name = %(user)s"""
+ cursor.execute(query, locals())
+ result = cursor.fetchone()
+ if result:
+ user_id = result[0]
+ else:
+ if context.opts.get('LoginCreatesUser'):
+ user_id = self.createUser(user)
+ else:
+ raise koji.AuthError, 'Unknown user: %s' % user
+ else:
+ 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]
self.checkLoginAllowed(user_id)
diff --git a/koji/server.py b/koji/server.py
index 52f13f5..5fbf832 100644
--- a/koji/server.py
+++ b/koji/server.py
@@ -36,6 +36,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 3173ba2..b1d93ca 100644
--- a/www/conf/kojiweb.conf
+++ b/www/conf/kojiweb.conf
@@ -49,6 +49,11 @@ Alias /koji "/usr/share/koji-web/scripts/wsgi_publisher.py"
# SSLOptions +StdEnvVars
# </Location>
+# uncomment this to enable authentication via BasicAuth
+# <Location /koji/login>
+# WSGIPassAuthorization On
+# </Location>
+
Alias /koji-static/ "/usr/share/koji-web/static/"
<Directory "/usr/share/koji-web/static/">
diff --git a/www/conf/web.conf b/www/conf/web.conf
index 38f0b61..faad004 100644
--- a/www/conf/web.conf
+++ b/www/conf/web.conf
@@ -18,6 +18,9 @@ KojiFilesURL =
http://server.example.com/kojifiles
# ClientCA = /etc/kojiweb/clientca.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 4be6131..656f1ff 100644
--- a/www/kojiweb/index.py
+++ b/www/kojiweb/index.py
@@ -33,7 +33,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
@@ -253,6 +253,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 e790815..7f167c1 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):
['ClientCA', 'string', '/etc/kojiweb/clientca.crt'],
['KojiHubCA', 'string', '/etc/kojiweb/kojihubca.crt'],
+ ['BasicAuthRealm', 'string', None],
+
['PythonDebug', 'boolean', False],
['LoginTimeout', 'integer', 72],
@@ -141,7 +143,6 @@ class Dispatcher(object):
config = None
else:
raise koji.GenericError, "Configuration missing"
-
opts = {}
for name, dtype, default in self.cfgmap:
if config:
@@ -395,6 +396,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)
--
2.4.3