You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by ke...@apache.org on 2022/09/22 15:02:18 UTC

[allura] branch master updated (b3df09680 -> 7a766a53b)

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

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


    from b3df09680 Fix phone.attempts_limit check, if user had surpassed it already
     new 5e39d9041 [#8461] convert oauth tests to not mock the oauth library, use requests_oauthlib as a helper to build requests instead
     new 107ba37d0 [#8461] update oauth lib in docs & wiki-copy.py examples
     new d5d83fdce [#8461] distinguish "api_key" used for consumer tokens vs request tokens, in tests
     new ef6326b7e [#8461] update test values (to be ok with oauthlib validations)
     new 97bba1dd0 [#8461] index (unique) on OAuthConsumerToken.api_key
     new 8830d2b64 [#8461] switch from python-oauth2 to oauthlib
     new 7a766a53b [#8461] include oauth_callback in our example clients, to match spec

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Allura/allura/controllers/rest.py                  | 289 ++++++++++------
 Allura/allura/model/oauth.py                       |  44 ++-
 .../allura/scripts/create_oauth1_dummy_tokens.py   |  24 +-
 Allura/allura/tests/decorators.py                  |  19 +-
 Allura/allura/tests/functional/test_auth.py        | 362 ++++++++++-----------
 Allura/allura/websetup/bootstrap.py                |   5 +
 Allura/docs/api-rest/docs.md                       |  52 +--
 AlluraTest/alluratest/controller.py                |  20 ++
 AlluraTest/alluratest/validation.py                |   2 +-
 requirements.in                                    |   5 +-
 requirements.txt                                   |  10 +-
 scripts/wiki-copy.py                               |  74 ++---
 12 files changed, 499 insertions(+), 407 deletions(-)
 copy scripts/migrations/026-install-activity-tool.py => Allura/allura/scripts/create_oauth1_dummy_tokens.py (68%)


[allura] 06/07: [#8461] switch from python-oauth2 to oauthlib

Posted by ke...@apache.org.
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 <db...@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                                   | 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


[allura] 05/07: [#8461] index (unique) on OAuthConsumerToken.api_key

Posted by ke...@apache.org.
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 97bba1dd0855b991a3aaba1e200a4bc261d64ce7
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Thu Sep 8 11:31:44 2022 -0400

    [#8461] index (unique) on OAuthConsumerToken.api_key
---
 Allura/allura/model/oauth.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/Allura/allura/model/oauth.py b/Allura/allura/model/oauth.py
index a5e390ff0..72f31f7e6 100644
--- a/Allura/allura/model/oauth.py
+++ b/Allura/allura/model/oauth.py
@@ -72,7 +72,10 @@ class OAuthConsumerToken(OAuthToken):
     class __mongometa__:
         polymorphic_identity = 'consumer'
         name = 'oauth_consumer_token'
-        unique_indexes = [('name', 'user_id')]
+        unique_indexes = [
+            ('name', 'user_id'),
+            ('api_key',),
+        ]
 
     query: 'Query[OAuthConsumerToken]'
 


[allura] 01/07: [#8461] convert oauth tests to not mock the oauth library, use requests_oauthlib as a helper to build requests instead

Posted by ke...@apache.org.
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 5e39d9041a168c1af576b0160ab948a812f2fe67
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Wed Sep 7 12:45:03 2022 -0400

    [#8461] convert oauth tests to not mock the oauth library, use requests_oauthlib as a helper to build requests instead
---
 Allura/allura/tests/decorators.py           |  19 +-
 Allura/allura/tests/functional/test_auth.py | 296 ++++++++++++++--------------
 AlluraTest/alluratest/controller.py         |  18 ++
 3 files changed, 180 insertions(+), 153 deletions(-)

diff --git a/Allura/allura/tests/decorators.py b/Allura/allura/tests/decorators.py
index 6c561c228..34854ad0f 100644
--- a/Allura/allura/tests/decorators.py
+++ b/Allura/allura/tests/decorators.py
@@ -209,18 +209,29 @@ def out_audits(*messages, **kwargs):
 
 
 # not a decorator but use it with LogCapture() context manager
-def assert_logmsg_and_no_warnings_or_errors(logs, msg):
+def assert_logmsg(logs, msg, maxlevel=logging.CRITICAL+1):
     """
+    can also use logs.check() or logs.check_present()
     :param testfixtures.logcapture.LogCapture logs: LogCapture() instance
-    :param str msg: Message to look for
+    :param str msg: Message substring to look for
     """
     found_msg = False
     for r in logs.records:
         if msg in r.getMessage():
             found_msg = True
-        if r.levelno > logging.INFO:
+        if r.levelno > maxlevel:
             raise AssertionError(f'unexpected log {r.levelname} {r.getMessage()}')
-    assert found_msg, 'Did not find {} in logs: {}'.format(msg, '\n'.join([r.getMessage() for r in logs.records]))
+    assert found_msg, \
+        'Did not find "{}" in these logs: {}'.format(msg, '\n'.join([r.getMessage() for r in logs.records]))
+
+
+def assert_logmsg_and_no_warnings_or_errors(logs, msg):
+    """
+    can also use logs.check() or logs.check_present()
+    :param testfixtures.logcapture.LogCapture logs: LogCapture() instance
+    :param str msg: Message substring to look for
+    """
+    return assert_logmsg(logs, msg, maxlevel=logging.INFO)
 
 
 def assert_equivalent_urls(url1, url2):
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index c6b8fae11..1d15220d5 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -14,17 +14,22 @@
 #       KIND, either express or implied.  See the License for the
 #       specific language governing permissions and limitations
 #       under the License.
+from __future__ import annotations
 
 import calendar
 from base64 import b32encode
 from datetime import datetime, time, timedelta
 from time import time as time_time
 import json
+
 from six.moves.urllib.parse import urlparse, parse_qs
 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
@@ -40,17 +45,15 @@ from alluratest.tools import (
     assert_false,
 )
 from tg import tmpl_context as c, app_globals as g
-import oauth2
 
 from allura.tests import TestController
 from allura.tests import decorators as td
-from allura.tests.decorators import audits, out_audits
-from alluratest.controller import setup_trove_categories, TestRestApiBase
+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.lib import plugin
 from allura.lib import helpers as h
 from allura.lib.multifactor import TotpService, RecoveryCodeService
-import six
 
 
 def unentity(s):
@@ -1854,109 +1857,55 @@ class TestOAuth(TestController):
             M.OAuthAccessToken.for_user(M.User.by_username('test-admin')), [])
 
     def test_interactive(self):
-        with mock.patch('allura.controllers.rest.oauth.Server') as Server, \
-                mock.patch('allura.controllers.rest.oauth.Request') as Request:   # these are the oauth2 libs
-            user = M.User.by_username('test-admin')
-            M.OAuthConsumerToken(
-                api_key='api_key',
-                user_id=user._id,
-                description='ctok_desc',
-            )
-            ThreadLocalORMSession.flush_all()
-            Request.from_request.return_value = {
-                'oauth_consumer_key': 'api_key',
-                'oauth_callback': 'http://my.domain.com/callback',
-            }
-            r = self.app.post('/rest/oauth/request_token', params={})
-            rtok = parse_qs(r.text)['oauth_token'][0]
-            r = self.app.post('/rest/oauth/authorize',
-                              params={'oauth_token': rtok})
-            r = r.forms[0].submit('yes')
-            assert r.location.startswith('http://my.domain.com/callback')
-            pin = parse_qs(urlparse(r.location).query)['oauth_verifier'][0]
-            Request.from_request.return_value = {
-                'oauth_consumer_key': 'api_key',
-                'oauth_token': rtok,
-                'oauth_verifier': pin,
-            }
-            r = self.app.get('/rest/oauth/access_token')
-            atok = parse_qs(r.text)
-            assert_equal(len(atok['oauth_token']), 1)
-            assert_equal(len(atok['oauth_token_secret']), 1)
-
-        # now use the tokens & secrets to make a full OAuth request:
-        oauth_secret = atok['oauth_token_secret'][0]
-        oauth_token = atok['oauth_token'][0]
-        consumer = oauth2.Consumer('api_key', oauth_secret)
-        M.OAuthConsumerToken.consumer = consumer
-        access_token = oauth2.Token(oauth_token, oauth_secret)
-        oauth_client = oauth2.Client(consumer, access_token)
-        # use the oauth2 lib, but intercept the request and then send it to self.app.get
-        with mock.patch('oauth2.httplib2.Http.request', name='hl2req') as oa2_req:
-            oauth_client.request('http://localhost/rest/p/test/', 'GET')
-            oa2url = oa2_req.call_args[0][1]
-            oa2url = oa2url.replace('http://localhost', '')
-            # print(oa2url)
-            oa2kwargs = oa2_req.call_args[1]
-        self.app.get(oa2url, headers=oa2kwargs['headers'], status=200)
-        self.app.get(oa2url.replace('oauth_signature=', 'removed='), headers=oa2kwargs['headers'], status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Server')
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_request_token_valid(self, Request, Server):
-        M.OAuthConsumerToken.consumer = mock.Mock()
-        user = M.User.by_username('test-user')
-        consumer_token = M.OAuthConsumerToken(
-            api_key='api_key',
-            user_id=user._id,
-        )
-        ThreadLocalORMSession.flush_all()
-        req = Request.from_request.return_value = {'oauth_consumer_key': 'api_key'}
-        r = self.app.post('/rest/oauth/request_token', params={'key': 'value'})
-
-        # dict-ify webob.EnvironHeaders
-        call = Request.from_request.call_args_list[0]
-        call[1]['headers'] = dict(call[1]['headers'])
-        # then check equality
-        assert_equal(Request.from_request.call_args_list, [
-            mock.call('POST', 'http://localhost/rest/oauth/request_token',
-                      headers={'Host': 'localhost:80',
-                               'Content-Type': 'application/x-www-form-urlencoded',
-                               'Content-Length': '9'},
-                      parameters={'key': 'value'},
-                      query_string='')
-        ])
-        Server().verify_request.assert_called_once_with(req, consumer_token.consumer, None)
-        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())
-
-    @mock.patch('allura.controllers.rest.oauth.Server')
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_request_token_no_consumer_token_matching(self, Request, Server):
-        Request.from_request.return_value = {'oauth_consumer_key': 'api_key'}
-        self.app.post('/rest/oauth/request_token',
-                      params={'key': 'value'}, status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Server')
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_request_token_no_consumer_token_given(self, Request, Server):
-        Request.from_request.return_value = {}
-        self.app.post('/rest/oauth/request_token', params={'key': 'value'}, status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Server')
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_request_token_invalid(self, Request, Server):
-        Server().verify_request.side_effect = oauth2.Error('test_request_token_invalid')
-        M.OAuthConsumerToken.consumer = mock.Mock()
-        user = M.User.by_username('test-user')
+        user = M.User.by_username('test-admin')
         M.OAuthConsumerToken(
             api_key='api_key',
+            secret_key='dummy-client-secret',
             user_id=user._id,
+            description='ctok_desc',
         )
         ThreadLocalORMSession.flush_all()
-        Request.from_request.return_value = {'oauth_consumer_key': 'api_key'}
-        self.app.post('/rest/oauth/request_token', params={'key': 'value'}, status=401)
+        oauth_params = dict(
+            client_key='api_key',
+            client_secret='dummy-client-secret',
+            callback_uri='http://my.domain.com/callback',
+        )
+        r = self.app.post(*oauth1_webtest('/rest/oauth/request_token', oauth_params, method='POST'))
+        rtok = parse_qs(r.text)['oauth_token'][0]
+        rsecr = parse_qs(r.text)['oauth_token_secret'][0]
+        assert rtok
+        assert rsecr
+        r = self.app.post('/rest/oauth/authorize',
+                          params={'oauth_token': rtok})
+        r = r.forms[0].submit('yes')
+        assert r.location.startswith('http://my.domain.com/callback')
+        pin = parse_qs(urlparse(r.location).query)['oauth_verifier'][0]
+        assert pin
+
+        oauth_params = dict(
+            client_key='api_key',
+            client_secret='dummy-client-secret',
+            resource_owner_key=rtok,
+            resource_owner_secret=rsecr,
+            verifier=pin,
+        )
+        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)
+
+        # 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(
+            client_key='api_key',
+            client_secret='dummy-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)
 
     def test_authorize_ok(self):
         user = M.User.by_username('test-admin')
@@ -2048,22 +1997,72 @@ class TestOAuth(TestController):
         r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key'})
         assert r.location.startswith('http://my.domain.com/callback?myparam=foo&oauth_token=api_key&oauth_verifier=')
 
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_access_token_no_consumer(self, Request):
-        Request.from_request.return_value = {
-            'oauth_consumer_key': 'api_key',
-            'oauth_token': 'api_key',
-            'oauth_verifier': 'good',
-        }
-        self.app.get('/rest/oauth/access_token', status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_access_token_no_request(self, Request):
-        Request.from_request.return_value = {
-            'oauth_consumer_key': 'api_key',
-            'oauth_token': 'api_key',
-            'oauth_verifier': 'good',
-        }
+
+class TestOAuthRequestToken(TestController):
+
+    oauth_params = dict(
+        client_key='api_key',
+        client_secret='dummy-client-secret',
+    )
+
+    def test_request_token_valid(self):
+        user = M.User.by_username('test-user')
+        consumer_token = M.OAuthConsumerToken(
+            api_key='api_key',
+            secret_key='dummy-client-secret',
+            user_id=user._id,
+        )
+        ThreadLocalORMSession.flush_all()
+        r = self.app.post(*oauth1_webtest('/rest/oauth/request_token', self.oauth_params, method='POST'))
+
+        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')
+
+    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 = url.replace('oauth_consumer_key', 'gone')
+        with LogCapture() as logs:
+            self.app.post(url, params, hdrs, status=401)
+        assert_logmsg(logs, 'Invalid consumer token')
+
+    def test_request_token_invalid(self):
+        user = M.User.by_username('test-user')
+        M.OAuthConsumerToken(
+            api_key='api_key',
+            user_id=user._id,
+            secret_key='dummy-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.")
+
+
+class TestOAuthAccessToken(TestController):
+
+    oauth_params = dict(
+        client_key='api_key',
+        client_secret='dummy-client-secret',
+        resource_owner_key='api_key',
+        resource_owner_secret='dummy-token-secret',
+        verifier='good',
+    )
+
+    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')
+
+    def test_access_token_no_request(self):
         user = M.User.by_username('test-admin')
         M.OAuthConsumerToken(
             api_key='api_key',
@@ -2071,15 +2070,11 @@ class TestOAuth(TestController):
             description='ctok_desc',
         )
         ThreadLocalORMSession.flush_all()
-        self.app.get('/rest/oauth/access_token', status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_access_token_bad_pin(self, Request):
-        Request.from_request.return_value = {
-            'oauth_consumer_key': 'api_key',
-            'oauth_token': 'api_key',
-            'oauth_verifier': 'bad',
-        }
+        with LogCapture() as logs:
+            self.app.get(*oauth1_webtest('/rest/oauth/access_token', self.oauth_params), status=401)
+        assert_logmsg(logs, 'Invalid request token')
+
+    def test_access_token_bad_pin(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
             api_key='api_key',
@@ -2094,21 +2089,20 @@ class TestOAuth(TestController):
             validation_pin='good',
         )
         ThreadLocalORMSession.flush_all()
-        self.app.get('/rest/oauth/access_token', status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Server')
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_access_token_bad_sig(self, Request, Server):
-        Request.from_request.return_value = {
-            'oauth_consumer_key': 'api_key',
-            'oauth_token': 'api_key',
-            'oauth_verifier': 'good',
-        }
+        with LogCapture() as logs:
+            oauth_params = self.oauth_params.copy()
+            oauth_params['verifier'] = 'bad'
+            self.app.get(*oauth1_webtest('/rest/oauth/access_token', oauth_params),
+                         status=401)
+        assert_logmsg(logs, 'Invalid verifier')
+
+    def test_access_token_bad_sig(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
             api_key='api_key',
             user_id=user._id,
             description='ctok_desc',
+            secret_key='dummy-client-secret',
         )
         M.OAuthRequestToken(
             api_key='api_key',
@@ -2116,34 +2110,38 @@ class TestOAuth(TestController):
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
             validation_pin='good',
+            secret_key='dummy-token-secret--INVALID',
         )
         ThreadLocalORMSession.flush_all()
-        Server().verify_request.side_effect = oauth2.Error('test_access_token_bad_sig')
-        self.app.get('/rest/oauth/access_token', status=401)
-
-    @mock.patch('allura.controllers.rest.oauth.Server')
-    @mock.patch('allura.controllers.rest.oauth.Request')
-    def test_access_token_ok(self, Request, Server):
-        Request.from_request.return_value = {
-            'oauth_consumer_key': 'api_key',
-            'oauth_token': 'api_key',
-            'oauth_verifier': 'good',
-        }
+        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.")
+
+    def test_access_token_ok(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
             api_key='api_key',
+            secret_key='dummy-client-secret',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
             api_key='api_key',
+            secret_key='dummy-token-secret',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
             validation_pin='good',
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.get('/rest/oauth/access_token')
+
+        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)
diff --git a/AlluraTest/alluratest/controller.py b/AlluraTest/alluratest/controller.py
index 4c7c01bc4..e1e73082e 100644
--- a/AlluraTest/alluratest/controller.py
+++ b/AlluraTest/alluratest/controller.py
@@ -16,6 +16,8 @@
 #       under the License.
 
 """Unit and functional test suite for allura."""
+from __future__ import annotations
+
 import os
 import six.moves.urllib.request
 import six.moves.urllib.parse
@@ -37,6 +39,8 @@ import ew
 from ming.orm import ThreadLocalORMSession
 import ming.orm
 import pkg_resources
+import requests
+import requests_oauthlib
 
 from allura import model as M
 from allura.command import CreateTroveCategoriesCommand
@@ -283,3 +287,17 @@ class TestRestApiBase(TestController):
 
     def api_delete(self, path, wrap_args=None, user='test-admin', status=None, **params):
         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]:
+    oauth1 = requests_oauthlib.OAuth1(**oauth_kwargs)
+    req = requests.Request(method, f'http://localhost{url}').prepare()
+    oauth1(req)
+    return request2webtest(req)
+
+
+def request2webtest(req: requests.PreparedRequest) -> tuple[str, dict, dict]:
+    url = req.url
+    params = {}
+    headers = {k: v.decode() for k,v in req.headers.items()}
+    return url, params, headers


[allura] 07/07: [#8461] include oauth_callback in our example clients, to match spec

Posted by ke...@apache.org.
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 7a766a53bf2debc4cb94701fa18fc05d7b17c738
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Tue Sep 13 18:25:42 2022 -0400

    [#8461] include oauth_callback in our example clients, to match spec
---
 Allura/docs/api-rest/docs.md |   2 +-
 requirements.txt             | 223 +++++++++++++++++++++++++++++++++++++++++++
 scripts/wiki-copy.py         |   2 +-
 3 files changed, 225 insertions(+), 2 deletions(-)

diff --git a/Allura/docs/api-rest/docs.md b/Allura/docs/api-rest/docs.md
index 425ae8912..886514719 100755
--- a/Allura/docs/api-rest/docs.md
+++ b/Allura/docs/api-rest/docs.md
@@ -93,7 +93,7 @@ If you want your application to be able to use the API on behalf of another user
     AUTHORIZE_URL = 'https://forge-allura.apache.org/rest/oauth/authorize'
     ACCESS_TOKEN_URL = 'https://forge-allura.apache.org/rest/oauth/access_token'
     
-    oauth = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET)
+    oauth = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET, callback_uri='oob')
     
     # Step 1: Get a request token. This is a temporary token that is used for 
     # having the user authorize an access token and to sign the request to obtain 
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 000000000..840326b53
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,223 @@
+#
+# 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
+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
+oauthlib==3.2.1
+    # via
+    #   -r requirements.in
+    #   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
+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
diff --git a/scripts/wiki-copy.py b/scripts/wiki-copy.py
index fc6553761..ddb3187d3 100644
--- a/scripts/wiki-copy.py
+++ b/scripts/wiki-copy.py
@@ -81,7 +81,7 @@ def make_oauth_client(base_url) -> requests.Session:
         oauth_token = cp.get(base_url, 'oauth_token')
         oauth_token_secret = cp.get(base_url, 'oauth_token_secret')
     except NoOptionError:
-        oauthSess = OAuth1Session(oauth_key, client_secret=oauth_secret)
+        oauthSess = OAuth1Session(oauth_key, client_secret=oauth_secret, callback_uri='oob')
         request_token = oauthSess.fetch_request_token(REQUEST_TOKEN_URL)
         pin_url = oauthSess.authorization_url(AUTHORIZE_URL, request_token['oauth_token'])
         if isinstance(webbrowser.get(), webbrowser.GenericBrowser):


[allura] 02/07: [#8461] update oauth lib in docs & wiki-copy.py examples

Posted by ke...@apache.org.
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 107ba37d0cc57e177ec01d9c3d7a712ddaf98765
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Wed Sep 7 15:41:07 2022 -0400

    [#8461] update oauth lib in docs & wiki-copy.py examples
---
 Allura/docs/api-rest/docs.md | 52 ++++++++++---------------------
 scripts/wiki-copy.py         | 74 +++++++++++++++++---------------------------
 2 files changed, 44 insertions(+), 82 deletions(-)

diff --git a/Allura/docs/api-rest/docs.md b/Allura/docs/api-rest/docs.md
index fb2b605d4..425ae8912 100755
--- a/Allura/docs/api-rest/docs.md
+++ b/Allura/docs/api-rest/docs.md
@@ -82,32 +82,24 @@ Python code example to create a new ticket:
 ### OAuth 1.0 Application Authorization (Third-Party Apps)
 
 
-If you want your application to be able to use the API on behalf of another user, that user must authorize your application to act on their behalf.  This is usually accomplished by obtaining a request token and directing the user authorize the request.  The following is an example of how one would authorize an application in Python using the python-oauth2 library.  First, run `pip install oauth2` and `pip install certifi`.
+If you want your application to be able to use the API on behalf of another user, that user must authorize your application to act on their behalf.  This is usually accomplished by obtaining a request token and directing the user authorize the request.  The following is an example of how one would authorize an application in Python using the requests_oauthlib library.  First, run `pip install requests_oauthlib`
 
-    import oauth2 as oauth  # misleading package name, oauth2 implements OAuth 1.0 spec
-    import certifi
-    from urllib.parse import parse_qs, parse_qsl, urlencode
+    from requests_oauthlib import OAuth1Session
     import webbrowser
 
     CONSUMER_KEY = '<consumer key from registration>'
     CONSUMER_SECRET = '<consumer secret from registration>'
-    REQUEST_TOKEN_URL = 'https://sourceforge.net/rest/oauth/request_token'
-    AUTHORIZE_URL = 'https://sourceforge.net/rest/oauth/authorize'
-    ACCESS_TOKEN_URL = 'https://sourceforge.net/rest/oauth/access_token'
+    REQUEST_TOKEN_URL = 'https://forge-allura.apache.org/rest/oauth/request_token'
+    AUTHORIZE_URL = 'https://forge-allura.apache.org/rest/oauth/authorize'
+    ACCESS_TOKEN_URL = 'https://forge-allura.apache.org/rest/oauth/access_token'
     
-    consumer = oauth.Consumer(CONSUMER_KEY, CONSUMER_SECRET)
-    client = oauth.Client(consumer)
-    client.ca_certs = certifi.where()
+    oauth = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET)
     
     # Step 1: Get a request token. This is a temporary token that is used for 
     # having the user authorize an access token and to sign the request to obtain 
     # said access token.
     
-    resp, content = client.request(REQUEST_TOKEN_URL, 'GET')
-    if resp['status'] != '200':
-        raise Exception("Invalid response %s." % resp['status'])
-    
-    request_token = dict(parse_qsl(content.decode('utf-8')))
+    request_token = oauth.fetch_request_token(REQUEST_TOKEN_URL)
     
     # these are intermediate tokens and not needed later
     # print("Request Token:")
@@ -119,7 +111,7 @@ If you want your application to be able to use the API on behalf of another user
     # redirect. In a web application you would redirect the user to the URL
     # below, specifying the additional parameter oauth_callback=<your callback URL>.
     
-    webbrowser.open("%s?oauth_token=%s" % (AUTHORIZE_URL, request_token['oauth_token']))
+    webbrowser.open(oauth.authorization_url(AUTHORIZE_URL, request_token['oauth_token']))
     
     # Since we didn't specify a callback, the user must now enter the PIN displayed in 
     # their browser.  If you had specified a callback URL, it would have been called with 
@@ -131,13 +123,7 @@ If you want your application to be able to use the API on behalf of another user
     # request token to sign this request. After this is done you throw away the
     # request token and use the access token returned. You should store this 
     # access token somewhere safe, like a database, for future use.
-    token = oauth.Token(request_token[b'oauth_token'].decode(), request_token[b'oauth_token_secret'].decode())
-    token.set_verifier(oauth_verifier)
-    client = oauth.Client(consumer, token)
-    client.ca_certs = certifi.where()
-    
-    resp, content = client.request(ACCESS_TOKEN_URL, "GET")
-    access_token = dict(parse_qsl(content.decode('utf-8')))
+    access_token = oauth.fetch_access_token(ACCESS_TOKEN_URL, oauth_verifier)
     
     print("Access Token:")
     print("    - oauth_token        = %s" % access_token['oauth_token'])
@@ -149,10 +135,7 @@ If you want your application to be able to use the API on behalf of another user
 
 You can then use your access token with the REST API.  For instance script to create a wiki page might look like this:
 
-    from urllib.parse import urlparse, parse_qsl, urlencode
-
-    import oauth2 as oauth
-    import certifi
+    from requests_oauthlib import OAuth1Session
     
     PROJECT='test'
     
@@ -162,17 +145,14 @@ You can then use your access token with the REST API.  For instance script to cr
     ACCESS_KEY='<access key from previous script>'
     ACCESS_SECRET='<access secret from previous script>'
     
-    URL_BASE='https://sourceforge.net/rest/'
+    URL_BASE='https://forge-allura.apache.org/rest/'
     
-    consumer = oauth.Consumer(CONSUMER_KEY, CONSUMER_SECRET)
-    access_token = oauth.Token(ACCESS_KEY, ACCESS_SECRET)
-    client = oauth.Client(consumer, access_token)
-    client.ca_certs = certifi.where()
+    oauth = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET,
+                          resource_owner_key=ACCESS_KEY, resource_owner_secret=ACCESS_SECRET)
     
-    response = client.request(
-        URL_BASE + 'p/' + PROJECT + '/wiki/TestPage', 'POST',
-        body=urlencode(dict(
-                text='This is a test page')))
+    response = oauth.post(URL_BASE + 'p/' + PROJECT + '/wiki/TestPage',
+                          data=dict(text='This is a test page'))
+    response.raise_for_status()
     print("Done.  Response was:")
     print(response)
 
diff --git a/scripts/wiki-copy.py b/scripts/wiki-copy.py
index 0f80a34d8..fc6553761 100644
--- a/scripts/wiki-copy.py
+++ b/scripts/wiki-copy.py
@@ -19,16 +19,12 @@
 
 import os
 import sys
-import six.moves.urllib.request
-import six.moves.urllib.parse
-import six.moves.urllib.error
-import six.moves.urllib.parse
 from optparse import OptionParser
-import json
-
-from six.moves.configparser import ConfigParser, NoOptionError
+from configparser import ConfigParser, NoOptionError
 import webbrowser
-import oauth2 as oauth
+
+import requests
+from requests_oauthlib import OAuth1Session
 
 
 def main():
@@ -45,32 +41,29 @@ def main():
     base_url = options.to_wiki.split('/rest/')[0]
     oauth_client = make_oauth_client(base_url)
 
-    wiki_data = six.moves.urllib.request.urlopen(options.from_wiki).read()
-    wiki_json = json.loads(wiki_data)['pages']
+    wiki_json = requests.get(options.from_wiki).json()['pages']
     for p in wiki_json:
-        from_url = options.from_wiki + six.moves.urllib.parse.quote(p)
-        to_url = options.to_wiki + six.moves.urllib.parse.quote(p)
+        from_url = options.from_wiki.rstrip('/') + '/' + p
+        to_url = options.to_wiki.rstrip('/') + '/' + p
         try:
-            page_data = six.moves.urllib.request.urlopen(from_url).read()
-            page_json = json.loads(page_data)
+            page_json = requests.get(from_url).json()
             if options.debug:
                 print(page_json['text'])
                 break
-            resp = oauth_client.request(
-                to_url, 'POST', body=six.moves.urllib.parse.urlencode(dict(text=page_json['text'].encode('utf-8'))))
-            if resp[0]['status'] == '200':
+            resp = oauth_client.post(to_url, data=dict(text=page_json['text']))
+            if resp.status_code == 200:
                 print("Posted {} to {}".format(page_json['title'], to_url))
             else:
-                print("Error posting {} to {}: {} (project may not exist)".format(page_json['title'], to_url, resp[0]['status']))
+                print("Error posting {} to {}: {} (project may not exist)".format(page_json['title'], to_url, resp.status_code))
                 break
         except Exception:
             print("Error processing " + p)
             raise
 
 
-def make_oauth_client(base_url):
+def make_oauth_client(base_url) -> requests.Session:
     """
-    Build an oauth.Client with which callers can query Allura.
+    Build an oauth client with which callers can query Allura.
     """
     config_file = os.path.join(os.environ['HOME'], '.allurarc')
     cp = ConfigParser()
@@ -80,49 +73,38 @@ def make_oauth_client(base_url):
     AUTHORIZE_URL = base_url + '/rest/oauth/authorize'
     ACCESS_TOKEN_URL = base_url + '/rest/oauth/access_token'
     oauth_key = option(cp, base_url, 'oauth_key',
-                       'Forge API OAuth Key (%s/auth/oauth/): ' % base_url)
+                       'Forge API OAuth Consumer Key (%s/auth/oauth/): ' % base_url)
     oauth_secret = option(cp, base_url, 'oauth_secret',
-                          'Forge API Oauth Secret: ')
-    consumer = oauth.Consumer(oauth_key, oauth_secret)
+                          'Forge API Oauth Consumer Secret: ')
 
     try:
         oauth_token = cp.get(base_url, 'oauth_token')
         oauth_token_secret = cp.get(base_url, 'oauth_token_secret')
     except NoOptionError:
-        client = oauth.Client(consumer)
-        resp, content = client.request(REQUEST_TOKEN_URL, 'GET')
-        assert resp['status'] == '200', resp
-
-        request_token = dict(six.moves.urllib.parse.parse_qsl(content))
-        pin_url = "{}?oauth_token={}".format(
-            AUTHORIZE_URL, request_token['oauth_token'])
-        if getattr(webbrowser.get(), 'name', '') == 'links':
-            # sandboxes
+        oauthSess = OAuth1Session(oauth_key, client_secret=oauth_secret)
+        request_token = oauthSess.fetch_request_token(REQUEST_TOKEN_URL)
+        pin_url = oauthSess.authorization_url(AUTHORIZE_URL, request_token['oauth_token'])
+        if isinstance(webbrowser.get(), webbrowser.GenericBrowser):
             print("Go to %s" % pin_url)
         else:
             webbrowser.open(pin_url)
         oauth_verifier = input('What is the PIN? ')
-
-        token = oauth.Token(
-            request_token['oauth_token'], request_token['oauth_token_secret'])
-        token.set_verifier(oauth_verifier)
-        client = oauth.Client(consumer, token)
-        resp, content = client.request(ACCESS_TOKEN_URL, "GET")
-        access_token = dict(six.moves.urllib.parse.parse_qsl(content))
+        access_token = oauthSess.fetch_access_token(ACCESS_TOKEN_URL, oauth_verifier)
         oauth_token = access_token['oauth_token']
         oauth_token_secret = access_token['oauth_token_secret']
 
         cp.set(base_url, 'oauth_token', oauth_token)
         cp.set(base_url, 'oauth_token_secret', oauth_token_secret)
+        # save oauth token for later use
+        cp.write(open(config_file, 'w'))
+        print(f'Saving oauth tokens in {config_file} for later re-use')
+        print()
 
-    # save oauth token for later use
-    cp.write(open(config_file, 'w'))
-    print(f'Saving oauth tokens in {config_file} for later re-use')
-    print()
+    else:
+        oauthSess = OAuth1Session(oauth_key, client_secret=oauth_secret,
+                                  resource_owner_key=oauth_token, resource_owner_secret=oauth_token_secret)
 
-    access_token = oauth.Token(oauth_token, oauth_token_secret)
-    oauth_client = oauth.Client(consumer, access_token)
-    return oauth_client
+    return oauthSess
 
 
 def option(cp, section, key, prompt=None):


[allura] 04/07: [#8461] update test values (to be ok with oauthlib validations)

Posted by ke...@apache.org.
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 ef6326b7e9a8da48e0e6b9bb9a4895f21b808639
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Thu Sep 8 11:25:00 2022 -0400

    [#8461] update test values (to be ok with oauthlib validations)
---
 Allura/allura/tests/functional/test_auth.py | 108 ++++++++++++++--------------
 1 file changed, 54 insertions(+), 54 deletions(-)

diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index a2b936f46..92806fb59 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -1859,15 +1859,15 @@ class TestOAuth(TestController):
     def test_interactive(self):
         user = M.User.by_username('test-admin')
         M.OAuthConsumerToken(
-            api_key='api_key',
-            secret_key='dummy-client-secret',
+            api_key='api_key_api_key_12345',
+            secret_key='test-client-secret',
             user_id=user._id,
             description='ctok_desc',
         )
         ThreadLocalORMSession.flush_all()
         oauth_params = dict(
-            client_key='api_key',
-            client_secret='dummy-client-secret',
+            client_key='api_key_api_key_12345',
+            client_secret='test-client-secret',
             callback_uri='http://my.domain.com/callback',
         )
         r = self.app.post(*oauth1_webtest('/rest/oauth/request_token', oauth_params, method='POST'))
@@ -1883,8 +1883,8 @@ class TestOAuth(TestController):
         assert pin
 
         oauth_params = dict(
-            client_key='api_key',
-            client_secret='dummy-client-secret',
+            client_key='api_key_api_key_12345',
+            client_secret='test-client-secret',
             resource_owner_key=rtok,
             resource_owner_secret=rsecr,
             verifier=pin,
@@ -1898,8 +1898,8 @@ class TestOAuth(TestController):
         oauth_token = atok['oauth_token'][0]
         oauth_secret = atok['oauth_token_secret'][0]
         oaurl, oaparams, oahdrs = oauth1_webtest('/rest/p/test/', dict(
-            client_key='api_key',
-            client_secret='dummy-client-secret',
+            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'
@@ -1910,106 +1910,106 @@ class TestOAuth(TestController):
     def test_authorize_ok(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='oob',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok'})
+        r = self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'})
         assert_in('ctok_desc', r.text)
-        assert_in('api_key_reqtok', r.text)
+        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'}, status=401)
+        self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok_12345'}, status=401)
 
     def test_do_authorize_no(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='oob',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
         self.app.post('/rest/oauth/do_authorize',
-                      params={'no': '1', 'oauth_token': 'api_key_reqtok'})
-        assert_is_none(M.OAuthRequestToken.query.get(api_key='api_key_reqtok'))
+                      params={'no': '1', 'oauth_token': 'api_key_reqtok_12345'})
+        assert_is_none(M.OAuthRequestToken.query.get(api_key='api_key_reqtok_12345'))
 
     def test_do_authorize_oob(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='oob',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok'})
+        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok_12345'})
         assert_is_not_none(r.html.find(text=re.compile('^PIN: ')))
 
     def test_do_authorize_cb(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok'})
-        assert r.location.startswith('http://my.domain.com/callback?oauth_token=api_key_reqtok&oauth_verifier=')
+        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok_12345'})
+        assert r.location.startswith('http://my.domain.com/callback?oauth_token=api_key_reqtok_12345&oauth_verifier=')
 
     def test_do_authorize_cb_params(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok'})
-        assert r.location.startswith('http://my.domain.com/callback?myparam=foo&oauth_token=api_key_reqtok&oauth_verifier=')
+        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok_12345'})
+        assert r.location.startswith('http://my.domain.com/callback?myparam=foo&oauth_token=api_key_reqtok_12345&oauth_verifier=')
 
 
 class TestOAuthRequestToken(TestController):
 
     oauth_params = dict(
-        client_key='api_key',
-        client_secret='dummy-client-secret',
+        client_key='api_key_api_key_12345',
+        client_secret='test-client-secret',
     )
 
     def test_request_token_valid(self):
         user = M.User.by_username('test-user')
         consumer_token = M.OAuthConsumerToken(
-            api_key='api_key',
-            secret_key='dummy-client-secret',
+            api_key='api_key_api_key_12345',
+            secret_key='test-client-secret',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
@@ -2036,9 +2036,9 @@ class TestOAuthRequestToken(TestController):
     def test_request_token_invalid(self):
         user = M.User.by_username('test-user')
         M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
-            secret_key='dummy-client-secret--INVALID',
+            secret_key='test-client-secret--INVALID',
         )
         ThreadLocalORMSession.flush_all()
         with LogCapture() as logs:
@@ -2050,11 +2050,11 @@ class TestOAuthRequestToken(TestController):
 class TestOAuthAccessToken(TestController):
 
     oauth_params = dict(
-        client_key='api_key',
-        client_secret='dummy-client-secret',
-        resource_owner_key='api_key_reqtok',
-        resource_owner_secret='dummy-token-secret',
-        verifier='good',
+        client_key='api_key_api_key_12345',
+        client_secret='test-client-secret',
+        resource_owner_key='api_key_reqtok_12345',
+        resource_owner_secret='test-token-secret',
+        verifier='good_verifier_123456',
     )
 
     def test_access_token_no_consumer(self):
@@ -2065,7 +2065,7 @@ class TestOAuthAccessToken(TestController):
     def test_access_token_no_request(self):
         user = M.User.by_username('test-admin')
         M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
@@ -2077,21 +2077,21 @@ class TestOAuthAccessToken(TestController):
     def test_access_token_bad_pin(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
-            validation_pin='good',
+            validation_pin='good_verifier_123456',
         )
         ThreadLocalORMSession.flush_all()
         with LogCapture() as logs:
             oauth_params = self.oauth_params.copy()
-            oauth_params['verifier'] = 'bad'
+            oauth_params['verifier'] = 'bad_verifier_1234567'
             self.app.get(*oauth1_webtest('/rest/oauth/access_token', oauth_params),
                          status=401)
         assert_logmsg(logs, 'Invalid verifier')
@@ -2099,18 +2099,18 @@ class TestOAuthAccessToken(TestController):
     def test_access_token_bad_sig(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
+            api_key='api_key_api_key_12345',
             user_id=user._id,
             description='ctok_desc',
-            secret_key='dummy-client-secret',
+            secret_key='test-client-secret',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
+            api_key='api_key_reqtok_12345',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
-            validation_pin='good',
-            secret_key='dummy-token-secret--INVALID',
+            validation_pin='good_verifier_123456',
+            secret_key='test-token-secret--INVALID',
         )
         ThreadLocalORMSession.flush_all()
         with LogCapture() as logs:
@@ -2120,18 +2120,18 @@ class TestOAuthAccessToken(TestController):
     def test_access_token_ok(self):
         user = M.User.by_username('test-admin')
         ctok = M.OAuthConsumerToken(
-            api_key='api_key',
-            secret_key='dummy-client-secret',
+            api_key='api_key_api_key_12345',
+            secret_key='test-client-secret',
             user_id=user._id,
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key_reqtok',
-            secret_key='dummy-token-secret',
+            api_key='api_key_reqtok_12345',
+            secret_key='test-token-secret',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
-            validation_pin='good',
+            validation_pin='good_verifier_123456',
         )
         ThreadLocalORMSession.flush_all()
 


[allura] 03/07: [#8461] distinguish "api_key" used for consumer tokens vs request tokens, in tests

Posted by ke...@apache.org.
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 d5d83fdced9d65f5208490fbbef243ff29d060ba
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Wed Sep 7 14:15:43 2022 -0400

    [#8461] distinguish "api_key" used for consumer tokens vs request tokens, in tests
---
 Allura/allura/tests/functional/test_auth.py | 38 ++++++++++++++---------------
 1 file changed, 19 insertions(+), 19 deletions(-)

diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 1d15220d5..a2b936f46 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -1915,18 +1915,18 @@ class TestOAuth(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='oob',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key'})
+        r = self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok'})
         assert_in('ctok_desc', r.text)
-        assert_in('api_key', r.text)
+        assert_in('api_key_reqtok', r.text)
 
     def test_authorize_invalid(self):
-        self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key'}, status=401)
+        self.app.post('/rest/oauth/authorize', params={'oauth_token': 'api_key_reqtok'}, status=401)
 
     def test_do_authorize_no(self):
         user = M.User.by_username('test-admin')
@@ -1936,15 +1936,15 @@ class TestOAuth(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='oob',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
         self.app.post('/rest/oauth/do_authorize',
-                      params={'no': '1', 'oauth_token': 'api_key'})
-        assert_is_none(M.OAuthRequestToken.query.get(api_key='api_key'))
+                      params={'no': '1', 'oauth_token': 'api_key_reqtok'})
+        assert_is_none(M.OAuthRequestToken.query.get(api_key='api_key_reqtok'))
 
     def test_do_authorize_oob(self):
         user = M.User.by_username('test-admin')
@@ -1954,13 +1954,13 @@ class TestOAuth(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='oob',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key'})
+        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok'})
         assert_is_not_none(r.html.find(text=re.compile('^PIN: ')))
 
     def test_do_authorize_cb(self):
@@ -1971,14 +1971,14 @@ class TestOAuth(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key'})
-        assert r.location.startswith('http://my.domain.com/callback?oauth_token=api_key&oauth_verifier=')
+        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok'})
+        assert r.location.startswith('http://my.domain.com/callback?oauth_token=api_key_reqtok&oauth_verifier=')
 
     def test_do_authorize_cb_params(self):
         user = M.User.by_username('test-admin')
@@ -1988,14 +1988,14 @@ class TestOAuth(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
         )
         ThreadLocalORMSession.flush_all()
-        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key'})
-        assert r.location.startswith('http://my.domain.com/callback?myparam=foo&oauth_token=api_key&oauth_verifier=')
+        r = self.app.post('/rest/oauth/do_authorize', params={'yes': '1', 'oauth_token': 'api_key_reqtok'})
+        assert r.location.startswith('http://my.domain.com/callback?myparam=foo&oauth_token=api_key_reqtok&oauth_verifier=')
 
 
 class TestOAuthRequestToken(TestController):
@@ -2052,7 +2052,7 @@ class TestOAuthAccessToken(TestController):
     oauth_params = dict(
         client_key='api_key',
         client_secret='dummy-client-secret',
-        resource_owner_key='api_key',
+        resource_owner_key='api_key_reqtok',
         resource_owner_secret='dummy-token-secret',
         verifier='good',
     )
@@ -2082,7 +2082,7 @@ class TestOAuthAccessToken(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
@@ -2105,7 +2105,7 @@ class TestOAuthAccessToken(TestController):
             secret_key='dummy-client-secret',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',
             user_id=user._id,
@@ -2126,7 +2126,7 @@ class TestOAuthAccessToken(TestController):
             description='ctok_desc',
         )
         M.OAuthRequestToken(
-            api_key='api_key',
+            api_key='api_key_reqtok',
             secret_key='dummy-token-secret',
             consumer_token_id=ctok._id,
             callback='http://my.domain.com/callback?myparam=foo',