Add policy checking for basic image operations
Partially implements bp interim-glance-authz-service. This checks basic image operations: get_images, get_image, add_image, modify_image, delete_image. It looks for a policy json file next to our config files. Change-Id: I07f29c11934c68d38a6bdadd39f9dc841b61648a
This commit is contained in:
parent
b712949ec5
commit
eeb4d1ee29
1
.mailmap
1
.mailmap
@ -1,6 +1,7 @@
|
||||
# Format is:
|
||||
# <preferred e-mail> <other e-mail 1>
|
||||
# <preferred e-mail> <other e-mail 2>
|
||||
<brian.waldon@rackspace.com> <bcwaldon@gmail.com>
|
||||
<corywright@gmail.com> <cory.wright@rackspace.com>
|
||||
<jsuh@isi.edu> <jsuh@bespin>
|
||||
<josh@jk0.org> <josh.kearney@rackspace.com>
|
||||
|
3
etc/policy.json
Normal file
3
etc/policy.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"default": []
|
||||
}
|
102
glance/api/policy.py
Normal file
102
glance/api/policy.py
Normal file
@ -0,0 +1,102 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 OpenStack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Policy Engine For Glance"""
|
||||
|
||||
import json
|
||||
import os.path
|
||||
|
||||
from glance.common import cfg
|
||||
from glance.common import exception
|
||||
from glance.common import policy
|
||||
|
||||
|
||||
class Enforcer(object):
|
||||
"""Responsible for loading and enforcing rules"""
|
||||
|
||||
policy_opts = (
|
||||
cfg.StrOpt('policy_file', default=None),
|
||||
cfg.StrOpt('policy_default_rule', default='default'),
|
||||
)
|
||||
|
||||
def __init__(self, conf):
|
||||
for opt in self.policy_opts:
|
||||
conf.register_opt(opt)
|
||||
|
||||
self.default_rule = conf.policy_default_rule
|
||||
self.policy_path = self._find_policy_file(conf)
|
||||
self.policy_file_mtime = None
|
||||
self.policy_file_contents = None
|
||||
|
||||
def set_rules(self, rules):
|
||||
"""Create a new Brain based on the provided dict of rules"""
|
||||
brain = policy.Brain(rules, self.default_rule)
|
||||
policy.set_brain(brain)
|
||||
|
||||
def load_rules(self):
|
||||
"""Set the rules found in the json file on disk"""
|
||||
rules = self._read_policy_file()
|
||||
self.set_rules(rules)
|
||||
|
||||
@staticmethod
|
||||
def _find_policy_file(conf):
|
||||
"""Locate the policy json data file"""
|
||||
if conf.policy_file:
|
||||
return conf.policy_file
|
||||
|
||||
matches = cfg.find_config_files('glance', 'policy', 'json')
|
||||
|
||||
try:
|
||||
return matches[0]
|
||||
except IndexError:
|
||||
raise cfg.ConfigFilesNotFoundError(('policy.json',))
|
||||
|
||||
def _read_policy_file(self):
|
||||
"""Read contents of the policy file
|
||||
|
||||
This re-caches policy data if the file has been changed.
|
||||
"""
|
||||
mtime = os.path.getmtime(self.policy_path)
|
||||
if not self.policy_file_contents or mtime != self.policy_file_mtime:
|
||||
with open(self.policy_path) as fap:
|
||||
raw_contents = fap.read()
|
||||
self.policy_file_contents = json.loads(raw_contents)
|
||||
self.policy_file_mtime = mtime
|
||||
return self.policy_file_contents
|
||||
|
||||
def enforce(self, context, action, target):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: Glance request context
|
||||
:param action: String representing the action to be checked
|
||||
:param object: Dictionary representing the object of the action.
|
||||
:raises: `glance.common.exception.NotAuthorized`
|
||||
:returns: None
|
||||
"""
|
||||
self.load_rules()
|
||||
|
||||
match_list = ('rule:%s' % action,)
|
||||
credentials = {
|
||||
'roles': context.roles,
|
||||
'user': context.user,
|
||||
'tenant': context.tenant,
|
||||
}
|
||||
|
||||
try:
|
||||
policy.enforce(match_list, target, credentials)
|
||||
except policy.NotAuthorized:
|
||||
raise exception.NotAuthorized(action=action)
|
@ -20,7 +20,6 @@
|
||||
"""
|
||||
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
@ -28,12 +27,11 @@ from webob.exc import (HTTPNotFound,
|
||||
HTTPConflict,
|
||||
HTTPBadRequest,
|
||||
HTTPForbidden,
|
||||
HTTPNoContent,
|
||||
HTTPUnauthorized)
|
||||
|
||||
from glance.api import policy
|
||||
import glance.api.v1
|
||||
from glance.api.v1 import controller
|
||||
from glance import image_cache
|
||||
from glance.common import cfg
|
||||
from glance.common import exception
|
||||
from glance.common import wsgi
|
||||
@ -48,8 +46,7 @@ from glance.store import (get_from_backend,
|
||||
get_size_from_backend,
|
||||
schedule_delete_from_backend,
|
||||
get_store_from_location,
|
||||
get_store_from_scheme,
|
||||
UnsupportedBackend)
|
||||
get_store_from_scheme)
|
||||
from glance import registry
|
||||
from glance import notifier
|
||||
|
||||
@ -86,6 +83,14 @@ class Controller(controller.BaseController):
|
||||
glance.store.create_stores(conf)
|
||||
self.notifier = notifier.Notifier(conf)
|
||||
registry.configure_registry_client(conf)
|
||||
self.policy = policy.Enforcer(conf)
|
||||
|
||||
def _enforce(self, req, action):
|
||||
"""Authorize an action against our policies"""
|
||||
try:
|
||||
self.policy.enforce(req.context, action, {})
|
||||
except exception.NotAuthorized:
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
def index(self, req):
|
||||
"""
|
||||
@ -110,6 +115,7 @@ class Controller(controller.BaseController):
|
||||
'size': <SIZE>}, ...
|
||||
]}
|
||||
"""
|
||||
self._enforce(req, 'get_images')
|
||||
params = self._get_query_params(req)
|
||||
try:
|
||||
images = registry.get_images_list(req.context, **params)
|
||||
@ -142,6 +148,7 @@ class Controller(controller.BaseController):
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
|
||||
]}
|
||||
"""
|
||||
self._enforce(req, 'get_images')
|
||||
params = self._get_query_params(req)
|
||||
try:
|
||||
images = registry.get_images_detail(req.context, **params)
|
||||
@ -194,6 +201,7 @@ class Controller(controller.BaseController):
|
||||
|
||||
:raises HTTPNotFound if image metadata is not available to user
|
||||
"""
|
||||
self._enforce(req, 'get_image')
|
||||
image_meta = self.get_image_meta_or_404(req, id)
|
||||
del image_meta['location']
|
||||
return {
|
||||
@ -210,6 +218,7 @@ class Controller(controller.BaseController):
|
||||
|
||||
:raises HTTPNotFound if image is not available to user
|
||||
"""
|
||||
self._enforce(req, 'get_image')
|
||||
image_meta = self.get_active_image_meta_or_404(req, id)
|
||||
|
||||
def get_from_store(image_meta):
|
||||
@ -470,6 +479,7 @@ class Controller(controller.BaseController):
|
||||
and the request body is not application/octet-stream
|
||||
image data.
|
||||
"""
|
||||
self._enforce(req, 'add_image')
|
||||
if req.context.read_only:
|
||||
msg = _("Read-only access")
|
||||
logger.debug(msg)
|
||||
@ -501,6 +511,7 @@ class Controller(controller.BaseController):
|
||||
|
||||
:retval Returns the updated image information as a mapping
|
||||
"""
|
||||
self._enforce(req, 'modify_image')
|
||||
if req.context.read_only:
|
||||
msg = _("Read-only access")
|
||||
logger.debug(msg)
|
||||
@ -565,6 +576,7 @@ class Controller(controller.BaseController):
|
||||
:raises HttpNotAuthorized if image or any chunk is not
|
||||
deleteable by the requesting user
|
||||
"""
|
||||
self._enforce(req, 'delete_image')
|
||||
if req.context.read_only:
|
||||
msg = _("Read-only access")
|
||||
logger.debug(msg)
|
||||
|
@ -298,7 +298,7 @@ class ConfigFileValueError(Error):
|
||||
pass
|
||||
|
||||
|
||||
def find_config_files(project=None, prog=None):
|
||||
def find_config_files(project=None, prog=None, filetype="conf"):
|
||||
"""Return a list of default configuration files.
|
||||
|
||||
We default to two config files: [${project}.conf, ${prog}.conf]
|
||||
@ -331,7 +331,8 @@ def find_config_files(project=None, prog=None):
|
||||
fix_path(os.path.join('~', '.' + project)) if project else None,
|
||||
fix_path('~'),
|
||||
os.path.join('/etc', project) if project else None,
|
||||
'/etc'
|
||||
'/etc',
|
||||
'etc',
|
||||
]
|
||||
cfg_dirs = filter(bool, cfg_dirs)
|
||||
|
||||
@ -342,9 +343,12 @@ def find_config_files(project=None, prog=None):
|
||||
return path
|
||||
|
||||
config_files = []
|
||||
|
||||
if project:
|
||||
config_files.append(search_dirs(cfg_dirs, '%s.conf' % project))
|
||||
config_files.append(search_dirs(cfg_dirs, '%s.conf' % prog))
|
||||
project_config = search_dirs(cfg_dirs, '%s.%s' % (project, filetype))
|
||||
config_files.append(project_config)
|
||||
|
||||
config_files.append(search_dirs(cfg_dirs, '%s.%s' % (prog, filetype)))
|
||||
|
||||
return filter(bool, config_files)
|
||||
|
||||
|
184
glance/common/policy.py
Normal file
184
glance/common/policy.py
Normal file
@ -0,0 +1,184 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 OpenStack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Common Policy Engine Implementation"""
|
||||
|
||||
import json
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
|
||||
class NotAuthorized(Exception):
|
||||
pass
|
||||
|
||||
|
||||
_BRAIN = None
|
||||
|
||||
|
||||
def set_brain(brain):
|
||||
"""Set the brain used by enforce().
|
||||
|
||||
Defaults use Brain() if not set.
|
||||
|
||||
"""
|
||||
global _BRAIN
|
||||
_BRAIN = brain
|
||||
|
||||
|
||||
def reset():
|
||||
"""Clear the brain used by enforce()."""
|
||||
global _BRAIN
|
||||
_BRAIN = None
|
||||
|
||||
|
||||
def enforce(match_list, target_dict, credentials_dict):
|
||||
"""Enforces authorization of some rules against credentials.
|
||||
|
||||
:param match_list: nested tuples of data to match against
|
||||
The basic brain supports three types of match lists:
|
||||
1) rules
|
||||
looks like: ('rule:compute:get_instance',)
|
||||
Retrieves the named rule from the rules dict and recursively
|
||||
checks against the contents of the rule.
|
||||
2) roles
|
||||
looks like: ('role:compute:admin',)
|
||||
Matches if the specified role is in credentials_dict['roles'].
|
||||
3) generic
|
||||
('tenant_id:%(tenant_id)s',)
|
||||
Substitutes values from the target dict into the match using
|
||||
the % operator and matches them against the creds dict.
|
||||
|
||||
Combining rules:
|
||||
The brain returns True if any of the outer tuple of rules match
|
||||
and also True if all of the inner tuples match. You can use this to
|
||||
perform simple boolean logic. For example, the following rule would
|
||||
return True if the creds contain the role 'admin' OR the if the
|
||||
tenant_id matches the target dict AND the the creds contains the
|
||||
role 'compute_sysadmin':
|
||||
|
||||
{
|
||||
"rule:combined": (
|
||||
'role:admin',
|
||||
('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Note that rule and role are reserved words in the credentials match, so
|
||||
you can't match against properties with those names. Custom brains may
|
||||
also add new reserved words. For example, the HttpBrain adds http as a
|
||||
reserved word.
|
||||
|
||||
:param target_dict: dict of object properties
|
||||
Target dicts contain as much information as we can about the object being
|
||||
operated on.
|
||||
|
||||
:param credentials_dict: dict of actor properties
|
||||
Credentials dicts contain as much information as we can about the user
|
||||
performing the action.
|
||||
|
||||
:raises NotAuthorized if the check fails
|
||||
|
||||
"""
|
||||
global _BRAIN
|
||||
if not _BRAIN:
|
||||
_BRAIN = Brain()
|
||||
if not _BRAIN.check(match_list, target_dict, credentials_dict):
|
||||
raise NotAuthorized()
|
||||
|
||||
|
||||
class Brain(object):
|
||||
"""Implements policy checking."""
|
||||
@classmethod
|
||||
def load_json(cls, data, default_rule=None):
|
||||
"""Init a brain using json instead of a rules dictionary."""
|
||||
rules_dict = json.loads(data)
|
||||
return cls(rules=rules_dict, default_rule=default_rule)
|
||||
|
||||
def __init__(self, rules=None, default_rule=None):
|
||||
self.rules = rules or {}
|
||||
self.default_rule = default_rule
|
||||
|
||||
def add_rule(self, key, match):
|
||||
self.rules[key] = match
|
||||
|
||||
def _check(self, match, target_dict, cred_dict):
|
||||
match_kind, match_value = match.split(':', 1)
|
||||
try:
|
||||
f = getattr(self, '_check_%s' % match_kind)
|
||||
except AttributeError:
|
||||
if not self._check_generic(match, target_dict, cred_dict):
|
||||
return False
|
||||
else:
|
||||
if not f(match_value, target_dict, cred_dict):
|
||||
return False
|
||||
return True
|
||||
|
||||
def check(self, match_list, target_dict, cred_dict):
|
||||
"""Checks authorization of some rules against credentials.
|
||||
|
||||
Detailed description of the check with examples in policy.enforce().
|
||||
|
||||
:param match_list: nested tuples of data to match against
|
||||
:param target_dict: dict of object properties
|
||||
:param credentials_dict: dict of actor properties
|
||||
|
||||
:returns: True if the check passes
|
||||
|
||||
"""
|
||||
if not match_list:
|
||||
return True
|
||||
for and_list in match_list:
|
||||
if isinstance(and_list, basestring):
|
||||
and_list = (and_list,)
|
||||
if all([self._check(item, target_dict, cred_dict)
|
||||
for item in and_list]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_rule(self, match, target_dict, cred_dict):
|
||||
"""Recursively checks credentials based on the brains rules."""
|
||||
try:
|
||||
new_match_list = self.rules[match]
|
||||
except KeyError:
|
||||
if self.default_rule and match != self.default_rule:
|
||||
new_match_list = ('rule:%s' % self.default_rule,)
|
||||
else:
|
||||
return False
|
||||
|
||||
return self.check(new_match_list, target_dict, cred_dict)
|
||||
|
||||
def _check_role(self, match, target_dict, cred_dict):
|
||||
"""Check that there is a matching role in the cred dict."""
|
||||
return match in cred_dict['roles']
|
||||
|
||||
def _check_generic(self, match, target_dict, cred_dict):
|
||||
"""Check an individual match.
|
||||
|
||||
Matches look like:
|
||||
|
||||
tenant:%(tenant_id)s
|
||||
role:compute:admin
|
||||
|
||||
"""
|
||||
|
||||
# TODO(termie): do dict inspection via dot syntax
|
||||
match = match % target_dict
|
||||
key, value = match.split(':', 1)
|
||||
if key in cred_dict:
|
||||
return value == cred_dict[key]
|
||||
return False
|
3
glance/tests/etc/policy.json
Normal file
3
glance/tests/etc/policy.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"default": []
|
||||
}
|
@ -26,7 +26,6 @@ and spinning down the servers.
|
||||
import datetime
|
||||
import functools
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
@ -160,7 +159,8 @@ class ApiServer(Server):
|
||||
Server object that starts/stops/manages the API server
|
||||
"""
|
||||
|
||||
def __init__(self, test_dir, port, registry_port, delayed_delete=False):
|
||||
def __init__(self, test_dir, port, registry_port, policy_file,
|
||||
delayed_delete=False):
|
||||
super(ApiServer, self).__init__(test_dir, port)
|
||||
self.server_name = 'api'
|
||||
self.default_store = 'file'
|
||||
@ -195,6 +195,8 @@ class ApiServer(Server):
|
||||
self.image_cache_dir = os.path.join(self.test_dir,
|
||||
'cache')
|
||||
self.image_cache_driver = 'sqlite'
|
||||
self.policy_file = policy_file
|
||||
self.policy_default_rule = 'default'
|
||||
self.conf_base = """[DEFAULT]
|
||||
verbose = %(verbose)s
|
||||
debug = %(debug)s
|
||||
@ -229,6 +231,8 @@ scrub_time = 5
|
||||
scrubber_datadir = %(scrubber_datadir)s
|
||||
image_cache_dir = %(image_cache_dir)s
|
||||
image_cache_driver = %(image_cache_driver)s
|
||||
policy_file = %(policy_file)s
|
||||
policy_default_rule = %(policy_default_rule)s
|
||||
[paste_deploy]
|
||||
flavor = %(deployment_flavor)s
|
||||
"""
|
||||
@ -359,9 +363,13 @@ class FunctionalTest(unittest.TestCase):
|
||||
self.api_port = get_unused_port()
|
||||
self.registry_port = get_unused_port()
|
||||
|
||||
self.copy_data_file('policy.json', self.test_dir)
|
||||
self.policy_file = os.path.join(self.test_dir, 'policy.json')
|
||||
|
||||
self.api_server = ApiServer(self.test_dir,
|
||||
self.api_port,
|
||||
self.registry_port)
|
||||
self.registry_port,
|
||||
self.policy_file)
|
||||
self.registry_server = RegistryServer(self.test_dir,
|
||||
self.registry_port)
|
||||
|
||||
@ -546,3 +554,9 @@ class FunctionalTest(unittest.TestCase):
|
||||
engine = create_engine(self.registry_server.sql_connection,
|
||||
pool_recycle=30)
|
||||
return engine.execute(sql)
|
||||
|
||||
def copy_data_file(self, file_name, dst_dir):
|
||||
src_file_name = os.path.join('glance/tests/etc', file_name)
|
||||
shutil.copy(src_file_name, dst_dir)
|
||||
dst_file_name = os.path.join(dst_dir, file_name)
|
||||
return dst_file_name
|
||||
|
@ -35,7 +35,7 @@ VERBOSE = False
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def stub_out_registry_and_store_server(stubs, images_dir):
|
||||
def stub_out_registry_and_store_server(stubs, base_dir):
|
||||
"""
|
||||
Mocks calls to 127.0.0.1 on 9191 and 9292 for testing so
|
||||
that a real Glance server does not need to be up and
|
||||
@ -125,7 +125,8 @@ def stub_out_registry_and_store_server(stubs, images_dir):
|
||||
'registry_host': '0.0.0.0',
|
||||
'registry_port': '9191',
|
||||
'default_store': 'file',
|
||||
'filesystem_store_datadir': images_dir
|
||||
'filesystem_store_datadir': base_dir,
|
||||
'policy_file': os.path.join(base_dir, 'policy.json'),
|
||||
})
|
||||
api = version_negotiation.VersionNegotiationFilter(
|
||||
context.ContextMiddleware(router.API(conf), conf),
|
||||
|
@ -15,13 +15,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
import stubout
|
||||
|
||||
from glance.common import utils
|
||||
from glance.tests import stubs
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
@ -37,14 +37,27 @@ class IsolatedUnitTest(unittest.TestCase):
|
||||
self.test_id, self.test_dir = test_utils.get_isolated_test_env()
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
stubs.stub_out_registry_and_store_server(self.stubs, self.test_dir)
|
||||
policy_file = self._copy_data_file('policy.json', self.test_dir)
|
||||
options = {'sql_connection': 'sqlite://',
|
||||
'verbose': False,
|
||||
'debug': False,
|
||||
'default_store': 'filesystem',
|
||||
'filesystem_store_datadir': os.path.join(self.test_dir)}
|
||||
'filesystem_store_datadir': os.path.join(self.test_dir),
|
||||
'policy_file': policy_file}
|
||||
|
||||
self.conf = test_utils.TestConfigOpts(options)
|
||||
|
||||
def _copy_data_file(self, file_name, dst_dir):
|
||||
src_file_name = os.path.join('glance/tests/etc', file_name)
|
||||
shutil.copy(src_file_name, dst_dir)
|
||||
dst_file_name = os.path.join(dst_dir, file_name)
|
||||
return dst_file_name
|
||||
|
||||
def set_policy_rules(self, rules):
|
||||
fap = open(self.conf.policy_file, 'w')
|
||||
fap.write(json.dumps(rules))
|
||||
fap.close()
|
||||
|
||||
def tearDown(self):
|
||||
self.stubs.UnsetAll()
|
||||
if os.path.exists(self.test_dir):
|
||||
|
@ -2086,6 +2086,24 @@ class TestGlanceAPI(base.IsolatedUnitTest):
|
||||
self.assertTrue('/images/%s' % res_body['id']
|
||||
in res.headers['location'])
|
||||
|
||||
def test_add_image_unauthorized(self):
|
||||
rules = {"add_image": [["false:false"]]}
|
||||
self.set_policy_rules(rules)
|
||||
fixture_headers = {'x-image-meta-store': 'file',
|
||||
'x-image-meta-disk-format': 'vhd',
|
||||
'x-image-meta-container-format': 'ovf',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k, v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
|
||||
req.headers['Content-Type'] = 'application/octet-stream'
|
||||
req.body = "chunk00000remainder"
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 401)
|
||||
|
||||
def test_register_and_upload(self):
|
||||
"""
|
||||
Test that the process of registering an image with
|
||||
@ -2360,6 +2378,20 @@ class TestGlanceAPI(base.IsolatedUnitTest):
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 400)
|
||||
|
||||
def test_get_images_detailed_unauthorized(self):
|
||||
rules = {"get_images": [["false:false"]]}
|
||||
self.set_policy_rules(rules)
|
||||
req = webob.Request.blank('/images/detail')
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 401)
|
||||
|
||||
def test_get_images_unauthorized(self):
|
||||
rules = {"get_images": [["false:false"]]}
|
||||
self.set_policy_rules(rules)
|
||||
req = webob.Request.blank('/images/detail')
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 401)
|
||||
|
||||
def test_store_location_not_revealed(self):
|
||||
"""
|
||||
Test that the internal store location is NOT revealed
|
||||
@ -2514,6 +2546,14 @@ class TestGlanceAPI(base.IsolatedUnitTest):
|
||||
for key, value in expected_headers.iteritems():
|
||||
self.assertEquals(value, res.headers[key])
|
||||
|
||||
def test_image_meta_unauthorized(self):
|
||||
rules = {"get_image": [["false:false"]]}
|
||||
self.set_policy_rules(rules)
|
||||
req = webob.Request.blank("/images/%s" % UUID2)
|
||||
req.method = 'HEAD'
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 401)
|
||||
|
||||
def test_show_image_basic(self):
|
||||
req = webob.Request.blank("/images/%s" % UUID2)
|
||||
res = req.get_response(self.api)
|
||||
@ -2526,6 +2566,13 @@ class TestGlanceAPI(base.IsolatedUnitTest):
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code)
|
||||
|
||||
def test_show_image_unauthorized(self):
|
||||
rules = {"get_image": [["false:false"]]}
|
||||
self.set_policy_rules(rules)
|
||||
req = webob.Request.blank("/images/%s" % UUID2)
|
||||
res = req.get_response(self.api)
|
||||
self.assertEqual(res.status_int, 401)
|
||||
|
||||
def test_delete_image(self):
|
||||
req = webob.Request.blank("/images/%s" % UUID2)
|
||||
req.method = 'DELETE'
|
||||
@ -2596,6 +2643,14 @@ class TestGlanceAPI(base.IsolatedUnitTest):
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, httplib.FORBIDDEN)
|
||||
|
||||
def test_delete_image_unauthorized(self):
|
||||
rules = {"delete_image": [["false:false"]]}
|
||||
self.set_policy_rules(rules)
|
||||
req = webob.Request.blank("/images/%s" % UUID2)
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(self.api)
|
||||
self.assertEquals(res.status_int, 401)
|
||||
|
||||
def test_get_details_invalid_marker(self):
|
||||
"""
|
||||
Tests that the /images/detail registry API returns a 400
|
||||
|
@ -21,10 +21,8 @@ import unittest
|
||||
|
||||
import stubout
|
||||
|
||||
from glance.api.middleware import version_negotiation
|
||||
from glance.api.v1 import images
|
||||
from glance.api.v1 import members
|
||||
from glance.common import config, context, utils
|
||||
from glance.common import config
|
||||
from glance.common import context
|
||||
from glance.image_cache import pruner
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
@ -52,34 +50,36 @@ class TestPasteApp(unittest.TestCase):
|
||||
f.flush()
|
||||
|
||||
if paste_copy:
|
||||
paste_from = os.path.join(os.getcwd(), 'etc/glance-api-paste.ini')
|
||||
paste_from = os.path.join(os.getcwd(),
|
||||
'etc/glance-registry-paste.ini')
|
||||
paste_to = os.path.join(conf.temp_file.replace('.conf',
|
||||
'-paste.ini'))
|
||||
_appendto(paste_from, paste_to, paste_append)
|
||||
|
||||
app = config.load_paste_app(conf, 'glance-api')
|
||||
app = config.load_paste_app(conf, 'glance-registry')
|
||||
|
||||
self.assertEquals(expected_app_type, type(app))
|
||||
|
||||
def test_load_paste_app(self):
|
||||
type = version_negotiation.VersionNegotiationFilter
|
||||
self._do_test_load_paste_app(type)
|
||||
expected_middleware = context.ContextMiddleware
|
||||
self._do_test_load_paste_app(expected_middleware)
|
||||
|
||||
def test_load_paste_app_with_paste_flavor(self):
|
||||
paste_group = {'paste_deploy': {'flavor': 'incomplete'}}
|
||||
pipeline = '[pipeline:glance-api-incomplete]\n' + \
|
||||
'pipeline = context apiv1app'
|
||||
pipeline = '[pipeline:glance-registry-incomplete]\n' + \
|
||||
'pipeline = context registryapp'
|
||||
|
||||
type = context.ContextMiddleware
|
||||
self._do_test_load_paste_app(type, paste_group, paste_append=pipeline)
|
||||
|
||||
def test_load_paste_app_with_paste_config_file(self):
|
||||
paste_config_file = os.path.join(os.getcwd(),
|
||||
'etc/glance-api-paste.ini')
|
||||
'etc/glance-registry-paste.ini')
|
||||
paste_group = {'paste_deploy': {'config_file': paste_config_file}}
|
||||
|
||||
type = version_negotiation.VersionNegotiationFilter
|
||||
self._do_test_load_paste_app(type, paste_group, paste_copy=False)
|
||||
expected_middleware = context.ContextMiddleware
|
||||
self._do_test_load_paste_app(expected_middleware,
|
||||
paste_group, paste_copy=False)
|
||||
|
||||
def test_load_paste_app_with_conf_name(self):
|
||||
def fake_join(*args):
|
||||
|
Loading…
Reference in New Issue
Block a user