Commit 0eade2ca authored by Jérome Perrin's avatar Jérome Perrin

oauth2_authorisation: py3

parent c1159e94
......@@ -70,7 +70,7 @@ from DateTime import DateTime
from Products.ERP5Type import Permissions
from Products.ERP5Type.Message import translateString
from Products.ERP5Type.UnrestrictedMethod import super_user
from Products.ERP5Type.Utils import bytes2str, str2bytes, unicode2str
from Products.ERP5Type.Utils import bytes2str, str2bytes, unicode2str, str2unicode
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Security.ERP5GroupManager import (
disableCache as ERP5GroupManager_disableCache,
......@@ -196,6 +196,7 @@ def substituteRequest(
request_container.REQUEST = inner_request
try:
__traceback_info__ = inner_request
print('inner_request =>', inner_request.text())
yield inner_request
finally:
request_container.REQUEST = request_from_container
......@@ -448,7 +449,7 @@ class _ERP5AuthorisationEndpoint(AuthorizationEndpoint):
}
for x in (
portal.portal_categories.resolveCategory(
'oauth2_scope/' + y.encode('utf-8'),
'oauth2_scope/' + unicode2str(y),
)
for y in scope_list
)
......@@ -553,7 +554,7 @@ class _ERP5RequestValidator(RequestValidator):
return token_callable(**kw)
except jwt.InvalidTokenError:
pass
raise
raise # pylint:disable=misplaced-bare-raise
def client_authentication_required(self, request, *args, **kwargs):
# Use this method, which is called early on most endpoints, to setup request.client .
......@@ -699,7 +700,7 @@ class _ERP5RequestValidator(RequestValidator):
client_value=request.client.erp5_client_value,
redirect_uri=request.redirect_uri,
scope_list=[
x.encode('utf-8')
unicode2str(x)
for x in request.scopes
],
code_challenge=request.code_challenge,
......@@ -859,13 +860,13 @@ def _callEndpoint(endpoint, self, REQUEST):
# not have to care about intermediate proxies).
request_header_dict['X_FORWARDED_FOR'] = REQUEST.getClientAddr()
request_body = REQUEST.get('BODY')
if request_body is None and content_type == 'application/x-www-form-urlencoded':
if (not request_body) and content_type == 'application/x-www-form-urlencoded':
# XXX: very imperfect, but should be good enough for OAuth2 usage:
# no standard OAuth2 POST field should be marshalled by Zope.
request_body = urlencode([
(x, y)
for x, y in six.iteritems(REQUEST.form)
if isinstance(y, six.text_type)
if isinstance(y, six.string_types)
])
uri = other.get('URL', '')
query_string = environ.get('QUERY_STRING')
......@@ -1287,7 +1288,7 @@ class OAuth2AuthorisationServerConnector(XMLObject):
ensure_ascii(token_dict[JWT_PAYLOAD_KEY]),
)
return token_dict
raise
raise # pylint:disable=misplaced-bare-raise
def _getRefreshTokenDict(self, value, request):
for _, algorithm, symetric_key in self.__getRefreshTokenKeyList():
......@@ -1309,14 +1310,14 @@ class OAuth2AuthorisationServerConnector(XMLObject):
continue
else:
return token_dict
raise
raise # pylint:disable=misplaced-bare-raise
def _checkCustomTokenPolicy(self, token, request):
"""
Validate non-standard jwt claims against request.
"""
if not isAddressInNetworkList(
address=request.headers['X_FORWARDED_FOR'].decode('utf-8'),
address=str2unicode(request.headers['X_FORWARDED_FOR']),
network_list=token[JWT_CLAIM_NETWORK_LIST_KEY],
):
raise jwt.InvalidTokenError
......@@ -1369,7 +1370,7 @@ class OAuth2AuthorisationServerConnector(XMLObject):
continue
else:
return token_dict['iss']
raise
raise # pylint:disable=misplaced-bare-raise
security.declarePrivate('getRefreshTokenClientId')
def getRefreshTokenClientId(self, value, request):
......@@ -1395,13 +1396,13 @@ class OAuth2AuthorisationServerConnector(XMLObject):
continue
else:
return token_dict['iss']
raise
raise # pylint:disable=misplaced-bare-raise
def _getSessionValueFromTokenDict(self, token_dict):
session_value = self._getSessionValue(
token_dict[JWT_PAYLOAD_KEY][
unicode2str(token_dict[JWT_PAYLOAD_KEY][
JWT_PAYLOAD_AUTHORISATION_SESSION_ID_KEY
].encode('utf-8'),
]),
'validated',
)
if session_value is not None:
......
......@@ -5,17 +5,20 @@ Mutate REQUEST to call standard OAuth2 /authorize endpoint from an ERP5 Form in
import json
import six
from erp5.component.document.OAuth2AuthorisationServerConnector import substituteRequest
from Products.ERP5Type.Utils import unicode2str
# XXX: Accessing REQUEST from acquisition is bad. But Base_callDialogMethod
# does not propagate the request cleanly, so no other way so far.
REQUEST = context.REQUEST
form = {
key.encode('utf-8'): value.encode('utf-8')
unicode2str(key): unicode2str(value)
for key, value in six.iteritems(json.loads(request_info_json))
}
if scope_list:
form['scopes'] = ' '.join(scope_list)
portal = context.getPortalObject()
from pprint import pprint
pprint(('substituteRequest form', substituteRequest))
with substituteRequest(
context=portal,
request=REQUEST,
......
......@@ -44,6 +44,8 @@ form = dict(parse_qsl(parsed_came_from.query))
login_retry_url = REQUEST.form.get('login_retry_url')
if login_retry_url is not None:
form['login_retry_url'] = login_retry_url
from pprint import pprint
pprint(('logged_in_once substituteRequest', form))
with substituteRequest(
context=portal,
request=REQUEST,
......
......@@ -43,11 +43,12 @@ import random
import pprint
from time import time
import unittest
from six.moves.urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
import six.moves.urllib as urllib
from six.moves.urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
from AccessControl.SecurityManagement import getSecurityManager, setSecurityManager
from DateTime import DateTime
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.Utils import bytes2str, str2bytes
from Products.ERP5Type.Utils import bytes2str, str2bytes, unicode2str
from Products.ERP5.ERP5Site import (
ERP5_AUTHORISATION_EXTRACTOR_USERNAME_NAME,
ERP5_AUTHORISATION_EXTRACTOR_PASSWORD_NAME,
......@@ -83,14 +84,19 @@ class FormExtractor(HTMLParser):
elif self.__in_form and tag in _HTML_FIELD_TAG_SET:
self.form_list[-1][1].append((
attr_dict['name'],
attr_dict.get('value', '').encode('utf-8'),
unicode2str(attr_dict.get('value', ''))
))
def handle_endtag(self, tag):
if tag == 'form':
self.__in_form = False
def error(self, message):
raise ValueError(message)
class TestOAuth2(ERP5TypeTestCase):
# pylint:disable=unused-private-member
__cleanup_list = None
__port = None
__query_trace = None
......@@ -428,7 +434,7 @@ class TestOAuth2(ERP5TypeTestCase):
cookie_value, cookie_attributes = cookie_body.split(';', 1)
cookie_value = cookie_value.strip('"')
cookie_value_dict = {
'value': six.moves.urllib.parse.unquote(cookie_value),
'value': urllib.parse.unquote(cookie_value),
}
for cookie_attribute in cookie_attributes.split(';'):
cookie_attribute = cookie_attribute.lstrip()
......@@ -497,7 +503,7 @@ class TestOAuth2(ERP5TypeTestCase):
b'',
# XXX: Tolerate the redirect URL being returned in the body.
# This is a bug, body should really be empty.
header_dict.get('location', b''),
str2bytes(header_dict.get('location', '')),
),
)
parsed_location = urlsplit(header_dict.get('location', ''))
......@@ -855,13 +861,13 @@ class TestOAuth2(ERP5TypeTestCase):
path=oauth2_server_connector + '/token',
method='POST',
content_type='application/x-www-form-urlencoded',
body=urlencode({
body=str2bytes(urlencode({
'grant_type': 'authorization_code',
'code': authorisation_code,
'client_id': client_id,
'code_verifier': code_verifier,
'redirect_uri': _EXTERNAL_CLIENT_REDIRECT_URI,
}),
})),
)
time_after = int(time())
self.assertEqual(status, 200, response)
......@@ -883,10 +889,10 @@ class TestOAuth2(ERP5TypeTestCase):
path=oauth2_server_connector + '/token',
method='POST',
content_type='application/x-www-form-urlencoded',
body=urlencode({
body=str2bytes(urlencode({
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
}),
})),
)
self.assertEqual(status, 200)
self.assertEqual(cookie_dict, {})
......@@ -900,10 +906,10 @@ class TestOAuth2(ERP5TypeTestCase):
path=oauth2_server_connector + '/revoke',
method='POST',
content_type='application/x-www-form-urlencoded',
body=urlencode({
body=str2bytes(urlencode({
'token_type_hint': 'refresh_token',
'token': refresh_token,
}),
})),
)
self.assertEqual(status, 200)
self.assertEqual(cookie_dict, {})
......@@ -916,10 +922,10 @@ class TestOAuth2(ERP5TypeTestCase):
path=oauth2_server_connector + '/token',
method='POST',
content_type='application/x-www-form-urlencoded',
body=urlencode({
body=str2bytes(urlencode({
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
}),
})),
)
self.assertEqual(status, 400)
self.assertEqual(cookie_dict, {})
......
......@@ -51,7 +51,7 @@ from OFS.Traversable import NotFound
from Products.ERP5Type import Permissions
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type.Timeout import getTimeLeft
from Products.ERP5Type.Utils import bytes2str, str2bytes, str2unicode
from Products.ERP5Type.Utils import bytes2str, unicode2str, str2bytes, str2unicode
from Products.ERP5Security.ERP5OAuth2ResourceServerPlugin import (
OAuth2AuthorisationClientConnectorMixIn,
ERP5OAuth2ResourceServerPlugin,
......@@ -227,13 +227,16 @@ class _OAuth2AuthorisationServerProxy(object):
)
else:
Connection = HTTPConnection
if six.PY2:
# Changed in version 3.4: The strict parameter was removed.
# HTTP 0.9-style "Simple Responses" are no longer supported.
Connection = functools.partial(Connection, strict=True)
timeout = getTimeLeft()
if timeout is None or timeout > self._timeout:
timeout = self._timeout
http_connection = Connection(
host=parsed_url.hostname,
port=parsed_url.port,
strict=True,
timeout=timeout,
source_address=self._bind_address,
)
......@@ -274,7 +277,7 @@ class _OAuth2AuthorisationServerProxy(object):
def _queryOAuth2(self, method, REQUEST, RESPONSE):
header_dict, body, status = self._query(
method,
body=urlencode(REQUEST.form.items()),
body=urlencode(REQUEST.form),
header_dict={
'CONTENT_TYPE': REQUEST.environ['CONTENT_TYPE'],
},
......@@ -313,7 +316,7 @@ class _OAuth2AuthorisationServerProxy(object):
def getAccessTokenSignatureAlgorithmAndPublicKeyList(self):
return tuple(
(signature_algorithm.encode('ascii'), public_key.encode('ascii'))
(unicode2str(signature_algorithm), unicode2str(public_key))
for signature_algorithm, public_key in self._queryERP5(
'getAccessTokenSignatureAlgorithmAndPublicKeyList',
)
......@@ -864,7 +867,7 @@ class OAuth2AuthorisationClientConnector(
try:
state_dict = json.loads(
self.__getMultiFernet().decrypt(
state,
str2bytes(state),
ttl=self._SESSION_STATE_VALIDITY,
),
)
......@@ -882,7 +885,7 @@ class OAuth2AuthorisationClientConnector(
came_from = state_dict.get(_STATE_CAME_FROM_NAME)
if came_from:
context = self # whatever
kw['redirect_url'] = came_from.encode('utf-8')
kw['redirect_url'] = unicode2str(came_from)
else:
context = self._getNeutralContextValue()
context.Base_redirect(**kw)
......@@ -930,7 +933,7 @@ class OAuth2AuthorisationClientConnector(
REQUEST=REQUEST,
RESPONSE=RESPONSE,
)
identifier_from_state = state_dict[_STATE_IDENTIFIER_NAME].encode('ascii')
identifier_from_state = unicode2str(state_dict[_STATE_IDENTIFIER_NAME])
for (
state_cookie_name,
identifier_from_cookie,
......@@ -965,7 +968,7 @@ class OAuth2AuthorisationClientConnector(
'code': code,
'redirect_uri': self.getRedirectUri(),
'client_id': self.getReference(),
'code_verifier': state_dict[_STATE_CODE_VERIFIER_NAME].encode('ascii'),
'code_verifier': unicode2str(state_dict[_STATE_CODE_VERIFIER_NAME])
},
)
access_token, _, error_message = self._setCookieFromTokenResponse(
......
......@@ -46,6 +46,7 @@ from Products.ERP5Type.TransactionalVariable import \
getTransactionalVariable, TransactionalResource
from Products.ERP5Type.dynamic.portal_type_class import synchronizeDynamicModules
from Products.ERP5Type.mixin.response_header_generator import ResponseHeaderGenerator
from Products.ERP5Type.Utils import str2bytes, bytes2str
from zLOG import LOG, INFO, WARNING, ERROR
from zExceptions import BadRequest
......@@ -248,10 +249,10 @@ class AutorisationExtractorBeforeTraverseHook(object):
ERP5_AUTHORISATION_EXTRACTOR_PASSWORD_NAME in form_dict
):
username = form_dict[ERP5_AUTHORISATION_EXTRACTOR_USERNAME_NAME]
request._auth = 'Basic ' + base64.b64encode('%s:%s' % (
request._auth = 'Basic ' + bytes2str(base64.b64encode(str2bytes('%s:%s' % (
username,
form_dict[ERP5_AUTHORISATION_EXTRACTOR_PASSWORD_NAME],
))
))))
request.response._auth = 1
_setUserNameForAccessLog(username, request)
......
......@@ -48,6 +48,7 @@ from Products.PluggableAuthService.interfaces.plugins import (
)
from Products.ERP5Security import _setUserNameForAccessLog
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type.Utils import bytes2str, str2bytes, str2unicode, unicode2str
# Public constants. Must not change once deployed.
......@@ -109,11 +110,11 @@ def encodeAccessTokenPayload(payload):
Encode given json-safe value into a format suitable for
decodeAccessTokenPayload.
"""
return base64.urlsafe_b64encode(
return bytes2str(base64.urlsafe_b64encode(
zlib.compress(
json.dumps(payload),
str2bytes(json.dumps(payload)),
),
)
))
def decodeAccessTokenPayload(encoded_payload):
"""
......@@ -353,7 +354,7 @@ class ERP5OAuth2ResourceServerPlugin(BasePlugin):
if client_id is None:
# Peek into token (without checking its signature) to guess the client_id
# to look for.
client_id = jwt.decode(
client_id = unicode2str(jwt.decode(
raw_token,
# no key.
# any algorithm is fine.
......@@ -361,7 +362,7 @@ class ERP5OAuth2ResourceServerPlugin(BasePlugin):
'verify_signature': False,
'verify_exp': False,
},
)['iss'].encode('utf-8')
)['iss'])
assert client_id is not None
web_service_value_list = list(self.__iterClientConnectorValue(
client_id=client_id,
......@@ -425,7 +426,7 @@ class ERP5OAuth2ResourceServerPlugin(BasePlugin):
The schema of this dictionary is purely an internal implementation detail
of this plugin.
"""
client_address = request.getClientAddr().decode('utf-8')
client_address = str2unicode(request.getClientAddr())
token = self.__checkTokenSignature(access_token)
if token is None and can_update_key:
self.__updateAccessTokenSignatureKeyList(request=request)
......@@ -440,9 +441,9 @@ class ERP5OAuth2ResourceServerPlugin(BasePlugin):
return
# JWT is known valid. Access its content.
token_payload = decodeAccessTokenPayload(
token[JWT_PAYLOAD_KEY].encode('ascii'),
bytes2str(token[JWT_PAYLOAD_KEY].encode('ascii')),
)
client_id = token['iss'].encode('utf-8')
client_id = unicode2str(token['iss'])
if self.__getWebServiceValue(
client_id=client_id,
).getSessionVersion(
......@@ -452,8 +453,8 @@ class ERP5OAuth2ResourceServerPlugin(BasePlugin):
return
return {
_PRIVATE_EXTRACTED_KEY: (
token_payload[JWT_PAYLOAD_USER_ID_KEY].encode('utf-8'),
token_payload[JWT_PAYLOAD_USER_CAPTION_KEY].encode('utf-8'),
unicode2str(token_payload[JWT_PAYLOAD_USER_ID_KEY]),
unicode2str(token_payload[JWT_PAYLOAD_USER_CAPTION_KEY]),
),
_PRIVATE_TOKEN_KEY: (access_token, refresh_token),
_PRIVATE_CLIENT_ID: client_id,
......@@ -467,10 +468,10 @@ class ERP5OAuth2ResourceServerPlugin(BasePlugin):
] or '',
},
_PRIVATE_GROUP_LIST_KEY: tuple(
x.encode('utf-8') for x in token_payload[JWT_PAYLOAD_GROUP_LIST_KEY]
unicode2str(x) for x in token_payload[JWT_PAYLOAD_GROUP_LIST_KEY]
),
_PRIVATE_ROLE_LIST_KEY: tuple(
x.encode('utf-8') for x in token_payload[JWT_PAYLOAD_ROLE_LIST_KEY]
unicode2str(x) for x in token_payload[JWT_PAYLOAD_ROLE_LIST_KEY]
),
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment