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

Reply via email to