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:24 UTC
[allura] 06/07: [#8461] switch from python-oauth2 to oauthlib
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