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:
Yu Ming Zhu 2021-11-04 12:07:12 +00:00
commit f5ba2a5c08
8 changed files with 92 additions and 27 deletions

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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(

View file

@ -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