authorizer.standalone.rst 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. Authorizers
  2. ===========
  3. Authorizers are objects that encapsulate knowledge about a particular
  4. web service's authentication scheme. lazr.restfulclient includes
  5. authorizers for common HTTP authentication schemes.
  6. The BasicHttpAuthorizer
  7. -----------------------
  8. This authorizer handles HTTP Basic Auth. To test it, we'll create a
  9. fake web service that serves some sample WADL.
  10. >>> import pkg_resources
  11. >>> wadl_string = pkg_resources.resource_string(
  12. ... 'wadllib.tests.data', 'launchpad-wadl.xml')
  13. >>> responses = { 'application/vnd.sun.wadl+xml' : wadl_string,
  14. ... 'application/json' : '{}' }
  15. >>> def sample_application(environ, start_response):
  16. ... media_type = environ['HTTP_ACCEPT']
  17. ... content = responses[media_type]
  18. ... start_response(
  19. ... '200', [('Content-type', media_type)])
  20. ... return [content]
  21. The WADL file will be protected with HTTP Basic Auth. To access it,
  22. you'll need to provide a username of "user" and a password of
  23. "password".
  24. >>> def authenticate(username, password):
  25. ... """Accepts "user/password", rejects everything else.
  26. ...
  27. ... :return: The username, if the credentials are valid.
  28. ... None, otherwise.
  29. ... """
  30. ... if username == "user" and password == "password":
  31. ... return username
  32. ... return None
  33. >>> from lazr.authentication.wsgi import BasicAuthMiddleware
  34. >>> def protected_application():
  35. ... return BasicAuthMiddleware(
  36. ... sample_application, authenticate_with=authenticate)
  37. Finally, we'll set up a WSGI intercept so that we can test the web
  38. service by making HTTP requests to http://api.launchpad.dev/. (This is
  39. the hostname mentioned in the WADL file.)
  40. >>> import wsgi_intercept
  41. >>> from wsgi_intercept.httplib2_intercept import install
  42. >>> install()
  43. >>> wsgi_intercept.add_wsgi_intercept(
  44. ... 'api.launchpad.dev', 80, protected_application)
  45. With no HttpAuthorizer, a ServiceRoot can't get access to the web service.
  46. >>> from lazr.restfulclient.resource import ServiceRoot
  47. >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
  48. Traceback (most recent call last):
  49. ...
  50. Unauthorized: HTTP Error 401: Unauthorized
  51. ...
  52. We can't get access if the authorizer doesn't have the right
  53. credentials.
  54. >>> from lazr.restfulclient.authorize import BasicHttpAuthorizer
  55. >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword")
  56. >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/")
  57. Traceback (most recent call last):
  58. ...
  59. Unauthorized: HTTP Error 401: Unauthorized
  60. ...
  61. If we provide the right credentials, we can retrieve the WADL. We'll
  62. still get an exception, because our fake web service is too fake for
  63. ServiceRoot--its 'service root' resource doesn't match the WADL--but
  64. we're able to make HTTP requests without getting 401 errors.
  65. Note that the HTTP request includes the User-Agent header, but that
  66. that header contains no special information about the authorization
  67. method. This will change when the authorization method is OAuth.
  68. >>> import httplib2
  69. >>> httplib2.debuglevel = 1
  70. >>> authorizer = BasicHttpAuthorizer("user", "password")
  71. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  72. send: 'GET / ...user-agent: lazr.restfulclient ...'
  73. ...
  74. The BasicHttpAuthorizer allows you to adds proper basic auth headers to the
  75. request, when asked to, using the username and password information it already
  76. knows about.
  77. >>> headers = {}
  78. >>> authorizer.authorizeRequest('/', 'GET', '', headers)
  79. >>> headers.get('authorization')
  80. 'Basic dXNlcjpwYXNzd29yZA=='
  81. Teardown.
  82. >>> httplib2.debuglevel = 0
  83. >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)
  84. The OAuthAuthorizer
  85. -------------------
  86. This authorizer handles OAuth authorization. To test it, we'll protect
  87. the sample application with a piece of OAuth middleware. The middleware
  88. will accept only one consumer/token combination, though it will also
  89. allow anonymous access: if you pass in an empty token and secret,
  90. you'll get a lower level of access.
  91. >>> from oauth.oauth import OAuthConsumer, OAuthToken
  92. >>> valid_consumer = OAuthConsumer("consumer", '')
  93. >>> valid_token = OAuthToken("token", "secret")
  94. >>> empty_token = OAuthToken("", "")
  95. Our authenticate() implementation checks against the one valid
  96. consumer and token.
  97. >>> def authenticate(consumer, token, parameters):
  98. ... """Accepts the valid consumer and token, rejects everything else.
  99. ...
  100. ... :return: The consumer, if the credentials are valid.
  101. ... None, otherwise.
  102. ... """
  103. ... if token.key == '' and token.secret == '':
  104. ... # Anonymous access.
  105. ... return consumer
  106. ... if consumer == valid_consumer and token == valid_token:
  107. ... return consumer
  108. ... return None
  109. Our data store helps the middleware look up consumer and token objects
  110. from the information provided in a signed OAuth request.
  111. >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore
  112. >>> class AnonymousAccessDataStore(SimpleOAuthDataStore):
  113. ... """A data store that will accept any consumer."""
  114. ... def lookup_consumer(self, consumer):
  115. ... """If there's no matching consumer, just create one.
  116. ...
  117. ... This will let anonymous requests succeed with any
  118. ... consumer key."""
  119. ... consumer = super(
  120. ... AnonymousAccessDataStore, self).lookup_consumer(
  121. ... consumer)
  122. ... if consumer is None:
  123. ... consumer = OAuthConsumer(consumer, '')
  124. ... return consumer
  125. >>> data_store = AnonymousAccessDataStore(
  126. ... {valid_consumer.key : valid_consumer},
  127. ... {valid_token.key : valid_token,
  128. ... empty_token.key : empty_token})
  129. Now we're ready to protect the sample_application with OAuthMiddleware,
  130. using our authenticate() implementation and our data store.
  131. >>> from lazr.authentication.wsgi import OAuthMiddleware
  132. >>> def protected_application():
  133. ... return OAuthMiddleware(
  134. ... sample_application, realm="OAuth test",
  135. ... authenticate_with=authenticate, data_store=data_store)
  136. >>> wsgi_intercept.add_wsgi_intercept(
  137. ... 'api.launchpad.dev', 80, protected_application)
  138. Let's try out some clients. As you'd expect, you can't get through the
  139. middleware with no HTTPAuthorizer at all.
  140. >>> from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
  141. >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
  142. Traceback (most recent call last):
  143. ...
  144. Unauthorized: HTTP Error 401: Unauthorized
  145. ...
  146. Invalid credentials are also no help.
  147. >>> authorizer = OAuthAuthorizer(
  148. ... valid_consumer.key, access_token=OAuthToken("invalid", "token"))
  149. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  150. Traceback (most recent call last):
  151. ...
  152. Unauthorized: HTTP Error 401: Unauthorized
  153. ...
  154. But valid credentials work fine (again, up to the point at which
  155. lazr.restfulclient runs against the limits of this simple web
  156. service). Note that the User-Agent header mentions the consumer key.
  157. >>> httplib2.debuglevel = 1
  158. >>> authorizer = OAuthAuthorizer(
  159. ... valid_consumer.key, access_token=valid_token)
  160. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  161. send: 'GET /...user-agent: lazr.restfulclient...; oauth_consumer="consumer"...'
  162. ...
  163. If the OAuthAuthorizer is created with an application name as well as
  164. a consumer key, the application name is mentioned in the User-Agent
  165. header as well.
  166. >>> authorizer = OAuthAuthorizer(
  167. ... valid_consumer.key, access_token=valid_token,
  168. ... application_name="the app")
  169. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  170. send: 'GET /...user-agent: lazr.restfulclient...; application="the app"; oauth_consumer="consumer"...'
  171. ...
  172. >>> httplib2.debuglevel = 0
  173. It's even possible to get anonymous access by providing an empty
  174. access token.
  175. >>> authorizer = OAuthAuthorizer(
  176. ... valid_consumer.key, access_token=empty_token)
  177. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  178. Because of the way the AnonymousAccessDataStore (defined
  179. earlier in the test) works, you can even get anonymous access by
  180. specifying an OAuth consumer that's not in the server-side list of
  181. valid consumers.
  182. >>> authorizer = OAuthAuthorizer(
  183. ... "random consumer", access_token=empty_token)
  184. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  185. A ServiceRoot object has a 'credentials' attribute which contains the
  186. Authorizer used to authorize outgoing requests.
  187. >>> from lazr.restfulclient.resource import ServiceRoot
  188. >>> root = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  189. >>> root.credentials
  190. <lazr.restfulclient.authorize.oauth.OAuthAuthorizer object...>
  191. If you try to provide credentials with an unrecognized OAuth consumer,
  192. you'll get an error--even if the credentials are valid. The data store
  193. used in this test only lets unrecognized OAuth consumers through when
  194. they request anonymous access.
  195. >>> authorizer = OAuthAuthorizer(
  196. ... 'random consumer', access_token=valid_token)
  197. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  198. Traceback (most recent call last):
  199. ...
  200. Unauthorized: HTTP Error 401: Unauthorized
  201. ...
  202. >>> authorizer = OAuthAuthorizer(
  203. ... 'random consumer', access_token=OAuthToken("invalid", "token"))
  204. >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
  205. Traceback (most recent call last):
  206. ...
  207. Unauthorized: HTTP Error 401: Unauthorized
  208. ...
  209. Teardown.
  210. >>> _ = wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)