enable authentication via SSL client certificates in the web interface
This commit is contained in:
parent
108e4da8c8
commit
2cb2f3684d
7 changed files with 117 additions and 44 deletions
|
|
@ -15,6 +15,8 @@ Alias /koji-hub "/usr/share/koji-hub/XMLRPC"
|
|||
PythonOption AuthPrincipal kojihub@EXAMPLE.COM
|
||||
PythonOption AuthKeytab /etc/koji.keytab
|
||||
PythonOption ProxyPrincipals kojihub@EXAMPLE.COM
|
||||
# separate multiple DNs with |
|
||||
PythonOption ProxyDNs "/C=US/ST=Massachusetts/O=Example Org/OU=Example User/CN=example/emailAddress=example@example.com"
|
||||
PythonOption LoginCreatesUser On
|
||||
PythonOption KojiWebURL http://kojiweb.example.com/koji
|
||||
#format string for host principals (%s = hostname)
|
||||
|
|
@ -32,5 +34,5 @@ Alias /koji-hub "/usr/share/koji-hub/XMLRPC"
|
|||
# SSLVerifyClient require
|
||||
# SSLVerifyDepth 10
|
||||
# SSLUserName SSL_CLIENT_S_DN_CN
|
||||
# SSLOptions +StdEnvVars +ExportCertData
|
||||
# SSLOptions +StdEnvVars
|
||||
# </Location>
|
||||
|
|
|
|||
|
|
@ -109,11 +109,13 @@ rm -rf $RPM_BUILD_ROOT
|
|||
%{_sbindir}/kojira
|
||||
%{_initrddir}/kojira
|
||||
%config(noreplace) %{_sysconfdir}/sysconfig/kojira
|
||||
%{_sysconfdir}/kojira
|
||||
%config(noreplace) %{_sysconfdir}/kojira/kojira.conf
|
||||
|
||||
%files web
|
||||
%defattr(-,root,root)
|
||||
%{_datadir}/koji-web
|
||||
%{_sysconfdir}/kojiweb
|
||||
%config(noreplace) /etc/httpd/conf.d/kojiweb.conf
|
||||
|
||||
%files builder
|
||||
|
|
@ -121,6 +123,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||
%{_sbindir}/kojid
|
||||
%{_initrddir}/kojid
|
||||
%config(noreplace) %{_sysconfdir}/sysconfig/kojid
|
||||
%{_sysconfdir}/kojid
|
||||
%config(noreplace) %{_sysconfdir}/kojid/kojid.conf
|
||||
%attr(-,kojibuilder,kojibuilder) /etc/mock/koji
|
||||
|
||||
|
|
|
|||
|
|
@ -1178,17 +1178,17 @@ class ClientSession(object):
|
|||
|
||||
return 'host/%s@%s' % (servername, domain)
|
||||
|
||||
def ssl_login(self, cert, ca, serverca):
|
||||
def ssl_login(self, cert, ca, serverca, proxyuser=None):
|
||||
if not self.baseurl.startswith('https:'):
|
||||
raise AuthError, '%s is not a SSL server URL, and you have configured SSL authentication' % self.baseurl
|
||||
self.baseurl = self.baseurl.replace('http:', 'https:')
|
||||
|
||||
certs = {}
|
||||
certs['key_and_cert'] = cert
|
||||
certs['ca_cert'] = ca
|
||||
certs['peer_ca_cert'] = serverca
|
||||
# only use a timeout during login
|
||||
self.proxy = ssl.XMLRPCServerProxy.PlgXMLRPCServerProxy(self.baseurl, certs, timeout=60)
|
||||
sinfo = self.callMethod('sslLogin')
|
||||
self.proxy = ssl.XMLRPCServerProxy.PlgXMLRPCServerProxy(self.baseurl, certs, timeout=60, **self.proxyOpts)
|
||||
sinfo = self.callMethod('sslLogin', proxyuser)
|
||||
if not sinfo:
|
||||
return False
|
||||
self.proxyClass = ssl.XMLRPCServerProxy.PlgXMLRPCServerProxy
|
||||
|
|
|
|||
36
koji/auth.py
36
koji/auth.py
|
|
@ -276,7 +276,7 @@ class Session(object):
|
|||
login_principal = cprinc.name
|
||||
user_id = self.getUserIdFromKerberos(login_principal)
|
||||
if not user_id:
|
||||
if context.opts.get('LoginCreatesUser', '').lower() in ('yes', 'on', 'true', '1'):
|
||||
if context.opts['LoginCreatesUser'].lower() in ('yes', 'on', 'true', '1'):
|
||||
user_id = self.createUserFromKerberos(login_principal)
|
||||
else:
|
||||
raise koji.AuthError, 'Unknown Kerberos principal: %s' % login_principal
|
||||
|
|
@ -320,7 +320,7 @@ class Session(object):
|
|||
|
||||
return (local_ip, local_port, remote_ip, remote_port)
|
||||
|
||||
def sslLogin(self):
|
||||
def sslLogin(self, proxyuser=None):
|
||||
if self.logged_in:
|
||||
raise koji.AuthError, "Already logged in"
|
||||
|
||||
|
|
@ -328,28 +328,40 @@ class Session(object):
|
|||
context.req.add_common_vars()
|
||||
env = context.req.subprocess_env
|
||||
|
||||
if not (env.has_key('HTTPS') and
|
||||
env['HTTPS'] == 'on'):
|
||||
if env.get('HTTPS') != 'on':
|
||||
raise koji.AuthError, 'cannot call sslLogin() via a non-https connection'
|
||||
|
||||
subject = context.req.user
|
||||
if not subject:
|
||||
raise koji.AuthError, 'could not determine the subject of the client certificate'
|
||||
if env.get('SSL_CLIENT_VERIFY') != 'SUCCESS':
|
||||
raise koji.AuthError, 'could not verify client: %s' % env.get('SSL_CLIENT_VERIFY')
|
||||
|
||||
# assume that the certificate subject is their Koji username
|
||||
client_name = env.get('SSL_CLIENT_S_DN_CN')
|
||||
if not client_name:
|
||||
raise koji.AuthError, 'unable to get user information from client certificate'
|
||||
|
||||
if proxyuser:
|
||||
client_dn = env.get('SSL_CLIENT_S_DN')
|
||||
proxy_dns = [dn.strip() for dn in context.opts['ProxyDNs'].split('|')]
|
||||
if client_dn in proxy_dns:
|
||||
# the SSL-authenticated user authorized to login other users
|
||||
username = proxyuser
|
||||
else:
|
||||
raise koji.AuthError, '%s is not authorized to login other users' % client_dn
|
||||
else:
|
||||
username = client_name
|
||||
|
||||
cursor = context.cnx.cursor()
|
||||
query = """SELECT id, status FROM users
|
||||
WHERE name = %(subject)s"""
|
||||
WHERE name = %(username)s"""
|
||||
cursor.execute(query, locals())
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
user_id, status = result
|
||||
else:
|
||||
if context.opts.get('LoginCreatesUser', '').lower() in ('yes', 'on', 'true', '1'):
|
||||
user_id = self.createUser(subject, koji.USERTYPES['NORMAL'], '')
|
||||
if context.opts['LoginCreatesUser'].lower() in ('yes', 'on', 'true', '1'):
|
||||
user_id = self.createUser(username, koji.USERTYPES['NORMAL'], '')
|
||||
status = None
|
||||
else:
|
||||
raise koji.AuthError, 'Unknown user: %s' % subject
|
||||
raise koji.AuthError, 'Unknown user: %s' % username
|
||||
|
||||
hostip = context.req.connection.remote_ip
|
||||
if hostip == '127.0.0.1':
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@ install:
|
|||
|
||||
mkdir -p $(DESTDIR)/etc/httpd/conf.d
|
||||
install -p -m 644 kojiweb.conf $(DESTDIR)/etc/httpd/conf.d/kojiweb.conf
|
||||
|
||||
mkdir -p $(DESTDIR)/etc/kojiweb
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ Alias /koji "/usr/share/koji-web/scripts/"
|
|||
PythonOption WebPrincipal koji/web@EXAMPLE.COM
|
||||
PythonOption WebKeytab /etc/httpd.keytab
|
||||
PythonOption WebCCache /var/tmp/kojiweb.ccache
|
||||
PythonOption WebCert /etc/kojiweb/kojiweb.crt
|
||||
PythonOption ClientCA /etc/kojiweb/clientca.crt
|
||||
PythonOption KojiHubCA /etc/kojiweb/kojihubca.crt
|
||||
PythonOption LoginTimeout 72
|
||||
# This must be changed before deployment
|
||||
PythonOption Secret CHANGE_ME
|
||||
|
|
@ -21,18 +24,25 @@ Alias /koji "/usr/share/koji-web/scripts/"
|
|||
PythonAutoReload Off
|
||||
</Directory>
|
||||
|
||||
# Authentication settings
|
||||
# uncomment this to enable authentication via Kerberos
|
||||
# <Location /koji/login>
|
||||
# AuthType Kerberos
|
||||
# AuthName "Koji Web UI"
|
||||
# KrbMethodNegotiate on
|
||||
# KrbMethodK5Passwd off
|
||||
# KrbServiceName HTTP
|
||||
# KrbAuthRealm EXAMPLE.COM
|
||||
# Krb5Keytab /etc/httpd.keytab
|
||||
# KrbSaveCredentials off
|
||||
# Require valid-user
|
||||
# ErrorDocument 401 /koji-static/errors/unauthorized.html
|
||||
# </Location>
|
||||
|
||||
# uncomment this to enable authentication via SSL client certificates
|
||||
<Location /koji/login>
|
||||
AuthType Kerberos
|
||||
AuthName "Koji Web UI"
|
||||
KrbMethodNegotiate on
|
||||
KrbMethodK5Passwd off
|
||||
KrbServiceName HTTP
|
||||
KrbAuthRealm EXAMPLE.COM
|
||||
Krb5Keytab /etc/httpd.keytab
|
||||
KrbSaveCredentials off
|
||||
Require valid-user
|
||||
ErrorDocument 401 /koji-static/errors/unauthorized.html
|
||||
SSLVerifyClient require
|
||||
SSLVerifyDepth 10
|
||||
SSLOptions +StdEnvVars
|
||||
</Location>
|
||||
|
||||
Alias /koji-static/ "/usr/share/koji-web/static/"
|
||||
|
|
|
|||
|
|
@ -46,13 +46,30 @@ def _krbLogin(req, session, principal):
|
|||
return session.krb_login(principal=wprinc, keytab=keytab,
|
||||
ccache=ccache, proxyuser=principal)
|
||||
|
||||
def _sslLogin(req, session, username):
|
||||
options = req.get_options()
|
||||
client_cert = options['WebCert']
|
||||
client_ca = options['ClientCA']
|
||||
server_ca = options['KojiHubCA']
|
||||
|
||||
return session.ssl_login(client_cert, client_ca, server_ca,
|
||||
proxyuser=username)
|
||||
|
||||
def _assertLogin(req):
|
||||
if not (hasattr(req, 'currentPrincipal') and
|
||||
session = req._session
|
||||
options = req.get_options()
|
||||
if not (hasattr(req, 'currentLogin') and
|
||||
hasattr(req, 'currentUser')):
|
||||
raise StandardError, '_getServer() must be called before _assertLogin()'
|
||||
elif req.currentPrincipal and req.currentUser:
|
||||
if not _krbLogin(req, req._session, req.currentPrincipal):
|
||||
raise koji.AuthError, 'could not login using principal: %s' % req.currentPrincipal
|
||||
elif req.currentLogin and req.currentUser:
|
||||
if options.get('WebCert'):
|
||||
if not _sslLogin(req, session, req.currentLogin):
|
||||
raise koji.AuthError, 'could not login %s via SSL' % req.currentLogin
|
||||
elif options.get('WebPrincipal'):
|
||||
if not _krbLogin(req, req._session, req.currentLogin):
|
||||
raise koji.AuthError, 'could not login using principal: %s' % req.currentLogin
|
||||
else:
|
||||
raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator'
|
||||
else:
|
||||
mod_python.util.redirect(req, 'login')
|
||||
assert False
|
||||
|
|
@ -81,12 +98,12 @@ def _getServer(req):
|
|||
serverURL = req.get_options()['KojiHubURL']
|
||||
session = koji.ClientSession(serverURL)
|
||||
|
||||
req.currentPrincipal = _getUserCookie(req)
|
||||
if req.currentPrincipal:
|
||||
req.currentUser = session.getUser(req.currentPrincipal)
|
||||
req.currentLogin = _getUserCookie(req)
|
||||
if req.currentLogin:
|
||||
req.currentUser = session.getUser(req.currentLogin)
|
||||
if not req.currentUser:
|
||||
raise koji.AuthError, 'could not get user for principal: %s' % req.currentPrincipal
|
||||
_setUserCookie(req, req.currentPrincipal)
|
||||
raise koji.AuthError, 'could not get user for principal: %s' % req.currentLogin
|
||||
_setUserCookie(req, req.currentLogin)
|
||||
else:
|
||||
req.currentUser = None
|
||||
|
||||
|
|
@ -103,16 +120,43 @@ def _redirectBack(req, page):
|
|||
|
||||
def login(req, page=None):
|
||||
session = _getServer(req)
|
||||
options = req.get_options()
|
||||
|
||||
principal = req.user
|
||||
if not principal:
|
||||
raise koji.AuthError, 'configuration error: an external module should have performed authentication before presenting this page'
|
||||
# try SSL first, fall back to Kerberos
|
||||
if options.get('WebCert'):
|
||||
req.add_common_vars()
|
||||
env = req.subprocess_env
|
||||
if not env.get('HTTPS') == 'on':
|
||||
https_url = options['KojiWebURL'].replace('http://', 'https://') + '/login'
|
||||
if req.args:
|
||||
https_url += '?' + req.args
|
||||
mod_python.util.redirect(req, https_url)
|
||||
return
|
||||
|
||||
# login via Kerberos to verify credentials and create the user if it doesn't exist
|
||||
if not _krbLogin(req, session, principal):
|
||||
raise koji.AuthError, 'could not login using principal: %s' % principal
|
||||
if env.get('SSL_CLIENT_VERIFY') != 'SUCCESS':
|
||||
raise koji.AuthError, 'could not verify client: %s' % env.get('SSL_CLIENT_VERIFY')
|
||||
|
||||
_setUserCookie(req, principal)
|
||||
# use the subject's common name as their username
|
||||
username = env.get('SSL_CLIENT_S_DN_CN')
|
||||
if not username:
|
||||
raise koji.AuthError, 'unable to get user information from client certificate'
|
||||
|
||||
if not _sslLogin(req, session, username):
|
||||
raise koji.AuthError, 'could not login %s using SSL certificates' % username
|
||||
|
||||
elif options.get('WebPrincipal'):
|
||||
principal = req.user
|
||||
if not principal:
|
||||
raise koji.AuthError, 'configuration error: mod_auth_kerb should have performed authentication before presenting this page'
|
||||
|
||||
if not _krbLogin(req, session, principal):
|
||||
raise koji.AuthError, 'could not login using principal: %s' % principal
|
||||
|
||||
username = principal
|
||||
else:
|
||||
raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator'
|
||||
|
||||
_setUserCookie(req, username)
|
||||
|
||||
_redirectBack(req, page)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue