PR#3008: Allow kojiweb to proxy users obtained via different mechanisms
Merges #3008 https://pagure.io/koji/pull-request/3008 Fixes: #2552 https://pagure.io/koji/issue/2552 Allow kojiweb to proxy users obtained via different mechanisms
This commit is contained in:
commit
f5ba2a5c08
8 changed files with 92 additions and 27 deletions
|
|
@ -54,6 +54,9 @@ KojiDir = /mnt/koji
|
|||
|
||||
## Other options ##
|
||||
LoginCreatesUser = On
|
||||
# Clients with ProxyPrincipals can use different method for proxying user than GSSAPI. In such case
|
||||
# it need to be explicitely allowed via AllowProxyAuthType.
|
||||
# AllowProxyAuthType = Off
|
||||
KojiWebURL = http://kojiweb.example.com/koji
|
||||
# The domain name that will be appended to Koji usernames
|
||||
# when creating email notifications
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ def load_config(environ):
|
|||
['CheckClientIP', 'boolean', True],
|
||||
|
||||
['LoginCreatesUser', 'boolean', True],
|
||||
['AllowProxyAuthType', 'boolean', False],
|
||||
['KojiWebURL', 'string', 'http://localhost.localdomain/koji'],
|
||||
['EmailDomain', 'string', None],
|
||||
['NotifyOnSuccess', 'boolean', True],
|
||||
|
|
|
|||
|
|
@ -2483,7 +2483,8 @@ class ClientSession(object):
|
|||
return self.gssapi_login(principal=principal, keytab=keytab,
|
||||
ccache=ccache, proxyuser=proxyuser)
|
||||
|
||||
def gssapi_login(self, principal=None, keytab=None, ccache=None, proxyuser=None):
|
||||
def gssapi_login(self, principal=None, keytab=None, ccache=None,
|
||||
proxyuser=None, proxyauthtype=None):
|
||||
if not reqgssapi:
|
||||
raise PythonImportError(
|
||||
"Please install python-requests-gssapi to use GSSAPI."
|
||||
|
|
@ -2526,7 +2527,10 @@ class ClientSession(object):
|
|||
# Depending on the server configuration, we might not be able to
|
||||
# connect without client certificate, which means that the conn
|
||||
# will fail with a handshake failure, which is retried by default.
|
||||
sinfo = self._callMethod('sslLogin', [proxyuser], retry=False)
|
||||
kwargs = {'proxyuser': proxyuser}
|
||||
if proxyauthtype is not None:
|
||||
kwargs['proxyauthtype'] = proxyauthtype
|
||||
sinfo = self._callMethod('sslLogin', [], kwargs, retry=False)
|
||||
except Exception as e:
|
||||
e_str = ''.join(traceback.format_exception_only(type(e), e)).strip('\n')
|
||||
e_str = '(gssapi auth failed: %s)\n' % e_str
|
||||
|
|
@ -2553,7 +2557,7 @@ class ClientSession(object):
|
|||
self.authtype = AUTHTYPE_GSSAPI
|
||||
return True
|
||||
|
||||
def ssl_login(self, cert=None, ca=None, serverca=None, proxyuser=None):
|
||||
def ssl_login(self, cert=None, ca=None, serverca=None, proxyuser=None, proxyauthtype=None):
|
||||
cert = cert or self.opts.get('cert')
|
||||
serverca = serverca or self.opts.get('serverca')
|
||||
if cert is None:
|
||||
|
|
@ -2582,7 +2586,11 @@ class ClientSession(object):
|
|||
self.opts['serverca'] = serverca
|
||||
e_str = None
|
||||
try:
|
||||
sinfo = self.callMethod('sslLogin', proxyuser)
|
||||
kwargs = {'proxyuser': proxyuser}
|
||||
if proxyauthtype is not None:
|
||||
kwargs['proxyauthtype'] = proxyauthtype
|
||||
sinfo = self._callMethod('sslLogin', [], kwargs)
|
||||
|
||||
except Exception as ex:
|
||||
e_str = ''.join(traceback.format_exception_only(type(ex), ex))
|
||||
e_str = 'ssl auth failed: %s' % e_str
|
||||
|
|
|
|||
24
koji/auth.py
24
koji/auth.py
|
|
@ -315,7 +315,19 @@ class Session(object):
|
|||
|
||||
return (local_ip, local_port, remote_ip, remote_port)
|
||||
|
||||
def sslLogin(self, proxyuser=None):
|
||||
def sslLogin(self, proxyuser=None, proxyauthtype=None):
|
||||
|
||||
"""Login into brew via SSL. proxyuser name can be specified and if it is
|
||||
allowed in the configuration file then connection is allowed to login as
|
||||
that user. By default we assume that proxyuser is coming via same
|
||||
authentication mechanism but proxyauthtype can be set to koji.AUTHTYPE_*
|
||||
value for different handling. Typical case is proxying kerberos user via
|
||||
web ui which itself is authenticated via SSL certificate. (See kojiweb
|
||||
for usage).
|
||||
|
||||
proxyauthtype is working only if AllowProxyAuthType option is set to
|
||||
'On' in the hub.conf
|
||||
"""
|
||||
if self.logged_in:
|
||||
raise koji.AuthError("Already logged in")
|
||||
|
||||
|
|
@ -362,6 +374,16 @@ class Session(object):
|
|||
else:
|
||||
raise koji.AuthError('%s is not authorized to login other users' % client_dn)
|
||||
|
||||
# in this point we can continue with proxied user in same way as if it is not proxied
|
||||
if proxyauthtype is not None:
|
||||
if not context.opts['AllowProxyAuthType']:
|
||||
raise koji.AuthError("Proxy must use same auth mechanism as hub (behaviour "
|
||||
"can be overriden via AllowProxyAuthType hub option)")
|
||||
if proxyauthtype not in (koji.AUTHTYPE_GSSAPI, koji.AUTHTYPE_SSL):
|
||||
raise koji.AuthError(
|
||||
"Proxied authtype %s is not valid for sslLogin" % proxyauthtype)
|
||||
authtype = proxyauthtype
|
||||
|
||||
if authtype == koji.AUTHTYPE_GSSAPI and '@' in username:
|
||||
user_id = self.getUserIdFromKerberos(username)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ class TestGSSAPI(unittest.TestCase):
|
|||
def test_gssapi_login(self):
|
||||
old_environ = dict(**os.environ)
|
||||
self.session.gssapi_login()
|
||||
self.session._callMethod.assert_called_once_with('sslLogin', [None],
|
||||
retry=False)
|
||||
self.session._callMethod.assert_called_once_with(
|
||||
'sslLogin', [], {'proxyuser': None}, retry=False)
|
||||
self.assertEqual(old_environ, dict(**os.environ))
|
||||
|
||||
@mock.patch('koji.reqgssapi.HTTPKerberosAuth')
|
||||
|
|
@ -46,9 +46,8 @@ class TestGSSAPI(unittest.TestCase):
|
|||
for accepted_version in accepted_versions:
|
||||
koji.reqgssapi.__version__ = accepted_version
|
||||
rv = self.session.gssapi_login(principal, keytab, ccache)
|
||||
self.session._callMethod.assert_called_once_with('sslLogin',
|
||||
[None],
|
||||
retry=False)
|
||||
self.session._callMethod.assert_called_once_with(
|
||||
'sslLogin', [], {'proxyuser': None}, retry=False)
|
||||
self.assertEqual(old_environ, dict(**os.environ))
|
||||
self.assertTrue(rv)
|
||||
self.session._callMethod.reset_mock()
|
||||
|
|
@ -84,8 +83,8 @@ class TestGSSAPI(unittest.TestCase):
|
|||
self.session._callMethod.side_effect = Exception('login failed')
|
||||
with self.assertRaises(koji.GSSAPIAuthError):
|
||||
self.session.gssapi_login()
|
||||
self.session._callMethod.assert_called_once_with('sslLogin', [None],
|
||||
retry=False)
|
||||
self.session._callMethod.assert_called_once_with(
|
||||
'sslLogin', [], {'proxyuser': None}, retry=False)
|
||||
self.assertEqual(old_environ, dict(**os.environ))
|
||||
|
||||
def test_gssapi_login_http(self):
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ KojiFilesURL = http://server.example.com/kojifiles
|
|||
# it already. Note, that it will override that bundle.
|
||||
# KojiHubCA = /etc/kojiweb/kojihubca.crt
|
||||
|
||||
# How the users authenticate to kojiweb, if different from the
|
||||
# way Kojiweb authenticates to the hub. This can be used
|
||||
# to have users authenticate to kojiweb via kerberos while
|
||||
# still using an SSL certificate to authenticate to the hub.
|
||||
# WebAuthType = kerberos
|
||||
|
||||
LoginTimeout = 72
|
||||
|
||||
# This must be CHANGED to random value and uncommented before deployment
|
||||
|
|
|
|||
|
|
@ -151,17 +151,18 @@ def _gssapiLogin(environ, session, principal):
|
|||
wprinc = options['WebPrincipal']
|
||||
keytab = options['WebKeytab']
|
||||
ccache = options['WebCCache']
|
||||
authtype = options['WebAuthType']
|
||||
return session.gssapi_login(principal=wprinc, keytab=keytab,
|
||||
ccache=ccache, proxyuser=principal)
|
||||
ccache=ccache, proxyuser=principal, proxyauthtype=authtype)
|
||||
|
||||
|
||||
def _sslLogin(environ, session, username):
|
||||
options = environ['koji.options']
|
||||
client_cert = options['WebCert']
|
||||
server_ca = options['KojiHubCA']
|
||||
|
||||
authtype = options['WebAuthType']
|
||||
return session.ssl_login(client_cert, None, server_ca,
|
||||
proxyuser=username)
|
||||
proxyuser=username, proxyauthtype=authtype)
|
||||
|
||||
|
||||
def _assertLogin(environ):
|
||||
|
|
@ -272,8 +273,9 @@ def login(environ, page=None):
|
|||
session = _getServer(environ)
|
||||
options = environ['koji.options']
|
||||
|
||||
# try SSL first, fall back to Kerberos
|
||||
if options['WebCert']:
|
||||
if options['WebAuthType'] == koji.AUTHTYPE_SSL:
|
||||
## Clients authenticate to KojiWeb by SSL, so extract
|
||||
## the username via the (verified) client certificate
|
||||
if environ['wsgi.url_scheme'] != 'https':
|
||||
dest = 'login'
|
||||
if page:
|
||||
|
|
@ -288,23 +290,31 @@ def login(environ, page=None):
|
|||
username = environ.get('SSL_CLIENT_S_DN_CN')
|
||||
if not username:
|
||||
raise koji.AuthError('unable to get user information from client certificate')
|
||||
|
||||
if not _sslLogin(environ, session, username):
|
||||
raise koji.AuthError('could not login %s using SSL certificates' % username)
|
||||
|
||||
authlogger.info('Successful SSL authentication by %s', username)
|
||||
|
||||
elif options['WebPrincipal']:
|
||||
elif options['WebAuthType'] == koji.AUTHTYPE_GSSAPI:
|
||||
## Clients authenticate to KojiWeb by Kerberos, so extract
|
||||
## the username via the REMOTE_USER which will be the
|
||||
## Kerberos principal
|
||||
principal = environ.get('REMOTE_USER')
|
||||
if not principal:
|
||||
raise koji.AuthError(
|
||||
'configuration error: mod_auth_gssapi should have performed authentication before '
|
||||
'presenting this page')
|
||||
|
||||
if not _gssapiLogin(environ, session, principal):
|
||||
raise koji.AuthError('could not login using principal: %s' % principal)
|
||||
|
||||
username = principal
|
||||
else:
|
||||
raise koji.AuthError(
|
||||
'configuration error: set WebAuthType or on of WebPrincipal/WebCert options')
|
||||
|
||||
## This now is how we proxy the user to the hub
|
||||
if options['WebCert']:
|
||||
if not _sslLogin(environ, session, username):
|
||||
raise koji.AuthError('could not login %s using SSL certificates' % username)
|
||||
|
||||
authlogger.info('Successful SSL authentication by %s', username)
|
||||
elif options['WebPrincipal']:
|
||||
if not _gssapiLogin(environ, session, username):
|
||||
raise koji.AuthError('could not login using principal: %s' % username)
|
||||
|
||||
authlogger.info('Successful Kerberos authentication by %s', username)
|
||||
else:
|
||||
raise koji.AuthError(
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ class Dispatcher(object):
|
|||
['KrbCanonHost', 'boolean', False],
|
||||
['KrbServerRealm', 'string', None],
|
||||
|
||||
['WebAuthType', 'string', None],
|
||||
|
||||
['WebCert', 'string', None],
|
||||
['KojiHubCA', 'string', '/etc/kojiweb/kojihubca.crt'],
|
||||
|
||||
|
|
@ -148,6 +150,20 @@ class Dispatcher(object):
|
|||
else:
|
||||
opts[name] = default
|
||||
opts['Secret'] = koji.util.HiddenValue(opts['Secret'])
|
||||
|
||||
if opts['WebAuthType'] not in (None, 'gssapi', 'ssl'):
|
||||
raise koji.ConfigurationError(f"Invalid value {opts['WebAuthType']} for "
|
||||
"WebAuthType (ssl/gssapi)")
|
||||
if opts['WebAuthType'] == 'gssapi':
|
||||
opts['WebAuthType'] = koji.AUTHTYPE_GSSAPI
|
||||
elif opts['WebAuthType'] == 'ssl':
|
||||
opts['WebAuthType'] = koji.AUTHTYPE_SSL
|
||||
# if there is no explicit request, use same authtype as web has
|
||||
elif opts['WebPrincipal']:
|
||||
opts['WebAuthType'] = koji.AUTHTYPE_GSSAPI
|
||||
elif opts['WebCert']:
|
||||
opts['WebAuthType'] = koji.AUTHTYPE_SSL
|
||||
|
||||
self.options = opts
|
||||
return opts
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue