-
Notifications
You must be signed in to change notification settings - Fork 137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow custom authorization mechanisms. #139
Changes from 3 commits
52742fe
04a4cb0
363bf52
3531934
794636a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from __future__ import print_function | ||
from twisted.internet.endpoints import serverFromString | ||
from twisted.internet.task import react | ||
from twisted.internet.defer import inlineCallbacks, maybeDeferred | ||
from twisted.web.resource import Resource | ||
from twisted.web.server import Site | ||
from twisted.web.http_headers import Headers | ||
from twisted.web.http import FORBIDDEN | ||
|
||
import treq | ||
|
||
|
||
class SillyAuthResource(Resource): | ||
""" | ||
A resource that uses a silly, header-based authentication | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking from the perspective of a non-native English speaker for a moment - the "silliness" here is not obvious, so perhaps it would be better to use a word like "custom"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hm, what about "trivial"? |
||
mechanism. | ||
""" | ||
isLeaf = True | ||
|
||
def __init__(self, header, secret): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps it's time to start using |
||
self._header = header | ||
self._secret = secret | ||
|
||
def render_GET(self, request): | ||
headers = request.requestHeaders | ||
request_secret = headers.getRawHeaders(self._header, [b''])[0] | ||
if request_secret != self._secret: | ||
request.setResponseCode(FORBIDDEN) | ||
return b"No good." | ||
return b"It's good!" | ||
|
||
|
||
class SillyAuth(object): | ||
""" | ||
I implement a silly, header-based authentication mechanism. | ||
""" | ||
|
||
def __init__(self, header, secret, agent): | ||
self._header = header | ||
self._secret = secret | ||
self._agent = agent | ||
|
||
def request(self, method, uri, headers=None, bodyProducer=None): | ||
if headers is None: | ||
headers = Headers({}) | ||
headers.setRawHeaders(self._header, [self._secret]) | ||
return self._agent.request(method, uri, headers, bodyProducer) | ||
|
||
|
||
@inlineCallbacks | ||
def main(reactor, *args): | ||
header = b'x-silly-auth' | ||
secret = b'secret' | ||
|
||
auth_resource = SillyAuthResource(header=header, secret=secret) | ||
|
||
endpoint = serverFromString(reactor, "tcp:8080") | ||
listener = yield endpoint.listen(Site(auth_resource)) | ||
|
||
def sillyAuthCallable(agent): | ||
return SillyAuth(header, secret, agent) | ||
|
||
response = yield treq.get( | ||
'http://localhost:8080/', | ||
auth=sillyAuthCallable, | ||
) | ||
|
||
content = yield response.content() | ||
print(content) | ||
|
||
yield maybeDeferred(listener.stopListening) | ||
|
||
|
||
react(main, []) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
from __future__ import absolute_import, division, print_function | ||
|
||
from twisted.web.http_headers import Headers | ||
from six.moves.urllib.parse import urlparse | ||
import base64 | ||
|
||
|
||
|
@@ -26,6 +27,31 @@ def request(self, method, uri, headers=None, bodyProducer=None): | |
method, uri, headers=headers, bodyProducer=bodyProducer) | ||
|
||
|
||
class _PinToFirstHostAgent(object): | ||
""" | ||
An {twisted.web.iweb.IAgent} implementing object that takes two | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "implementing object" is really a content-free phrase here; those words could just be deleted with no loss of meaning. |
||
agents, using the first as when the current request's host name | ||
matches first request's host name, and the second when it does | ||
not. | ||
""" | ||
|
||
def __init__(self, first_agent, second_agent): | ||
self._first_agent = first_agent | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm inclined to suggest Twisted coding-standard names rather than pep8, which seems to be the standard in this repo https://github.com/twisted/treq/blob/master/src/treq/testing.py#L45-L46 and is officially the standard in Klein (despite a few lapses there) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lots of treq.client, for example, prefers |
||
self._second_agent = second_agent | ||
self._first_host = None | ||
|
||
def request(self, method, uri, headers=None, bodyProducer=None): | ||
hostname = urlparse(uri).hostname | ||
if self._first_host in (None, hostname): | ||
self._first_host = hostname | ||
agent = self._first_agent | ||
else: | ||
agent = self._second_agent | ||
|
||
return agent.request( | ||
method, uri, headers=headers, bodyProducer=bodyProducer) | ||
|
||
|
||
def add_basic_auth(agent, username, password): | ||
creds = base64.b64encode( | ||
'{0}:{1}'.format(username, password).encode('ascii')) | ||
|
@@ -37,5 +63,7 @@ def add_basic_auth(agent, username, password): | |
def add_auth(agent, auth_config): | ||
if isinstance(auth_config, tuple): | ||
return add_basic_auth(agent, auth_config[0], auth_config[1]) | ||
elif callable(auth_config): | ||
return auth_config(agent) | ||
|
||
raise UnknownAuthConfig(auth_config) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,15 @@ | |
|
||
from twisted.web.client import Agent | ||
from twisted.web.http_headers import Headers | ||
from twisted.web.iweb import IAgent | ||
|
||
from treq.test.util import TestCase | ||
from treq.auth import _RequestHeaderSettingAgent, add_auth, UnknownAuthConfig | ||
from treq.auth import (_RequestHeaderSettingAgent, | ||
_PinToFirstHostAgent, | ||
add_auth, | ||
UnknownAuthConfig) | ||
|
||
from zope.interface import implementer | ||
|
||
|
||
class RequestHeaderSettingAgentTests(TestCase): | ||
|
@@ -42,6 +48,61 @@ def test_overrides_per_request_headers(self): | |
) | ||
|
||
|
||
class PinToFirstHostAgentTests(TestCase): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests should really have docstrings. |
||
def setUp(self): | ||
self.first_agent = mock.Mock(Agent) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really dislike There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may like |
||
self.second_agent = mock.Mock(Agent) | ||
self.pinning_agent = _PinToFirstHostAgent(self.first_agent, | ||
self.second_agent) | ||
|
||
def test_first_request_uses_first_agent(self): | ||
self.pinning_agent.request("method", "http://www.something.com/") | ||
|
||
self.first_agent.request.assert_called_once_with( | ||
"method", "http://www.something.com/", | ||
headers=None, bodyProducer=None) | ||
self.assertFalse(self.second_agent.request.called) | ||
|
||
def test_request_matching_first_uses_first_agent(self): | ||
self.pinning_agent.request("method", "http://www.something.com/a") | ||
self.pinning_agent.request("method", "http://www.something.com/b") | ||
|
||
self.assertEqual(self.first_agent.request.call_args_list, [ | ||
mock.call("method", "http://www.something.com/a", | ||
headers=None, bodyProducer=None), | ||
mock.call("method", "http://www.something.com/b", | ||
headers=None, bodyProducer=None) | ||
]) | ||
self.assertFalse(self.second_agent.request.called) | ||
|
||
def test_request_not_matching_first_uses_second_agent(self): | ||
self.pinning_agent.request("method", "http://www.something.com/a") | ||
self.pinning_agent.request("method", "http://www.other.com/a") | ||
|
||
self.first_agent.request.assert_called_once_with( | ||
"method", "http://www.something.com/a", | ||
headers=None, bodyProducer=None) | ||
|
||
self.second_agent.request.assert_called_once_with( | ||
"method", "http://www.other.com/a", | ||
headers=None, bodyProducer=None) | ||
|
||
def test_some_request_matching_first_uses_second_agent(self): | ||
self.pinning_agent.request("method", "http://www.something.com/a") | ||
self.pinning_agent.request("method", "http://www.other.com/a") | ||
self.pinning_agent.request("method", "http://www.something.com/b") | ||
|
||
self.assertEqual(self.first_agent.request.call_args_list, [ | ||
mock.call("method", "http://www.something.com/a", | ||
headers=None, bodyProducer=None), | ||
mock.call("method", "http://www.something.com/b", | ||
headers=None, bodyProducer=None) | ||
]) | ||
self.second_agent.request.assert_called_once_with( | ||
"method", "http://www.other.com/a", | ||
headers=None, bodyProducer=None) | ||
|
||
|
||
class AddAuthTests(TestCase): | ||
def setUp(self): | ||
self.rhsa_patcher = mock.patch('treq.auth._RequestHeaderSettingAgent') | ||
|
@@ -70,6 +131,22 @@ def test_add_basic_auth_huge(self): | |
agent, | ||
Headers({b'authorization': [auth]})) | ||
|
||
def test_auth_callable(self): | ||
agent = mock.Mock() | ||
|
||
@implementer(IAgent) | ||
class AuthorizingAgent(object): | ||
|
||
def __init__(self, agent): | ||
self.wrapped_agent = agent | ||
|
||
def request(self, method, uri, headers=None, bodyProducer=None): | ||
"""Not called by this test""" | ||
|
||
wrapping_agent = add_auth(agent, AuthorizingAgent) | ||
self.assertIsInstance(wrapping_agent, AuthorizingAgent) | ||
self.assertIs(wrapping_agent.wrapped_agent, agent) | ||
|
||
def test_add_unknown_auth(self): | ||
agent = mock.Mock() | ||
self.assertRaises(UnknownAuthConfig, add_auth, agent, mock.Mock()) | ||
self.assertRaises(UnknownAuthConfig, add_auth, agent, object()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type(Resource) is ClassType
so this should beResource, object
.