use header-based auth
This commit is contained in:
parent
850a161e2b
commit
82d2d4dd55
5 changed files with 101 additions and 57 deletions
|
|
@ -153,6 +153,17 @@ The following options control aspects of authentication when using ``mod_auth_gs
|
||||||
option.
|
option.
|
||||||
The default value of False is recommended.
|
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.
|
Enabling gssapi auth also requires settings in the httpd config.
|
||||||
|
|
||||||
SSL client certificate auth configuration
|
SSL client certificate auth configuration
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ KojiDir = /mnt/koji
|
||||||
# AllowedKrbRealms = *
|
# AllowedKrbRealms = *
|
||||||
## TODO: this option should be removed in future release
|
## TODO: this option should be removed in future release
|
||||||
# DisableGSSAPIProxyDNFallback = False
|
# DisableGSSAPIProxyDNFallback = False
|
||||||
|
## TODO: this option should be turned True in future release
|
||||||
|
# DisableURLSessions = False
|
||||||
|
|
||||||
## end Kerberos auth configuration
|
## end Kerberos auth configuration
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
# Mike McLean <mikem@redhat.com>
|
# Mike McLean <mikem@redhat.com>
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import http.cookies
|
import email
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -449,6 +449,8 @@ def load_config(environ):
|
||||||
['AllowedKrbRealms', 'string', '*'],
|
['AllowedKrbRealms', 'string', '*'],
|
||||||
# TODO: this option should be removed in future release
|
# TODO: this option should be removed in future release
|
||||||
['DisableGSSAPIProxyDNFallback', 'boolean', False],
|
['DisableGSSAPIProxyDNFallback', 'boolean', False],
|
||||||
|
# TODO: this option should be turned True in future release
|
||||||
|
['DisableURLSessions', 'boolean', False],
|
||||||
|
|
||||||
['DNUsernameComponent', 'string', 'CN'],
|
['DNUsernameComponent', 'string', 'CN'],
|
||||||
['ProxyDNs', 'string', ''],
|
['ProxyDNs', 'string', ''],
|
||||||
|
|
@ -808,24 +810,18 @@ def application(environ, start_response):
|
||||||
except RequestTimeout as e:
|
except RequestTimeout as e:
|
||||||
return error_reply(start_response, '408 Request Timeout', str(e) + '\n')
|
return error_reply(start_response, '408 Request Timeout', str(e) + '\n')
|
||||||
response = response.encode()
|
response = response.encode()
|
||||||
headers = [
|
headers = email.message.EmailMessage()
|
||||||
('Content-Length', str(len(response))),
|
headers['Content-Length'] = str(len(response))
|
||||||
('Content-Type', "text/xml"),
|
headers['Content-Type'] = "text/xml"
|
||||||
]
|
|
||||||
cookies = http.cookies.SimpleCookie(environ.get('HTTP_SET_COOKIE'))
|
|
||||||
if hasattr(context, 'session') and context.session.logged_in:
|
if hasattr(context, 'session') and context.session.logged_in:
|
||||||
cookies['session-id'] = context.session.id
|
headers.add_header(
|
||||||
cookies['session-key'] = context.session.key
|
'X-Session-Data',
|
||||||
cookies['callnum'] = context.session.callnum
|
'1', # logged in
|
||||||
cookies['logged_in'] = "1"
|
session_id=str(context.session.id),
|
||||||
else:
|
session_key=str(context.session.key),
|
||||||
# good for not mistaking for the older hub with small overhead
|
callnum=str(context.session.callnum),
|
||||||
cookies['logged_in'] = "0"
|
)
|
||||||
for c in cookies.values():
|
start_response('200 OK', headers.items())
|
||||||
c.path = '/'
|
|
||||||
c.secure = True
|
|
||||||
headers.append(('Set-Cookie', c.OutputString()))
|
|
||||||
start_response('200 OK', headers)
|
|
||||||
if h.traceback:
|
if h.traceback:
|
||||||
# rollback
|
# rollback
|
||||||
context.cnx.rollback()
|
context.cnx.rollback()
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from __future__ import absolute_import, division
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
import email
|
||||||
import errno
|
import errno
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -57,7 +58,6 @@ except ImportError: # pragma: no cover
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import http.cookies
|
|
||||||
import requests
|
import requests
|
||||||
import six
|
import six
|
||||||
import six.moves.configparser
|
import six.moves.configparser
|
||||||
|
|
@ -2701,12 +2701,19 @@ class ClientSession(object):
|
||||||
if name == 'rawUpload':
|
if name == 'rawUpload':
|
||||||
return self._prepUpload(*args, **kwargs)
|
return self._prepUpload(*args, **kwargs)
|
||||||
args = encode_args(*args, **kwargs)
|
args = encode_args(*args, **kwargs)
|
||||||
|
headers = email.message.EmailMessage()
|
||||||
if self.logged_in:
|
if self.logged_in:
|
||||||
for c in self.rsession.cookies:
|
sinfo = self.sinfo.copy()
|
||||||
if c.name == 'callnum':
|
sinfo['callnum'] = self.callnum
|
||||||
c.value = str(self.callnum)
|
|
||||||
self.callnum += 1
|
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':
|
elif name == 'sslLogin':
|
||||||
handler = self.baseurl + '/ssllogin'
|
handler = self.baseurl + '/ssllogin'
|
||||||
else:
|
else:
|
||||||
|
|
@ -2717,12 +2724,9 @@ class ClientSession(object):
|
||||||
# encoded as UTF-8. For python3 it means "return a str with an appropriate
|
# encoded as UTF-8. For python3 it means "return a str with an appropriate
|
||||||
# xml declaration for encoding as UTF-8".
|
# xml declaration for encoding as UTF-8".
|
||||||
request = request.encode('utf-8')
|
request = request.encode('utf-8')
|
||||||
headers = [
|
headers['User-Agent'] = 'koji/1'
|
||||||
# connection class handles Host
|
headers['Content-Type'] = 'text/xml'
|
||||||
('User-Agent', 'koji/1'),
|
headers['Content-Length'] = str(len(request))
|
||||||
('Content-Type', 'text/xml'),
|
|
||||||
('Content-Length', str(len(request))),
|
|
||||||
]
|
|
||||||
return handler, headers, request
|
return handler, headers, request
|
||||||
|
|
||||||
def _sanitize_url(self, url):
|
def _sanitize_url(self, url):
|
||||||
|
|
@ -2801,13 +2805,6 @@ class ClientSession(object):
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
r = self.rsession.post(handler, **callopts)
|
r = self.rsession.post(handler, **callopts)
|
||||||
r.raise_for_status()
|
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:
|
try:
|
||||||
ret = self._read_xmlrpc_response(r)
|
ret = self._read_xmlrpc_response(r)
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -3039,24 +3036,31 @@ class ClientSession(object):
|
||||||
"""prep a rawUpload call"""
|
"""prep a rawUpload call"""
|
||||||
if not self.logged_in:
|
if not self.logged_in:
|
||||||
raise ActionNotAllowed("you must be logged in to upload")
|
raise ActionNotAllowed("you must be logged in to upload")
|
||||||
args = self.sinfo.copy()
|
sinfo = self.sinfo.copy()
|
||||||
args['callnum'] = self.callnum
|
sinfo['callnum'] = self.callnum
|
||||||
args['filename'] = name
|
args = {
|
||||||
args['filepath'] = path
|
'filename': name,
|
||||||
args['fileverify'] = verify
|
'filepath': path,
|
||||||
args['offset'] = str(offset)
|
'fileverify': verify,
|
||||||
|
'offset': str(offset),
|
||||||
|
}
|
||||||
if overwrite:
|
if overwrite:
|
||||||
args['overwrite'] = "1"
|
args['overwrite'] = "1"
|
||||||
if volume is not None:
|
if volume is not None:
|
||||||
args['volume'] = volume
|
args['volume'] = volume
|
||||||
size = len(chunk)
|
size = len(chunk)
|
||||||
self.callnum += 1
|
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))
|
handler = "%s?%s" % (self.baseurl, six.moves.urllib.parse.urlencode(args))
|
||||||
headers = [
|
headers['User-Agent'] = 'koji/1'
|
||||||
('User-Agent', 'koji/1'),
|
headers["Content-Type"] = "application/octet-stream"
|
||||||
("Content-Type", "application/octet-stream"),
|
headers["Content-length"] = str(size)
|
||||||
("Content-length", str(size)),
|
|
||||||
]
|
|
||||||
request = chunk
|
request = chunk
|
||||||
if six.PY3 and isinstance(chunk, str):
|
if six.PY3 and isinstance(chunk, str):
|
||||||
request = chunk.encode('utf-8')
|
request = chunk.encode('utf-8')
|
||||||
|
|
|
||||||
51
koji/auth.py
51
koji/auth.py
|
|
@ -79,20 +79,47 @@ class Session(object):
|
||||||
self._groups = None
|
self._groups = None
|
||||||
self._host_id = ''
|
self._host_id = ''
|
||||||
environ = getattr(context, 'environ', {})
|
environ = getattr(context, 'environ', {})
|
||||||
# prefer new cookie-based sessions
|
# prefer new header-based sessions
|
||||||
if 'HTTP_COOKIE' in environ:
|
if 'HTTP_X_SESSION_DATA' in environ:
|
||||||
cookies = http.cookies.SimpleCookie(environ['HTTP_COOKIE'])
|
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:
|
try:
|
||||||
id = int(cookies['session-id'].value)
|
id = int(args['session-id'][0])
|
||||||
key = str(cookies['session-key'].value)
|
key = args['session-key'][0]
|
||||||
except KeyError as field:
|
except KeyError as field:
|
||||||
raise koji.AuthError('%s not specified in session args' % field)
|
raise koji.AuthError('%s not specified in session args' % field)
|
||||||
try:
|
try:
|
||||||
callnum = int(cookies['callnum'].value)
|
callnum = args['callnum'][0]
|
||||||
except KeyError:
|
except Exception:
|
||||||
callnum = None
|
callnum = None
|
||||||
else:
|
else:
|
||||||
self.message = 'no session cookies'
|
self.message = 'no X-Session-Data header'
|
||||||
return
|
return
|
||||||
hostip = self.get_remote_ip(override=hostip)
|
hostip = self.get_remote_ip(override=hostip)
|
||||||
# lookup the session
|
# lookup the session
|
||||||
|
|
@ -498,14 +525,18 @@ class Session(object):
|
||||||
insert.execute()
|
insert.execute()
|
||||||
context.cnx.commit()
|
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.id = session_id
|
||||||
context.session.key = key
|
context.session.key = key
|
||||||
context.session.logged_in = True
|
context.session.logged_in = True
|
||||||
context.session.callnum = 0
|
context.session.callnum = 0
|
||||||
|
|
||||||
# return session info
|
# 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):
|
def subsession(self):
|
||||||
"Create a subsession"
|
"Create a subsession"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue