use header-based auth

This commit is contained in:
Tomas Kopecek 2022-09-05 16:59:35 +02:00
parent 850a161e2b
commit 82d2d4dd55
5 changed files with 101 additions and 57 deletions

View file

@ -153,6 +153,17 @@ The following options control aspects of authentication when using ``mod_auth_gs
option.
The default value of False is recommended.
DisableURLSessions
Type: boolean
Default: ``False``
If set to ``False``, it enables older clients to log in via session parameters
encoded in URL. New behaviour uses header-based parameteres. This default
will be changed in future to ``True`` effectively disabling older clients. It is
encouraged to set it to ``True`` as soon as possible when no older clients are
using the hub. (Added in 1.30)
Enabling gssapi auth also requires settings in the httpd config.
SSL client certificate auth configuration

View file

@ -35,6 +35,8 @@ KojiDir = /mnt/koji
# AllowedKrbRealms = *
## TODO: this option should be removed in future release
# DisableGSSAPIProxyDNFallback = False
## TODO: this option should be turned True in future release
# DisableURLSessions = False
## end Kerberos auth configuration

View file

@ -19,7 +19,7 @@
# Mike McLean <mikem@redhat.com>
import datetime
import http.cookies
import email
import inspect
import logging
import os
@ -449,6 +449,8 @@ def load_config(environ):
['AllowedKrbRealms', 'string', '*'],
# TODO: this option should be removed in future release
['DisableGSSAPIProxyDNFallback', 'boolean', False],
# TODO: this option should be turned True in future release
['DisableURLSessions', 'boolean', False],
['DNUsernameComponent', 'string', 'CN'],
['ProxyDNs', 'string', ''],
@ -808,24 +810,18 @@ def application(environ, start_response):
except RequestTimeout as e:
return error_reply(start_response, '408 Request Timeout', str(e) + '\n')
response = response.encode()
headers = [
('Content-Length', str(len(response))),
('Content-Type', "text/xml"),
]
cookies = http.cookies.SimpleCookie(environ.get('HTTP_SET_COOKIE'))
headers = email.message.EmailMessage()
headers['Content-Length'] = str(len(response))
headers['Content-Type'] = "text/xml"
if hasattr(context, 'session') and context.session.logged_in:
cookies['session-id'] = context.session.id
cookies['session-key'] = context.session.key
cookies['callnum'] = context.session.callnum
cookies['logged_in'] = "1"
else:
# good for not mistaking for the older hub with small overhead
cookies['logged_in'] = "0"
for c in cookies.values():
c.path = '/'
c.secure = True
headers.append(('Set-Cookie', c.OutputString()))
start_response('200 OK', headers)
headers.add_header(
'X-Session-Data',
'1', # logged in
session_id=str(context.session.id),
session_key=str(context.session.key),
callnum=str(context.session.callnum),
)
start_response('200 OK', headers.items())
if h.traceback:
# rollback
context.cnx.rollback()

View file

