123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- Authorizers
- ===========
- Authorizers are objects that encapsulate knowledge about a particular
- web service's authentication scheme. lazr.restfulclient includes
- authorizers for common HTTP authentication schemes.
- The BasicHttpAuthorizer
- -----------------------
- This authorizer handles HTTP Basic Auth. To test it, we'll create a
- fake web service that serves some sample WADL.
- >>> import pkg_resources
- >>> wadl_string = pkg_resources.resource_string(
- ... 'wadllib.tests.data', 'launchpad-wadl.xml')
- >>> responses = { 'application/vnd.sun.wadl+xml' : wadl_string,
- ... 'application/json' : '{}' }
- >>> def sample_application(environ, start_response):
- ... media_type = environ['HTTP_ACCEPT']
- ... content = responses[media_type]
- ... start_response(
- ... '200', [('Content-type', media_type)])
- ... return [content]
- The WADL file will be protected with HTTP Basic Auth. To access it,
- you'll need to provide a username of "user" and a password of
- "password".
- >>> def authenticate(username, password):
- ... """Accepts "user/password", rejects everything else.
- ...
- ... :return: The username, if the credentials are valid.
- ... None, otherwise.
- ... """
- ... if username == "user" and password == "password":
- ... return username
- ... return None
- >>> from lazr.authentication.wsgi import BasicAuthMiddleware
- >>> def protected_application():
- ... return BasicAuthMiddleware(
- ... sample_application, authenticate_with=authenticate)
- Finally, we'll set up a WSGI intercept so that we can test the web
- service by making HTTP requests to http://api.launchpad.dev/. (This is
- the hostname mentioned in the WADL file.)
- >>> import wsgi_intercept
- >>> from wsgi_intercept.httplib2_intercept import install
- >>> install()
- >>> wsgi_intercept.add_wsgi_intercept(
- ... 'api.launchpad.dev', 80, protected_application)
- With no HttpAuthorizer, a ServiceRoot can't get access to the web service.
- >>> from lazr.restfulclient.resource import ServiceRoot
- >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
- Traceback (most recent call last):
- ...
- Unauthorized: HTTP Error 401: Unauthorized
- ...
- We can't get access if the authorizer doesn't have the right
- credentials.
- >>> from lazr.restfulclient.authorize import BasicHttpAuthorizer
- >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword")
- >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/")
- Traceback (most recent call last):
- ...
- Unauthorized: HTTP Error 401: Unauthorized
- ...
- If we provide the right credentials, we can retrieve the WADL. We'll
- still get an exception, because our fake web service is too fake for
- ServiceRoot--its 'service root' resource doesn't match the WADL--but
- we're able to make HTTP requests without getting 401 errors.
- Note that the HTTP request includes the User-Agent header, but that
- that header contains no special information about the authorization
- method. This will change when the authorization method is OAuth.
- >>> import httplib2
- >>> httplib2.debuglevel = 1
- >>> authorizer = BasicHttpAuthorizer("user", "password")
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- send: 'GET / ...user-agent: lazr.restfulclient ...'
- ...
- The BasicHttpAuthorizer allows you to adds proper basic auth headers to the
- request, when asked to, using the username and password information it already
- knows about.
- >>> headers = {}
- >>> authorizer.authorizeRequest('/', 'GET', '', headers)
- >>> headers.get('authorization')
- 'Basic dXNlcjpwYXNzd29yZA=='
- Teardown.
- >>> httplib2.debuglevel = 0
- >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)
- The OAuthAuthorizer
- -------------------
- This authorizer handles OAuth authorization. To test it, we'll protect
- the sample application with a piece of OAuth middleware. The middleware
- will accept only one consumer/token combination, though it will also
- allow anonymous access: if you pass in an empty token and secret,
- you'll get a lower level of access.
- >>> from oauth.oauth import OAuthConsumer, OAuthToken
- >>> valid_consumer = OAuthConsumer("consumer", '')
- >>> valid_token = OAuthToken("token", "secret")
- >>> empty_token = OAuthToken("", "")
- Our authenticate() implementation checks against the one valid
- consumer and token.
- >>> def authenticate(consumer, token, parameters):
- ... """Accepts the valid consumer and token, rejects everything else.
- ...
- ... :return: The consumer, if the credentials are valid.
- ... None, otherwise.
- ... """
- ... if token.key == '' and token.secret == '':
- ... # Anonymous access.
- ... return consumer
- ... if consumer == valid_consumer and token == valid_token:
- ... return consumer
- ... return None
- Our data store helps the middleware look up consumer and token objects
- from the information provided in a signed OAuth request.
- >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore
- >>> class AnonymousAccessDataStore(SimpleOAuthDataStore):
- ... """A data store that will accept any consumer."""
- ... def lookup_consumer(self, consumer):
- ... """If there's no matching consumer, just create one.
- ...
- ... This will let anonymous requests succeed with any
- ... consumer key."""
- ... consumer = super(
- ... AnonymousAccessDataStore, self).lookup_consumer(
- ... consumer)
- ... if consumer is None:
- ... consumer = OAuthConsumer(consumer, '')
- ... return consumer
- >>> data_store = AnonymousAccessDataStore(
- ... {valid_consumer.key : valid_consumer},
- ... {valid_token.key : valid_token,
- ... empty_token.key : empty_token})
- Now we're ready to protect the sample_application with OAuthMiddleware,
- using our authenticate() implementation and our data store.
- >>> from lazr.authentication.wsgi import OAuthMiddleware
- >>> def protected_application():
- ... return OAuthMiddleware(
- ... sample_application, realm="OAuth test",
- ... authenticate_with=authenticate, data_store=data_store)
- >>> wsgi_intercept.add_wsgi_intercept(
- ... 'api.launchpad.dev', 80, protected_application)
- Let's try out some clients. As you'd expect, you can't get through the
- middleware with no HTTPAuthorizer at all.
- >>> from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
- >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
- Traceback (most recent call last):
- ...
- Unauthorized: HTTP Error 401: Unauthorized
- ...
- Invalid credentials are also no help.
- >>> authorizer = OAuthAuthorizer(
- ... valid_consumer.key, access_token=OAuthToken("invalid", "token"))
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- Traceback (most recent call last):
- ...
- Unauthorized: HTTP Error 401: Unauthorized
- ...
- But valid credentials work fine (again, up to the point at which
- lazr.restfulclient runs against the limits of this simple web
- service). Note that the User-Agent header mentions the consumer key.
- >>> httplib2.debuglevel = 1
- >>> authorizer = OAuthAuthorizer(
- ... valid_consumer.key, access_token=valid_token)
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- send: 'GET /...user-agent: lazr.restfulclient...; oauth_consumer="consumer"...'
- ...
- If the OAuthAuthorizer is created with an application name as well as
- a consumer key, the application name is mentioned in the User-Agent
- header as well.
- >>> authorizer = OAuthAuthorizer(
- ... valid_consumer.key, access_token=valid_token,
- ... application_name="the app")
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- send: 'GET /...user-agent: lazr.restfulclient...; application="the app"; oauth_consumer="consumer"...'
- ...
- >>> httplib2.debuglevel = 0
- It's even possible to get anonymous access by providing an empty
- access token.
- >>> authorizer = OAuthAuthorizer(
- ... valid_consumer.key, access_token=empty_token)
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- Because of the way the AnonymousAccessDataStore (defined
- earlier in the test) works, you can even get anonymous access by
- specifying an OAuth consumer that's not in the server-side list of
- valid consumers.
- >>> authorizer = OAuthAuthorizer(
- ... "random consumer", access_token=empty_token)
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- A ServiceRoot object has a 'credentials' attribute which contains the
- Authorizer used to authorize outgoing requests.
- >>> from lazr.restfulclient.resource import ServiceRoot
- >>> root = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- >>> root.credentials
- <lazr.restfulclient.authorize.oauth.OAuthAuthorizer object...>
- If you try to provide credentials with an unrecognized OAuth consumer,
- you'll get an error--even if the credentials are valid. The data store
- used in this test only lets unrecognized OAuth consumers through when
- they request anonymous access.
- >>> authorizer = OAuthAuthorizer(
- ... 'random consumer', access_token=valid_token)
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- Traceback (most recent call last):
- ...
- Unauthorized: HTTP Error 401: Unauthorized
- ...
- >>> authorizer = OAuthAuthorizer(
- ... 'random consumer', access_token=OAuthToken("invalid", "token"))
- >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
- Traceback (most recent call last):
- ...
- Unauthorized: HTTP Error 401: Unauthorized
- ...
- Teardown.
- >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)
|