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

[allura] branch db/8461 created (now ae26886d4)

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

brondsem pushed a change to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git


      at ae26886d4 [#8461] include oauth_callback in our example clients, to match spec

This branch includes the following new commits:

     new 50a05541e [#8461] convert oauth tests to not mock the oauth library, use requests_oauthlib as a helper to build requests instead
     new c9974e663 [#8461] update oauth lib in docs & wiki-copy.py examples
     new a91a0aa14 [#8461] distinguish "api_key" used for consumer tokens vs request tokens, in tests
     new 54a934985 [#8461] update test values (to be ok with oauthlib validations)
     new 385be59f8 [#8461] index (unique) on OAuthConsumerToken.api_key
     new 2f3fae6b9 [#8461] switch from python-oauth2 to oauthlib
     new ae26886d4 [#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.



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

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit c9974e663b1b85b6cd1918d4b95df867c2a3c6fa
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] 07/07: [#8461] include oauth_callback in our example clients, to match spec

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit ae26886d459e9ed63772dcf48991e22e168babff
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 +-
 scripts/wiki-copy.py         | 2 +-
 2 files changed, 2 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/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] 04/07: [#8461] update test values (to be ok with oauthlib validations)

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 54a934985ca01aa7e77b17c0cb3f7e42c85eea68
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] 01/07: [#8461] convert oauth tests to not mock the oauth library, use requests_oauthlib as a helper to build requests instead

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 50a05541e9cb7a6a652dee209277e1d5e731afdf
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] 03/07: [#8461] distinguish "api_key" used for consumer tokens vs request tokens, in tests

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit a91a0aa14315e60e75d3799e48f2c107e7e83f90
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',


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

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 2f3fae6b90cd950fd81884cb8f3e45a9aeb0faeb
Author: Dave Brondsema <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                                   |  10 +-
 9 files changed, 306 insertions(+), 169 deletions(-)

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


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

Posted by br...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch db/8461
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 385be59f88c37ea7fa03f1f89ec2f5247520e2fc
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]'