This is an automated email from the ASF dual-hosted git repository. brondsem pushed a commit to branch db/8461 in repository https://gitbox.apache.org/repos/asf/allura.git
commit 2f3fae6b90cd950fd81884cb8f3e45a9aeb0faeb Author: Dave Brondsema <dbronds...@slashdotmedia.com> AuthorDate: Thu Sep 8 11:33:55 2022 -0400 [#8461] switch from python-oauth2 to oauthlib --- Allura/allura/controllers/rest.py | 289 ++++++++++++++------- Allura/allura/model/oauth.py | 39 ++- .../allura/scripts/create_oauth1_dummy_tokens.py | 37 +++ Allura/allura/tests/functional/test_auth.py | 82 +++--- Allura/allura/websetup/bootstrap.py | 5 + AlluraTest/alluratest/controller.py | 6 +- AlluraTest/alluratest/validation.py | 2 +- requirements.in | 5 +- requirements.txt | 10 +- 9 files changed, 306 insertions(+), 169 deletions(-) diff --git a/Allura/allura/controllers/rest.py b/Allura/allura/controllers/rest.py index 5bf4c786c..5121972da 100644 --- a/Allura/allura/controllers/rest.py +++ b/Allura/allura/controllers/rest.py @@ -18,9 +18,10 @@ """REST Controller""" import json import logging -from six.moves.urllib.parse import unquote +from urllib.parse import unquote, urlparse, parse_qs -import oauth2 as oauth +import oauthlib.oauth1 +import oauthlib.common from paste.util.converters import asbool from webob import exc import tg @@ -118,14 +119,136 @@ class RestController: return NeighborhoodRestController(neighborhood), remainder +class Oauth1Validator(oauthlib.oauth1.RequestValidator): + + def validate_client_key(self, client_key: str, request: oauthlib.common.Request) -> bool: + return M.OAuthConsumerToken.query.get(api_key=client_key) is not None + + def get_client_secret(self, client_key, request): + return M.OAuthConsumerToken.query.get(api_key=client_key).secret_key # NoneType error? you need dummy_oauths() + + def save_request_token(self, token: dict, request: oauthlib.common.Request) -> None: + consumer_token = M.OAuthConsumerToken.query.get(api_key=request.client_key) + req_token = M.OAuthRequestToken( + api_key=token['oauth_token'], + secret_key=token['oauth_token_secret'], + consumer_token_id=consumer_token._id, + callback=request.oauth_params.get('oauth_callback', 'oob'), + ) + session(req_token).flush() + log.info('Saving new request token with key: %s', req_token.api_key) + + def verify_request_token(self, token: str, request: oauthlib.common.Request) -> bool: + return M.OAuthRequestToken.query.get(api_key=token) is not None + + def validate_request_token(self, client_key: str, token: str, request: oauthlib.common.Request) -> bool: + req_tok = M.OAuthRequestToken.query.get(api_key=token) + if not req_tok: + return False + return oauthlib.common.safe_string_equals(req_tok.consumer_token.api_key, client_key) + + def invalidate_request_token(self, client_key: str, request_token: str, request: oauthlib.common.Request) -> None: + M.OAuthRequestToken.query.remove({'api_key': request_token}) + + def validate_verifier(self, client_key: str, token: str, verifier: str, request: oauthlib.common.Request) -> bool: + req_tok = M.OAuthRequestToken.query.get(api_key=token) + return oauthlib.common.safe_string_equals(req_tok.validation_pin, verifier) # NoneType error? you need dummy_oauths() + + def save_verifier(self, token: str, verifier: dict, request: oauthlib.common.Request) -> None: + req_tok = M.OAuthRequestToken.query.get(api_key=token) + req_tok.validation_pin = verifier['oauth_verifier'] + session(req_tok).flush(req_tok) + + def get_redirect_uri(self, token: str, request: oauthlib.common.Request) -> str: + return M.OAuthRequestToken.query.get(api_key=token).callback + + def get_request_token_secret(self, client_key: str, token: str, request: oauthlib.common.Request) -> str: + return M.OAuthRequestToken.query.get(api_key=token).secret_key # NoneType error? you need dummy_oauths() + + def save_access_token(self, token: dict, request: oauthlib.common.Request) -> None: + consumer_token = M.OAuthConsumerToken.query.get(api_key=request.client_key) + request_token = M.OAuthRequestToken.query.get(api_key=request.resource_owner_key) + tok = M.OAuthAccessToken( + api_key=token['oauth_token'], + secret_key=token['oauth_token_secret'], + consumer_token_id=consumer_token._id, + request_token_id=request_token._id, + user_id=request_token.user_id, + ) + session(tok).flush(tok) + + def validate_access_token(self, client_key: str, token: str, request: oauthlib.common.Request) -> bool: + return M.OAuthAccessToken.query.get(api_key=token) is not None + + def get_access_token_secret(self, client_key: str, token: str, request: oauthlib.common.Request) -> str: + return M.OAuthAccessToken.query.get(api_key=token).secret_key # NoneType error? you need dummy_oauths() + + @property + def enforce_ssl(self) -> bool: + # don't enforce SSL in limited situations + if request.environ.get('paste.testing'): + # test suite is running + return False + elif asbool(config.get('debug')) and config['base_url'].startswith('http://'): + # development w/o https + return False + else: + return True + + @property + def safe_characters(self): + # add a few characters, so tests can have clear readable values + return super(Oauth1Validator, self).safe_characters | {'_', '-'} + + def get_default_realms(self, client_key, request): + return [] + + def validate_requested_realms(self, client_key, realms, request): + return True + + def get_realms(self, token, request): + return [] + + def validate_realms(self, client_key, token, request, uri=None, realms=None) -> bool: + return True + + def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, + request, request_token=None, access_token=None) -> bool: + # TODO: record and check nonces from reuse + return True + + def validate_redirect_uri(self, client_key, redirect_uri, request) -> bool: + # TODO: have application owner specify redirect uris, save on OAuthConsumerToken + return True + + @property + def dummy_client(self) -> str: + return 'dummy-client-key-for-oauthlib' + + @property + def dummy_request_token(self) -> str: + return 'dummy-request-token-for-oauthlib' + + @property + def dummy_access_token(self) -> str: + return 'dummy-access-token-for-oauthlib' + + +class AlluraOauth1Server(oauthlib.oauth1.WebApplicationServer): + def validate_request_token_request(self, request): + # this is NOT standard OAuth1 (spec requires the param) + # but initial Allura implementation defaulted it to "oob" so we'll continue to do that + # (this is called within create_request_token_response) + if not request.redirect_uri: + request.redirect_uri = 'oob' + return super().validate_request_token_request(request) + + class OAuthNegotiator: @property def server(self): - result = oauth.Server() - result.add_signature_method(oauth.SignatureMethod_PLAINTEXT()) - result.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) - return result + return AlluraOauth1Server(Oauth1Validator()) def _authenticate(self): bearer_token_prefix = 'Bearer ' @@ -152,70 +275,57 @@ class OAuthNegotiator: raise exc.HTTPUnauthorized access_token.last_access = datetime.utcnow() return access_token - req = oauth.Request.from_request( - request.method, - request.url.split('?')[0], + + provider = oauthlib.oauth1.ResourceEndpoint(Oauth1Validator()) + valid: bool + oauth_req: oauthlib.common.Request + valid, oauth_req = provider.validate_protected_resource_request( + request.url, + http_method=request.method, + body=request.body, headers=request.headers, - parameters=dict(request.params), - query_string=request.query_string - ) - if 'oauth_consumer_key' not in req: - log.error('Missing consumer token') - return None - if 'oauth_token' not in req: - log.error('Missing access token') - raise exc.HTTPUnauthorized - consumer_token = M.OAuthConsumerToken.query.get(api_key=req['oauth_consumer_key']) - access_token = M.OAuthAccessToken.query.get(api_key=req['oauth_token']) - if consumer_token is None: - log.error('Invalid consumer token') - return None - if access_token is None: - log.error('Invalid access token') - raise exc.HTTPUnauthorized - consumer = consumer_token.consumer - try: - self.server.verify_request(req, consumer, access_token.as_token()) - except oauth.Error as e: - log.error('Invalid signature %s %s', type(e), e) + realms=[]) + if not valid: raise exc.HTTPUnauthorized + + access_token = M.OAuthAccessToken.query.get(api_key=oauth_req.oauth_params['oauth_token']) access_token.last_access = datetime.utcnow() return access_token @expose() def request_token(self, **kw): - req = oauth.Request.from_request( - request.method, - request.url.split('?')[0], - headers=request.headers, - parameters=dict(request.params), - query_string=request.query_string - ) - consumer_token = M.OAuthConsumerToken.query.get(api_key=req.get('oauth_consumer_key')) - if consumer_token is None: - log.error('Invalid consumer token') - raise exc.HTTPUnauthorized - consumer = consumer_token.consumer - try: - self.server.verify_request(req, consumer, None) - except oauth.Error as e: - log.error('Invalid signature %s %s', type(e), e) - raise exc.HTTPUnauthorized - req_token = M.OAuthRequestToken( - consumer_token_id=consumer_token._id, - callback=req.get('oauth_callback', 'oob') - ) - session(req_token).flush() - log.info('Saving new request token with key: %s', req_token.api_key) - return req_token.to_string() + headers, body, status = self.server.create_request_token_response( + request.url, + http_method=request.method, + body=request.body, + headers=request.headers) + response.headers = headers + response.status_int = status + return body @expose('jinja:allura:templates/oauth_authorize.html') - def authorize(self, oauth_token=None): + def authorize(self, **kwargs): security.require_authenticated() + + try: + realms, credentials = self.server.get_realms_and_credentials( + request.url, + http_method=request.method, + body=request.body, + headers=request.headers) + except oauthlib.oauth1.OAuth1Error as oae: + log.info(f'oauth1 authorize error: {oae!r}') + response.headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response.status_int = oae.status_code + body = oae.urlencoded + return body + oauth_token = credentials.get('resource_owner_key', 'unknown') + rtok = M.OAuthRequestToken.query.get(api_key=oauth_token) if rtok is None: log.error('Invalid token %s', oauth_token) raise exc.HTTPUnauthorized + # store what user this is, so later use of the token can act as them rtok.user_id = c.user._id return dict( oauth_token=oauth_token, @@ -225,60 +335,39 @@ class OAuthNegotiator: @require_post() def do_authorize(self, yes=None, no=None, oauth_token=None): security.require_authenticated() + rtok = M.OAuthRequestToken.query.get(api_key=oauth_token) if no: rtok.delete() flash('%s NOT AUTHORIZED' % rtok.consumer_token.name, 'error') redirect('/auth/oauth/') - if rtok.callback == 'oob': - rtok.validation_pin = h.nonce(6) + + headers, body, status = self.server.create_authorization_response( + request.url, + http_method=request.method, + body=request.body, + headers=request.headers, + realms=[]) + + if status == 200: + verifier = str(parse_qs(body)['oauth_verifier'][0]) + rtok.validation_pin = verifier return dict(rtok=rtok) - rtok.validation_pin = h.nonce(20) - if '?' in rtok.callback: - url = rtok.callback + '&' else: - url = rtok.callback + '?' - url += 'oauth_token={}&oauth_verifier={}'.format( - rtok.api_key, rtok.validation_pin) - redirect(url) + response.headers = headers + response.status_int = status + return body @expose() def access_token(self, **kw): - req = oauth.Request.from_request( - request.method, - request.url.split('?')[0], - headers=request.headers, - parameters=dict(request.params), - query_string=request.query_string - ) - consumer_token = M.OAuthConsumerToken.query.get( - api_key=req['oauth_consumer_key']) - request_token = M.OAuthRequestToken.query.get( - api_key=req['oauth_token']) - if consumer_token is None: - log.error('Invalid consumer token') - raise exc.HTTPUnauthorized - if request_token is None: - log.error('Invalid request token') - raise exc.HTTPUnauthorized - pin = req['oauth_verifier'] - if pin != request_token.validation_pin: - log.error('Invalid verifier') - raise exc.HTTPUnauthorized - rtok = request_token.as_token() - rtok.set_verifier(pin) - consumer = consumer_token.consumer - try: - self.server.verify_request(req, consumer, rtok) - except oauth.Error as e: - log.error('Invalid signature %s %s', type(e), e) - raise exc.HTTPUnauthorized - acc_token = M.OAuthAccessToken( - consumer_token_id=consumer_token._id, - request_token_id=request_token._id, - user_id=request_token.user_id, - ) - return acc_token.to_string() + headers, body, status = self.server.create_access_token_response( + request.url, + http_method=request.method, + body=request.body, + headers=request.headers) + response.headers = headers + response.status_int = status + return body def rest_has_access(obj, user, perm): diff --git a/Allura/allura/model/oauth.py b/Allura/allura/model/oauth.py index 72f31f7e6..69778b434 100644 --- a/Allura/allura/model/oauth.py +++ b/Allura/allura/model/oauth.py @@ -19,8 +19,6 @@ import logging import typing from datetime import datetime - -import oauth2 as oauth from tg import tmpl_context as c, app_globals as g from paste.deploy.converters import aslist @@ -60,12 +58,6 @@ class OAuthToken(MappedClass): secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce) last_access = FieldProperty(datetime) - def to_string(self): - return oauth.Token(self.api_key, self.secret_key).to_string() - - def as_token(self): - return oauth.Token(self.api_key, self.secret_key) - class OAuthConsumerToken(OAuthToken): @@ -91,11 +83,6 @@ class OAuthConsumerToken(OAuthToken): def description_html(self): return g.markdown.cached_convert(self, 'description') - @property - def consumer(self): - '''OAuth compatible consumer object''' - return oauth.Consumer(self.api_key, self.secret_key) - @classmethod def upsert(cls, name, user): params = dict(name=name, user_id=user._id) @@ -130,7 +117,7 @@ class OAuthRequestToken(OAuthToken): callback = FieldProperty(str) validation_pin = FieldProperty(str) - consumer_token = RelationProperty('OAuthConsumerToken') + consumer_token: OAuthConsumerToken = RelationProperty('OAuthConsumerToken') class OAuthAccessToken(OAuthToken): @@ -162,3 +149,27 @@ class OAuthAccessToken(OAuthToken): if self.api_key in tokens: return True return False + + +def dummy_oauths(): + from allura.controllers.rest import Oauth1Validator + # oauthlib implementation NEEDS these "dummy" values. If a request comes in with an invalid param, it runs + # the regular oauth methods but using these dummy values, so that everything takes constant time + # so these need to exist in the database even though they're called "dummy" values + dummy_cons_tok = OAuthConsumerToken( + api_key=Oauth1Validator().dummy_client, + name='dummy client, for oauthlib implementation', + user_id=None, + ) + session(dummy_cons_tok).flush(dummy_cons_tok) + dummy_req_tok = OAuthRequestToken( + api_key=Oauth1Validator().dummy_request_token, + user_id=None, + validation_pin='dummy-pin', + ) + session(dummy_req_tok).flush(dummy_req_tok) + dummy_access_tok = OAuthAccessToken( + api_key=Oauth1Validator().dummy_access_token, + user_id=None, + ) + session(dummy_access_tok).flush(dummy_access_tok) \ No newline at end of file diff --git a/Allura/allura/scripts/create_oauth1_dummy_tokens.py b/Allura/allura/scripts/create_oauth1_dummy_tokens.py new file mode 100644 index 000000000..9b50aa13c --- /dev/null +++ b/Allura/allura/scripts/create_oauth1_dummy_tokens.py @@ -0,0 +1,37 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import argparse + +from allura.model.oauth import dummy_oauths +from allura.scripts import ScriptTask + + +class CreateOauth1DummyTokens(ScriptTask): + + @classmethod + def parser(cls): + return argparse.ArgumentParser(description="Create dummy oauth1 tokens needed by oauthlib implementation") + + @classmethod + def execute(cls, options): + dummy_oauths() + print('Done') + + +if __name__ == '__main__': + CreateOauth1DummyTokens.main() diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py index 92806fb59..4c4dddc14 100644 --- a/Allura/allura/tests/functional/test_auth.py +++ b/Allura/allura/tests/functional/test_auth.py @@ -28,8 +28,6 @@ from six.moves.urllib.parse import urlencode from bson import ObjectId import re -from testfixtures import LogCapture - from ming.orm.ormsession import ThreadLocalORMSession, session from tg import config, expose from mock import patch, Mock @@ -51,6 +49,7 @@ from allura.tests import decorators as td from allura.tests.decorators import audits, out_audits, assert_logmsg from alluratest.controller import setup_trove_categories, TestRestApiBase, oauth1_webtest from allura import model as M +from allura.model.oauth import dummy_oauths from allura.lib import plugin from allura.lib import helpers as h from allura.lib.multifactor import TotpService, RecoveryCodeService @@ -1897,15 +1896,22 @@ class TestOAuth(TestController): # now use the tokens & secrets to make a full OAuth request: oauth_token = atok['oauth_token'][0] oauth_secret = atok['oauth_token_secret'][0] - oaurl, oaparams, oahdrs = oauth1_webtest('/rest/p/test/', dict( + oaurl, oaparams, oahdrs, oaextraenv = oauth1_webtest('/rest/p/test/', dict( client_key='api_key_api_key_12345', client_secret='test-client-secret', resource_owner_key=oauth_token, resource_owner_secret=oauth_secret, signature_type='query' )) - self.app.get(oaurl, oaparams, oahdrs, status=200) - self.app.get(oaurl.replace('oauth_signature=', 'removed='), oaparams, oahdrs, status=401) + resp = self.app.get(oaurl, oaparams, oahdrs, oaextraenv, status=200) + for tool in resp.json['tools']: + if tool['name'] == 'admin': + break # good, found Admin + else: + raise AssertionError(f"No 'admin' tool in response, maybe authorizing as correct user failed. {resp.json}") + + # definitely bad request + self.app.get(oaurl.replace('oauth_signature=', 'removed='), oaparams, oahdrs, oaextraenv, status=401) def test_authorize_ok(self): user = M.User.by_username('test-admin') @@ -1926,7 +1932,8 @@ class TestOAuth(TestController): assert_in('api_key_reqtok_12345', r.text) def test_authorize_invalid(self): - self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'}, status=401) + resp = self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'}, status=400) + resp.mustcontain('error=invalid_client') def test_do_authorize_no(self): user = M.User.by_username('test-admin') @@ -2005,6 +2012,10 @@ class TestOAuthRequestToken(TestController): client_secret='test-client-secret', ) + def setUp(self): + super().setUp() + dummy_oauths() + def test_request_token_valid(self): user = M.User.by_username('test-user') consumer_token = M.OAuthConsumerToken( @@ -2014,24 +2025,21 @@ class TestOAuthRequestToken(TestController): ) ThreadLocalORMSession.flush_all() r = self.app.post(*oauth1_webtest('/rest/oauth/request_token', self.oauth_params, method='POST')) - + r.mustcontain('oauth_token=') + r.mustcontain('oauth_token_secret=') request_token = M.OAuthRequestToken.query.get(consumer_token_id=consumer_token._id) assert_is_not_none(request_token) - assert_equal(r.text, request_token.to_string()) def test_request_token_no_consumer_token_matching(self): - with LogCapture() as logs: - self.app.post(*oauth1_webtest('/rest/oauth/request_token', self.oauth_params), status=401) - assert_logmsg(logs, 'Invalid consumer token') + self.app.post(*oauth1_webtest('/rest/oauth/request_token', self.oauth_params), status=401) def test_request_token_no_consumer_token_given(self): oauth_params = self.oauth_params.copy() oauth_params['signature_type'] = 'query' # so we can more easily remove a param next - url, params, hdrs = oauth1_webtest('/rest/oauth/request_token', oauth_params) + url, params, hdrs, extraenv = oauth1_webtest('/rest/oauth/request_token', oauth_params) url = url.replace('oauth_consumer_key', 'gone') - with LogCapture() as logs: - self.app.post(url, params, hdrs, status=401) - assert_logmsg(logs, 'Invalid consumer token') + resp = self.app.post(url, params, hdrs, extraenv, status=400) + resp.mustcontain('error_description=Missing+mandatory+OAuth+parameters') def test_request_token_invalid(self): user = M.User.by_username('test-user') @@ -2041,10 +2049,8 @@ class TestOAuthRequestToken(TestController): secret_key='test-client-secret--INVALID', ) ThreadLocalORMSession.flush_all() - with LogCapture() as logs: - self.app.post(*oauth1_webtest('/rest/oauth/request_token', self.oauth_params, method='POST'), - status=401) - assert_logmsg(logs, "Invalid signature <class 'oauth2.Error'> Invalid signature.") + self.app.post(*oauth1_webtest('/rest/oauth/request_token', self.oauth_params, method='POST'), + status=401) class TestOAuthAccessToken(TestController): @@ -2057,10 +2063,12 @@ class TestOAuthAccessToken(TestController): verifier='good_verifier_123456', ) + def setUp(self): + super().setUp() + dummy_oauths() + def test_access_token_no_consumer(self): - with LogCapture() as logs: - self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401) - assert_logmsg(logs, 'Invalid consumer token') + self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401) def test_access_token_no_request(self): user = M.User.by_username('test-admin') @@ -2070,9 +2078,7 @@ class TestOAuthAccessToken(TestController): description='ctok_desc', ) ThreadLocalORMSession.flush_all() - with LogCapture() as logs: - self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401) - assert_logmsg(logs, 'Invalid request token') + self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401) def test_access_token_bad_pin(self): user = M.User.by_username('test-admin') @@ -2089,12 +2095,10 @@ class TestOAuthAccessToken(TestController): validation_pin='good_verifier_123456', ) ThreadLocalORMSession.flush_all() - with LogCapture() as logs: - oauth_params = self.oauth_params.copy() - oauth_params['verifier'] = 'bad_verifier_1234567' - self.app.get(*oauth1_webtest('/rest/oauth/access_token', oauth_params), - status=401) - assert_logmsg(logs, 'Invalid verifier') + oauth_params = self.oauth_params.copy() + oauth_params['verifier'] = 'bad_verifier_1234567' + self.app.get(*oauth1_webtest('/rest/oauth/access_token', oauth_params), + status=401) def test_access_token_bad_sig(self): user = M.User.by_username('test-admin') @@ -2113,11 +2117,9 @@ class TestOAuthAccessToken(TestController): secret_key='test-token-secret--INVALID', ) ThreadLocalORMSession.flush_all() - with LogCapture() as logs: - self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401) - assert_logmsg(logs, "Invalid signature <class 'oauth2.Error'> Invalid signature.") + self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401) - def test_access_token_ok(self): + def test_access_token_ok(self, signature_type='auth_header'): user = M.User.by_username('test-admin') ctok = M.OAuthConsumerToken( api_key='api_key_api_key_12345', @@ -2125,7 +2127,7 @@ class TestOAuthAccessToken(TestController): user_id=user._id, description='ctok_desc', ) - M.OAuthRequestToken( + req_tok = M.OAuthRequestToken( api_key='api_key_reqtok_12345', secret_key='test-token-secret', consumer_token_id=ctok._id, @@ -2135,16 +2137,14 @@ class TestOAuthAccessToken(TestController): ) ThreadLocalORMSession.flush_all() + oauth_params = dict(self.oauth_params, signature_type=signature_type) r = self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params)) atok = parse_qs(r.text) assert_equal(len(atok['oauth_token']), 1) assert_equal(len(atok['oauth_token_secret']), 1) - oauth_params = dict(self.oauth_params, signature_type='query') - r = self.app.get(*oauth1_webtest('/rest/oauth/access_token', oauth_params)) - atok = parse_qs(r.text) - assert_equal(len(atok['oauth_token']), 1) - assert_equal(len(atok['oauth_token_secret']), 1) + def test_access_token_ok_by_query(self): + self.test_access_token_ok(signature_type='query') class TestDisableAccount(TestController): diff --git a/Allura/allura/websetup/bootstrap.py b/Allura/allura/websetup/bootstrap.py index d7053f38b..6f6d37664 100644 --- a/Allura/allura/websetup/bootstrap.py +++ b/Allura/allura/websetup/bootstrap.py @@ -27,6 +27,7 @@ from tg import tmpl_context as c, app_globals as g from paste.deploy.converters import asbool import ew +from allura.model.oauth import dummy_oauths from ming import Session, mim from ming.orm import state, session from ming.orm.ormsession import ThreadLocalORMSession @@ -266,6 +267,10 @@ def bootstrap(command, conf, vars): with h.push_config(c, user=u_admin): sub.install_app('wiki') + if not test_run: + # only when running setup-app do we need this. the few tests that need it do it themselves + dummy_oauths() + ThreadLocalORMSession.flush_all() ThreadLocalORMSession.close_all() diff --git a/AlluraTest/alluratest/controller.py b/AlluraTest/alluratest/controller.py index e1e73082e..1299a2bc2 100644 --- a/AlluraTest/alluratest/controller.py +++ b/AlluraTest/alluratest/controller.py @@ -289,11 +289,13 @@ class TestRestApiBase(TestController): return self._api_call('DELETE', path, wrap_args, user, status, **params) -def oauth1_webtest(url: str, oauth_kwargs: dict, method='GET') -> tuple[str, dict, dict]: +def oauth1_webtest(url: str, oauth_kwargs: dict, method='GET') -> tuple[str, dict, dict, dict]: oauth1 = requests_oauthlib.OAuth1(**oauth_kwargs) req = requests.Request(method, f'http://localhost{url}').prepare() oauth1(req) - return request2webtest(req) + url, params, headers = request2webtest(req) + extra_environ = {'username': '*anonymous'} # we don't want to be magically logged in when hitting /rest/oauth/ + return url, params, headers, extra_environ def request2webtest(req: requests.PreparedRequest) -> tuple[str, dict, dict]: diff --git a/AlluraTest/alluratest/validation.py b/AlluraTest/alluratest/validation.py index df838e968..37cc972dc 100644 --- a/AlluraTest/alluratest/validation.py +++ b/AlluraTest/alluratest/validation.py @@ -321,7 +321,7 @@ class ValidatingTestApp(PostParamCheckingTestApp): import feedparser d = feedparser.parse(resp.text) assert d.bozo == 0, 'Non-wellformed feed' - elif content_type.startswith('image/'): + elif content_type.startswith(('image/', 'application/x-www-form-urlencoded')): pass else: assert False, 'Unexpected output content type: ' + content_type diff --git a/requirements.in b/requirements.in index 4bfbabcd0..2d938e146 100644 --- a/requirements.in +++ b/requirements.in @@ -18,10 +18,7 @@ Markdown markdown-checklist MarkupSafe!=2.1.1 Ming -# TODO: move to "oauthlib" instead -# oauth2 doesn't have a release with py3.6 support, but does have fixes on master: -# archive/.../.zip URL is preferable over git+https://... since it supports pip hash generating+checking -https://github.com/joestump/python-oauth2/archive/b94f69b1ad195513547924e380d9265133e995fa.zip#egg=oauth2 +oauthlib paginate Paste PasteDeploy diff --git a/requirements.txt b/requirements.txt index 1b49cf600..1c47ee109 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,8 +61,6 @@ html5lib==1.1 # -r requirements.in # pypeline # textile -httplib2==0.19.0 - # via oauth2 idna==3.3 # via requests importlib-metadata==4.12.0 @@ -91,10 +89,10 @@ ming==0.12.0 # via -r requirements.in mock==4.0.3 # via -r requirements.in -oauth2 @ https://github.com/joestump/python-oauth2/archive/b94f69b1ad195513547924e380d9265133e995fa.zip - # via -r requirements.in oauthlib==3.2.0 - # via requests-oauthlib + # via + # -r requirements.in + # requests-oauthlib paginate==0.5.6 # via -r requirements.in paste==3.5.1 @@ -123,8 +121,6 @@ pymongo==3.11.4 # -r requirements.in # activitystream # ming -pyparsing==2.4.7 - # via httplib2 pypeline[creole,markdown,rst,textile]==0.6.0 # via -r requirements.in pysolr==3.9.0