This is an automated email from the ASF dual-hosted git repository.

kentontaylor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 8830d2b64b99c8b47b56e164fbff2ee2565e1f37
Author: Dave Brondsema <[email protected]>
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                                   | 227 ----------------
 9 files changed, 303 insertions(+), 389 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
deleted file mode 100644
index e6e411352..000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,227 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.7
-# To update, run:
-#
-#    pip-compile
-#
-activitystream==0.4.0
-    # via -r requirements.in
-beaker==1.11.0
-    # via -r requirements.in
-beautifulsoup4==4.11.1
-    # via
-    #   -r requirements.in
-    #   webtest
-bleach[css]==5.0.1
-    # via pypeline
-cchardet==2.1.7
-    # via -r requirements.in
-certifi==2021.10.8
-    # via requests
-cffi==1.15.1
-    # via cryptography
-charset-normalizer==2.0.12
-    # via requests
-colander==1.8.3
-    # via -r requirements.in
-crank==0.8.1
-    # via turbogears2
-creoleparser==0.7.5
-    # via pypeline
-cryptography==37.0.4
-    # via -r requirements.in
-datadiff==2.0.0
-    # via -r requirements.in
-decorator==5.1.1
-    # via -r requirements.in
-docutils==0.18.1
-    # via pypeline
-easywidgets==0.4.1
-    # via -r requirements.in
-emoji==1.7.0
-    # via -r requirements.in
-feedgenerator==2.0.0
-    # via -r requirements.in
-feedparser==6.0.10
-    # via -r requirements.in
-formencode==2.0.1
-    # via
-    #   -r requirements.in
-    #   easywidgets
-genshi==0.7.7
-    # via creoleparser
-gitdb==4.0.9
-    # via gitpython
-gitpython==3.1.27
-    # via -r requirements.in
-gunicorn==20.1.0
-    # via -r requirements.in
-html5lib==1.1
-    # via
-    #   -r requirements.in
-    #   pypeline
-    #   textile
-httplib2==0.19.0
-    # via oauth2
-idna==3.3
-    # via requests
-importlib-metadata==4.12.0
-    # via markdown
-inflection==0.5.1
-    # via profanityfilter
-iso8601==1.0.2
-    # via colander
-jinja2==3.1.2
-    # via -r requirements.in
-markdown==3.3.7
-    # via
-    #   -r requirements.in
-    #   markdown-checklist
-    #   pypeline
-markdown-checklist==0.4.3
-    # via -r requirements.in
-markupsafe==2.1.0
-    # via
-    #   -r requirements.in
-    #   easywidgets
-    #   jinja2
-    #   turbogears2
-    #   webhelpers2
-ming==0.12.1
-    # 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.1
-    # via requests-oauthlib
-paginate==0.5.6
-    # via -r requirements.in
-paste==3.5.1
-    # via
-    #   -r requirements.in
-    #   easywidgets
-    #   pastescript
-pastedeploy==2.1.1
-    # via
-    #   -r requirements.in
-    #   pastescript
-pastescript==3.2.1
-    # via -r requirements.in
-pillow==9.2.0
-    # via -r requirements.in
-profanityfilter==2.0.6
-    # via -r requirements.in
-pycparser==2.21
-    # via cffi
-pyflakes==2.4.0
-    # via -r requirements.in
-pygments==2.12.0
-    # via -r requirements.in
-pymongo==3.11.4
-    # via
-    #   -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
-    # via -r requirements.in
-python-dateutil==2.8.2
-    # via
-    #   -r requirements.in
-    #   easywidgets
-python-magic==0.4.27
-    # via -r requirements.in
-python-oembed==0.2.4
-    # via -r requirements.in
-pytz==2022.1
-    # via
-    #   -r requirements.in
-    #   feedgenerator
-    #   ming
-qrcode==7.3.1
-    # via -r requirements.in
-regex==2022.6.2
-    # via
-    #   regex-as-re-globally
-    #   textile
-regex-as-re-globally==0.0.2
-    # via -r requirements.in
-repoze-lru==0.7
-    # via turbogears2
-requests==2.27.1
-    # via
-    #   -r requirements.in
-    #   pysolr
-    #   requests-oauthlib
-requests-oauthlib==1.3.1
-    # via -r requirements.in
-setproctitle==1.2.3
-    # via -r requirements.in
-sgmllib3k==1.0.0
-    # via feedparser
-six==1.16.0
-    # via
-    #   -r requirements.in
-    #   bleach
-    #   creoleparser
-    #   easywidgets
-    #   formencode
-    #   genshi
-    #   html5lib
-    #   paste
-    #   pastescript
-    #   python-dateutil
-    #   webhelpers2
-smmap==5.0.0
-    # via gitdb
-soupsieve==2.3.2.post1
-    # via beautifulsoup4
-testfixtures==6.18.5
-    # via -r requirements.in
-textile==4.0.2
-    # via pypeline
-timermiddleware==0.6.2
-    # via -r requirements.in
-tinycss2==1.1.1
-    # via bleach
-translationstring==1.4
-    # via colander
-turbogears2==2.3.12
-    # via -r requirements.in
-typing-extensions==4.3.0
-    # via
-    #   gitpython
-    #   importlib-metadata
-urllib3==1.26.9
-    # via requests
-waitress==2.1.2
-    # via webtest
-webencodings==0.5.1
-    # via
-    #   bleach
-    #   html5lib
-    #   tinycss2
-webhelpers2==2.0
-    # via -r requirements.in
-webob==1.7.4
-    # via
-    #   -r requirements.in
-    #   easywidgets
-    #   timermiddleware
-    #   turbogears2
-    #   webtest
-webtest==3.0.0
-    # via -r requirements.in
-werkzeug==2.1.2
-    # via -r requirements.in
-wrapt==1.14.1
-    # via -r requirements.in
-zipp==3.8.0
-    # via importlib-metadata
-
-# The following packages are considered to be unsafe in a requirements file:
-# setuptools


Reply via email to