Implement private share_types
Add share_type_access extension which introduces the ability to manage share type access: * Share types are public by default * Private share types can be created by setting the is_public boolean field to False at creation time. * Access to a private share type can be controlled by adding or removing a project from it. * Private share types without projects are only visible by users with the admin role/context. Implementation details and unit tests were mostly adapted from Cinder and Nova access extensions. Implements bp private-share-types Change-Id: I83ee57c6a516b5382d074c0082525ad7feadd59c
This commit is contained in:
parent
9bcb9ad76c
commit
35b3a508f8
|
@ -39,6 +39,10 @@
|
|||
"share_extension:types_manage": [["rule:admin_api"]],
|
||||
"share_extension:types_extra_specs": [["rule:admin_api"]],
|
||||
|
||||
"share_extension:share_type_access": [],
|
||||
"share_extension:share_type_access:addProjectAccess": [["rule:admin_api"]],
|
||||
"share_extension:share_type_access:removeProjectAccess": [["rule:admin_api"]],
|
||||
|
||||
"security_service:create": [["rule:default"]],
|
||||
"security_service:delete": [["rule:default"]],
|
||||
"security_service:update": [["rule:default"]],
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
"""The share type access extension."""
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
import webob
|
||||
|
||||
|
||||
from manila.api import extensions
|
||||
from manila.api.openstack import wsgi
|
||||
from manila.api import xmlutil
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.share import share_types
|
||||
|
||||
|
||||
soft_authorize = extensions.soft_extension_authorizer('share',
|
||||
'share_type_access')
|
||||
authorize = extensions.extension_authorizer('share', 'share_type_access')
|
||||
|
||||
|
||||
def make_share_type(elem):
|
||||
elem.set('{%s}is_public' % Share_type_access.namespace,
|
||||
'%s:is_public' % Share_type_access.alias)
|
||||
|
||||
|
||||
def make_share_type_access(elem):
|
||||
elem.set('share_type_id')
|
||||
elem.set('project_id')
|
||||
|
||||
|
||||
class ShareTypeTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('share_type', selector='share_type')
|
||||
make_share_type(root)
|
||||
alias = Share_type_access.alias
|
||||
namespace = Share_type_access.namespace
|
||||
return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class ShareTypesTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('share_types')
|
||||
elem = xmlutil.SubTemplateElement(
|
||||
root, 'share_type', selector='share_types')
|
||||
make_share_type(elem)
|
||||
alias = Share_type_access.alias
|
||||
namespace = Share_type_access.namespace
|
||||
return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class ShareTypeAccessTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('share_type_access')
|
||||
elem = xmlutil.SubTemplateElement(root, 'access',
|
||||
selector='share_type_access')
|
||||
make_share_type_access(elem)
|
||||
return xmlutil.MasterTemplate(root, 1)
|
||||
|
||||
|
||||
def _marshall_share_type_access(share_type):
|
||||
rval = []
|
||||
for project_id in share_type['projects']:
|
||||
rval.append({'share_type_id': share_type['id'],
|
||||
'project_id': project_id})
|
||||
|
||||
return {'share_type_access': rval}
|
||||
|
||||
|
||||
class ShareTypeAccessController(object):
|
||||
"""The share type access API controller for the OpenStack API."""
|
||||
|
||||
@wsgi.serializers(xml=ShareTypeAccessTemplate)
|
||||
def index(self, req, type_id):
|
||||
context = req.environ['manila.context']
|
||||
authorize(context)
|
||||
|
||||
try:
|
||||
share_type = share_types.get_share_type(
|
||||
context, type_id, expected_fields=['projects'])
|
||||
except exception.ShareTypeNotFound:
|
||||
explanation = _("Share type %s not found.") % type_id
|
||||
raise webob.exc.HTTPNotFound(explanation=explanation)
|
||||
|
||||
if share_type['is_public']:
|
||||
expl = _("Access list not available for public share types.")
|
||||
raise webob.exc.HTTPNotFound(explanation=expl)
|
||||
|
||||
return _marshall_share_type_access(share_type)
|
||||
|
||||
|
||||
class ShareTypeActionController(wsgi.Controller):
|
||||
"""The share type access API controller for the OpenStack API."""
|
||||
|
||||
def _check_body(self, body, action_name):
|
||||
if not self.is_valid_body(body, action_name):
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
access = body[action_name]
|
||||
project = access.get('project')
|
||||
if not uuidutils.is_uuid_like(project):
|
||||
msg = _("Bad project format: "
|
||||
"project is not in proper format (%s)") % project
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _extend_share_type(self, share_type_rval, share_type_ref):
|
||||
if share_type_ref:
|
||||
key = "%s:is_public" % (Share_type_access.alias)
|
||||
share_type_rval[key] = share_type_ref.get('is_public', True)
|
||||
|
||||
@wsgi.extends
|
||||
def show(self, req, resp_obj, id):
|
||||
context = req.environ['manila.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=ShareTypeTemplate())
|
||||
share_type = share_types.get_share_type(context, id)
|
||||
self._extend_share_type(resp_obj.obj['share_type'], share_type)
|
||||
|
||||
@wsgi.extends
|
||||
def index(self, req, resp_obj):
|
||||
context = req.environ['manila.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=ShareTypesTemplate())
|
||||
for share_type_rval in list(resp_obj.obj['share_types']):
|
||||
type_id = share_type_rval['id']
|
||||
share_type = share_types.get_share_type(context, type_id)
|
||||
self._extend_share_type(share_type_rval, share_type)
|
||||
|
||||
@wsgi.extends(action='create')
|
||||
def create(self, req, body, resp_obj):
|
||||
context = req.environ['manila.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=ShareTypeTemplate())
|
||||
type_id = resp_obj.obj['share_type']['id']
|
||||
share_type = share_types.get_share_type(context, type_id)
|
||||
self._extend_share_type(resp_obj.obj['share_type'], share_type)
|
||||
|
||||
@wsgi.action('addProjectAccess')
|
||||
def _addProjectAccess(self, req, id, body):
|
||||
context = req.environ['manila.context']
|
||||
authorize(context, action="addProjectAccess")
|
||||
self._check_body(body, 'addProjectAccess')
|
||||
project = body['addProjectAccess']['project']
|
||||
|
||||
try:
|
||||
share_type = share_types.get_share_type(context, id)
|
||||
|
||||
if share_type['is_public']:
|
||||
msg = _("You cannot add project to public share_type.")
|
||||
raise webob.exc.HTTPForbidden(explanation=msg)
|
||||
|
||||
except exception.ShareTypeNotFound as err:
|
||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||
|
||||
try:
|
||||
share_types.add_share_type_access(context, id, project)
|
||||
except exception.ShareTypeAccessExists as err:
|
||||
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.action('removeProjectAccess')
|
||||
def _removeProjectAccess(self, req, id, body):
|
||||
context = req.environ['manila.context']
|
||||
authorize(context, action="removeProjectAccess")
|
||||
self._check_body(body, 'removeProjectAccess')
|
||||
project = body['removeProjectAccess']['project']
|
||||
|
||||
try:
|
||||
share_types.remove_share_type_access(context, id, project)
|
||||
except (exception.ShareTypeNotFound,
|
||||
exception.ShareTypeAccessNotFound) as err:
|
||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
|
||||
class Share_type_access(extensions.ExtensionDescriptor):
|
||||
"""share type access support."""
|
||||
|
||||
name = "ShareTypeAccess"
|
||||
alias = "os-share-type-access"
|
||||
namespace = ("http://docs.openstack.org/share/"
|
||||
"ext/os-share-type-access/api/v1")
|
||||
updated = "2015-03-02T00:00:00Z"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
res = extensions.ResourceExtension(
|
||||
Share_type_access.alias,
|
||||
ShareTypeAccessController(),
|
||||
parent=dict(member_name='type', collection_name='types'))
|
||||
resources.append(res)
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = ShareTypeActionController()
|
||||
extension = extensions.ControllerExtension(self, 'types', controller)
|
||||
return [extension]
|
|
@ -53,6 +53,7 @@ class ShareTypesManageController(wsgi.Controller):
|
|||
share_type = body['volume_type']
|
||||
name = share_type.get('name', None)
|
||||
specs = share_type.get('extra_specs', {})
|
||||
is_public = share_type.get('os-share-type-access:is_public', True)
|
||||
|
||||
if name is None or name == "" or len(name) > 255:
|
||||
msg = _("Type name is not valid.")
|
||||
|
@ -66,7 +67,7 @@ class ShareTypesManageController(wsgi.Controller):
|
|||
raise webob.exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||
|
||||
try:
|
||||
share_types.create(context, name, specs)
|
||||
share_types.create(context, name, specs, is_public)
|
||||
share_type = share_types.get_share_type_by_name(context, name)
|
||||
share_type['required_extra_specs'] = required_extra_specs
|
||||
notifier_info = dict(share_types=share_type)
|
||||
|
|
|
@ -393,12 +393,15 @@ def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
|
|||
|
||||
|
||||
def extension_authorizer(api_name, extension_name):
|
||||
def authorize(context, target=None):
|
||||
def authorize(context, target=None, action=None):
|
||||
if target is None:
|
||||
target = {'project_id': context.project_id,
|
||||
'user_id': context.user_id}
|
||||
action = '%s_extension:%s' % (api_name, extension_name)
|
||||
manila.policy.enforce(context, action, target)
|
||||
if action is None:
|
||||
act = '%s_extension:%s' % (api_name, extension_name)
|
||||
else:
|
||||
act = '%s_extension:%s:%s' % (api_name, extension_name, action)
|
||||
manila.policy.enforce(context, act, target)
|
||||
return authorize
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
"""The share type & share types extra specs extension."""
|
||||
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from webob import exc
|
||||
|
||||
|
@ -31,9 +32,8 @@ class ShareTypesController(wsgi.Controller):
|
|||
|
||||
def index(self, req):
|
||||
"""Returns the list of share types."""
|
||||
context = req.environ['manila.context']
|
||||
shr_types = share_types.get_all_types(context).values()
|
||||
return self._view_builder.index(req, shr_types)
|
||||
limited_types = self._get_share_types(req)
|
||||
return self._view_builder.index(req, limited_types)
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return a single share type item."""
|
||||
|
@ -65,6 +65,40 @@ class ShareTypesController(wsgi.Controller):
|
|||
share_type['id'] = six.text_type(share_type['id'])
|
||||
return self._view_builder.show(req, share_type)
|
||||
|
||||
def _get_share_types(self, req):
|
||||
"""Helper function that returns a list of type dicts."""
|
||||
filters = {}
|
||||
context = req.environ['manila.context']
|
||||
if context.is_admin:
|
||||
# Only admin has query access to all share types
|
||||
filters['is_public'] = self._parse_is_public(
|
||||
req.params.get('is_public'))
|
||||
else:
|
||||
filters['is_public'] = True
|
||||
limited_types = share_types.get_all_types(
|
||||
context, search_opts=filters).values()
|
||||
return limited_types
|
||||
|
||||
@staticmethod
|
||||
def _parse_is_public(is_public):
|
||||
"""Parse is_public into something usable.
|
||||
|
||||
* True: API should list public share types only
|
||||
* False: API should list private share types only
|
||||
* None: API should list both public and private share types
|
||||
"""
|
||||
if is_public is None:
|
||||
# preserve default value of showing only public types
|
||||
return True
|
||||
elif six.text_type(is_public).lower() == "all":
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
return strutils.bool_from_string(is_public, strict=True)
|
||||
except ValueError:
|
||||
msg = _('Invalid is_public filter [%s]') % is_public
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ShareTypesController())
|
||||
|
|
|
@ -623,19 +623,39 @@ def share_server_backend_details_get(context, share_server_id):
|
|||
##################
|
||||
|
||||
|
||||
def share_type_create(context, values):
|
||||
def share_type_create(context, values, projects=None):
|
||||
"""Create a new share type."""
|
||||
return IMPL.share_type_create(context, values)
|
||||
return IMPL.share_type_create(context, values, projects)
|
||||
|
||||
|
||||
def share_type_get_all(context, inactive=False):
|
||||
"""Get all share types."""
|
||||
return IMPL.share_type_get_all(context, inactive)
|
||||
def share_type_get_all(context, inactive=False, filters=None):
|
||||
"""Get all share types.
|
||||
|
||||
:param context: context to query under
|
||||
:param inactive: Include inactive share types to the result set
|
||||
:param filters: Filters for the query in the form of key/value.
|
||||
:is_public: Filter share types based on visibility:
|
||||
|
||||
* **True**: List public share types only
|
||||
* **False**: List private share types only
|
||||
* **None**: List both public and private share types
|
||||
|
||||
:returns: list of matching share types
|
||||
"""
|
||||
return IMPL.share_type_get_all(context, inactive, filters)
|
||||
|
||||
|
||||
def share_type_get(context, id, inactive=False):
|
||||
"""Get share type by id."""
|
||||
return IMPL.share_type_get(context, id, inactive)
|
||||
def share_type_get(context, type_id, inactive=False, expected_fields=None):
|
||||
"""Get share type by id.
|
||||
|
||||
:param context: context to query under
|
||||
:param type_id: share type id to get.
|
||||
:param inactive: Consider inactive share types when searching
|
||||
:param expected_fields: Return those additional fields.
|
||||
Supported fields are: projects.
|
||||
:returns: share type
|
||||
"""
|
||||
return IMPL.share_type_get(context, type_id, inactive, expected_fields)
|
||||
|
||||
|
||||
def share_type_get_by_name(context, name):
|
||||
|
@ -643,6 +663,21 @@ def share_type_get_by_name(context, name):
|
|||
return IMPL.share_type_get_by_name(context, name)
|
||||
|
||||
|
||||
def share_type_access_get_all(context, type_id):
|
||||
"""Get all share type access of a share type."""
|
||||
return IMPL.share_type_access_get_all(context, type_id)
|
||||
|
||||
|
||||
def share_type_access_add(context, type_id, project_id):
|
||||
"""Add share type access for project."""
|
||||
return IMPL.share_type_access_add(context, type_id, project_id)
|
||||
|
||||
|
||||
def share_type_access_remove(context, type_id, project_id):
|
||||
"""Remove share type access for project."""
|
||||
return IMPL.share_type_access_remove(context, type_id, project_id)
|
||||
|
||||
|
||||
def share_type_qos_specs_get(context, type_id):
|
||||
"""Get all qos specs for given share type."""
|
||||
return IMPL.share_type_qos_specs_get(context, type_id)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# 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.
|
||||
|
||||
"""Add_share_type_projects
|
||||
|
||||
Revision ID: ef0c02b4366
|
||||
Revises: 17115072e1c3
|
||||
Create Date: 2015-02-20 10:49:40.744974
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ef0c02b4366'
|
||||
down_revision = '59eb64046740'
|
||||
|
||||
from alembic import op
|
||||
from oslo_log import log
|
||||
import sqlalchemy as sql
|
||||
|
||||
from manila.i18n import _LE
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade():
|
||||
meta = sql.MetaData()
|
||||
meta.bind = op.get_bind()
|
||||
is_public = sql.Column('is_public', sql.Boolean)
|
||||
|
||||
try:
|
||||
op.add_column('share_types', is_public)
|
||||
|
||||
share_types = sql.Table('share_types', meta, is_public.copy())
|
||||
share_types.update().values(is_public=True).execute()
|
||||
except Exception:
|
||||
LOG.error(_LE("Column |%s| not created!"), repr(is_public))
|
||||
raise
|
||||
|
||||
try:
|
||||
op.create_table(
|
||||
'share_type_projects',
|
||||
sql.Column('id', sql.Integer, primary_key=True, nullable=False),
|
||||
sql.Column('created_at', sql.DateTime),
|
||||
sql.Column('updated_at', sql.DateTime),
|
||||
sql.Column('deleted_at', sql.DateTime),
|
||||
sql.Column('share_type_id', sql.String(36),
|
||||
sql.ForeignKey('share_types.id', name="stp_id_fk")),
|
||||
sql.Column('project_id', sql.String(length=255)),
|
||||
sql.Column('deleted', sql.Boolean(create_constraint=True,
|
||||
name=None)),
|
||||
sql.UniqueConstraint('share_type_id', 'project_id', 'deleted',
|
||||
name="stp_project_id_uc"),
|
||||
mysql_engine='InnoDB',
|
||||
)
|
||||
except Exception:
|
||||
LOG.error(_LE("Table |%s| not created!"), 'share_type_projects')
|
||||
raise
|
||||
|
||||
|
||||
def downgrade():
|
||||
try:
|
||||
op.drop_column('share_types', 'is_public')
|
||||
except Exception:
|
||||
LOG.error(_LE("share_types.is_public column not dropped"))
|
||||
raise
|
||||
|
||||
try:
|
||||
op.drop_table('share_type_projects')
|
||||
except Exception:
|
||||
LOG.error(_LE("share_type_projects table not dropped"))
|
||||
raise
|
|
@ -32,6 +32,7 @@ import six
|
|||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql.expression import literal_column
|
||||
from sqlalchemy.sql.expression import true
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from manila.common import constants
|
||||
|
@ -2037,8 +2038,8 @@ to a single dict:
|
|||
|
||||
|
||||
@require_admin_context
|
||||
def share_type_create(context, values):
|
||||
"""Create a new instance type.
|
||||
def share_type_create(context, values, projects=None):
|
||||
"""Create a new share type.
|
||||
|
||||
In order to pass in extra specs, the values dict should contain a
|
||||
'extra_specs' key/value pair:
|
||||
|
@ -2047,6 +2048,8 @@ def share_type_create(context, values):
|
|||
if not values.get('id'):
|
||||
values['id'] = str(uuid.uuid4())
|
||||
|
||||
projects = projects or []
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
try:
|
||||
|
@ -2059,21 +2062,61 @@ def share_type_create(context, values):
|
|||
raise exception.ShareTypeExists(id=values['name'])
|
||||
except Exception as e:
|
||||
raise db_exception.DBError(e)
|
||||
|
||||
for project in set(projects):
|
||||
access_ref = models.ShareTypeProjects()
|
||||
access_ref.update({"share_type_id": share_type_ref.id,
|
||||
"project_id": project})
|
||||
access_ref.save(session=session)
|
||||
|
||||
return share_type_ref
|
||||
|
||||
|
||||
def _share_type_get_query(context, session=None, read_deleted=None,
|
||||
expected_fields=None):
|
||||
expected_fields = expected_fields or []
|
||||
query = model_query(context,
|
||||
models.ShareTypes,
|
||||
session=session,
|
||||
read_deleted=read_deleted). \
|
||||
options(joinedload('extra_specs')).options(joinedload('shares'))
|
||||
|
||||
if 'projects' in expected_fields:
|
||||
query = query.options(joinedload('projects'))
|
||||
|
||||
if not context.is_admin:
|
||||
the_filter = [models.ShareTypes.is_public == true()]
|
||||
projects_attr = getattr(models.ShareTypes, 'projects')
|
||||
the_filter.extend([
|
||||
projects_attr.any(project_id=context.project_id)
|
||||
])
|
||||
query = query.filter(or_(*the_filter))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@require_context
|
||||
def share_type_get_all(context, inactive=False, filters=None):
|
||||
"""Returns a dict describing all share_types with name as key."""
|
||||
filters = filters or {}
|
||||
|
||||
read_deleted = "yes" if inactive else "no"
|
||||
rows = model_query(context, models.ShareTypes,
|
||||
read_deleted=read_deleted).\
|
||||
options(joinedload('extra_specs')).\
|
||||
options(joinedload('shares')).\
|
||||
order_by("name").\
|
||||
all()
|
||||
|
||||
query = _share_type_get_query(context, read_deleted=read_deleted)
|
||||
|
||||
if 'is_public' in filters and filters['is_public'] is not None:
|
||||
the_filter = [models. ShareTypes.is_public == filters['is_public']]
|
||||
if filters['is_public'] and context.project_id is not None:
|
||||
projects_attr = getattr(models. ShareTypes, 'projects')
|
||||
the_filter.extend([
|
||||
projects_attr.any(project_id=context.project_id, deleted=False)
|
||||
])
|
||||
if len(the_filter) > 1:
|
||||
query = query.filter(or_(*the_filter))
|
||||
else:
|
||||
query = query.filter(the_filter[0])
|
||||
|
||||
rows = query.order_by("name").all()
|
||||
|
||||
result = {}
|
||||
for row in rows:
|
||||
|
@ -2082,28 +2125,49 @@ def share_type_get_all(context, inactive=False, filters=None):
|
|||
return result
|
||||
|
||||
|
||||
def _share_type_get_id_from_share_type_query(context, id, session=None):
|
||||
return model_query(
|
||||
context, models.ShareTypes, read_deleted="no", session=session).\
|
||||
filter_by(id=id)
|
||||
|
||||
|
||||
def _share_type_get_id_from_share_type(context, id, session=None):
|
||||
result = _share_type_get_id_from_share_type_query(
|
||||
context, id, session=session).first()
|
||||
if not result:
|
||||
raise exception.ShareTypeNotFound(share_type_id=id)
|
||||
return result['id']
|
||||
|
||||
|
||||
@require_context
|
||||
def _share_type_get(context, id, session=None, inactive=False):
|
||||
def _share_type_get(context, id, session=None, inactive=False,
|
||||
expected_fields=None):
|
||||
expected_fields = expected_fields or []
|
||||
read_deleted = "yes" if inactive else "no"
|
||||
result = model_query(context,
|
||||
models.ShareTypes,
|
||||
session=session,
|
||||
read_deleted=read_deleted).\
|
||||
options(joinedload('extra_specs')).\
|
||||
filter_by(id=id).\
|
||||
options(joinedload('shares')).\
|
||||
result = _share_type_get_query(
|
||||
context, session, read_deleted, expected_fields). \
|
||||
filter_by(id=id). \
|
||||
options(joinedload('shares')). \
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.ShareTypeNotFound(share_type_id=id)
|
||||
|
||||
return _dict_with_extra_specs(result)
|
||||
share_type = _dict_with_extra_specs(result)
|
||||
|
||||
if 'projects' in expected_fields:
|
||||
share_type['projects'] = [p['project_id'] for p in result['projects']]
|
||||
|
||||
return share_type
|
||||
|
||||
|
||||
@require_context
|
||||
def share_type_get(context, id, inactive=False):
|
||||
def share_type_get(context, id, inactive=False, expected_fields=None):
|
||||
"""Return a dict describing specific share_type."""
|
||||
return _share_type_get(context, id, None, inactive)
|
||||
return _share_type_get(context, id,
|
||||
session=None,
|
||||
inactive=inactive,
|
||||
expected_fields=expected_fields)
|
||||
|
||||
|
||||
@require_context
|
||||
|
@ -2116,8 +2180,8 @@ def _share_type_get_by_name(context, name, session=None):
|
|||
|
||||
if not result:
|
||||
raise exception.ShareTypeNotFoundByName(share_type_name=name)
|
||||
else:
|
||||
return _dict_with_extra_specs(result)
|
||||
|
||||
return _dict_with_extra_specs(result)
|
||||
|
||||
|
||||
@require_context
|
||||
|
@ -2150,6 +2214,51 @@ def share_type_destroy(context, id):
|
|||
'updated_at': literal_column('updated_at')})
|
||||
|
||||
|
||||
def _share_type_access_query(context, session=None):
|
||||
return model_query(context, models.ShareTypeProjects, session=session,
|
||||
read_deleted="no")
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def share_type_access_get_all(context, type_id):
|
||||
share_type_id = _share_type_get_id_from_share_type(context, type_id)
|
||||
return _share_type_access_query(context).\
|
||||
filter_by(share_type_id=share_type_id).all()
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def share_type_access_add(context, type_id, project_id):
|
||||
"""Add given tenant to the share type access list."""
|
||||
share_type_id = _share_type_get_id_from_share_type(context, type_id)
|
||||
|
||||
access_ref = models.ShareTypeProjects()
|
||||
access_ref.update({"share_type_id": share_type_id,
|
||||
"project_id": project_id})
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
try:
|
||||
access_ref.save(session=session)
|
||||
except db_exception.DBDuplicateEntry:
|
||||
raise exception.ShareTypeAccessExists(share_type_id=type_id,
|
||||
project_id=project_id)
|
||||
return access_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def share_type_access_remove(context, type_id, project_id):
|
||||
"""Remove given tenant from the share type access list."""
|
||||
share_type_id = _share_type_get_id_from_share_type(context, type_id)
|
||||
|
||||
count = _share_type_access_query(context).\
|
||||
filter_by(share_type_id=share_type_id).\
|
||||
filter_by(project_id=project_id).\
|
||||
soft_delete(synchronize_session=False)
|
||||
if count == 0:
|
||||
raise exception.ShareTypeAccessNotFound(
|
||||
share_type_id=type_id, project_id=project_id)
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_get_active_by_window(context,
|
||||
begin,
|
||||
|
|
|
@ -23,7 +23,7 @@ from oslo_config import cfg
|
|||
from oslo_db.sqlalchemy import models
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy import Column, Integer, String, schema
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import ForeignKey, DateTime, Boolean, Enum
|
||||
|
@ -205,6 +205,7 @@ class ShareTypes(BASE, ManilaBase):
|
|||
__tablename__ = "share_types"
|
||||
id = Column(String(36), primary_key=True)
|
||||
name = Column(String(255))
|
||||
is_public = Column(Boolean, default=True)
|
||||
shares = orm.relationship(Share,
|
||||
backref=orm.backref('share_type',
|
||||
uselist=False),
|
||||
|
@ -214,6 +215,27 @@ class ShareTypes(BASE, ManilaBase):
|
|||
'ShareTypes.deleted == False)')
|
||||
|
||||
|
||||
class ShareTypeProjects(BASE, ManilaBase):
|
||||
"""Represent projects associated share_types."""
|
||||
__tablename__ = "share_type_projects"
|
||||
__table_args__ = (schema.UniqueConstraint(
|
||||
"share_type_id", "project_id", "deleted",
|
||||
name="uniq_share_type_projects0share_type_id0project_id0deleted"),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
share_type_id = Column(Integer, ForeignKey('share_types.id'),
|
||||
nullable=False)
|
||||
project_id = Column(String(255))
|
||||
|
||||
share_type = orm.relationship(
|
||||
ShareTypes,
|
||||
backref="projects",
|
||||
foreign_keys=share_type_id,
|
||||
primaryjoin='and_('
|
||||
'ShareTypeProjects.share_type_id == ShareTypes.id,'
|
||||
'ShareTypeProjects.deleted == False)')
|
||||
|
||||
|
||||
class ShareTypeExtraSpecs(BASE, ManilaBase):
|
||||
"""Represents additional specs as key/value pairs for a share_type."""
|
||||
__tablename__ = 'share_type_extra_specs'
|
||||
|
|
|
@ -417,6 +417,11 @@ class ShareTypeNotFound(NotFound):
|
|||
message = _("Share type %(share_type_id)s could not be found.")
|
||||
|
||||
|
||||
class ShareTypeAccessNotFound(NotFound):
|
||||
message = _("Share type access not found for %(share_type_id)s / "
|
||||
"%(project_id)s combination.")
|
||||
|
||||
|
||||
class ShareTypeNotFoundByName(ShareTypeNotFound):
|
||||
message = _("Share type with name %(share_type_name)s "
|
||||
"could not be found.")
|
||||
|
@ -436,6 +441,11 @@ class ShareTypeExists(ManilaException):
|
|||
message = _("Share Type %(id)s already exists.")
|
||||
|
||||
|
||||
class ShareTypeAccessExists(ManilaException):
|
||||
message = _("Share type access for %(share_type_id)s / "
|
||||
"%(project_id)s combination already exists.")
|
||||
|
||||
|
||||
class ShareTypeCreateFailed(ManilaException):
|
||||
message = _("Cannot create share_type with "
|
||||
"name %(name)s and specs %(extra_specs)s.")
|
||||
|
|
|
@ -33,8 +33,11 @@ CONF = cfg.CONF
|
|||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def create(context, name, extra_specs):
|
||||
def create(context, name, extra_specs=None, is_public=True, projects=None):
|
||||
"""Creates share types."""
|
||||
extra_specs = extra_specs or {}
|
||||
projects = projects or []
|
||||
|
||||
try:
|
||||
get_valid_required_extra_specs(extra_specs)
|
||||
except exception.InvalidExtraSpec as e:
|
||||
|
@ -43,7 +46,9 @@ def create(context, name, extra_specs):
|
|||
try:
|
||||
type_ref = db.share_type_create(context,
|
||||
dict(name=name,
|
||||
extra_specs=extra_specs))
|
||||
extra_specs=extra_specs,
|
||||
is_public=is_public),
|
||||
projects=projects)
|
||||
except db_exception.DBError as e:
|
||||
LOG.exception(_LE('DB error: %s'), e)
|
||||
raise exception.ShareTypeCreateFailed(name=name,
|
||||
|
@ -60,12 +65,17 @@ def destroy(context, id):
|
|||
db.share_type_destroy(context, id)
|
||||
|
||||
|
||||
def get_all_types(context, inactive=0, search_opts={}):
|
||||
def get_all_types(context, inactive=0, search_opts=None):
|
||||
"""Get all non-deleted share_types.
|
||||
|
||||
Pass true as argument if you want deleted share types returned also.
|
||||
"""
|
||||
share_types = db.share_type_get_all(context, inactive)
|
||||
search_opts = search_opts or {}
|
||||
filters = {}
|
||||
|
||||
if 'is_public' in search_opts:
|
||||
filters['is_public'] = search_opts.pop('is_public')
|
||||
|
||||
share_types = db.share_type_get_all(context, inactive, filters=filters)
|
||||
|
||||
for type_name, type_args in six.iteritems(share_types):
|
||||
required_extra_specs = {}
|
||||
|
@ -112,7 +122,7 @@ def get_all_types(context, inactive=0, search_opts={}):
|
|||
return share_types
|
||||
|
||||
|
||||
def get_share_type(ctxt, id):
|
||||
def get_share_type(ctxt, id, expected_fields=None):
|
||||
"""Retrieves single share type by id."""
|
||||
if id is None:
|
||||
msg = _("id cannot be None")
|
||||
|
@ -121,7 +131,7 @@ def get_share_type(ctxt, id):
|
|||
if ctxt is None:
|
||||
ctxt = context.get_admin_context()
|
||||
|
||||
return db.share_type_get(ctxt, id)
|
||||
return db.share_type_get(ctxt, id, expected_fields=expected_fields)
|
||||
|
||||
|
||||
def get_share_type_by_name(context, name):
|
||||
|
@ -216,6 +226,22 @@ def get_valid_required_extra_specs(extra_specs):
|
|||
return required_extra_specs
|
||||
|
||||
|
||||
def add_share_type_access(context, share_type_id, project_id):
|
||||
"""Add access to share type for project_id."""
|
||||
if share_type_id is None:
|
||||
msg = _("share_type_id cannot be None")
|
||||
raise exception.InvalidShareType(reason=msg)
|
||||
return db.share_type_access_add(context, share_type_id, project_id)
|
||||
|
||||
|
||||
def remove_share_type_access(context, share_type_id, project_id):
|
||||
"""Remove access to share type for project_id."""
|
||||
if share_type_id is None:
|
||||
msg = _("share_type_id cannot be None")
|
||||
raise exception.InvalidShareType(reason=msg)
|
||||
return db.share_type_access_remove(context, share_type_id, project_id)
|
||||
|
||||
|
||||
def share_types_diff(context, share_type_id1, share_type_id2):
|
||||
"""Returns a 'diff' of two share types and whether they are equal.
|
||||
|
||||
|
|
|
@ -0,0 +1,316 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
|
||||
import mock
|
||||
import webob
|
||||
|
||||
from manila.api.contrib import share_type_access as type_access
|
||||
from manila.api.v1 import share_types
|
||||
from manila import context
|
||||
from manila import db
|
||||
from manila import exception
|
||||
from manila.share import share_types as share_types_api
|
||||
from manila import test
|
||||
from manila.tests.api import fakes
|
||||
|
||||
|
||||
def generate_type(type_id, is_public):
|
||||
return {
|
||||
'id': type_id,
|
||||
'name': u'test',
|
||||
'deleted': False,
|
||||
'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1),
|
||||
'updated_at': None,
|
||||
'deleted_at': None,
|
||||
'is_public': bool(is_public),
|
||||
'extra_specs': {}
|
||||
}
|
||||
|
||||
|
||||
SHARE_TYPES = {
|
||||
'0': generate_type('0', True),
|
||||
'1': generate_type('1', True),
|
||||
'2': generate_type('2', False),
|
||||
'3': generate_type('3', False)}
|
||||
|
||||
PROJ1_UUID = '11111111-1111-1111-1111-111111111111'
|
||||
PROJ2_UUID = '22222222-2222-2222-2222-222222222222'
|
||||
PROJ3_UUID = '33333333-3333-3333-3333-333333333333'
|
||||
|
||||
ACCESS_LIST = [{'share_type_id': '2', 'project_id': PROJ2_UUID},
|
||||
{'share_type_id': '2', 'project_id': PROJ3_UUID},
|
||||
{'share_type_id': '3', 'project_id': PROJ3_UUID}]
|
||||
|
||||
|
||||
def fake_share_type_get(context, id, inactive=False, expected_fields=None):
|
||||
vol = SHARE_TYPES[id]
|
||||
if expected_fields and 'projects' in expected_fields:
|
||||
vol['projects'] = [a['project_id']
|
||||
for a in ACCESS_LIST if a['share_type_id'] == id]
|
||||
return vol
|
||||
|
||||
|
||||
def _has_type_access(type_id, project_id):
|
||||
for access in ACCESS_LIST:
|
||||
if (access['share_type_id'] == type_id
|
||||
and access['project_id'] == project_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fake_share_type_get_all(context, inactive=False, filters=None):
|
||||
if filters is None or filters.get('is_public', None) is None:
|
||||
return SHARE_TYPES
|
||||
res = {}
|
||||
for k, v in SHARE_TYPES.iteritems():
|
||||
if filters['is_public'] and _has_type_access(k, context.project_id):
|
||||
res.update({k: v})
|
||||
continue
|
||||
if v['is_public'] == filters['is_public']:
|
||||
res.update({k: v})
|
||||
return res
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
obj = {'share_type': {'id': '0'},
|
||||
'share_types': [
|
||||
{'id': '0'},
|
||||
{'id': '2'}]}
|
||||
|
||||
def attach(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class FakeRequest(object):
|
||||
environ = {"manila.context": context.get_admin_context()}
|
||||
|
||||
def cached_resource_by_id(self, resource_id, name=None):
|
||||
return SHARE_TYPES[resource_id]
|
||||
|
||||
|
||||
class ShareTypeAccessTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ShareTypeAccessTest, self).setUp()
|
||||
self.type_access_controller = type_access.ShareTypeAccessController()
|
||||
self.type_action_controller = type_access.ShareTypeActionController()
|
||||
self.type_controller = share_types.ShareTypesController()
|
||||
self.req = FakeRequest()
|
||||
self.context = self.req.environ['manila.context']
|
||||
self.mock_object(db, 'share_type_get',
|
||||
fake_share_type_get)
|
||||
self.mock_object(db, 'share_type_get_all',
|
||||
fake_share_type_get_all)
|
||||
|
||||
def assertShareTypeListEqual(self, expected, observed):
|
||||
self.assertEqual(len(expected), len(observed))
|
||||
expected = sorted(expected, key=lambda item: item['id'])
|
||||
observed = sorted(observed, key=lambda item: item['id'])
|
||||
for d1, d2 in zip(expected, observed):
|
||||
self.assertEqual(d1['id'], d2['id'])
|
||||
|
||||
def test_list_type_access_public(self):
|
||||
"""Querying os-share-type-access on public type should return 404."""
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types/os-share-type-access',
|
||||
use_admin_context=True)
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.type_access_controller.index,
|
||||
req, '1')
|
||||
|
||||
def test_list_type_access_private(self):
|
||||
expected = {'share_type_access': [
|
||||
{'share_type_id': '2', 'project_id': PROJ2_UUID},
|
||||
{'share_type_id': '2', 'project_id': PROJ3_UUID}]}
|
||||
result = self.type_access_controller.index(self.req, '2')
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_list_with_no_context(self):
|
||||
req = fakes.HTTPRequest.blank('/v1/types/fake/types')
|
||||
|
||||
def fake_authorize(context, target=None, action=None):
|
||||
raise exception.PolicyNotAuthorized(action='index')
|
||||
self.mock_object(type_access, 'authorize', fake_authorize)
|
||||
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.type_access_controller.index,
|
||||
req, 'fake')
|
||||
|
||||
def test_list_type_with_admin_default_proj1(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types',
|
||||
use_admin_context=True)
|
||||
req.environ['manila.context'].project_id = PROJ1_UUID
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_admin_default_proj2(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}, {'id': '2'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
||||
use_admin_context=True)
|
||||
req.environ['manila.context'].project_id = PROJ2_UUID
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_true(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
||||
use_admin_context=True)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_false(self):
|
||||
expected = {'share_types': [{'id': '2'}, {'id': '3'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||
use_admin_context=True)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_false_proj2(self):
|
||||
expected = {'share_types': [{'id': '2'}, {'id': '3'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||
use_admin_context=True)
|
||||
req.environ['manila.context'].project_id = PROJ2_UUID
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_none(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}, {'id': '2'},
|
||||
{'id': '3'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=all',
|
||||
use_admin_context=True)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_no_admin_default(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_no_admin_ispublic_true(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_no_admin_ispublic_false(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_list_type_with_no_admin_ispublic_none(self):
|
||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=all',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller.index(req)
|
||||
self.assertShareTypeListEqual(expected['share_types'],
|
||||
result['share_types'])
|
||||
|
||||
def test_show(self):
|
||||
resp = FakeResponse()
|
||||
self.type_action_controller.show(self.req, resp, '0')
|
||||
self.assertEqual({'id': '0', 'os-share-type-access:is_public': True},
|
||||
resp.obj['share_type'])
|
||||
self.type_action_controller.show(self.req, resp, '2')
|
||||
self.assertEqual({'id': '0', 'os-share-type-access:is_public': False},
|
||||
resp.obj['share_type'])
|
||||
|
||||
def test_create(self):
|
||||
resp = FakeResponse()
|
||||
self.type_action_controller.create(self.req, {}, resp)
|
||||
self.assertEqual({'id': '0', 'os-share-type-access:is_public': True},
|
||||
resp.obj['share_type'])
|
||||
|
||||
def test_add_project_access(self):
|
||||
def stub_add_share_type_access(context, type_id, project_id):
|
||||
self.assertEqual('3', type_id, "type_id")
|
||||
self.assertEqual(PROJ2_UUID, project_id, "project_id")
|
||||
self.mock_object(db, 'share_type_access_add',
|
||||
stub_add_share_type_access)
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
result = self.type_action_controller._addProjectAccess(req, '3', body)
|
||||
self.assertEqual(202, result.status_code)
|
||||
|
||||
def test_add_project_access_with_no_admin_user(self):
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=False)
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.type_action_controller._addProjectAccess,
|
||||
req, '2', body)
|
||||
|
||||
def test_add_project_access_with_already_added_access(self):
|
||||
def stub_add_share_type_access(context, type_id, project_id):
|
||||
raise exception.ShareTypeAccessExists(share_type_id=type_id,
|
||||
project_id=project_id)
|
||||
self.mock_object(db, 'share_type_access_add',
|
||||
stub_add_share_type_access)
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
self.assertRaises(webob.exc.HTTPConflict,
|
||||
self.type_action_controller._addProjectAccess,
|
||||
req, '3', body)
|
||||
|
||||
def test_add_project_access_to_public_share_type(self):
|
||||
share_type_id = '3'
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
self.mock_object(share_types_api, 'get_share_type',
|
||||
mock.Mock(return_value={"is_public": True}))
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
|
||||
self.assertRaises(webob.exc.HTTPForbidden,
|
||||
self.type_action_controller._addProjectAccess,
|
||||
req, share_type_id, body)
|
||||
share_types_api.get_share_type.assert_called_once_with(
|
||||
mock.ANY, share_type_id)
|
||||
|
||||
def test_remove_project_access_with_bad_access(self):
|
||||
def stub_remove_share_type_access(context, type_id, project_id):
|
||||
raise exception.ShareTypeAccessNotFound(share_type_id=type_id,
|
||||
project_id=project_id)
|
||||
self.mock_object(db, 'share_type_access_remove',
|
||||
stub_remove_share_type_access)
|
||||
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.type_action_controller._removeProjectAccess,
|
||||
req, '3', body)
|
||||
|
||||
def test_remove_project_access_with_no_admin_user(self):
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=False)
|
||||
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.type_action_controller._removeProjectAccess,
|
||||
req, '2', body)
|
|
@ -57,7 +57,7 @@ def stub_share_type_extra_specs():
|
|||
return specs
|
||||
|
||||
|
||||
def share_type_get(context, share_type_id):
|
||||
def share_type_get(context, id, inactive=False, expected_fields=None):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ def return_share_types_with_volumes_destroy(context, id):
|
|||
pass
|
||||
|
||||
|
||||
def return_share_types_create(context, name, specs):
|
||||
def return_share_types_create(context, name, specs, is_public):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -14,14 +14,18 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ddt
|
||||
import iso8601
|
||||
from lxml import etree
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
import webob
|
||||
|
||||
from manila.api import extensions
|
||||
from manila.api.v1 import router
|
||||
from manila.api import xmlutil
|
||||
from manila import policy
|
||||
from manila import test
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
@ -150,3 +154,32 @@ class ExtensionControllerTest(ExtensionTestCase):
|
|||
'The Fox In Socks Extension.')
|
||||
|
||||
xmlutil.validate_schema(root, 'extension')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ExtensionAuthorizeTestCase(test.TestCase):
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data({'action': 'fake', 'valid': 'api_extension:fake:fake'},
|
||||
{'action': None, 'valid': 'api_extension:fake'})
|
||||
def test_extension_authorizer(self, action, valid):
|
||||
self.mock_object(policy, 'enforce')
|
||||
target = 'fake'
|
||||
|
||||
extensions.extension_authorizer('api', 'fake')(
|
||||
{}, target, action)
|
||||
|
||||
policy.enforce.assert_called_once_with(mock.ANY, valid, target)
|
||||
|
||||
def test_extension_authorizer_empty_target(self):
|
||||
self.mock_object(policy, 'enforce')
|
||||
target = None
|
||||
context = mock.Mock()
|
||||
context.project_id = 'fake'
|
||||
context.user_id = 'fake'
|
||||
|
||||
extensions.extension_authorizer('api', 'fake')(
|
||||
context, target, 'fake')
|
||||
|
||||
policy.enforce.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, {'project_id': 'fake', 'user_id': 'fake'})
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
@ -45,7 +46,7 @@ def stub_share_type(id):
|
|||
)
|
||||
|
||||
|
||||
def return_share_types_get_all_types(context):
|
||||
def return_share_types_get_all_types(context, search_opts=None):
|
||||
return dict(
|
||||
share_type_1=stub_share_type(1),
|
||||
share_type_2=stub_share_type(2),
|
||||
|
@ -53,7 +54,7 @@ def return_share_types_get_all_types(context):
|
|||
)
|
||||
|
||||
|
||||
def return_empty_share_types_get_all_types(context):
|
||||
def return_empty_share_types_get_all_types(context, search_opts=None):
|
||||
return {}
|
||||
|
||||
|
||||
|
@ -69,6 +70,7 @@ def return_share_types_get_by_name(context, name):
|
|||
return stub_share_type(int(name.split("_")[2]))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ShareTypesApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(ShareTypesApiTest, self).setUp()
|
||||
|
@ -199,3 +201,13 @@ class ShareTypesApiTest(test.TestCase):
|
|||
)
|
||||
self.assertDictMatch(output['share_types'][i],
|
||||
expected_share_type)
|
||||
|
||||
@ddt.data(None, True, 'true', 'false', 'all')
|
||||
def test_parse_is_public_valid(self, value):
|
||||
result = self.controller._parse_is_public(value)
|
||||
self.assertTrue(result in (True, False, None))
|
||||
|
||||
def test_parse_is_public_invalid(self):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._parse_is_public,
|
||||
'fakefakefake')
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
"share_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]],
|
||||
"share_extension:types_manage": [],
|
||||
"share_extension:types_extra_specs": [],
|
||||
"share_extension:share_type_access": "",
|
||||
"share_extension:share_type_access:addProjectAccess": "rule:admin_api",
|
||||
"share_extension:share_type_access:removeProjectAccess": "rule:admin_api",
|
||||
|
||||
"security_service:index": [],
|
||||
"security_service:get_all_security_services": [["rule:admin_api"]],
|
||||
|
|
|
@ -103,14 +103,16 @@ class ShareTypesTestCase(test.TestCase):
|
|||
returned_type = share_types.get_all_types(self.context)
|
||||
self.assertItemsEqual(share_type, returned_type)
|
||||
|
||||
def test_get_all_types_filter(self):
|
||||
def test_get_all_types_search(self):
|
||||
share_type = self.fake_type_w_extra
|
||||
search_filter = {"extra_specs": {"gold": "True"}}
|
||||
search_filter = {"extra_specs": {"gold": "True"}, 'is_public': True}
|
||||
self.mock_object(db,
|
||||
'share_type_get_all',
|
||||
mock.Mock(return_value=share_type))
|
||||
returned_type = share_types.get_all_types(self.context,
|
||||
search_opts=search_filter)
|
||||
db.share_type_get_all.assert_called_once_with(
|
||||
mock.ANY, 0, filters={'is_public': True})
|
||||
self.assertItemsEqual(share_type, returned_type)
|
||||
search_filter = {"extra_specs": {"gold": "False"}}
|
||||
returned_type = share_types.get_all_types(self.context,
|
||||
|
@ -213,4 +215,43 @@ class ShareTypesTestCase(test.TestCase):
|
|||
@ddt.data(None, {})
|
||||
def test_get_valid_required_extra_specs_invalid(self, specs):
|
||||
self.assertRaises(exception.InvalidExtraSpec,
|
||||
share_types.get_valid_required_extra_specs, specs)
|
||||
share_types.get_valid_required_extra_specs, specs)
|
||||
|
||||
def test_add_access(self):
|
||||
project_id = '456'
|
||||
extra_specs = {
|
||||
constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS: 'true'
|
||||
}
|
||||
share_type = share_types.create(self.context, 'type1', extra_specs)
|
||||
share_type_id = share_type.get('id')
|
||||
|
||||
share_types.add_share_type_access(self.context, share_type_id,
|
||||
project_id)
|
||||
stype_access = db.share_type_access_get_all(self.context,
|
||||
share_type_id)
|
||||
self.assertIn(project_id, [a.project_id for a in stype_access])
|
||||
|
||||
def test_add_access_invalid(self):
|
||||
self.assertRaises(exception.InvalidShareType,
|
||||
share_types.add_share_type_access,
|
||||
'fake', None, 'fake')
|
||||
|
||||
def test_remove_access(self):
|
||||
project_id = '456'
|
||||
extra_specs = {
|
||||
constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS: 'true'
|
||||
}
|
||||
share_type = share_types.create(
|
||||
self.context, 'type1', projects=['456'], extra_specs=extra_specs)
|
||||
share_type_id = share_type.get('id')
|
||||
|
||||
share_types.remove_share_type_access(self.context, share_type_id,
|
||||
project_id)
|
||||
stype_access = db.share_type_access_get_all(self.context,
|
||||
share_type_id)
|
||||
self.assertNotIn(project_id, stype_access)
|
||||
|
||||
def test_remove_access_invalid(self):
|
||||
self.assertRaises(exception.InvalidShareType,
|
||||
share_types.remove_share_type_access,
|
||||
'fake', None, 'fake')
|
||||
|
|
Loading…
Reference in New Issue