Merge lp:~allenap/maas/version-without-auth--bug-1583715 into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5039
Proposed branch: lp:~allenap/maas/version-without-auth--bug-1583715
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 192 lines (+108/-4)
4 files modified
src/maasserver/api/support.py (+26/-0)
src/maasserver/api/tests/test_support.py (+67/-1)
src/maasserver/api/tests/test_version.py (+14/-2)
src/maasserver/urls_api.py (+1/-1)
To merge this branch: bzr merge lp:~allenap/maas/version-without-auth--bug-1583715
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+295333@code.launchpad.net

Commit message

Ensure that restricted resources also perform meaningful authentication of clients.

Also demonstrate that /api/2.0/versions/ is available anonymously.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/support.py'
2--- src/maasserver/api/support.py 2016-04-28 01:11:50 +0000
3+++ src/maasserver/api/support.py 2016-05-20 15:20:50 +0000
4@@ -16,6 +16,7 @@
5 from django.http import Http404
6 from maasserver.api.doc import get_api_description_hash
7 from maasserver.exceptions import MAASAPIBadRequest
8+from piston3.authentication import NoAuthentication
9 from piston3.emitters import Emitter
10 from piston3.handler import (
11 AnonymousBaseHandler,
12@@ -67,10 +68,35 @@
13 else:
14 raise
15
16+ @property
17+ def is_authentication_attempted(self):
18+ """Will use of this resource attempt to authenticate the client?
19+
20+ For example, `None`, ``[]``, and :class:`NoAuthentication` are all
21+ examples of authentication handlers that do *not* count.
22+ """
23+ return len(self.authentication) != 0 and not any(
24+ isinstance(auth, NoAuthentication) for auth in self.authentication)
25+
26
27 class RestrictedResource(OperationsResource):
28 """A resource that's restricted to active users."""
29
30+ def __init__(self, handler, *, authentication):
31+ """A value for `authentication` MUST be provided AND be meaningful.
32+
33+ This prevents the situation where none of the following are restricted
34+ at all::
35+
36+ handler = RestrictedResource(HandlerClass)
37+ handler = RestrictedResource(HandlerClass, authentication=None)
38+ handler = RestrictedResource(HandlerClass, authentication=[])
39+
40+ """
41+ super(RestrictedResource, self).__init__(handler, authentication)
42+ if not self.is_authentication_attempted:
43+ raise AssertionError("Authentication must be attempted.")
44+
45 def authenticate(self, request, rm):
46 actor, anonymous = super(
47 RestrictedResource, self).authenticate(request, rm)
48
49=== modified file 'src/maasserver/api/tests/test_support.py'
50--- src/maasserver/api/tests/test_support.py 2016-05-12 19:07:37 +0000
51+++ src/maasserver/api/tests/test_support.py 2016-05-20 15:20:50 +0000
52@@ -19,7 +19,10 @@
53 from maasserver.api.doc import get_api_description_hash
54 from maasserver.api.support import (
55 admin_method,
56+ AdminRestrictedResource,
57 OperationsHandlerMixin,
58+ OperationsResource,
59+ RestrictedResource,
60 )
61 from maasserver.models.config import (
62 Config,
63@@ -28,7 +31,17 @@
64 from maasserver.testing.api import APITestCase
65 from maasserver.testing.factory import factory
66 from maastesting.testcase import MAASTestCase
67-from testtools.matchers import Equals
68+from piston3.authentication import NoAuthentication
69+from testtools.matchers import (
70+ Equals,
71+ Is,
72+)
73+
74+
75+class StubHandler:
76+ """A stub handler class that breaks when called."""
77+ def __call__(self, request):
78+ raise AssertionError("Do not call the stub handler.")
79
80
81 class TestOperationsResource(APITestCase):
82@@ -67,6 +80,59 @@
83 response["X-MAAS-API-Hash"],
84 Equals(get_api_description_hash()))
85
86+ def test_authenticated_is_False_when_no_authentication_provided(self):
87+ resource = OperationsResource(StubHandler)
88+ self.assertThat(resource.is_authentication_attempted, Is(False))
89+
90+ def test_authenticated_is_False_when_authentication_is_empty(self):
91+ resource = OperationsResource(StubHandler, authentication=[])
92+ self.assertThat(resource.is_authentication_attempted, Is(False))
93+
94+ def test_authenticated_is_False_when_authentication_is_NoAuthn(self):
95+ resource = OperationsResource(
96+ StubHandler, authentication=NoAuthentication())
97+ self.assertThat(resource.is_authentication_attempted, Is(False))
98+
99+ def test_authenticated_is_True_when_authentication_is_provided(self):
100+ resource = OperationsResource(
101+ StubHandler, authentication=sentinel.authentication)
102+ self.assertThat(resource.is_authentication_attempted, Is(True))
103+
104+
105+class TestRestrictedResources(APITestCase):
106+ """Tests for `RestrictedResource` and `AdminRestrictedResource`."""
107+
108+ scenarios = (
109+ ("user", dict(resource_type=RestrictedResource)),
110+ ("admin", dict(resource_type=AdminRestrictedResource)),
111+ )
112+
113+ def test_authentication_must_not_be_None(self):
114+ error = self.assertRaises(
115+ AssertionError, self.resource_type, StubHandler,
116+ authentication=None)
117+ self.assertThat(str(error), Equals(
118+ "Authentication must be attempted."))
119+
120+ def test_authentication_must_be_non_empty(self):
121+ error = self.assertRaises(
122+ AssertionError, self.resource_type, StubHandler,
123+ authentication=[])
124+ self.assertThat(str(error), Equals(
125+ "Authentication must be attempted."))
126+
127+ def test_authentication_must_be_meaningful(self):
128+ error = self.assertRaises(
129+ AssertionError, self.resource_type, StubHandler,
130+ authentication=NoAuthentication())
131+ self.assertThat(str(error), Equals(
132+ "Authentication must be attempted."))
133+
134+ def test_authentication_is_okay(self):
135+ resource = self.resource_type(
136+ StubHandler, authentication=sentinel.authentication)
137+ self.assertThat(resource.is_authentication_attempted, Is(True))
138+
139
140 class TestAdminMethodDecorator(APITestCase):
141
142
143=== modified file 'src/maasserver/api/tests/test_version.py'
144--- src/maasserver/api/tests/test_version.py 2016-04-15 16:33:02 +0000
145+++ src/maasserver/api/tests/test_version.py 2016-05-20 15:20:50 +0000
146@@ -10,19 +10,31 @@
147 import json
148
149 from django.conf import settings
150+from django.contrib.auth.models import AnonymousUser
151 from django.core.urlresolvers import reverse
152 from maasserver.api.version import API_CAPABILITIES_LIST
153+from maasserver.testing.api import MultipleUsersScenarios
154+from maasserver.testing.factory import factory
155 from maasserver.testing.testcase import MAASServerTestCase
156 from maasserver.utils import version as version_module
157
158
159-class TestFindingResources(MAASServerTestCase):
160- """Tests for /version/ API."""
161+class TestVersionAPIBasics(MAASServerTestCase):
162+ """Basic tests for /version/ API."""
163
164 def test_handler_path(self):
165 self.assertEqual(
166 '/api/2.0/version/', reverse('version_handler'))
167
168+
169+class TestVersionAPI(MultipleUsersScenarios, MAASServerTestCase):
170+ """Tests for /version/ API."""
171+
172+ scenarios = [
173+ ('anon', dict(userfactory=AnonymousUser)),
174+ ('user', dict(userfactory=factory.make_User)),
175+ ]
176+
177 def test_GET_returns_details(self):
178 mock_apt = self.patch(version_module, "get_version_from_apt")
179 mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
180
181=== modified file 'src/maasserver/urls_api.py'
182--- src/maasserver/urls_api.py 2016-04-12 17:36:35 +0000
183+++ src/maasserver/urls_api.py 2016-05-20 15:20:50 +0000
184@@ -238,7 +238,7 @@
185 InterfacesHandler, authentication=api_auth)
186 tag_handler = OperationsResource(TagHandler, authentication=api_auth)
187 tags_handler = RestrictedResource(TagsHandler, authentication=api_auth)
188-version_handler = RestrictedResource(VersionHandler)
189+version_handler = OperationsResource(VersionHandler)
190 node_results_handler = RestrictedResource(
191 NodeResultsHandler, authentication=api_auth)
192 sshkey_handler = RestrictedResource(SSHKeyHandler, authentication=api_auth)