Merge lp:~klmitch/glance/private-images into lp:~hudson-openstack/glance/trunk
- private-images
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 166 |
Merged at revision: | 158 |
Proposed branch: | lp:~klmitch/glance/private-images |
Merge into: | lp:~hudson-openstack/glance/trunk |
Diff against target: |
1052 lines (+510/-54) 17 files modified
Authors (+1/-0) etc/glance-api.conf (+4/-1) etc/glance-registry.conf (+7/-1) glance/api/v1/images.py (+53/-9) glance/client.py (+4/-2) glance/common/client.py (+5/-1) glance/common/context.py (+96/-0) glance/registry/__init__.py (+17/-16) glance/registry/client.py (+3/-2) glance/registry/db/api.py (+16/-2) glance/registry/db/migrate_repo/versions/007_add_owner.py (+82/-0) glance/registry/db/models.py (+1/-0) glance/registry/server.py (+52/-13) tests/functional/__init__.py (+11/-2) tests/stubs.py (+5/-2) tests/unit/test_api.py (+5/-3) tests/unit/test_context.py (+148/-0) |
To merge this branch: | bzr merge lp:~klmitch/glance/private-images |
Related bugs: | |
Related blueprints: |
Authentication
(High)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Devin Carlen (community) | Approve | ||
Rick Harris (community) | Approve | ||
Review via email: mp+68905@code.launchpad.net |
Commit message
Description of the change
Adds authentication middleware support in glance (integration to keystone will be performed as a piece of middleware extending this and committed to the keystone repository). Also implements private images. No limited-visibility shared image support is provided yet.
Jay Pipes (jaypipes) wrote : | # |
FYI, all tests pass BTW: http://
Rick Harris (rconradharris) wrote : | # |
Rockin' job here Kevin.
Femto-nit:
> self.auth_tok
Would prefer to see this as `auth_token`, better matches what's already in keystone and nova.
Devin Carlen (devcamcar) wrote : | # |
Glad to see this! lgtm
Kevin L. Mitchell (klmitch) wrote : | # |
On Fri, 2011-07-22 at 22:47 +0000, Jay Pipes wrote:
> This is excellent work, Kevin. Excellent.
Thank you :)
> Some tiny nits from me:
>
> 156 + # Is this a read-only context?
> 157 + if req.context.
>
> That comment is redundant with the if statement IMHO :) There's a few
> of that comment copy/pasted, too.
Yeah, I have a tendency to do that :) I'll remove. Do you see any
other instance of that I should pull out? (I can look, but I might not
even notice...)
> Please use this docstring style in Glance (sorry, yes this is
> different from Nova ... but the same as Swift):
Yeah, that was some of the first code I wrote, so I hadn't gotten a
handle on the docstring style. Ironically, the style you suggest is
pretty much what I do on my own code :)
> 344 + # No owner == image visible
> 345 + if image.owner is None:
> 346 + return True
> 347 +
> 348 + # Image is_public == image visible
> 349 + if image.is_public:
> 350 + return True
>
> Actually, in order to keep existing behaviour, the above would need to be:
>
> if image.owner is None:
> return image.is_public
When you're using the default, non-Keystone-aware ContextMiddleware,
these lines are not even reached, because it sets the is_admin flag on
the context. When you're using a Keystone-aware middleware, the
combined effect of these lines is to allow reading of unowned
is_public=False images, the justification being that providers may wish
to make images available for beta testers without having them show up in
the list to confuse regular users. (Remember that image_visible() is
not used for filtering the list of images, only for checking that users
have permission to read a given image.)
> For boolean things, please name the method something like
> is_image_visible() to make it easier to tell it has a boolean return.
> Also, you might want to consider making it a @property to be
> consistent with the owner @property of RequestContext.
It's not possible to make image_visible() be a property; it has to take
the image as an argument. Renaming it is_image_visible() makes sense,
though.
> 385 + req.context = self.make_
>
> Please remove the is_admin property and concept of RequestContext. We
> can add this functionality later. For now, let's focus on the owner
> (tenant) and the concept of image ownership, which you've done a great
> job on.
I need some kind of context to convey the tenant from the authentication
middleware to its final destination, be it authorization checks or
setting image ownership information. It also gives me an ideal place to
write those authorization checks, i.e.,
image_visible(
done without it.
> 659 + return db_api.
>
> I think you need req.context there, not just context...
That's in glance.
passed the context, not the request.
> 723 + except exception.
> 724 + # If it's private and doesn't belong to them, don't let on
> 725 + # that it exists
> 726 + raise exc.HTTPNotFound()
>
> That's perfectly fine, and good behaviour, but please add a
> logger.info() o...
Kevin L. Mitchell (klmitch) wrote : | # |
> > 385 + req.context = self.make_
> >
> > Please remove the is_admin property and concept of RequestContext. We
> > can add this functionality later. For now, let's focus on the owner
> > (tenant) and the concept of image ownership, which you've done a great
> > job on.
>
> I need some kind of context to convey the tenant from the authentication
> middleware to its final destination, be it authorization checks or
> setting image ownership information. It also gives me an ideal place to
> write those authorization checks, i.e.,
> image_visible(
> done without it.
I'm sorry, I just realized I misunderstood you. As it happens, though,
we need to have the is_admin concept there in order to be fully
backwards-
sets is_admin to True, and it has to be False for regular users under
the keystone-aware middleware that I've already written or the
authorization checks become meaningless. (In fact, I initially did not
set is_admin to True, and ended up with one failing unit test.)
If you want, we could rename it to something like 'compatibility'
the keystone-aware middleware that I've written actually does set it
based on the presence of the 'Admin' role, so it makes more sense to me,
looking at the big picture, to preserve the existing name.
--
Kevin L. Mitchell <email address hidden>
This email may include confidential information. If you received it in error, please delete it.
- 166. By Kevin L. Mitchell
-
Take out extraneous comments; tune up doc string; rename image_visible() to is_image_visible(); log authorization failures
Jay Pipes (jaypipes) wrote : | # |
> I'm sorry, I just realized I misunderstood you. As it happens, though,
> we need to have the is_admin concept there in order to be fully
> backwards-
> sets is_admin to True, and it has to be False for regular users under
> the keystone-aware middleware that I've already written or the
> authorization checks become meaningless. (In fact, I initially did not
> set is_admin to True, and ended up with one failing unit test.)
Right, I understand a little better now.
> If you want, we could rename it to something like 'compatibility'
> the keystone-aware middleware that I've written actually does set it
> based on the presence of the 'Admin' role, so it makes more sense to me,
> looking at the big picture, to preserve the existing name.
OK, no worries.
-jay
Jay Pipes (jaypipes) wrote : | # |
> > Some tiny nits from me:
> >
> > 156 + # Is this a read-only context?
> > 157 + if req.context.
> >
> > That comment is redundant with the if statement IMHO :) There's a few
> > of that comment copy/pasted, too.
>
> Yeah, I have a tendency to do that :) I'll remove. Do you see any
> other instance of that I should pull out? (I can look, but I might not
> even notice...)
Yup, just do a search on "read-only" :)
> > 344 + # No owner == image visible
> > 345 + if image.owner is None:
> > 346 + return True
> > 347 +
> > 348 + # Image is_public == image visible
> > 349 + if image.is_public:
> > 350 + return True
> >
> > Actually, in order to keep existing behaviour, the above would need to be:
> >
> > if image.owner is None:
> > return image.is_public
>
> When you're using the default, non-Keystone-aware ContextMiddleware,
> these lines are not even reached, because it sets the is_admin flag on
> the context.
Right, I now understand that after your last comment...
> When you're using a Keystone-aware middleware, the
> combined effect of these lines is to allow reading of unowned
> is_public=False images, the justification being that providers may wish
> to make images available for beta testers without having them show up in
> the list to confuse regular users. (Remember that image_visible() is
> not used for filtering the list of images, only for checking that users
> have permission to read a given image.)
Right, that's the part I was getting confused on... I understand it now.
> > For boolean things, please name the method something like
> > is_image_visible() to make it easier to tell it has a boolean return.
> > Also, you might want to consider making it a @property to be
> > consistent with the owner @property of RequestContext.
>
> It's not possible to make image_visible() be a property; it has to take
> the image as an argument. Renaming it is_image_visible() makes sense,
> though.
Ah, yes, cool.
> > 659 + return db_api.
> >
> > I think you need req.context there, not just context...
>
> That's in glance.
> passed the context, not the request.
Ah, no prob. Just scanning the code, it looked out of place with the references to req.context around it.
> > 723 + except exception.
> > 724 + # If it's private and doesn't belong to them, don't let on
> > 725 + # that it exists
> > 726 + raise exc.HTTPNotFound()
> >
> > That's perfectly fine, and good behaviour, but please add a
> > logger.info() of the unauthorized access so that some admin will be
> > able to tell what actually happened when the inevitable helpdesk call
> > comes in saying "Why doesn't my image exist?! Whhhaaaaa!"
>
> *nod* makes sense. There weren't very many places in this code that had
> logging, which is why I didn't put it in there.
Ya, getting better over time, but a long way to go :)
-jay
Preview Diff
1 | === modified file 'Authors' |
2 | --- Authors 2011-07-02 04:16:33 +0000 |
3 | +++ Authors 2011-07-22 23:36:34 +0000 |
4 | @@ -12,6 +12,7 @@ |
5 | Josh Kearney <josh@jk0.org> |
6 | Justin Shepherd <jshepher@rackspace.com> |
7 | Ken Pepple <ken.pepple@gmail.com> |
8 | +Kevin L. Mitchell <kevin.mitchell@rackspace.com> |
9 | Matt Dietz <matt.dietz@rackspace.com> |
10 | Monty Taylor <mordred@inaugust.com> |
11 | Rick Clark <rick@openstack.org> |
12 | |
13 | === modified file 'etc/glance-api.conf' |
14 | --- etc/glance-api.conf 2011-05-11 23:03:51 +0000 |
15 | +++ etc/glance-api.conf 2011-07-22 23:36:34 +0000 |
16 | @@ -52,7 +52,7 @@ |
17 | swift_store_create_container_on_put = False |
18 | |
19 | [pipeline:glance-api] |
20 | -pipeline = versionnegotiation apiv1app |
21 | +pipeline = versionnegotiation context apiv1app |
22 | |
23 | [pipeline:versions] |
24 | pipeline = versionsapp |
25 | @@ -65,3 +65,6 @@ |
26 | |
27 | [filter:versionnegotiation] |
28 | paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory |
29 | + |
30 | +[filter:context] |
31 | +paste.filter_factory = glance.common.context:filter_factory |
32 | |
33 | === modified file 'etc/glance-registry.conf' |
34 | --- etc/glance-registry.conf 2011-05-09 18:55:46 +0000 |
35 | +++ etc/glance-registry.conf 2011-07-22 23:36:34 +0000 |
36 | @@ -29,5 +29,11 @@ |
37 | # before MySQL can drop the connection. |
38 | sql_idle_timeout = 3600 |
39 | |
40 | -[app:glance-registry] |
41 | +[pipeline:glance-registry] |
42 | +pipeline = context registryapp |
43 | + |
44 | +[app:registryapp] |
45 | paste.app_factory = glance.registry.server:app_factory |
46 | + |
47 | +[filter:context] |
48 | +paste.filter_factory = glance.common.context:filter_factory |
49 | |
50 | === modified file 'glance/api/v1/images.py' |
51 | --- glance/api/v1/images.py 2011-07-18 18:22:41 +0000 |
52 | +++ glance/api/v1/images.py 2011-07-22 23:36:34 +0000 |
53 | @@ -27,7 +27,8 @@ |
54 | import webob |
55 | from webob.exc import (HTTPNotFound, |
56 | HTTPConflict, |
57 | - HTTPBadRequest) |
58 | + HTTPBadRequest, |
59 | + HTTPForbidden) |
60 | |
61 | from glance.common import exception |
62 | from glance.common import wsgi |
63 | @@ -96,7 +97,8 @@ |
64 | """ |
65 | params = self._get_query_params(req) |
66 | try: |
67 | - images = registry.get_images_list(self.options, **params) |
68 | + images = registry.get_images_list(self.options, req.context, |
69 | + **params) |
70 | except exception.Invalid, e: |
71 | raise HTTPBadRequest(explanation=str(e)) |
72 | |
73 | @@ -126,7 +128,8 @@ |
74 | """ |
75 | params = self._get_query_params(req) |
76 | try: |
77 | - images = registry.get_images_detail(self.options, **params) |
78 | + images = registry.get_images_detail(self.options, req.context, |
79 | + **params) |
80 | except exception.Invalid, e: |
81 | raise HTTPBadRequest(explanation=str(e)) |
82 | return dict(images=images) |
83 | @@ -226,6 +229,7 @@ |
84 | |
85 | try: |
86 | image_meta = registry.add_image_metadata(self.options, |
87 | + req.context, |
88 | image_meta) |
89 | return image_meta |
90 | except exception.Duplicate: |
91 | @@ -238,6 +242,11 @@ |
92 | for line in msg.split('\n'): |
93 | logger.error(line) |
94 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") |
95 | + except exception.NotAuthorized: |
96 | + msg = "Not authorized to reserve image." |
97 | + logger.error(msg) |
98 | + raise HTTPForbidden(msg, request=req, |
99 | + content_type="text/plain") |
100 | |
101 | def _upload(self, req, image_meta): |
102 | """ |
103 | @@ -267,7 +276,7 @@ |
104 | |
105 | image_id = image_meta['id'] |
106 | logger.debug("Setting image %s to status 'saving'" % image_id) |
107 | - registry.update_image_metadata(self.options, image_id, |
108 | + registry.update_image_metadata(self.options, req.context, image_id, |
109 | {'status': 'saving'}) |
110 | try: |
111 | logger.debug("Uploading image data for image %(image_id)s " |
112 | @@ -294,7 +303,8 @@ |
113 | logger.debug("Updating image %(image_id)s data. " |
114 | "Checksum set to %(checksum)s, size set " |
115 | "to %(size)d" % locals()) |
116 | - registry.update_image_metadata(self.options, image_id, |
117 | + registry.update_image_metadata(self.options, req.context, |
118 | + image_id, |
119 | {'checksum': checksum, |
120 | 'size': size}) |
121 | |
122 | @@ -306,6 +316,13 @@ |
123 | self._safe_kill(req, image_id) |
124 | raise HTTPConflict(msg, request=req) |
125 | |
126 | + except exception.NotAuthorized, e: |
127 | + msg = ("Unauthorized upload attempt: %s") % str(e) |
128 | + logger.error(msg) |
129 | + self._safe_kill(req, image_id) |
130 | + raise HTTPForbidden(msg, request=req, |
131 | + content_type='text/plain') |
132 | + |
133 | except Exception, e: |
134 | msg = ("Error uploading image: %s") % str(e) |
135 | logger.error(msg) |
136 | @@ -325,6 +342,7 @@ |
137 | image_meta['location'] = location |
138 | image_meta['status'] = 'active' |
139 | return registry.update_image_metadata(self.options, |
140 | + req.context, |
141 | image_id, |
142 | image_meta) |
143 | |
144 | @@ -336,6 +354,7 @@ |
145 | :param image_id: Opaque image identifier |
146 | """ |
147 | registry.update_image_metadata(self.options, |
148 | + req.context, |
149 | image_id, |
150 | {'status': 'killed'}) |
151 | |
152 | @@ -404,6 +423,12 @@ |
153 | and the request body is not application/octet-stream |
154 | image data. |
155 | """ |
156 | + if req.context.read_only: |
157 | + msg = "Read-only access" |
158 | + logger.debug(msg) |
159 | + raise HTTPForbidden(msg, request=req, |
160 | + content_type="text/plain") |
161 | + |
162 | image_meta = self._reserve(req, image_meta) |
163 | image_id = image_meta['id'] |
164 | |
165 | @@ -425,6 +450,12 @@ |
166 | |
167 | :retval Returns the updated image information as a mapping |
168 | """ |
169 | + if req.context.read_only: |
170 | + msg = "Read-only access" |
171 | + logger.debug(msg) |
172 | + raise HTTPForbidden(msg, request=req, |
173 | + content_type="text/plain") |
174 | + |
175 | orig_image_meta = self.get_image_meta_or_404(req, id) |
176 | orig_status = orig_image_meta['status'] |
177 | |
178 | @@ -432,8 +463,9 @@ |
179 | raise HTTPConflict("Cannot upload to an unqueued image") |
180 | |
181 | try: |
182 | - image_meta = registry.update_image_metadata(self.options, id, |
183 | - image_meta, True) |
184 | + image_meta = registry.update_image_metadata(self.options, |
185 | + req.context, id, |
186 | + image_meta, True) |
187 | if image_data is not None: |
188 | image_meta = self._upload_and_activate(req, image_meta) |
189 | except exception.Invalid, e: |
190 | @@ -457,6 +489,12 @@ |
191 | :raises HttpNotAuthorized if image or any chunk is not |
192 | deleteable by the requesting user |
193 | """ |
194 | + if req.context.read_only: |
195 | + msg = "Read-only access" |
196 | + logger.debug(msg) |
197 | + raise HTTPForbidden(msg, request=req, |
198 | + content_type="text/plain") |
199 | + |
200 | image = self.get_image_meta_or_404(req, id) |
201 | |
202 | # The image's location field may be None in the case |
203 | @@ -471,7 +509,7 @@ |
204 | "Continuing with deletion from registry." |
205 | logger.error(msg % (image['location'],)) |
206 | |
207 | - registry.delete_image_metadata(self.options, id) |
208 | + registry.delete_image_metadata(self.options, req.context, id) |
209 | |
210 | def get_image_meta_or_404(self, request, id): |
211 | """ |
212 | @@ -484,12 +522,18 @@ |
213 | :raises HTTPNotFound if image does not exist |
214 | """ |
215 | try: |
216 | - return registry.get_image_metadata(self.options, id) |
217 | + return registry.get_image_metadata(self.options, |
218 | + request.context, id) |
219 | except exception.NotFound: |
220 | msg = "Image with identifier %s not found" % id |
221 | logger.debug(msg) |
222 | raise HTTPNotFound(msg, request=request, |
223 | content_type='text/plain') |
224 | + except exception.NotAuthorized: |
225 | + msg = "Unauthorized image access" |
226 | + logger.debug(msg) |
227 | + raise HTTPForbidden(msg, request=request, |
228 | + content_type='text/plain') |
229 | |
230 | def get_store_or_400(self, request, store_name): |
231 | """ |
232 | |
233 | === modified file 'glance/client.py' |
234 | --- glance/client.py 2011-06-29 15:42:49 +0000 |
235 | +++ glance/client.py 2011-07-22 23:36:34 +0000 |
236 | @@ -35,7 +35,8 @@ |
237 | |
238 | DEFAULT_PORT = 9292 |
239 | |
240 | - def __init__(self, host, port=None, use_ssl=False, doc_root="/v1"): |
241 | + def __init__(self, host, port=None, use_ssl=False, doc_root="/v1", |
242 | + auth_tok=None): |
243 | """ |
244 | Creates a new client to a Glance API service. |
245 | |
246 | @@ -43,10 +44,11 @@ |
247 | :param port: The port where Glance resides (defaults to 9292) |
248 | :param use_ssl: Should we use HTTPS? (defaults to False) |
249 | :param doc_root: Prefix for all URLs we request from host |
250 | + :param auth_tok: The auth token to pass to the server |
251 | """ |
252 | port = port or self.DEFAULT_PORT |
253 | self.doc_root = doc_root |
254 | - super(Client, self).__init__(host, port, use_ssl) |
255 | + super(Client, self).__init__(host, port, use_ssl, auth_tok) |
256 | |
257 | def do_request(self, method, action, body=None, headers=None, params=None): |
258 | action = "%s/%s" % (self.doc_root, action.lstrip("/")) |
259 | |
260 | === modified file 'glance/common/client.py' |
261 | --- glance/common/client.py 2011-06-28 22:01:08 +0000 |
262 | +++ glance/common/client.py 2011-07-22 23:36:34 +0000 |
263 | @@ -41,17 +41,19 @@ |
264 | |
265 | CHUNKSIZE = 65536 |
266 | |
267 | - def __init__(self, host, port, use_ssl): |
268 | + def __init__(self, host, port, use_ssl, auth_tok): |
269 | """ |
270 | Creates a new client to some service. |
271 | |
272 | :param host: The host where service resides |
273 | :param port: The port where service resides |
274 | :param use_ssl: Should we use HTTPS? |
275 | + :param auth_tok: The auth token to pass to the server |
276 | """ |
277 | self.host = host |
278 | self.port = port |
279 | self.use_ssl = use_ssl |
280 | + self.auth_tok = auth_tok |
281 | self.connection = None |
282 | |
283 | def get_connection_type(self): |
284 | @@ -99,6 +101,8 @@ |
285 | try: |
286 | connection_type = self.get_connection_type() |
287 | headers = headers or {} |
288 | + if 'x-auth-token' not in headers and self.auth_tok: |
289 | + headers['x-auth-token'] = self.auth_tok |
290 | c = connection_type(self.host, self.port) |
291 | |
292 | # Do a simple request or a chunked request, depending |
293 | |
294 | === added file 'glance/common/context.py' |
295 | --- glance/common/context.py 1970-01-01 00:00:00 +0000 |
296 | +++ glance/common/context.py 2011-07-22 23:36:34 +0000 |
297 | @@ -0,0 +1,96 @@ |
298 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
299 | + |
300 | +# Copyright 2011 OpenStack LLC. |
301 | +# All Rights Reserved. |
302 | +# |
303 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
304 | +# not use this file except in compliance with the License. You may obtain |
305 | +# a copy of the License at |
306 | +# |
307 | +# http://www.apache.org/licenses/LICENSE-2.0 |
308 | +# |
309 | +# Unless required by applicable law or agreed to in writing, software |
310 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
311 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
312 | +# License for the specific language governing permissions and limitations |
313 | +# under the License. |
314 | + |
315 | +from glance.common import utils |
316 | +from glance.common import wsgi |
317 | + |
318 | + |
319 | +class RequestContext(object): |
320 | + """ |
321 | + Stores information about the security context under which the user |
322 | + accesses the system, as well as additional request information. |
323 | + """ |
324 | + |
325 | + def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False, |
326 | + read_only=False): |
327 | + self.auth_tok = auth_tok |
328 | + self.user = user |
329 | + self.tenant = tenant |
330 | + self.is_admin = is_admin |
331 | + self.read_only = read_only |
332 | + |
333 | + def is_image_visible(self, image): |
334 | + """Return True if the image is visible in this context.""" |
335 | + # Is admin == image visible |
336 | + if self.is_admin: |
337 | + return True |
338 | + |
339 | + # No owner == image visible |
340 | + if image.owner is None: |
341 | + return True |
342 | + |
343 | + # Image is_public == image visible |
344 | + if image.is_public: |
345 | + return True |
346 | + |
347 | + # Private image |
348 | + return self.owner is not None and self.owner == image.owner |
349 | + |
350 | + @property |
351 | + def owner(self): |
352 | + """Return the owner to correlate with an image.""" |
353 | + return self.tenant |
354 | + |
355 | + |
356 | +class ContextMiddleware(wsgi.Middleware): |
357 | + def __init__(self, app, options): |
358 | + self.options = options |
359 | + super(ContextMiddleware, self).__init__(app) |
360 | + |
361 | + def make_context(self, *args, **kwargs): |
362 | + """ |
363 | + Create a context with the given arguments. |
364 | + """ |
365 | + |
366 | + # Determine the context class to use |
367 | + ctxcls = RequestContext |
368 | + if 'context_class' in self.options: |
369 | + ctxcls = utils.import_class(self.options['context_class']) |
370 | + |
371 | + return ctxcls(*args, **kwargs) |
372 | + |
373 | + def process_request(self, req): |
374 | + """ |
375 | + Extract any authentication information in the request and |
376 | + construct an appropriate context from it. |
377 | + """ |
378 | + # Use the default empty context, with admin turned on for |
379 | + # backwards compatibility |
380 | + req.context = self.make_context(is_admin=True) |
381 | + |
382 | + |
383 | +def filter_factory(global_conf, **local_conf): |
384 | + """ |
385 | + Factory method for paste.deploy |
386 | + """ |
387 | + conf = global_conf.copy() |
388 | + conf.update(local_conf) |
389 | + |
390 | + def filter(app): |
391 | + return ContextMiddleware(app, conf) |
392 | + |
393 | + return filter |
394 | |
395 | === modified file 'glance/registry/__init__.py' |
396 | --- glance/registry/__init__.py 2011-05-16 14:43:33 +0000 |
397 | +++ glance/registry/__init__.py 2011-07-22 23:36:34 +0000 |
398 | @@ -26,33 +26,33 @@ |
399 | logger = logging.getLogger('glance.registry') |
400 | |
401 | |
402 | -def get_registry_client(options): |
403 | +def get_registry_client(options, cxt): |
404 | host = options['registry_host'] |
405 | port = int(options['registry_port']) |
406 | - return client.RegistryClient(host, port) |
407 | - |
408 | - |
409 | -def get_images_list(options, **kwargs): |
410 | - c = get_registry_client(options) |
411 | + return client.RegistryClient(host, port, auth_tok=cxt.auth_tok) |
412 | + |
413 | + |
414 | +def get_images_list(options, context, **kwargs): |
415 | + c = get_registry_client(options, context) |
416 | return c.get_images(**kwargs) |
417 | |
418 | |
419 | -def get_images_detail(options, **kwargs): |
420 | - c = get_registry_client(options) |
421 | +def get_images_detail(options, context, **kwargs): |
422 | + c = get_registry_client(options, context) |
423 | return c.get_images_detailed(**kwargs) |
424 | |
425 | |
426 | -def get_image_metadata(options, image_id): |
427 | - c = get_registry_client(options) |
428 | +def get_image_metadata(options, context, image_id): |
429 | + c = get_registry_client(options, context) |
430 | return c.get_image(image_id) |
431 | |
432 | |
433 | -def add_image_metadata(options, image_meta): |
434 | +def add_image_metadata(options, context, image_meta): |
435 | if options['debug']: |
436 | logger.debug("Adding image metadata...") |
437 | _debug_print_metadata(image_meta) |
438 | |
439 | - c = get_registry_client(options) |
440 | + c = get_registry_client(options, context) |
441 | new_image_meta = c.add_image(image_meta) |
442 | |
443 | if options['debug']: |
444 | @@ -63,12 +63,13 @@ |
445 | return new_image_meta |
446 | |
447 | |
448 | -def update_image_metadata(options, image_id, image_meta, purge_props=False): |
449 | +def update_image_metadata(options, context, image_id, image_meta, |
450 | + purge_props=False): |
451 | if options['debug']: |
452 | logger.debug("Updating image metadata for image %s...", image_id) |
453 | _debug_print_metadata(image_meta) |
454 | |
455 | - c = get_registry_client(options) |
456 | + c = get_registry_client(options, context) |
457 | new_image_meta = c.update_image(image_id, image_meta, purge_props) |
458 | |
459 | if options['debug']: |
460 | @@ -79,9 +80,9 @@ |
461 | return new_image_meta |
462 | |
463 | |
464 | -def delete_image_metadata(options, image_id): |
465 | +def delete_image_metadata(options, context, image_id): |
466 | logger.debug("Deleting image metadata for image %s...", image_id) |
467 | - c = get_registry_client(options) |
468 | + c = get_registry_client(options, context) |
469 | return c.delete_image(image_id) |
470 | |
471 | |
472 | |
473 | === modified file 'glance/registry/client.py' |
474 | --- glance/registry/client.py 2011-06-28 14:37:31 +0000 |
475 | +++ glance/registry/client.py 2011-07-22 23:36:34 +0000 |
476 | @@ -33,16 +33,17 @@ |
477 | |
478 | DEFAULT_PORT = 9191 |
479 | |
480 | - def __init__(self, host, port=None, use_ssl=False): |
481 | + def __init__(self, host, port=None, use_ssl=False, auth_tok=None): |
482 | """ |
483 | Creates a new client to a Glance Registry service. |
484 | |
485 | :param host: The host where Glance resides |
486 | :param port: The port where Glance resides (defaults to 9191) |
487 | :param use_ssl: Should we use HTTPS? (defaults to False) |
488 | + :param auth_tok: The auth token to pass to the server |
489 | """ |
490 | port = port or self.DEFAULT_PORT |
491 | - super(RegistryClient, self).__init__(host, port, use_ssl) |
492 | + super(RegistryClient, self).__init__(host, port, use_ssl, auth_tok) |
493 | |
494 | def get_images(self, **kwargs): |
495 | """ |
496 | |
497 | === modified file 'glance/registry/db/api.py' |
498 | --- glance/registry/db/api.py 2011-07-20 20:52:44 +0000 |
499 | +++ glance/registry/db/api.py 2011-07-22 23:36:34 +0000 |
500 | @@ -46,7 +46,8 @@ |
501 | |
502 | IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size', |
503 | 'disk_format', 'container_format', |
504 | - 'is_public', 'location', 'checksum']) |
505 | + 'is_public', 'location', 'checksum', |
506 | + 'owner']) |
507 | |
508 | CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf'] |
509 | DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', |
510 | @@ -120,7 +121,7 @@ |
511 | """Get an image or raise if it does not exist.""" |
512 | session = session or get_session() |
513 | try: |
514 | - return session.query(models.Image).\ |
515 | + image = session.query(models.Image).\ |
516 | options(joinedload(models.Image.properties)).\ |
517 | filter_by(deleted=_deleted(context)).\ |
518 | filter_by(id=image_id).\ |
519 | @@ -128,6 +129,12 @@ |
520 | except exc.NoResultFound: |
521 | raise exception.NotFound("No image found with ID %s" % image_id) |
522 | |
523 | + # Make sure they can look at it |
524 | + if not context.is_image_visible(image): |
525 | + raise exception.NotAuthorized("Image not visible to you") |
526 | + |
527 | + return image |
528 | + |
529 | |
530 | def image_get_all(context, filters=None, marker=None, limit=None, |
531 | sort_key='created_at', sort_dir='desc'): |
532 | @@ -168,6 +175,13 @@ |
533 | query = query.filter(models.Image.size <= filters['size_max']) |
534 | del filters['size_max'] |
535 | |
536 | + if 'is_public' in filters and filters['is_public'] is not None: |
537 | + the_filter = models.Image.is_public == filters['is_public'] |
538 | + if filters['is_public'] and context.owner is not None: |
539 | + the_filter = or_(the_filter, models.Image.owner == context.owner) |
540 | + query = query.filter(the_filter) |
541 | + del filters['is_public'] |
542 | + |
543 | for (k, v) in filters.pop('properties', {}).items(): |
544 | query = query.filter(models.Image.properties.any(name=k, value=v)) |
545 | |
546 | |
547 | === added file 'glance/registry/db/migrate_repo/versions/007_add_owner.py' |
548 | --- glance/registry/db/migrate_repo/versions/007_add_owner.py 1970-01-01 00:00:00 +0000 |
549 | +++ glance/registry/db/migrate_repo/versions/007_add_owner.py 2011-07-22 23:36:34 +0000 |
550 | @@ -0,0 +1,82 @@ |
551 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
552 | + |
553 | +# Copyright 2011 OpenStack LLC. |
554 | +# All Rights Reserved. |
555 | +# |
556 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
557 | +# not use this file except in compliance with the License. You may obtain |
558 | +# a copy of the License at |
559 | +# |
560 | +# http://www.apache.org/licenses/LICENSE-2.0 |
561 | +# |
562 | +# Unless required by applicable law or agreed to in writing, software |
563 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
564 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
565 | +# License for the specific language governing permissions and limitations |
566 | +# under the License. |
567 | + |
568 | +from migrate.changeset import * |
569 | +from sqlalchemy import * |
570 | +from sqlalchemy.sql import and_, not_ |
571 | + |
572 | +from glance.registry.db.migrate_repo.schema import ( |
573 | + Boolean, DateTime, BigInteger, Integer, String, |
574 | + Text, from_migration_import) |
575 | + |
576 | + |
577 | +def get_images_table(meta): |
578 | + """ |
579 | + Returns the Table object for the images table that corresponds to |
580 | + the images table definition of this version. |
581 | + """ |
582 | + images = Table('images', meta, |
583 | + Column('id', Integer(), primary_key=True, nullable=False), |
584 | + Column('name', String(255)), |
585 | + Column('disk_format', String(20)), |
586 | + Column('container_format', String(20)), |
587 | + Column('size', BigInteger()), |
588 | + Column('status', String(30), nullable=False), |
589 | + Column('is_public', Boolean(), nullable=False, default=False, |
590 | + index=True), |
591 | + Column('location', Text()), |
592 | + Column('created_at', DateTime(), nullable=False), |
593 | + Column('updated_at', DateTime()), |
594 | + Column('deleted_at', DateTime()), |
595 | + Column('deleted', Boolean(), nullable=False, default=False, |
596 | + index=True), |
597 | + Column('checksum', String(32)), |
598 | + Column('owner', String(255)), |
599 | + mysql_engine='InnoDB', |
600 | + useexisting=True) |
601 | + |
602 | + return images |
603 | + |
604 | + |
605 | +def get_image_properties_table(meta): |
606 | + """ |
607 | + No changes to the image properties table from 006... |
608 | + """ |
609 | + (get_image_properties_table,) = from_migration_import( |
610 | + '006_key_to_name', ['get_image_properties_table']) |
611 | + |
612 | + image_properties = get_image_properties_table(meta) |
613 | + return image_properties |
614 | + |
615 | + |
616 | +def upgrade(migrate_engine): |
617 | + meta = MetaData() |
618 | + meta.bind = migrate_engine |
619 | + |
620 | + images = get_images_table(meta) |
621 | + |
622 | + owner = Column('owner', String(255)) |
623 | + owner.create(images) |
624 | + |
625 | + |
626 | +def downgrade(migrate_engine): |
627 | + meta = MetaData() |
628 | + meta.bind = migrate_engine |
629 | + |
630 | + images = get_images_table(meta) |
631 | + |
632 | + images.columns['owner'].drop() |
633 | |
634 | === modified file 'glance/registry/db/models.py' |
635 | --- glance/registry/db/models.py 2011-07-08 15:43:56 +0000 |
636 | +++ glance/registry/db/models.py 2011-07-22 23:36:34 +0000 |
637 | @@ -105,6 +105,7 @@ |
638 | is_public = Column(Boolean, nullable=False, default=False) |
639 | location = Column(Text) |
640 | checksum = Column(String(32)) |
641 | + owner = Column(String(255)) |
642 | |
643 | |
644 | class ImageProperty(BASE, ModelBase): |
645 | |
646 | === modified file 'glance/registry/server.py' |
647 | --- glance/registry/server.py 2011-07-18 18:22:41 +0000 |
648 | +++ glance/registry/server.py 2011-07-22 23:36:34 +0000 |
649 | @@ -61,7 +61,7 @@ |
650 | Get images, wrapping in exception if necessary. |
651 | """ |
652 | try: |
653 | - return db_api.image_get_all(None, **params) |
654 | + return db_api.image_get_all(context, **params) |
655 | except exception.NotFound, e: |
656 | msg = "Invalid marker. Image could not be found." |
657 | raise exc.HTTPBadRequest(explanation=msg) |
658 | @@ -87,7 +87,7 @@ |
659 | } |
660 | """ |
661 | params = self._get_query_params(req) |
662 | - images = self._get_images(None, **params) |
663 | + images = self._get_images(req.context, **params) |
664 | |
665 | results = [] |
666 | for image in images: |
667 | @@ -111,7 +111,7 @@ |
668 | """ |
669 | params = self._get_query_params(req) |
670 | |
671 | - images = self._get_images(None, **params) |
672 | + images = self._get_images(req.context, **params) |
673 | image_dicts = [make_image_dict(i) for i in images] |
674 | return dict(images=image_dicts) |
675 | |
676 | @@ -146,7 +146,11 @@ |
677 | filters = {} |
678 | properties = {} |
679 | |
680 | - filters['is_public'] = self._get_is_public(req) |
681 | + if req.context.is_admin: |
682 | + # Only admin gets to look for non-public images |
683 | + filters['is_public'] = self._get_is_public(req) |
684 | + else: |
685 | + filters['is_public'] = True |
686 | for param in req.str_params: |
687 | if param in SUPPORTED_FILTERS: |
688 | filters[param] = req.str_params.get(param) |
689 | @@ -223,9 +227,15 @@ |
690 | def show(self, req, id): |
691 | """Return data about the given image id.""" |
692 | try: |
693 | - image = db_api.image_get(None, id) |
694 | + image = db_api.image_get(req.context, id) |
695 | except exception.NotFound: |
696 | raise exc.HTTPNotFound() |
697 | + except exception.NotAuthorized: |
698 | + # If it's private and doesn't belong to them, don't let on |
699 | + # that it exists |
700 | + logger.info("Access by %s to image %s denied" % |
701 | + (req.context.user, id)) |
702 | + raise exc.HTTPNotFound() |
703 | |
704 | return dict(image=make_image_dict(image)) |
705 | |
706 | @@ -238,11 +248,19 @@ |
707 | |
708 | :retval Returns 200 if delete was successful, a fault if not. |
709 | """ |
710 | - context = None |
711 | + if req.context.read_only: |
712 | + raise exc.HTTPForbidden() |
713 | + |
714 | try: |
715 | - db_api.image_destroy(context, id) |
716 | + db_api.image_destroy(req.context, id) |
717 | except exception.NotFound: |
718 | return exc.HTTPNotFound() |
719 | + except exception.NotAuthorized: |
720 | + # If it's private and doesn't belong to them, don't let on |
721 | + # that it exists |
722 | + logger.info("Access by %s to image %s denied" % |
723 | + (req.context.user, id)) |
724 | + raise exc.HTTPNotFound() |
725 | |
726 | def create(self, req, body): |
727 | """ |
728 | @@ -255,14 +273,20 @@ |
729 | which will include the newly-created image's internal id |
730 | in the 'id' field |
731 | """ |
732 | + if req.context.read_only: |
733 | + raise exc.HTTPForbidden() |
734 | + |
735 | image_data = body['image'] |
736 | |
737 | # Ensure the image has a status set |
738 | image_data.setdefault('status', 'active') |
739 | |
740 | - context = None |
741 | + # Set up the image owner |
742 | + if not req.context.is_admin or 'owner' not in image_data: |
743 | + image_data['owner'] = req.context.owner |
744 | + |
745 | try: |
746 | - image_data = db_api.image_create(context, image_data) |
747 | + image_data = db_api.image_create(req.context, image_data) |
748 | return dict(image=make_image_dict(image_data)) |
749 | except exception.Duplicate: |
750 | msg = ("Image with identifier %s already exists!" % id) |
751 | @@ -283,18 +307,25 @@ |
752 | |
753 | :retval Returns the updated image information as a mapping, |
754 | """ |
755 | + if req.context.read_only: |
756 | + raise exc.HTTPForbidden() |
757 | + |
758 | image_data = body['image'] |
759 | |
760 | + # Prohibit modification of 'owner' |
761 | + if not req.context.is_admin and 'owner' in image_data: |
762 | + del image_data['owner'] |
763 | + |
764 | purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false") |
765 | - context = None |
766 | try: |
767 | logger.debug("Updating image %(id)s with metadata: %(image_data)r" |
768 | % locals()) |
769 | if purge_props == "true": |
770 | - updated_image = db_api.image_update(context, id, image_data, |
771 | - True) |
772 | + updated_image = db_api.image_update(req.context, id, |
773 | + image_data, True) |
774 | else: |
775 | - updated_image = db_api.image_update(context, id, image_data) |
776 | + updated_image = db_api.image_update(req.context, id, |
777 | + image_data) |
778 | return dict(image=make_image_dict(updated_image)) |
779 | except exception.Invalid, e: |
780 | msg = ("Failed to update image metadata. " |
781 | @@ -305,6 +336,14 @@ |
782 | raise exc.HTTPNotFound(body='Image not found', |
783 | request=req, |
784 | content_type='text/plain') |
785 | + except exception.NotAuthorized: |
786 | + # If it's private and doesn't belong to them, don't let on |
787 | + # that it exists |
788 | + logger.info("Access by %s to image %s denied" % |
789 | + (req.context.user, id)) |
790 | + raise exc.HTTPNotFound(body='Image not found', |
791 | + request=req, |
792 | + content_type='text/plain') |
793 | |
794 | |
795 | def create_resource(options): |
796 | |
797 | === modified file 'tests/functional/__init__.py' |
798 | --- tests/functional/__init__.py 2011-07-08 15:43:56 +0000 |
799 | +++ tests/functional/__init__.py 2011-07-22 23:36:34 +0000 |
800 | @@ -149,7 +149,7 @@ |
801 | log_file = %(log_file)s |
802 | |
803 | [pipeline:glance-api] |
804 | -pipeline = versionnegotiation apiv1app |
805 | +pipeline = versionnegotiation context apiv1app |
806 | |
807 | [pipeline:versions] |
808 | pipeline = versionsapp |
809 | @@ -162,6 +162,9 @@ |
810 | |
811 | [filter:versionnegotiation] |
812 | paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory |
813 | + |
814 | +[filter:context] |
815 | +paste.filter_factory = glance.common.context:filter_factory |
816 | """ |
817 | |
818 | |
819 | @@ -191,8 +194,14 @@ |
820 | sql_connection = %(sql_connection)s |
821 | sql_idle_timeout = 3600 |
822 | |
823 | -[app:glance-registry] |
824 | +[pipeline:glance-registry] |
825 | +pipeline = context registryapp |
826 | + |
827 | +[app:registryapp] |
828 | paste.app_factory = glance.registry.server:app_factory |
829 | + |
830 | +[filter:context] |
831 | +paste.filter_factory = glance.common.context:filter_factory |
832 | """ |
833 | |
834 | |
835 | |
836 | === modified file 'tests/stubs.py' |
837 | --- tests/stubs.py 2011-07-20 16:41:38 +0000 |
838 | +++ tests/stubs.py 2011-07-22 23:36:34 +0000 |
839 | @@ -29,6 +29,7 @@ |
840 | import webob |
841 | |
842 | import glance.common.client |
843 | +from glance.common import context |
844 | from glance.common import exception |
845 | from glance.registry import server as rserver |
846 | from glance.api import v1 as server |
847 | @@ -172,7 +173,8 @@ |
848 | "sqlite://") |
849 | options = {'sql_connection': sql_connection, 'verbose': VERBOSE, |
850 | 'debug': DEBUG} |
851 | - res = self.req.get_response(rserver.API(options)) |
852 | + api = context.ContextMiddleware(rserver.API(options), options) |
853 | + res = self.req.get_response(api) |
854 | |
855 | # httplib.Response has a read() method...fake it out |
856 | def fake_reader(): |
857 | @@ -223,7 +225,8 @@ |
858 | 'registry_port': '9191', |
859 | 'default_store': 'file', |
860 | 'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR} |
861 | - res = self.req.get_response(server.API(options)) |
862 | + api = context.ContextMiddleware(server.API(options), options) |
863 | + res = self.req.get_response(api) |
864 | |
865 | # httplib.Response has a read() method...fake it out |
866 | def fake_reader(): |
867 | |
868 | === modified file 'tests/unit/test_api.py' |
869 | --- tests/unit/test_api.py 2011-07-18 18:22:41 +0000 |
870 | +++ tests/unit/test_api.py 2011-07-22 23:36:34 +0000 |
871 | @@ -26,6 +26,7 @@ |
872 | import webob |
873 | |
874 | from glance.api import v1 as server |
875 | +from glance.common import context |
876 | from glance.registry import server as rserver |
877 | import glance.registry.db.api |
878 | from tests import stubs |
879 | @@ -41,8 +42,9 @@ |
880 | stubs.stub_out_registry_and_store_server(self.stubs) |
881 | stubs.stub_out_registry_db_image_api(self.stubs) |
882 | stubs.stub_out_filesystem_backend() |
883 | - self.api = rserver.API({'verbose': VERBOSE, |
884 | - 'debug': DEBUG}) |
885 | + options = {'verbose': VERBOSE, |
886 | + 'debug': DEBUG} |
887 | + self.api = context.ContextMiddleware(rserver.API(options), options) |
888 | |
889 | def tearDown(self): |
890 | """Clear the test environment""" |
891 | @@ -1458,7 +1460,7 @@ |
892 | 'sql_connection': sql_connection, |
893 | 'default_store': 'file', |
894 | 'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR} |
895 | - self.api = server.API(options) |
896 | + self.api = context.ContextMiddleware(server.API(options), options) |
897 | |
898 | def tearDown(self): |
899 | """Clear the test environment""" |
900 | |
901 | === added file 'tests/unit/test_context.py' |
902 | --- tests/unit/test_context.py 1970-01-01 00:00:00 +0000 |
903 | +++ tests/unit/test_context.py 2011-07-22 23:36:34 +0000 |
904 | @@ -0,0 +1,148 @@ |
905 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
906 | + |
907 | +# Copyright 2010-2011 OpenStack, LLC |
908 | +# All Rights Reserved. |
909 | +# |
910 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
911 | +# not use this file except in compliance with the License. You may obtain |
912 | +# a copy of the License at |
913 | +# |
914 | +# http://www.apache.org/licenses/LICENSE-2.0 |
915 | +# |
916 | +# Unless required by applicable law or agreed to in writing, software |
917 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
918 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
919 | +# License for the specific language governing permissions and limitations |
920 | +# under the License. |
921 | + |
922 | +import unittest |
923 | + |
924 | +from glance.common import context |
925 | + |
926 | + |
927 | +class FakeImage(object): |
928 | + """ |
929 | + Fake image for providing the image attributes needed for |
930 | + TestContext. |
931 | + """ |
932 | + |
933 | + def __init__(self, owner, is_public): |
934 | + self.owner = owner |
935 | + self.is_public = is_public |
936 | + |
937 | + |
938 | +class TestContext(unittest.TestCase): |
939 | + def do_visible(self, exp_res, img_owner, img_public, **kwargs): |
940 | + """ |
941 | + Perform a context test. Creates a (fake) image with the |
942 | + specified owner and is_public attributes, then creates a |
943 | + context with the given keyword arguments and expects exp_res |
944 | + as the result of an is_image_visible() call on the context. |
945 | + """ |
946 | + |
947 | + img = FakeImage(img_owner, img_public) |
948 | + ctx = context.RequestContext(**kwargs) |
949 | + |
950 | + self.assertEqual(ctx.is_image_visible(img), exp_res) |
951 | + |
952 | + def test_empty_public(self): |
953 | + """ |
954 | + Tests that an empty context (with is_admin set to True) can |
955 | + access an image with is_public set to True. |
956 | + """ |
957 | + self.do_visible(True, None, True, is_admin=True) |
958 | + |
959 | + def test_empty_public_owned(self): |
960 | + """ |
961 | + Tests that an empty context (with is_admin set to True) can |
962 | + access an owned image with is_public set to True. |
963 | + """ |
964 | + self.do_visible(True, 'pattieblack', True, is_admin=True) |
965 | + |
966 | + def test_empty_private(self): |
967 | + """ |
968 | + Tests that an empty context (with is_admin set to True) can |
969 | + access an image with is_public set to False. |
970 | + """ |
971 | + self.do_visible(True, None, False, is_admin=True) |
972 | + |
973 | + def test_empty_private_owned(self): |
974 | + """ |
975 | + Tests that an empty context (with is_admin set to True) can |
976 | + access an owned image with is_public set to False. |
977 | + """ |
978 | + self.do_visible(True, 'pattieblack', False, is_admin=True) |
979 | + |
980 | + def test_anon_public(self): |
981 | + """ |
982 | + Tests that an anonymous context (with is_admin set to False) |
983 | + can access an image with is_public set to True. |
984 | + """ |
985 | + self.do_visible(True, None, True) |
986 | + |
987 | + def test_anon_public_owned(self): |
988 | + """ |
989 | + Tests that an anonymous context (with is_admin set to False) |
990 | + can access an owned image with is_public set to True. |
991 | + """ |
992 | + self.do_visible(True, 'pattieblack', True) |
993 | + |
994 | + def test_anon_private(self): |
995 | + """ |
996 | + Tests that an anonymous context (with is_admin set to False) |
997 | + can access an unowned image with is_public set to False. |
998 | + """ |
999 | + self.do_visible(True, None, False) |
1000 | + |
1001 | + def test_anon_private_owned(self): |
1002 | + """ |
1003 | + Tests that an anonymous context (with is_admin set to False) |
1004 | + cannot access an owned image with is_public set to False. |
1005 | + """ |
1006 | + self.do_visible(False, 'pattieblack', False) |
1007 | + |
1008 | + def test_auth_public(self): |
1009 | + """ |
1010 | + Tests that an authenticated context (with is_admin set to |
1011 | + False) can access an image with is_public set to True. |
1012 | + """ |
1013 | + self.do_visible(True, None, True, tenant='froggy') |
1014 | + |
1015 | + def test_auth_public_unowned(self): |
1016 | + """ |
1017 | + Tests that an authenticated context (with is_admin set to |
1018 | + False) can access an image (which it does not own) with |
1019 | + is_public set to True. |
1020 | + """ |
1021 | + self.do_visible(True, 'pattieblack', True, tenant='froggy') |
1022 | + |
1023 | + def test_auth_public_owned(self): |
1024 | + """ |
1025 | + Tests that an authenticated context (with is_admin set to |
1026 | + False) can access an image (which it does own) with is_public |
1027 | + set to True. |
1028 | + """ |
1029 | + self.do_visible(True, 'pattieblack', True, tenant='pattieblack') |
1030 | + |
1031 | + def test_auth_private(self): |
1032 | + """ |
1033 | + Tests that an authenticated context (with is_admin set to |
1034 | + False) can access an image with is_public set to False. |
1035 | + """ |
1036 | + self.do_visible(True, None, False, tenant='froggy') |
1037 | + |
1038 | + def test_auth_private_unowned(self): |
1039 | + """ |
1040 | + Tests that an authenticated context (with is_admin set to |
1041 | + False) cannot access an image (which it does not own) with |
1042 | + is_public set to False. |
1043 | + """ |
1044 | + self.do_visible(False, 'pattieblack', False, tenant='froggy') |
1045 | + |
1046 | + def test_auth_private_owned(self): |
1047 | + """ |
1048 | + Tests that an authenticated context (with is_admin set to |
1049 | + False) can access an image (which it does own) with is_public |
1050 | + set to False. |
1051 | + """ |
1052 | + self.do_visible(True, 'pattieblack', False, tenant='pattieblack') |
This is excellent work, Kevin. Excellent.
Some tiny nits from me:
156 + # Is this a read-only context? read_only:
157 + if req.context.
That comment is redundant with the if statement IMHO :) There's a few of that comment copy/pasted, too.
323 + """Security context and request information.
324 +
325 + Stores information about the security context under which the user
326 + accesses the system, as well as additional request information.
327 +
328 + """
Please use this docstring style in Glance (sorry, yes this is different from Nova ... but the same as Swift):
"""
Security context and request information.
Stores information about the security context under which the user
accesses the system, as well as additional request information.
"""
There is also no need to put a short description, followed by a longer description for no real reason. In other words, you can just do this:
"""
Object that stores information about the security context under
which the user accesses the system, as well as additional request information.
"""
344 + # No owner == image visible
345 + if image.owner is None:
346 + return True
347 +
348 + # Image is_public == image visible
349 + if image.is_public:
350 + return True
Actually, in order to keep existing behaviour, the above would need to be:
if image.owner is None:
return image.is_public
If there is no image owner, then the image's visibility should default to its is_public setting in the registry database...
338 + def image_visible(self, image):
339 + """Return True if the image is visible in this context."""
For boolean things, please name the method something like is_image_visible() to make it easier to tell it has a boolean return. Also, you might want to consider making it a @property to be consistent with the owner @property of RequestContext.
385 + req.context = self.make_ context( is_admin= True)
Please remove the is_admin property and concept of RequestContext. We can add this functionality later. For now, let's focus on the owner (tenant) and the concept of image ownership, which you've done a great job on.
659 + return db_api. image_get_ all(context, **params)
I think you need req.context there, not just context...
723 + except exception. NotAuthorized:
724 + # If it's private and doesn't belong to them, don't let on
725 + # that it exists
726 + raise exc.HTTPNotFound()
That's perfectly fine, and good behaviour, but please add a logger.info() of the unauthorized access so that some admin will be able to tell what actually happened when the inevitable helpdesk call comes in saying "Why doesn't my image exist?! Whhhaaaaa!"
Cheers, and great patch, Kevin! Thank you for taking this task on.
-jay