@ -26,6 +26,7 @@ from __future__ import absolute_import, division
import base64
import datetime
import email
import errno
import hashlib
import json
@ -57,7 +58,6 @@ except ImportError: # pragma: no cover
from fnmatch import fnmatch
import dateutil.parser
import http.cookies
import requests
import six
import six.moves.configparser
@ -2701,12 +2701,19 @@ class ClientSession(object):
if name == 'rawUpload':
return self._prepUpload(*args, **kwargs)
args = encode_args(*args, **kwargs)
headers = email.message.EmailMessage()
if self.logged_in:
for c in self.rsession.cookies:
if c.name == 'callnum':
c.value = str(self.callnum)
sinfo = self.sinfo.copy()
sinfo['callnum'] = self.callnum
self.callnum += 1
handler = self.baseurl
if sinfo.get('header-auth'):
for k, v in sinfo.items():
sinfo[k] = str(v)
handler = self.baseurl
headers.add_header('X-Session-Data', '1', **sinfo)
else:
# old server
handler = "%s?%s" % (self.baseurl, six.moves.urllib.parse.urlencode(sinfo))
elif name == 'sslLogin':
handler = self.baseurl + '/ssllogin'
else:
@ -2717,12 +2724,9 @@ class ClientSession(object):
# encoded as UTF-8. For python3 it means "return a str with an appropriate
# xml declaration for encoding as UTF-8".
request = request.encode('utf-8')
headers = [
# connection class handles Host
('User-Agent', 'koji/1'),
('Content-Type', 'text/xml'),
('Content-Length', str(len(request))),
]
headers['User-Agent'] = 'koji/1'
headers['Content-Type'] = 'text/xml'
headers['Content-Length'] = str(len(request))
return handler, headers, request
def _sanitize_url(self, url):
@ -2801,13 +2805,6 @@ class ClientSession(object):
warnings.simplefilter("ignore")
r = self.rsession.post(handler, **callopts)
r.raise_for_status()
if self.logged_in and len(r.cookies.items()) == 0:
# we have session, sent the cookies, but server is old
# and didn't sent them back, use old url-encoded style
sinfo = self.sinfo.copy()
handler = "%s?%s" % (handler, six.moves.urllib.parse.urlencode(sinfo))
r = self.rsession.post(handler, **callopts)
r.raise_for_status()
try:
ret = self._read_xmlrpc_response(r)
finally:
@ -3039,24 +3036,31 @@ class ClientSession(object):
"""prep a rawUpload call"""
if not self.logged_in:
raise ActionNotAllowed("you must be logged in to upload")
args = self.sinfo.copy()
args['callnum'] = self.callnum
args['filename'] = name
args['filepath'] = path
args['fileverify'] = verify
args['offset'] = str(offset)
sinfo = self.sinfo.copy()
sinfo['callnum'] = self.callnum
args = {
'filename': name,
'filepath': path,
'fileverify': verify,
'offset': str(offset),
}
if overwrite:
args['overwrite'] = "1"
if volume is not None:
args['volume'] = volume
size = len(chunk)
self.callnum += 1
headers = email.message.EmailMessage()
if sinfo.get('header-auth'):
for k, v in sinfo.items():
sinfo[k] = str(v)
headers.add_header('X-Session-Data', '1', **sinfo)
else:
args.update(sinfo)
handler = "%s?%s" % (self.baseurl, six.moves.urllib.parse.urlencode(args))
headers = [
('User-Agent', 'koji/1'),
("Content-Type", "application/octet-stream"),
("Content-length", str(size)),
]
headers['User-Agent'] = 'koji/1'
headers["Content-Type"] = "application/octet-stream"
headers["Content-length"] = str(size)
request = chunk
if six.PY3 and isinstance(chunk, str):
request = chunk.encode('utf-8')

View file

@ -79,20 +79,47 @@ class Session(object):
self._groups = None
self._host_id = ''
environ = getattr(context, 'environ', {})
# prefer new cookie-based sessions
if 'HTTP_COOKIE' in environ:
cookies = http.cookies.SimpleCookie(environ['HTTP_COOKIE'])
# prefer new header-based sessions
if 'HTTP_X_SESSION_DATA' in environ:
header = environ['HTTP_X_SESSION_DATA']
params = header.split(';')
id, key, callnum = None, None, None
for p in params[1:]:
k, v = [x.strip() for x in p.split('=')]
v = v.strip('"')
if k == 'session-id':
id = int(v)
elif k == 'session-key':
key = v
elif k == 'callnum':
callnum = v
elif k == 'header-auth':
pass
else:
raise koji.AuthError("Unexpected key in X-Session-Data: %s" % k)
if id is None:
raise koji.AuthError('session-id not specified in session args')
elif key is None:
raise koji.AuthError('session-key not specified in session args')
elif not context.opts['DisableURLSessions'] and args is not None:
# old deprecated method with session values in query string
# Option will be turned off by default in future release and removed later
args = environ.get('QUERY_STRING', '')
if not args:
self.message = 'nor session header nor session args'
return
args = urllib.parse.parse_qs(args, strict_parsing=True)
try:
id = int(cookies['session-id'].value)
key = str(cookies['session-key'].value)
id = int(args['session-id'][0])
key = args['session-key'][0]
except KeyError as field:
raise koji.AuthError('%s not specified in session args' % field)
try:
callnum = int(cookies['callnum'].value)
except KeyError:
callnum = args['callnum'][0]
except Exception:
callnum = None
else:
self.message = 'no session cookies'
self.message = 'no X-Session-Data header'
return
hostip = self.get_remote_ip(override=hostip)
# lookup the session
@ -498,14 +525,18 @@ class Session(object):
insert.execute()
context.cnx.commit()
# update it here, so it can be propagated to the cookies in kojixmlrpc.py
# update it here, so it can be propagated to the headers in kojixmlrpc.py
context.session.id = session_id
context.session.key = key
context.session.logged_in = True
context.session.callnum = 0
# return session info
return {'session-id': session_id, 'session-key': key}
return {
'session-id': session_id,
'session-key': key,
'header-auth': True, # signalize to client to use new session handling in 1.30
}
def subsession(self):
"Create a subsession"