Merge "Allow restricting access rules fields and deletion"
This commit is contained in:
commit
44014e6827
@ -1632,6 +1632,22 @@ links:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: array
|
type: array
|
||||||
|
lock_deletion:
|
||||||
|
description: |
|
||||||
|
Whether the resource should have its deletion locked or not.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
min_version: 2.82
|
||||||
|
lock_visibility:
|
||||||
|
description: |
|
||||||
|
Whether the resource should have its sensitive fields restricted or not.
|
||||||
|
When enabled, other users will see the "access_to" and "access_secret"
|
||||||
|
fields set to ******
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
min_version: 2.82
|
||||||
manage_host:
|
manage_host:
|
||||||
description: |
|
description: |
|
||||||
The host of the destination back end, in this format: ``host@backend``.
|
The host of the destination back end, in this format: ``host@backend``.
|
||||||
@ -3927,6 +3943,14 @@ unit:
|
|||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
unrestrict_access:
|
||||||
|
description: |
|
||||||
|
Whether the service should attempt to remove deletion restrictions during
|
||||||
|
the access rule deletion or not.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
min_version: 2.82
|
||||||
updated_at:
|
updated_at:
|
||||||
description: |
|
description: |
|
||||||
The date and time stamp when the resource was last updated within the
|
The date and time stamp when the resource was last updated within the
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
"metadata":{
|
"metadata":{
|
||||||
"key1": "value1",
|
"key1": "value1",
|
||||||
"key2": "value2"
|
"key2": "value2"
|
||||||
}
|
},
|
||||||
|
"lock_visibility": false,
|
||||||
|
"lock_deletion": true,
|
||||||
|
"lock_reason": "Locked for deletion until year end audit."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"deny_access": {
|
"deny_access": {
|
||||||
"access_id": "a25b2df3-90bd-4add-afa6-5f0dbbd50452"
|
"access_id": "a25b2df3-90bd-4add-afa6-5f0dbbd50452",
|
||||||
|
"unrestrict": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,12 @@ Share access rules (since API v2.45)
|
|||||||
|
|
||||||
Retrieve details about access rules
|
Retrieve details about access rules
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Starting from API version 2.82, access rule visibility can be restricted
|
||||||
|
by a project user, or any user with "service" or "admin" roles. When
|
||||||
|
restricted, the access_to and access_key fields will be redacted to other
|
||||||
|
users. This redaction applies irrespective of the API version.
|
||||||
|
|
||||||
Describe share access rule
|
Describe share access rule
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -74,6 +80,12 @@ Lists the share access rules on a share.
|
|||||||
This API replaces the older :ref:`List share access rules
|
This API replaces the older :ref:`List share access rules
|
||||||
<get-access-rules-before-2-45>` API from version 2.45.
|
<get-access-rules-before-2-45>` API from version 2.45.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Starting from API version 2.82, access rule visibility can be restricted
|
||||||
|
by a project user, or any user with "service" or "admin" roles. When
|
||||||
|
restricted, the access_to and access_key fields will be redacted to other
|
||||||
|
users. This redaction applies irrespective of the API version.
|
||||||
|
|
||||||
Response codes
|
Response codes
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -59,6 +59,13 @@ methods:
|
|||||||
|
|
||||||
IPv6 based access is only supported with API version 2.38 and beyond.
|
IPv6 based access is only supported with API version 2.38 and beyond.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Starting from API version 2.82, it is possible to lock the deletion,
|
||||||
|
restrict the visibility of sensible fields of the access rules, and specify a
|
||||||
|
reason for such locks while invoking the grant access API through the
|
||||||
|
parameters ``lock_deletion``, ``lock_visibility`` and ``lock_reason``
|
||||||
|
respectively.
|
||||||
|
|
||||||
- ``cert``. Authenticates an instance through a TLS certificate.
|
- ``cert``. Authenticates an instance through a TLS certificate.
|
||||||
Specify the TLS identity as the IDENTKEY. A valid value is any
|
Specify the TLS identity as the IDENTKEY. A valid value is any
|
||||||
string up to 64 characters long in the common name (CN) of the
|
string up to 64 characters long in the common name (CN) of the
|
||||||
@ -99,6 +106,9 @@ Request
|
|||||||
- access_type: access_type
|
- access_type: access_type
|
||||||
- access_to: access_to
|
- access_to: access_to
|
||||||
- metadata: access_metadata_grant_access
|
- metadata: access_metadata_grant_access
|
||||||
|
- lock_visibility: lock_visibility
|
||||||
|
- lock_deletion: lock_deletion
|
||||||
|
- lock_reason: resource_lock_lock_reason
|
||||||
|
|
||||||
Request example
|
Request example
|
||||||
---------------
|
---------------
|
||||||
@ -138,6 +148,10 @@ The shared file systems service stores each access rule in its database and
|
|||||||
assigns it a unique ID. This ID can be used to revoke access after access
|
assigns it a unique ID. This ID can be used to revoke access after access
|
||||||
has been requested.
|
has been requested.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In case the access rule had its deletion locked, it will be necessary to
|
||||||
|
provide the ``unrestrict`` parameter in the revoke access request.
|
||||||
|
|
||||||
Response codes
|
Response codes
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
@ -161,6 +175,7 @@ Request
|
|||||||
- share_id: share_id
|
- share_id: share_id
|
||||||
- deny_access: deny_access
|
- deny_access: deny_access
|
||||||
- access_id: access_id
|
- access_id: access_id
|
||||||
|
- unrestrict: unrestrict_access
|
||||||
|
|
||||||
|
|
||||||
Request example
|
Request example
|
||||||
|
@ -740,6 +740,17 @@ Allow access to the share with ``user`` access type:
|
|||||||
features support mapping <https://docs.openstack.org/manila/latest/admin
|
features support mapping <https://docs.openstack.org/manila/latest/admin
|
||||||
/share_back_ends_feature_support_mapping.html>`_.
|
/share_back_ends_feature_support_mapping.html>`_.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
Starting from the 2023.2 (Bobcat) release, in case you want to restrict the
|
||||||
|
visibility of the sensitive fields (``access_to`` and ``access_key``), or
|
||||||
|
avoid the access rule being deleted by other users, you can specify
|
||||||
|
``--lock-visibility`` and ``--lock-deletion`` in the Manila OpenStack command
|
||||||
|
for creating access rules. A reason (``--lock-reason``) can also be provided.
|
||||||
|
Only the user that placed the lock, system administrators and services will
|
||||||
|
be able to view sensitive fields of, or manipulate such access rules by
|
||||||
|
virtue of default RBAC.
|
||||||
|
|
||||||
To verify that the access rules (ACL) were configured correctly for a share,
|
To verify that the access rules (ACL) were configured correctly for a share,
|
||||||
you list permissions for a share:
|
you list permissions for a share:
|
||||||
|
|
||||||
@ -766,3 +777,10 @@ access rule list:
|
|||||||
+--------------------------------------+-------------+-----------+--------------+-------+
|
+--------------------------------------+-------------+-----------+--------------+-------+
|
||||||
| 4f391c6b-fb4f-47f5-8b4b-88c5ec9d568a | user | demo | rw | error |
|
| 4f391c6b-fb4f-47f5-8b4b-88c5ec9d568a | user | demo | rw | error |
|
||||||
+--------------------------------------+-------------+-----------+--------------+-------+
|
+--------------------------------------+-------------+-----------+--------------+-------+
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Starting from the 2023.2 (Bobcat) release, it is possible to prevent the
|
||||||
|
deletion of an access rule. In case the deletion was locked, the
|
||||||
|
``--unrestrict`` argument from the Manila's OpenStack Client must be used
|
||||||
|
in the request to revoke the access.
|
||||||
|
@ -7,7 +7,7 @@ Create and manage shares
|
|||||||
.. contents:: :local:
|
.. contents:: :local:
|
||||||
|
|
||||||
General Concepts
|
General Concepts
|
||||||
~~~~~~~~~~~~~~~~
|
----------------
|
||||||
|
|
||||||
A ``share`` is filesystem storage that you can create with manila. You can pick
|
A ``share`` is filesystem storage that you can create with manila. You can pick
|
||||||
a network protocol for the underlying storage, manage access and perform
|
a network protocol for the underlying storage, manage access and perform
|
||||||
@ -97,7 +97,7 @@ important terms:
|
|||||||
|
|
||||||
|
|
||||||
Usage and Limits
|
Usage and Limits
|
||||||
~~~~~~~~~~~~~~~~
|
----------------
|
||||||
|
|
||||||
* List the resource limits and usages that apply to your project
|
* List the resource limits and usages that apply to your project
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ Usage and Limits
|
|||||||
+----------------------------+-------+
|
+----------------------------+-------+
|
||||||
|
|
||||||
Share types
|
Share types
|
||||||
~~~~~~~~~~~
|
-----------
|
||||||
|
|
||||||
* List share types
|
* List share types
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ Share types
|
|||||||
+--------------------------------------+-----------------------------------+------------+------------+--------------------------------------+--------------------------------------------+---------------------------------------------------------+
|
+--------------------------------------+-----------------------------------+------------+------------+--------------------------------------+--------------------------------------------+---------------------------------------------------------+
|
||||||
|
|
||||||
Share networks
|
Share networks
|
||||||
~~~~~~~~~~~~~~
|
--------------
|
||||||
|
|
||||||
* Create a share network.
|
* Create a share network.
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ Share networks
|
|||||||
+--------------------------------------+----------------+
|
+--------------------------------------+----------------+
|
||||||
|
|
||||||
Create a share
|
Create a share
|
||||||
~~~~~~~~~~~~~~
|
--------------
|
||||||
|
|
||||||
* Create a share
|
* Create a share
|
||||||
|
|
||||||
@ -366,6 +366,19 @@ Create a share
|
|||||||
| 40de4f4c-4588-4d9c-844b-f74d8951053a | myshare2 | 1 | NFS | available | False | default | nosb-devstack@lisboa#LISBOA | nova |
|
| 40de4f4c-4588-4d9c-844b-f74d8951053a | myshare2 | 1 | NFS | available | False | default | nosb-devstack@lisboa#LISBOA | nova |
|
||||||
+--------------------------------------+-----------+------+-------------+-----------+-----------+-----------------+-----------------------------+-------------------+
|
+--------------------------------------+-----------+------+-------------+-----------+-----------+-----------------+-----------------------------+-------------------+
|
||||||
|
|
||||||
|
Grant and revoke share access
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
Starting from the 2023.2 (Bobcat) release, in case you want to restrict the
|
||||||
|
visibility of the sensitive fields (``access_to`` and ``access_key``), or
|
||||||
|
avoid the access rule being deleted by other users, you can specify
|
||||||
|
``--lock-visibility`` and ``--lock-deletion`` in the Manila OpenStack command
|
||||||
|
for creating access rules. A reason (``--lock-reason``) can also be provided.
|
||||||
|
Only the user that placed the lock, system administrators and services will
|
||||||
|
be able to manipulate such access rules.
|
||||||
|
|
||||||
Allow read-write access
|
Allow read-write access
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -448,8 +461,14 @@ Allow read-only access
|
|||||||
|
|
||||||
Another access rule is created.
|
Another access rule is created.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
In case one or more access rules had its visibility locked, you might not be
|
||||||
|
able to see the content of the fields containing sensitive information
|
||||||
|
(``access_to`` and ``access_key``).
|
||||||
|
|
||||||
Update access rules metadata
|
Update access rules metadata
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
----------------------------
|
||||||
|
|
||||||
#. Add a new metadata.
|
#. Add a new metadata.
|
||||||
|
|
||||||
@ -494,7 +513,7 @@ Update access rules metadata
|
|||||||
+--------------+--------------------------------------+
|
+--------------+--------------------------------------+
|
||||||
|
|
||||||
Deny access
|
Deny access
|
||||||
~~~~~~~~~~~
|
-----------
|
||||||
|
|
||||||
* Deny access.
|
* Deny access.
|
||||||
|
|
||||||
@ -503,6 +522,13 @@ Deny access
|
|||||||
$ manila access-deny myshare 45b0a030-306a-4305-9e2a-36aeffb2d5b7
|
$ manila access-deny myshare 45b0a030-306a-4305-9e2a-36aeffb2d5b7
|
||||||
$ manila access-deny myshare e30bde96-9217-4f90-afdc-27c092af1c77
|
$ manila access-deny myshare e30bde96-9217-4f90-afdc-27c092af1c77
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Starting from the 2023.2 (Bobcat) release, it is possible to prevent the
|
||||||
|
deletion of an access rule. In case you have placed a deletion lock during
|
||||||
|
the access rule creation, the ``--unrestrict`` argument from the Manila's
|
||||||
|
OpenStack Client must be used in the request to revoke the access.
|
||||||
|
|
||||||
* List access.
|
* List access.
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
@ -516,7 +542,7 @@ Deny access
|
|||||||
The access rules are removed.
|
The access rules are removed.
|
||||||
|
|
||||||
Create snapshot
|
Create snapshot
|
||||||
~~~~~~~~~~~~~~~
|
---------------
|
||||||
|
|
||||||
* Create a snapshot.
|
* Create a snapshot.
|
||||||
|
|
||||||
@ -556,7 +582,7 @@ Create snapshot
|
|||||||
+--------------------------------------+--------------------------------------+-----------+------------+------------+
|
+--------------------------------------+--------------------------------------+-----------+------------+------------+
|
||||||
|
|
||||||
Create share from snapshot
|
Create share from snapshot
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
--------------------------
|
||||||
|
|
||||||
* Create a share from a snapshot.
|
* Create a share from a snapshot.
|
||||||
|
|
||||||
@ -661,7 +687,7 @@ Create share from snapshot
|
|||||||
+---------------------------------------+----------------------------------------------------------------------------------------------------------------------+
|
+---------------------------------------+----------------------------------------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
Delete share
|
Delete share
|
||||||
~~~~~~~~~~~~
|
------------
|
||||||
|
|
||||||
* Delete a share.
|
* Delete a share.
|
||||||
|
|
||||||
@ -684,7 +710,7 @@ Delete share
|
|||||||
The share is being deleted.
|
The share is being deleted.
|
||||||
|
|
||||||
Delete snapshot
|
Delete snapshot
|
||||||
~~~~~~~~~~~~~~~
|
---------------
|
||||||
|
|
||||||
* Delete a snapshot.
|
* Delete a snapshot.
|
||||||
|
|
||||||
@ -706,7 +732,7 @@ Delete snapshot
|
|||||||
The snapshot is deleted.
|
The snapshot is deleted.
|
||||||
|
|
||||||
Extend share
|
Extend share
|
||||||
~~~~~~~~~~~~
|
------------
|
||||||
|
|
||||||
* Extend share.
|
* Extend share.
|
||||||
|
|
||||||
@ -803,7 +829,7 @@ Extend share
|
|||||||
+---------------------------------------+----------------------------------------------------------------------------------------------------------------------+
|
+---------------------------------------+----------------------------------------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
Shrink share
|
Shrink share
|
||||||
~~~~~~~~~~~~
|
------------
|
||||||
|
|
||||||
* Shrink a share.
|
* Shrink a share.
|
||||||
|
|
||||||
@ -900,7 +926,7 @@ Shrink share
|
|||||||
+---------------------------------------+----------------------------------------------------------------------------------------------------------------------+
|
+---------------------------------------+----------------------------------------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
Share metadata
|
Share metadata
|
||||||
~~~~~~~~~~~~~~
|
--------------
|
||||||
|
|
||||||
* Set metadata items on your share
|
* Set metadata items on your share
|
||||||
|
|
||||||
@ -938,7 +964,7 @@ Share metadata
|
|||||||
$ manila metadata myshare unset year_started
|
$ manila metadata myshare unset year_started
|
||||||
|
|
||||||
Share revert to snapshot
|
Share revert to snapshot
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
------------------------
|
||||||
|
|
||||||
* Share revert to snapshot
|
* Share revert to snapshot
|
||||||
|
|
||||||
@ -955,7 +981,7 @@ Share revert to snapshot
|
|||||||
$ manila revert-to-snapshot mysnapshot
|
$ manila revert-to-snapshot mysnapshot
|
||||||
|
|
||||||
Share Transfer
|
Share Transfer
|
||||||
~~~~~~~~~~~~~~
|
--------------
|
||||||
|
|
||||||
* Transfer a share to a different project
|
* Transfer a share to a different project
|
||||||
|
|
||||||
@ -1032,7 +1058,7 @@ Share Transfer
|
|||||||
+------------------------+--------------------------------------+
|
+------------------------+--------------------------------------+
|
||||||
|
|
||||||
Resource locks
|
Resource locks
|
||||||
~~~~~~~~~~~~~~
|
--------------
|
||||||
|
|
||||||
* Prevent a share from being deleted by creating a ``resource lock``:
|
* Prevent a share from being deleted by creating a ``resource lock``:
|
||||||
|
|
||||||
|
@ -199,13 +199,14 @@ REST_API_VERSION_HISTORY = """
|
|||||||
count info.
|
count info.
|
||||||
* 2.80 - Added share backup APIs.
|
* 2.80 - Added share backup APIs.
|
||||||
* 2.81 - Added API methods, endpoint /resource-locks.
|
* 2.81 - Added API methods, endpoint /resource-locks.
|
||||||
|
* 2.82 - Added lock and restriction to share access rules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
# The default api version request is defined to be the
|
# The default api version request is defined to be the
|
||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
_MIN_API_VERSION = "2.0"
|
_MIN_API_VERSION = "2.0"
|
||||||
_MAX_API_VERSION = "2.81"
|
_MAX_API_VERSION = "2.82"
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -439,3 +439,8 @@ ____
|
|||||||
----
|
----
|
||||||
Introduce resource locks as a way users can restrict certain actions on
|
Introduce resource locks as a way users can restrict certain actions on
|
||||||
resources. Only share deletion can be prevented at this version.
|
resources. Only share deletion can be prevented at this version.
|
||||||
|
|
||||||
|
2.82
|
||||||
|
----
|
||||||
|
Introduce the ability to lock access rules and restrict the visibility of
|
||||||
|
sensitive fields.
|
||||||
|
@ -32,6 +32,7 @@ from manila.common import constants
|
|||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.i18n import _
|
from manila.i18n import _
|
||||||
|
from manila.lock import api as resource_locks
|
||||||
from manila import share
|
from manila import share
|
||||||
from manila.share import share_types
|
from manila.share import share_types
|
||||||
from manila import utils
|
from manila import utils
|
||||||
@ -455,10 +456,61 @@ class ShareMixin(object):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _create_access_locks(
|
||||||
|
self, context, access, lock_deletion=False, lock_visibility=False,
|
||||||
|
lock_reason=None):
|
||||||
|
"""Creates locks for access rules and rollback if it fails."""
|
||||||
|
|
||||||
|
# We must populate project_id and user_id in the access object, as this
|
||||||
|
# is not in this entity
|
||||||
|
access['project_id'] = context.project_id
|
||||||
|
access['user_id'] = context.user_id
|
||||||
|
|
||||||
|
def raise_lock_failed(access, lock_action):
|
||||||
|
word_mapping = {
|
||||||
|
constants.RESOURCE_ACTION_SHOW: 'visibility',
|
||||||
|
constants.RESOURCE_ACTION_DELETE: 'deletion'
|
||||||
|
}
|
||||||
|
msg = _("Failed to lock the %(action)s of the access rule "
|
||||||
|
"%(rule)s.") % {
|
||||||
|
'action': word_mapping[lock_action],
|
||||||
|
'rule': access['id']
|
||||||
|
}
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
deletion_lock = {}
|
||||||
|
|
||||||
|
if lock_deletion:
|
||||||
|
try:
|
||||||
|
deletion_lock = self.resource_locks_api.create(
|
||||||
|
context, resource_id=access['id'],
|
||||||
|
resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_DELETE,
|
||||||
|
resource=access, lock_reason=lock_reason)
|
||||||
|
except Exception:
|
||||||
|
raise_lock_failed(access, constants.RESOURCE_ACTION_DELETE)
|
||||||
|
|
||||||
|
if lock_visibility:
|
||||||
|
try:
|
||||||
|
self.resource_locks_api.create(
|
||||||
|
context, resource_id=access['id'],
|
||||||
|
resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_SHOW,
|
||||||
|
resource=access, lock_reason=lock_reason)
|
||||||
|
except Exception:
|
||||||
|
# If a deletion lock was placed and the visibility wasn't,
|
||||||
|
# we should rollback the deletion lock.
|
||||||
|
if deletion_lock:
|
||||||
|
self.resource_locks_api.delete(
|
||||||
|
context, deletion_lock['id'])
|
||||||
|
raise_lock_failed(access, constants.RESOURCE_ACTION_SHOW)
|
||||||
|
|
||||||
@wsgi.Controller.authorize('allow_access')
|
@wsgi.Controller.authorize('allow_access')
|
||||||
def _allow_access(self, req, id, body, enable_ceph=False,
|
def _allow_access(self, req, id, body, enable_ceph=False,
|
||||||
allow_on_error_status=False, enable_ipv6=False,
|
allow_on_error_status=False, enable_ipv6=False,
|
||||||
enable_metadata=False, allow_on_error_state=False):
|
enable_metadata=False, allow_on_error_state=False,
|
||||||
|
lock_visibility=False, lock_deletion=False,
|
||||||
|
lock_reason=None):
|
||||||
"""Add share access rule."""
|
"""Add share access rule."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
access_data = body.get('allow_access', body.get('os-allow_access'))
|
access_data = body.get('allow_access', body.get('os-allow_access'))
|
||||||
@ -487,6 +539,11 @@ class ShareMixin(object):
|
|||||||
}
|
}
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
if not (lock_visibility or lock_deletion) and lock_reason:
|
||||||
|
msg = _("Lock reason can only be specified when locking the "
|
||||||
|
"visibility or the deletion of an access rule.")
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
access_type = access_data['access_type']
|
access_type = access_data['access_type']
|
||||||
access_to = access_data['access_to']
|
access_to = access_data['access_to']
|
||||||
common.validate_access(access_type=access_type,
|
common.validate_access(access_type=access_type,
|
||||||
@ -507,15 +564,67 @@ class ShareMixin(object):
|
|||||||
except exception.InvalidMetadataSize as error:
|
except exception.InvalidMetadataSize as error:
|
||||||
raise exc.HTTPBadRequest(explanation=error.msg)
|
raise exc.HTTPBadRequest(explanation=error.msg)
|
||||||
|
|
||||||
|
if lock_deletion or lock_visibility:
|
||||||
|
self._create_access_locks(
|
||||||
|
context, access, lock_deletion=lock_deletion,
|
||||||
|
lock_visibility=lock_visibility, lock_reason=lock_reason)
|
||||||
|
|
||||||
return self._access_view_builder.view(req, access)
|
return self._access_view_builder.view(req, access)
|
||||||
|
|
||||||
|
def _check_for_access_rule_locks(self, context, access_data, access_id,
|
||||||
|
share_id):
|
||||||
|
"""Fetches locks for access rules and attempts deleting them."""
|
||||||
|
|
||||||
|
# ensure the requester is asking to remove the restrictions of the rule
|
||||||
|
unrestrict = access_data.get('unrestrict', False)
|
||||||
|
search_opts = {
|
||||||
|
'resource_id': access_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
locks, locks_count = (
|
||||||
|
self.resource_locks_api.get_all(
|
||||||
|
context, search_opts=search_opts, show_count=True) or []
|
||||||
|
)
|
||||||
|
|
||||||
|
# no locks placed, nothing to do
|
||||||
|
if not locks:
|
||||||
|
return
|
||||||
|
|
||||||
|
def raise_rule_is_locked(share_id, unrestrict=False):
|
||||||
|
msg = _(
|
||||||
|
"Cannot deny access for share '%s' since it has been "
|
||||||
|
"locked. Please remove the locks and retry the "
|
||||||
|
"operation") % share_id
|
||||||
|
if unrestrict:
|
||||||
|
msg = _(
|
||||||
|
"Unable to drop access rule restrictions that are not "
|
||||||
|
"placed by you.")
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
|
if locks_count and not unrestrict:
|
||||||
|
raise_rule_is_locked(share_id)
|
||||||
|
|
||||||
|
non_deletable_locks = []
|
||||||
|
for lock in locks:
|
||||||
|
try:
|
||||||
|
self.resource_locks_api.ensure_context_can_delete_lock(
|
||||||
|
context, lock['id'])
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
non_deletable_locks.append(lock)
|
||||||
|
|
||||||
|
if non_deletable_locks:
|
||||||
|
raise_rule_is_locked(share_id, unrestrict=unrestrict)
|
||||||
|
|
||||||
@wsgi.Controller.authorize('deny_access')
|
@wsgi.Controller.authorize('deny_access')
|
||||||
def _deny_access(self, req, id, body, allow_on_error_state=False):
|
def _deny_access(self, req, id, body, allow_on_error_state=False):
|
||||||
"""Remove share access rule."""
|
"""Remove share access rule."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
|
|
||||||
access_id = body.get(
|
access_data = body.get('deny_access', body.get('os-deny_access'))
|
||||||
'deny_access', body.get('os-deny_access'))['access_id']
|
access_id = access_data['access_id']
|
||||||
|
|
||||||
|
self._check_for_access_rule_locks(context, access_data, access_id, id)
|
||||||
|
|
||||||
share = self.share_api.get(context, id)
|
share = self.share_api.get(context, id)
|
||||||
|
|
||||||
@ -637,6 +746,7 @@ class ShareController(wsgi.Controller, ShareMixin, wsgi.AdminActionsMixin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ShareController, self).__init__()
|
super(ShareController, self).__init__()
|
||||||
self.share_api = share.API()
|
self.share_api = share.API()
|
||||||
|
self.resource_locks_api = resource_locks.API()
|
||||||
self._access_view_builder = share_access_views.ViewBuilder()
|
self._access_view_builder = share_access_views.ViewBuilder()
|
||||||
|
|
||||||
@wsgi.action('os-reset_status')
|
@wsgi.action('os-reset_status')
|
||||||
|
@ -45,13 +45,17 @@ class ResourceLocksController(wsgi.Controller):
|
|||||||
_view_builder_class = resource_locks_view.ViewBuilder
|
_view_builder_class = resource_locks_view.ViewBuilder
|
||||||
resource_name = 'resource_lock'
|
resource_name = 'resource_lock'
|
||||||
|
|
||||||
def _check_body(self, body, for_update=False):
|
def _check_body(self, body, lock_to_update=None):
|
||||||
if 'resource_lock' not in body:
|
if 'resource_lock' not in body:
|
||||||
raise exc.HTTPBadRequest(
|
raise exc.HTTPBadRequest(
|
||||||
explanation="Malformed request body.")
|
explanation="Malformed request body.")
|
||||||
lock_data = body['resource_lock']
|
lock_data = body['resource_lock']
|
||||||
|
resource_type = (
|
||||||
|
lock_to_update['resource_type']
|
||||||
|
if lock_to_update
|
||||||
|
else lock_data.get('resource_type', constants.SHARE_RESOURCE_TYPE)
|
||||||
|
)
|
||||||
resource_id = lock_data.get('resource_id') or ''
|
resource_id = lock_data.get('resource_id') or ''
|
||||||
resource_type = lock_data.get('resource_type') or ''
|
|
||||||
resource_action = (lock_data.get('resource_action') or
|
resource_action = (lock_data.get('resource_action') or
|
||||||
constants.RESOURCE_ACTION_DELETE)
|
constants.RESOURCE_ACTION_DELETE)
|
||||||
lock_reason = lock_data.get('lock_reason') or ''
|
lock_reason = lock_data.get('lock_reason') or ''
|
||||||
@ -59,12 +63,20 @@ class ResourceLocksController(wsgi.Controller):
|
|||||||
if len(lock_reason) > 1023:
|
if len(lock_reason) > 1023:
|
||||||
msg = _("'lock_reason' can contain a maximum of 1023 characters.")
|
msg = _("'lock_reason' can contain a maximum of 1023 characters.")
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
if resource_action not in constants.RESOURCE_LOCK_RESOURCE_ACTIONS:
|
if resource_type not in constants.RESOURCE_LOCK_RESOURCE_TYPES:
|
||||||
|
msg = _("'resource_type' is required and must be one "
|
||||||
|
"of %(resource_types)s") % {
|
||||||
|
'resource_types': constants.RESOURCE_LOCK_RESOURCE_TYPES
|
||||||
|
}
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
resource_type_lock_actions = (
|
||||||
|
constants.RESOURCE_LOCK_ACTIONS_MAPPING[resource_type])
|
||||||
|
if resource_action not in resource_type_lock_actions:
|
||||||
msg = _("'resource_action' can only be one of %(actions)s" %
|
msg = _("'resource_action' can only be one of %(actions)s" %
|
||||||
{'actions': constants.RESOURCE_LOCK_RESOURCE_ACTIONS})
|
{'actions': resource_type_lock_actions})
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
if for_update:
|
if lock_to_update:
|
||||||
if set(lock_data.keys()) - {'resource_action', 'lock_reason'}:
|
if set(lock_data.keys()) - {'resource_action', 'lock_reason'}:
|
||||||
msg = _("Only 'resource_action' and 'lock_reason' "
|
msg = _("Only 'resource_action' and 'lock_reason' "
|
||||||
"can be updated.")
|
"can be updated.")
|
||||||
@ -73,10 +85,6 @@ class ResourceLocksController(wsgi.Controller):
|
|||||||
if not uuidutils.is_uuid_like(resource_id):
|
if not uuidutils.is_uuid_like(resource_id):
|
||||||
msg = _("Resource ID is required and must be in uuid format.")
|
msg = _("Resource ID is required and must be in uuid format.")
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
if resource_type not in constants.RESOURCE_LOCK_RESOURCE_TYPES:
|
|
||||||
msg = _("'resource_type' is required and must be one "
|
|
||||||
"of %s" % constants.RESOURCE_LOCK_RESOURCE_TYPES)
|
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.resource_locks_api = resource_locks.API()
|
self.resource_locks_api = resource_locks.API()
|
||||||
@ -165,6 +173,9 @@ class ResourceLocksController(wsgi.Controller):
|
|||||||
explanation="No such resource found.")
|
explanation="No such resource found.")
|
||||||
except exception.InvalidInput as error:
|
except exception.InvalidInput as error:
|
||||||
raise exc.HTTPConflict(explanation=error.msg)
|
raise exc.HTTPConflict(explanation=error.msg)
|
||||||
|
except exception.ResourceVisibilityLockExists:
|
||||||
|
raise exc.HTTPConflict(
|
||||||
|
"Resource's visibility is already locked by other user.")
|
||||||
return self._view_builder.detail(req, resource_lock)
|
return self._view_builder.detail(req, resource_lock)
|
||||||
|
|
||||||
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
||||||
@ -172,14 +183,21 @@ class ResourceLocksController(wsgi.Controller):
|
|||||||
def update(self, req, id, body):
|
def update(self, req, id, body):
|
||||||
"""Update an existing resource lock."""
|
"""Update an existing resource lock."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
self._check_body(body, for_update=True)
|
try:
|
||||||
lock_data = body['resource_lock']
|
resource_lock = self.resource_locks_api.get(context, id)
|
||||||
|
except exception.NotFound as e:
|
||||||
|
raise exc.HTTPNotFound(explanation=e.msg)
|
||||||
|
|
||||||
resource_lock = self.resource_locks_api.update(
|
self._check_body(body, lock_to_update=resource_lock)
|
||||||
context,
|
lock_data = body['resource_lock']
|
||||||
id,
|
try:
|
||||||
lock_data,
|
resource_lock = self.resource_locks_api.update(
|
||||||
)
|
context,
|
||||||
|
resource_lock,
|
||||||
|
lock_data,
|
||||||
|
)
|
||||||
|
except exception.InvalidInput as e:
|
||||||
|
raise exc.HTTPBadRequest(explanation=e.msg)
|
||||||
return self._view_builder.detail(req, resource_lock)
|
return self._view_builder.detail(req, resource_lock)
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,10 +19,13 @@ import ast
|
|||||||
|
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
|
from manila.api import common
|
||||||
from manila.api.openstack import wsgi
|
from manila.api.openstack import wsgi
|
||||||
from manila.api.views import share_accesses as share_access_views
|
from manila.api.views import share_accesses as share_access_views
|
||||||
|
from manila.common import constants
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.i18n import _
|
from manila.i18n import _
|
||||||
|
from manila.lock import api as resource_locks
|
||||||
from manila import share
|
from manila import share
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +38,7 @@ class ShareAccessesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ShareAccessesController, self).__init__()
|
super(ShareAccessesController, self).__init__()
|
||||||
self.share_api = share.API()
|
self.share_api = share.API()
|
||||||
|
self.resource_locks_api = resource_locks.API()
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.45')
|
@wsgi.Controller.api_version('2.45')
|
||||||
@wsgi.Controller.authorize('get')
|
@wsgi.Controller.authorize('get')
|
||||||
@ -42,8 +46,25 @@ class ShareAccessesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
"""Return data about the given share access rule."""
|
"""Return data about the given share access rule."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
share_access = self._get_share_access(context, id)
|
share_access = self._get_share_access(context, id)
|
||||||
|
restricted = self._is_rule_restricted(context, id)
|
||||||
|
if restricted:
|
||||||
|
share_access['restricted'] = True
|
||||||
return self._view_builder.view(req, share_access)
|
return self._view_builder.view(req, share_access)
|
||||||
|
|
||||||
|
def _is_rule_restricted(self, context, id):
|
||||||
|
search_opts = {
|
||||||
|
'resource_id': id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_SHOW,
|
||||||
|
'resource_type': 'access_rule'
|
||||||
|
}
|
||||||
|
locks, count = self.resource_locks_api.get_all(
|
||||||
|
context, search_opts, show_count=True)
|
||||||
|
|
||||||
|
if count:
|
||||||
|
return self.resource_locks_api.access_is_restricted(context,
|
||||||
|
locks[0])
|
||||||
|
return False
|
||||||
|
|
||||||
def _get_share_access(self, context, share_access_id):
|
def _get_share_access(self, context, share_access_id):
|
||||||
try:
|
try:
|
||||||
return self.share_api.access_get(context, share_access_id)
|
return self.share_api.access_get(context, share_access_id)
|
||||||
@ -51,9 +72,34 @@ class ShareAccessesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
msg = _("Share access rule %s not found.") % share_access_id
|
msg = _("Share access rule %s not found.") % share_access_id
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.45')
|
def _validate_search_opts(self, req, search_opts):
|
||||||
@wsgi.Controller.authorize
|
"""Check if search opts parameters are valid."""
|
||||||
def index(self, req):
|
access_type = search_opts.get('access_type', None)
|
||||||
|
access_to = search_opts.get('access_to', None)
|
||||||
|
|
||||||
|
if access_type and access_type not in ['ip', 'user', 'cert', 'cephx']:
|
||||||
|
raise exception.InvalidShareAccessType(type=access_type)
|
||||||
|
|
||||||
|
# If access_to is present but access type is not, it gets tricky to
|
||||||
|
# validate its content
|
||||||
|
if access_to and not access_type:
|
||||||
|
msg = _("'access_type' parameter must be provided when specifying "
|
||||||
|
"'access_to'.")
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
|
||||||
|
if access_type and access_to:
|
||||||
|
common.validate_access(access_type=access_type,
|
||||||
|
access_to=access_to,
|
||||||
|
enable_ceph=True,
|
||||||
|
enable_ipv6=True)
|
||||||
|
|
||||||
|
access_level = search_opts.get('access_level')
|
||||||
|
if ('access_level' in search_opts and (
|
||||||
|
search_opts['access_level'] not in constants.ACCESS_LEVELS)):
|
||||||
|
raise exception.InvalidShareAccessLevel(level=access_level)
|
||||||
|
|
||||||
|
@wsgi.Controller.authorize('index')
|
||||||
|
def _index(self, req, support_for_access_filters=False):
|
||||||
"""Returns the list of access rules for a given share."""
|
"""Returns the list of access rules for a given share."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
search_opts = {}
|
search_opts = {}
|
||||||
@ -66,6 +112,12 @@ class ShareAccessesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
if 'metadata' in search_opts:
|
if 'metadata' in search_opts:
|
||||||
search_opts['metadata'] = ast.literal_eval(
|
search_opts['metadata'] = ast.literal_eval(
|
||||||
search_opts['metadata'])
|
search_opts['metadata'])
|
||||||
|
if support_for_access_filters:
|
||||||
|
try:
|
||||||
|
self._validate_search_opts(req, search_opts)
|
||||||
|
except (exception.InvalidShareAccessLevel,
|
||||||
|
exception.InvalidShareAccessType) as e:
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=e.msg)
|
||||||
try:
|
try:
|
||||||
share = self.share_api.get(context, share_id)
|
share = self.share_api.get(context, share_id)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
@ -73,8 +125,24 @@ class ShareAccessesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
access_rules = self.share_api.access_get_all(
|
access_rules = self.share_api.access_get_all(
|
||||||
context, share, search_opts)
|
context, share, search_opts)
|
||||||
|
rule_list = []
|
||||||
|
for rule in access_rules:
|
||||||
|
restricted = self._is_rule_restricted(context, rule['id'])
|
||||||
|
rule['restricted'] = restricted
|
||||||
|
if (('access_to' in search_opts or 'access_key' in search_opts)
|
||||||
|
and restricted):
|
||||||
|
continue
|
||||||
|
rule_list.append(rule)
|
||||||
|
|
||||||
return self._view_builder.list_view(req, access_rules)
|
return self._view_builder.list_view(req, rule_list)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('2.45', '2.81')
|
||||||
|
def index(self, req):
|
||||||
|
return self._index(req)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('2.82')
|
||||||
|
def index(self, req): # pylint: disable=function-redefined # noqa F811
|
||||||
|
return self._index(req, support_for_access_filters=True)
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
|
@ -33,6 +33,7 @@ from manila.common import constants
|
|||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.i18n import _
|
from manila.i18n import _
|
||||||
|
from manila.lock import api as resource_locks
|
||||||
from manila import policy
|
from manila import policy
|
||||||
from manila import share
|
from manila import share
|
||||||
from manila import utils
|
from manila import utils
|
||||||
@ -53,6 +54,7 @@ class ShareController(wsgi.Controller,
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ShareController, self).__init__()
|
super(ShareController, self).__init__()
|
||||||
self.share_api = share.API()
|
self.share_api = share.API()
|
||||||
|
self.resource_locks_api = resource_locks.API()
|
||||||
self._access_view_builder = share_access_views.ViewBuilder()
|
self._access_view_builder = share_access_views.ViewBuilder()
|
||||||
self._migration_view_builder = share_migration_views.ViewBuilder()
|
self._migration_view_builder = share_migration_views.ViewBuilder()
|
||||||
|
|
||||||
@ -474,6 +476,13 @@ class ShareController(wsgi.Controller,
|
|||||||
kwargs['enable_metadata'] = True
|
kwargs['enable_metadata'] = True
|
||||||
if req.api_version_request >= api_version.APIVersionRequest("2.74"):
|
if req.api_version_request >= api_version.APIVersionRequest("2.74"):
|
||||||
kwargs['allow_on_error_state'] = True
|
kwargs['allow_on_error_state'] = True
|
||||||
|
if req.api_version_request >= api_version.APIVersionRequest("2.82"):
|
||||||
|
access_data = body.get('allow_access')
|
||||||
|
kwargs['lock_visibility'] = access_data.get(
|
||||||
|
'lock_visibility', False)
|
||||||
|
kwargs['lock_deletion'] = access_data.get('lock_deletion', False)
|
||||||
|
kwargs['lock_reason'] = access_data.get('lock_reason')
|
||||||
|
|
||||||
return self._allow_access(*args, **kwargs)
|
return self._allow_access(*args, **kwargs)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.0', '2.6')
|
@wsgi.Controller.api_version('2.0', '2.6')
|
||||||
|
@ -34,6 +34,13 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
return {'access_list': [self.summary_view(request, access)['access']
|
return {'access_list': [self.summary_view(request, access)['access']
|
||||||
for access in accesses]}
|
for access in accesses]}
|
||||||
|
|
||||||
|
def _redact_restricted_fields(self, access, access_dict):
|
||||||
|
if access.get('restricted', False):
|
||||||
|
fields_to_redact = ['access_key', 'access_to']
|
||||||
|
for field in fields_to_redact:
|
||||||
|
access_dict[field] = '******'
|
||||||
|
return access_dict
|
||||||
|
|
||||||
def summary_view(self, request, access):
|
def summary_view(self, request, access):
|
||||||
"""Summarized view of a single share access."""
|
"""Summarized view of a single share access."""
|
||||||
access_dict = {
|
access_dict = {
|
||||||
@ -45,6 +52,7 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
}
|
}
|
||||||
self.update_versioned_resource_dict(
|
self.update_versioned_resource_dict(
|
||||||
request, access_dict, access)
|
request, access_dict, access)
|
||||||
|
access_dict = self._redact_restricted_fields(access, access_dict)
|
||||||
return {'access': access_dict}
|
return {'access': access_dict}
|
||||||
|
|
||||||
def view(self, request, access):
|
def view(self, request, access):
|
||||||
@ -59,6 +67,7 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
}
|
}
|
||||||
self.update_versioned_resource_dict(
|
self.update_versioned_resource_dict(
|
||||||
request, access_dict, access)
|
request, access_dict, access)
|
||||||
|
access_dict = self._redact_restricted_fields(access, access_dict)
|
||||||
return {'access': access_dict}
|
return {'access': access_dict}
|
||||||
|
|
||||||
def view_metadata(self, request, metadata):
|
def view_metadata(self, request, metadata):
|
||||||
|
@ -54,6 +54,7 @@ STATUS_BACKUP_RESTORING_ERROR = 'backup_restoring_error'
|
|||||||
|
|
||||||
# Transfer resource type
|
# Transfer resource type
|
||||||
SHARE_RESOURCE_TYPE = 'share'
|
SHARE_RESOURCE_TYPE = 'share'
|
||||||
|
SHARE_ACCESS_RESOURCE_TYPE = 'access_rule'
|
||||||
|
|
||||||
# Access rule states
|
# Access rule states
|
||||||
ACCESS_STATE_QUEUED_TO_APPLY = 'queued_to_apply'
|
ACCESS_STATE_QUEUED_TO_APPLY = 'queued_to_apply'
|
||||||
@ -255,13 +256,38 @@ REPLICATION_TYPE_DR = 'dr'
|
|||||||
POLICY_EXTEND_BEYOND_MAX_SHARE_SIZE = 'extend_beyond_max_share_size_spec'
|
POLICY_EXTEND_BEYOND_MAX_SHARE_SIZE = 'extend_beyond_max_share_size_spec'
|
||||||
|
|
||||||
RESOURCE_ACTION_DELETE = 'delete' # delete, soft-delete, unmanage
|
RESOURCE_ACTION_DELETE = 'delete' # delete, soft-delete, unmanage
|
||||||
|
RESOURCE_ACTION_SHOW = 'show'
|
||||||
|
|
||||||
RESOURCE_LOCK_RESOURCE_TYPES = (
|
RESOURCE_LOCK_RESOURCE_TYPES = (
|
||||||
SHARE_RESOURCE_TYPE,
|
SHARE_RESOURCE_TYPE,
|
||||||
|
SHARE_ACCESS_RESOURCE_TYPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
RESOURCE_LOCK_RESOURCE_ACTIONS = (
|
RESOURCE_LOCK_RESOURCE_ACTIONS = (
|
||||||
RESOURCE_ACTION_DELETE,
|
RESOURCE_ACTION_DELETE,
|
||||||
|
RESOURCE_ACTION_SHOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
RESOURCE_LOCK_ACTIONS_MAPPING = {
|
||||||
|
"share": [RESOURCE_ACTION_DELETE],
|
||||||
|
"access_rule": [RESOURCE_ACTION_DELETE, RESOURCE_ACTION_SHOW],
|
||||||
|
}
|
||||||
|
|
||||||
|
DISALLOWED_STATUS_WHEN_LOCKING_SHARES = (
|
||||||
|
STATUS_DELETING,
|
||||||
|
STATUS_ERROR_DELETING,
|
||||||
|
STATUS_UNMANAGING,
|
||||||
|
STATUS_MANAGE_ERROR_UNMANAGING,
|
||||||
|
STATUS_UNMANAGE_ERROR,
|
||||||
|
STATUS_UNMANAGED, # not possible, future proofing
|
||||||
|
STATUS_DELETED, # not possible, future proofing
|
||||||
|
)
|
||||||
|
|
||||||
|
DISALLOWED_STATUS_WHEN_LOCKING_ACCESS_RULES = (
|
||||||
|
ACCESS_STATE_QUEUED_TO_DENY,
|
||||||
|
ACCESS_STATE_DENYING,
|
||||||
|
ACCESS_STATE_ERROR,
|
||||||
|
ACCESS_STATE_DELETED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -559,6 +559,11 @@ def share_access_get(context, access_id):
|
|||||||
return IMPL.share_access_get(context, access_id)
|
return IMPL.share_access_get(context, access_id)
|
||||||
|
|
||||||
|
|
||||||
|
def share_access_get_with_context(context, access_id):
|
||||||
|
"""Get share access rule."""
|
||||||
|
return IMPL.share_access_get_with_context(context, access_id)
|
||||||
|
|
||||||
|
|
||||||
def share_access_get_all_for_share(context, share_id, filters=None):
|
def share_access_get_all_for_share(context, share_id, filters=None):
|
||||||
"""Get all access rules for given share."""
|
"""Get all access rules for given share."""
|
||||||
return IMPL.share_access_get_all_for_share(context, share_id,
|
return IMPL.share_access_get_all_for_share(context, share_id,
|
||||||
|
@ -2908,6 +2908,21 @@ def share_access_get(context, access_id, session=None):
|
|||||||
raise exception.NotFound()
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def share_access_get_with_context(context, access_id, session=None):
|
||||||
|
"""Get access record."""
|
||||||
|
session = session or get_session()
|
||||||
|
|
||||||
|
access = _share_access_get_query(
|
||||||
|
context, session,
|
||||||
|
{'id': access_id}).options(joinedload('share')).first()
|
||||||
|
if access:
|
||||||
|
access['project_id'] = access['share']['project_id']
|
||||||
|
return access
|
||||||
|
else:
|
||||||
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
|
||||||
@require_context
|
@require_context
|
||||||
def share_instance_access_get(context, access_id, instance_id,
|
def share_instance_access_get(context, access_id, instance_id,
|
||||||
with_share_access_data=True):
|
with_share_access_data=True):
|
||||||
@ -2930,16 +2945,23 @@ def share_access_get_all_for_share(context, share_id, filters=None,
|
|||||||
session=None):
|
session=None):
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
session = session or get_session()
|
session = session or get_session()
|
||||||
|
share_access_mapping = models.ShareAccessMapping
|
||||||
query = (_share_access_get_query(
|
query = (_share_access_get_query(
|
||||||
context, session, {'share_id': share_id}).filter(
|
context, session, {'share_id': share_id}).filter(
|
||||||
models.ShareAccessMapping.instance_mappings.any()))
|
models.ShareAccessMapping.instance_mappings.any()))
|
||||||
|
|
||||||
|
legal_filter_keys = ('id', 'access_type', 'access_key',
|
||||||
|
'access_to', 'access_level')
|
||||||
|
|
||||||
if 'metadata' in filters:
|
if 'metadata' in filters:
|
||||||
for k, v in filters['metadata'].items():
|
for k, v in filters['metadata'].items():
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(models.ShareAccessMapping.
|
or_(models.ShareAccessMapping.
|
||||||
share_access_rules_metadata.any(key=k, value=v)))
|
share_access_rules_metadata.any(key=k, value=v)))
|
||||||
|
|
||||||
|
query = exact_filter(
|
||||||
|
query, share_access_mapping, filters, legal_filter_keys)
|
||||||
|
|
||||||
return query.all()
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
@ -3057,6 +3079,19 @@ def share_instance_access_delete(context, mapping_id):
|
|||||||
if not mapping:
|
if not mapping:
|
||||||
exception.NotFound()
|
exception.NotFound()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'resource_id': mapping['access_id'],
|
||||||
|
'all_projects': True
|
||||||
|
}
|
||||||
|
locks, __ = resource_lock_get_all(
|
||||||
|
context.elevated(), filters=filters
|
||||||
|
)
|
||||||
|
if locks:
|
||||||
|
for lock in locks:
|
||||||
|
resource_lock_delete(
|
||||||
|
context.elevated(), lock['id']
|
||||||
|
)
|
||||||
|
|
||||||
mapping.soft_delete(session, update_status=True,
|
mapping.soft_delete(session, update_status=True,
|
||||||
status_field_name='state')
|
status_field_name='state')
|
||||||
|
|
||||||
|
@ -577,6 +577,15 @@ class ShareAccessMapping(BASE, ManilaBase):
|
|||||||
'ShareInstanceAccessMapping.deleted == "False")'
|
'ShareInstanceAccessMapping.deleted == "False")'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
share = orm.relationship(
|
||||||
|
"Share",
|
||||||
|
primaryjoin=(
|
||||||
|
'and_('
|
||||||
|
'ShareAccessMapping.share_id == '
|
||||||
|
'Share.id, '
|
||||||
|
'Share.deleted == "False")'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ShareAccessRulesMetadata(BASE, ManilaBase):
|
class ShareAccessRulesMetadata(BASE, ManilaBase):
|
||||||
|
@ -213,6 +213,10 @@ class ResourceLockNotFound(NotFound):
|
|||||||
message = _("Resource lock %(lock_id)s could not be found.")
|
message = _("Resource lock %(lock_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceVisibilityLockExists(ManilaException):
|
||||||
|
message = _("Resource %(resource_id)s is already locked.")
|
||||||
|
|
||||||
|
|
||||||
class Found(ManilaException):
|
class Found(ManilaException):
|
||||||
message = _("Resource was found.")
|
message = _("Resource was found.")
|
||||||
code = 302
|
code = 302
|
||||||
|
@ -28,6 +28,11 @@ class API(base.Base):
|
|||||||
|
|
||||||
resource_get = {
|
resource_get = {
|
||||||
"share": "share_get",
|
"share": "share_get",
|
||||||
|
"access_rule": "share_access_get_with_context"
|
||||||
|
}
|
||||||
|
resource_lock_disallowed_statuses = {
|
||||||
|
"share": constants.DISALLOWED_STATUS_WHEN_LOCKING_SHARES,
|
||||||
|
"access_rule": constants.DISALLOWED_STATUS_WHEN_LOCKING_ACCESS_RULES
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_lock_context(self, context):
|
def _get_lock_context(self, context):
|
||||||
@ -73,6 +78,29 @@ class API(base.Base):
|
|||||||
"manipulated by user. Please "
|
"manipulated by user. Please "
|
||||||
"contact the administrator.")
|
"contact the administrator.")
|
||||||
|
|
||||||
|
def access_is_restricted(self, context, resource_lock):
|
||||||
|
"""Ensure the requester doesn't have visibility restrictions
|
||||||
|
|
||||||
|
Call the check allow lock manipulation method as a first validation.
|
||||||
|
In case it fails, the requester should not have the access rules
|
||||||
|
fields entirely visible. In case it passes and the access visibility
|
||||||
|
is restricted, the users will have visibility of all fields only if
|
||||||
|
they have originally created the lock.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._check_allow_lock_manipulation(context, resource_lock)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy.check_policy(
|
||||||
|
context, 'resource_lock', 'bypass_locked_show_action',
|
||||||
|
resource_lock)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def get(self, context, lock_id):
|
def get(self, context, lock_id):
|
||||||
"""Return resource lock with the specified id."""
|
"""Return resource lock with the specified id."""
|
||||||
return self.db.resource_lock_get(context, lock_id)
|
return self.db.resource_lock_get(context, lock_id)
|
||||||
@ -109,22 +137,37 @@ class API(base.Base):
|
|||||||
return locks, count
|
return locks, count
|
||||||
|
|
||||||
def create(self, context, resource_id=None, resource_type=None,
|
def create(self, context, resource_id=None, resource_type=None,
|
||||||
resource_action=None, lock_reason=None):
|
resource_action=None, lock_reason=None, resource=None):
|
||||||
"""Create a resource lock with the specified information."""
|
"""Create a resource lock with the specified information."""
|
||||||
get_res_method = getattr(self.db, self.resource_get[resource_type])
|
get_res_method = getattr(self.db, self.resource_get[resource_type])
|
||||||
resource = get_res_method(context, resource_id)
|
if resource_action == constants.RESOURCE_ACTION_SHOW:
|
||||||
|
# We can't allow visibility locks to be placed more than once,
|
||||||
|
# otherwise the resource might become visible to someone else.
|
||||||
|
visibility_locks, __ = self.db.resource_lock_get_all(
|
||||||
|
context.elevated(),
|
||||||
|
filters={'resource_id': resource_id,
|
||||||
|
'resource_action': resource_action,
|
||||||
|
'all_projects': True})
|
||||||
|
if visibility_locks:
|
||||||
|
raise exception.ResourceVisibilityLockExists(
|
||||||
|
resource_id=resource_id)
|
||||||
|
if resource is None:
|
||||||
|
resource = get_res_method(context, resource_id)
|
||||||
policy.check_policy(context, 'resource_lock', 'create', resource)
|
policy.check_policy(context, 'resource_lock', 'create', resource)
|
||||||
self._check_resource_state_for_locking(resource_action, resource)
|
self._check_resource_state_for_locking(
|
||||||
|
resource_action, resource, resource_type=resource_type)
|
||||||
lock_context_data = self._get_lock_context(context)
|
lock_context_data = self._get_lock_context(context)
|
||||||
resource_lock = lock_context_data.copy()
|
resource_lock = lock_context_data.copy()
|
||||||
resource_lock.update({
|
resource_lock.update({
|
||||||
'resource_id': resource_id,
|
'resource_id': resource_id,
|
||||||
'resource_action': resource_action,
|
'resource_action': resource_action,
|
||||||
'lock_reason': lock_reason,
|
'lock_reason': lock_reason,
|
||||||
|
'resource_type': resource_type
|
||||||
})
|
})
|
||||||
return self.db.resource_lock_create(context, resource_lock)
|
return self.db.resource_lock_create(context, resource_lock)
|
||||||
|
|
||||||
def _check_resource_state_for_locking(self, resource_action, resource):
|
def _check_resource_state_for_locking(self, resource_action, resource,
|
||||||
|
resource_type='share'):
|
||||||
"""Check if resource is in a "disallowed" state for locking.
|
"""Check if resource is in a "disallowed" state for locking.
|
||||||
|
|
||||||
For example, deletion lock on a "deleting" resource would be futile.
|
For example, deletion lock on a "deleting" resource would be futile.
|
||||||
@ -133,28 +176,37 @@ class API(base.Base):
|
|||||||
disallowed_statuses = ()
|
disallowed_statuses = ()
|
||||||
if resource_action == 'delete':
|
if resource_action == 'delete':
|
||||||
disallowed_statuses = (
|
disallowed_statuses = (
|
||||||
constants.STATUS_DELETING,
|
self.resource_lock_disallowed_statuses[resource_type])
|
||||||
constants.STATUS_ERROR_DELETING,
|
|
||||||
constants.STATUS_UNMANAGING,
|
|
||||||
constants.STATUS_MANAGE_ERROR_UNMANAGING,
|
|
||||||
constants.STATUS_UNMANAGE_ERROR,
|
|
||||||
constants.STATUS_UNMANAGED, # not possible, future proofing
|
|
||||||
constants.STATUS_DELETED, # not possible, future proofing
|
|
||||||
)
|
|
||||||
if resource_state in disallowed_statuses:
|
if resource_state in disallowed_statuses:
|
||||||
msg = "Resource status not suitable for locking"
|
msg = "Resource status not suitable for locking"
|
||||||
raise exception.InvalidInput(reason=msg)
|
raise exception.InvalidInput(reason=msg)
|
||||||
resource_is_soft_deleted = resource.get('is_soft_deleted', False)
|
if resource_type == constants.SHARE_RESOURCE_TYPE:
|
||||||
if resource_is_soft_deleted:
|
resource_is_soft_deleted = resource.get('is_soft_deleted', False)
|
||||||
msg = "Resource cannot be locked since it has been soft deleted."
|
if resource_is_soft_deleted:
|
||||||
raise exception.InvalidInput(reason=msg)
|
msg = (
|
||||||
|
"Resource cannot be locked since it has been soft deleted."
|
||||||
|
)
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
|
||||||
def update(self, context, lock_id, updates):
|
def update(self, context, resource_lock, updates):
|
||||||
"""Update a resource lock with the specified information."""
|
"""Update a resource lock with the specified information."""
|
||||||
resource_lock = self.db.resource_lock_get(context, lock_id)
|
lock_id = resource_lock['id']
|
||||||
policy.check_policy(context, 'resource_lock', 'update', resource_lock)
|
policy.check_policy(context, 'resource_lock', 'update', resource_lock)
|
||||||
self._check_allow_lock_manipulation(context, resource_lock)
|
self._check_allow_lock_manipulation(context, resource_lock)
|
||||||
if 'resource_action' in updates:
|
if 'resource_action' in updates:
|
||||||
|
# A resource can have only one visibility lock
|
||||||
|
if (updates['resource_action'] == constants.RESOURCE_ACTION_SHOW
|
||||||
|
and resource_lock['resource_action'] !=
|
||||||
|
constants.RESOURCE_ACTION_SHOW):
|
||||||
|
filters = {
|
||||||
|
"resource_id": resource_lock['resource_id'],
|
||||||
|
"resource_action": constants.RESOURCE_ACTION_SHOW
|
||||||
|
}
|
||||||
|
visibility_locks = self.get_all(
|
||||||
|
context.elevated(), search_opts=filters)
|
||||||
|
if visibility_locks:
|
||||||
|
msg = "The resource already has a visibility lock."
|
||||||
|
raise exception.InvalidInput(reason=msg)
|
||||||
get_res_method = getattr(
|
get_res_method = getattr(
|
||||||
self.db,
|
self.db,
|
||||||
self.resource_get[resource_lock['resource_type']],
|
self.resource_get[resource_lock['resource_type']],
|
||||||
@ -164,9 +216,13 @@ class API(base.Base):
|
|||||||
updates['resource_action'], resource)
|
updates['resource_action'], resource)
|
||||||
return self.db.resource_lock_update(context, lock_id, updates)
|
return self.db.resource_lock_update(context, lock_id, updates)
|
||||||
|
|
||||||
def delete(self, context, lock_id):
|
def ensure_context_can_delete_lock(self, context, lock_id):
|
||||||
"""Delete resource lock with the specified id."""
|
"""Ensure the requester is able to delete locks."""
|
||||||
resource_lock = self.db.resource_lock_get(context, lock_id)
|
resource_lock = self.db.resource_lock_get(context, lock_id)
|
||||||
policy.check_policy(context, 'resource_lock', 'delete', resource_lock)
|
policy.check_policy(context, 'resource_lock', 'delete', resource_lock)
|
||||||
self._check_allow_lock_manipulation(context, resource_lock)
|
self._check_allow_lock_manipulation(context, resource_lock)
|
||||||
|
|
||||||
|
def delete(self, context, lock_id):
|
||||||
|
"""Delete resource lock with the specified id."""
|
||||||
|
self.ensure_context_can_delete_lock(context, lock_id)
|
||||||
self.db.resource_lock_delete(context, lock_id)
|
self.db.resource_lock_delete(context, lock_id)
|
||||||
|
@ -57,7 +57,16 @@ deprecated_lock_delete = policy.DeprecatedRule(
|
|||||||
deprecated_reason=DEPRECATED_REASON,
|
deprecated_reason=DEPRECATED_REASON,
|
||||||
deprecated_since='2023.2/Bobcat',
|
deprecated_since='2023.2/Bobcat',
|
||||||
)
|
)
|
||||||
|
deprecated_bypass_locked_show_action = policy.DeprecatedRule(
|
||||||
|
name=BASE_POLICY_NAME % 'bypass_locked_show_action',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER_USER,
|
||||||
|
deprecated_reason=DEPRECATED_REASON,
|
||||||
|
deprecated_since='2023.2/Bobcat',
|
||||||
|
)
|
||||||
|
|
||||||
|
# We anticipate bypassing is desirable only for resource visibility locks.
|
||||||
|
# Without a bypass, the lock would have to be set aside each time the lock
|
||||||
|
# owner wants to view the resource.
|
||||||
|
|
||||||
lock_policies = [
|
lock_policies = [
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
@ -147,6 +156,24 @@ lock_policies = [
|
|||||||
],
|
],
|
||||||
deprecated_rule=deprecated_lock_delete,
|
deprecated_rule=deprecated_lock_delete,
|
||||||
),
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=BASE_POLICY_NAME % 'bypass_locked_show_action',
|
||||||
|
check_str=base.ADMIN_OR_SERVICE_OR_OWNER_USER,
|
||||||
|
scope_types=['project'],
|
||||||
|
description="Bypass a visibility lock placed in a resource.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/share-access-rules/{share_access_id}'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': ('/share-access-rules?share_id={share_id}'
|
||||||
|
'&key1=value1&key2=value2')
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deprecated_rule=deprecated_bypass_locked_show_action,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ from manila.common import constants
|
|||||||
from manila import context
|
from manila import context
|
||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
|
from manila.lock import api as resource_locks
|
||||||
from manila import policy
|
from manila import policy
|
||||||
from manila.share import api as share_api
|
from manila.share import api as share_api
|
||||||
from manila.share import share_types
|
from manila.share import share_types
|
||||||
@ -974,6 +975,12 @@ class ShareActionsTest(test.TestCase):
|
|||||||
{'access_type': 'cert', 'access_to': 'x'},
|
{'access_type': 'cert', 'access_to': 'x'},
|
||||||
{'access_type': 'cert', 'access_to': 'tenant.example.com'},
|
{'access_type': 'cert', 'access_to': 'tenant.example.com'},
|
||||||
{'access_type': 'cert', 'access_to': 'x' * 64},
|
{'access_type': 'cert', 'access_to': 'x' * 64},
|
||||||
|
{'access_type': 'cert', 'access_to': 'x' * 64,
|
||||||
|
'lock_visibility': True},
|
||||||
|
{'access_type': 'cert', 'access_to': 'x' * 64, 'lock_deletion': True},
|
||||||
|
{'access_type': 'cert', 'access_to': 'x' * 64, 'lock_deletion': True},
|
||||||
|
{'access_type': 'cert', 'access_to': 'x' * 64, 'lock_deletion': True,
|
||||||
|
'lock_visibility': True, 'lock_reason': 'locked_for_testing'},
|
||||||
)
|
)
|
||||||
def test_allow_access(self, access):
|
def test_allow_access(self, access):
|
||||||
self.mock_object(share_api.API,
|
self.mock_object(share_api.API,
|
||||||
@ -982,17 +989,218 @@ class ShareActionsTest(test.TestCase):
|
|||||||
self.mock_object(self.controller._access_view_builder, 'view',
|
self.mock_object(self.controller._access_view_builder, 'view',
|
||||||
mock.Mock(return_value={'access':
|
mock.Mock(return_value={'access':
|
||||||
{'fake': 'fake'}}))
|
{'fake': 'fake'}}))
|
||||||
|
self.mock_object(self.controller, '_create_access_locks')
|
||||||
|
|
||||||
id = 'fake_share_id'
|
id = 'fake_share_id'
|
||||||
body = {'os-allow_access': access}
|
body = {'os-allow_access': access}
|
||||||
expected = {'access': {'fake': 'fake'}}
|
expected = {'access': {'fake': 'fake'}}
|
||||||
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id)
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id)
|
||||||
|
lock_visibility = access.pop('lock_visibility', None)
|
||||||
|
lock_deletion = access.pop('lock_deletion', None)
|
||||||
|
lock_reason = access.pop('lock_reason', None)
|
||||||
|
|
||||||
res = self.controller._allow_access(req, id, body)
|
res = self.controller._allow_access(
|
||||||
|
req, id, body, lock_visibility=lock_visibility,
|
||||||
|
lock_deletion=lock_deletion, lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(expected, res)
|
self.assertEqual(expected, res)
|
||||||
self.mock_policy_check.assert_called_once_with(
|
self.mock_policy_check.assert_called_once_with(
|
||||||
req.environ['manila.context'], 'share', 'allow_access')
|
req.environ['manila.context'], 'share', 'allow_access')
|
||||||
|
if lock_visibility or lock_deletion:
|
||||||
|
self.controller._create_access_locks.assert_called_once_with(
|
||||||
|
req.environ['manila.context'],
|
||||||
|
expected['access'],
|
||||||
|
lock_deletion=lock_deletion,
|
||||||
|
lock_visibility=lock_visibility,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
{'lock_visibility': True, 'lock_deletion': True,
|
||||||
|
'lock_reason': 'test lock reason'},
|
||||||
|
{'lock_visibility': True, 'lock_deletion': False, 'lock_reason': None},
|
||||||
|
{'lock_visibility': False, 'lock_deletion': True, 'lock_reason': None},
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test__create_access_locks(self, lock_visibility, lock_deletion,
|
||||||
|
lock_reason):
|
||||||
|
access = {
|
||||||
|
'id': 'fake',
|
||||||
|
'access_type': 'ip',
|
||||||
|
'access_to': '127.0.0.1',
|
||||||
|
}
|
||||||
|
self.mock_object(resource_locks.API, 'create')
|
||||||
|
|
||||||
|
id = 'fake_share_id'
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/tenant1/shares/%s/action' % id, version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access['project_id'] = context.project_id
|
||||||
|
access['user_id'] = context.user_id
|
||||||
|
|
||||||
|
self.controller._create_access_locks(
|
||||||
|
req.environ['manila.context'],
|
||||||
|
access,
|
||||||
|
lock_deletion=lock_deletion,
|
||||||
|
lock_visibility=lock_visibility,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
|
||||||
|
restrict_calls = []
|
||||||
|
if lock_deletion:
|
||||||
|
restrict_calls.append(
|
||||||
|
mock.call(
|
||||||
|
context, resource_id=access['id'],
|
||||||
|
resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_DELETE,
|
||||||
|
resource=access,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if lock_visibility:
|
||||||
|
restrict_calls.append(
|
||||||
|
mock.call(
|
||||||
|
context, resource_id=access['id'],
|
||||||
|
resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_SHOW,
|
||||||
|
resource=access,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resource_locks.API.create.assert_has_calls(restrict_calls)
|
||||||
|
|
||||||
|
def test__create_access_visibility_locks_creation_failed(self):
|
||||||
|
access = {
|
||||||
|
'id': 'fake',
|
||||||
|
'access_type': 'ip',
|
||||||
|
'access_to': '127.0.0.1',
|
||||||
|
}
|
||||||
|
lock_reason = 'locked for testing'
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, 'create',
|
||||||
|
mock.Mock(side_effect=exception.NotAuthorized)
|
||||||
|
)
|
||||||
|
|
||||||
|
id = 'fake_share_id'
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/tenant1/shares/%s/action' % id, version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access['project_id'] = context.project_id
|
||||||
|
access['user_id'] = context.user_id
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._create_access_locks,
|
||||||
|
req.environ['manila.context'],
|
||||||
|
access,
|
||||||
|
lock_deletion=False,
|
||||||
|
lock_visibility=True,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_locks.API.create.assert_called_once_with(
|
||||||
|
context, resource_id=access['id'], resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_SHOW, resource=access,
|
||||||
|
lock_reason=lock_reason)
|
||||||
|
|
||||||
|
def test__create_access_deletion_locks_creation_failed(self):
|
||||||
|
access = {
|
||||||
|
'id': 'fake',
|
||||||
|
'access_type': 'ip',
|
||||||
|
'access_to': '127.0.0.1',
|
||||||
|
}
|
||||||
|
lock_reason = 'locked for testing'
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, 'create',
|
||||||
|
mock.Mock(side_effect=exception.NotAuthorized)
|
||||||
|
)
|
||||||
|
|
||||||
|
id = 'fake_share_id'
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/tenant1/shares/%s/action' % id, version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access['project_id'] = context.project_id
|
||||||
|
access['user_id'] = context.user_id
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._create_access_locks,
|
||||||
|
req.environ['manila.context'],
|
||||||
|
access,
|
||||||
|
lock_deletion=True,
|
||||||
|
lock_visibility=False,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_locks.API.create.assert_called_once_with(
|
||||||
|
context, resource_id=access['id'], resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_DELETE, resource=access,
|
||||||
|
lock_reason=lock_reason)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
{'lock_visibility': True, 'lock_deletion': True,
|
||||||
|
'lock_reason': 'test lock reason'},
|
||||||
|
{'lock_visibility': True, 'lock_deletion': False, 'lock_reason': None},
|
||||||
|
{'lock_visibility': False, 'lock_deletion': True, 'lock_reason': None},
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_allow_access_visibility_restrictions(self, lock_visibility,
|
||||||
|
lock_deletion, lock_reason):
|
||||||
|
access = {'id': 'fake'}
|
||||||
|
self.mock_object(share_api.API,
|
||||||
|
'allow_access',
|
||||||
|
mock.Mock(return_value=access))
|
||||||
|
self.mock_object(self.controller._access_view_builder, 'view',
|
||||||
|
mock.Mock(return_value={'access': {'fake': 'fake'}}))
|
||||||
|
self.mock_object(resource_locks.API, 'create')
|
||||||
|
|
||||||
|
id = 'fake_share_id'
|
||||||
|
body = {
|
||||||
|
'allow_access': {
|
||||||
|
'access_type': 'ip',
|
||||||
|
'access_to': '127.0.0.1',
|
||||||
|
'lock_visibility': lock_visibility,
|
||||||
|
'lock_deletion': lock_deletion,
|
||||||
|
'lock_reason': lock_reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected = {'access': {'fake': 'fake'}}
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/tenant1/shares/%s/action' % id, version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access['project_id'] = context.project_id
|
||||||
|
access['user_id'] = context.user_id
|
||||||
|
|
||||||
|
res = self.controller._allow_access(
|
||||||
|
req, id, body, lock_visibility=lock_visibility,
|
||||||
|
lock_deletion=lock_deletion, lock_reason=lock_reason)
|
||||||
|
|
||||||
|
self.assertEqual(expected, res)
|
||||||
|
self.mock_policy_check.assert_called_once_with(
|
||||||
|
context, 'share', 'allow_access')
|
||||||
|
restrict_calls = []
|
||||||
|
if lock_deletion:
|
||||||
|
restrict_calls.append(
|
||||||
|
mock.call(
|
||||||
|
context, resource_id=access['id'],
|
||||||
|
resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_DELETE,
|
||||||
|
resource=access,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if lock_visibility:
|
||||||
|
restrict_calls.append(
|
||||||
|
mock.call(
|
||||||
|
context, resource_id=access['id'],
|
||||||
|
resource_type='access_rule',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_SHOW,
|
||||||
|
resource=access,
|
||||||
|
lock_reason=lock_reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resource_locks.API.create.assert_has_calls(restrict_calls)
|
||||||
|
|
||||||
def test_allow_access_with_network_id(self):
|
def test_allow_access_with_network_id(self):
|
||||||
share_network = db_utils.create_share_network()
|
share_network = db_utils.create_share_network()
|
||||||
@ -1032,15 +1240,19 @@ class ShareActionsTest(test.TestCase):
|
|||||||
{'access_type': 'cert', 'access_to': ''},
|
{'access_type': 'cert', 'access_to': ''},
|
||||||
{'access_type': 'cert', 'access_to': ' '},
|
{'access_type': 'cert', 'access_to': ' '},
|
||||||
{'access_type': 'cert', 'access_to': 'x' * 65},
|
{'access_type': 'cert', 'access_to': 'x' * 65},
|
||||||
{'access_type': 'cephx', 'access_to': 'alice'}
|
{'access_type': 'cephx', 'access_to': 'alice'},
|
||||||
|
{'access_type': 'ip', 'access_to': '127.0.0.0/24',
|
||||||
|
'lock_reason': 'fake_lock_reason'},
|
||||||
)
|
)
|
||||||
def test_allow_access_error(self, access):
|
def test_allow_access_error(self, access):
|
||||||
id = 'fake_share_id'
|
id = 'fake_share_id'
|
||||||
|
lock_reason = access.pop('lock_reason', None)
|
||||||
body = {'os-allow_access': access}
|
body = {'os-allow_access': access}
|
||||||
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id)
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id)
|
||||||
|
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.controller._allow_access, req, id, body)
|
self.controller._allow_access, req, id, body,
|
||||||
|
lock_reason=lock_reason)
|
||||||
self.mock_policy_check.assert_called_once_with(
|
self.mock_policy_check.assert_called_once_with(
|
||||||
req.environ['manila.context'], 'share', 'allow_access')
|
req.environ['manila.context'], 'share', 'allow_access')
|
||||||
|
|
||||||
@ -1097,6 +1309,181 @@ class ShareActionsTest(test.TestCase):
|
|||||||
self.mock_policy_check.assert_called_once_with(
|
self.mock_policy_check.assert_called_once_with(
|
||||||
req.environ['manila.context'], 'share', 'deny_access')
|
req.environ['manila.context'], 'share', 'deny_access')
|
||||||
|
|
||||||
|
def test_deny_access_delete_locks(self):
|
||||||
|
def _stub_deny_access(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.mock_object(share_api.API, "deny_access", _stub_deny_access)
|
||||||
|
self.mock_object(share_api.API, "access_get", _fake_access_get)
|
||||||
|
self.mock_object(self.controller, '_check_for_access_rule_locks')
|
||||||
|
|
||||||
|
id = 'fake_share_id'
|
||||||
|
body_data = {"access_id": 'fake_acces_id'}
|
||||||
|
body = {"deny_access": body_data}
|
||||||
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id,
|
||||||
|
version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
|
||||||
|
res = self.controller._deny_access(req, id, body)
|
||||||
|
|
||||||
|
self.assertEqual(202, res.status_int)
|
||||||
|
self.mock_policy_check.assert_called_once_with(
|
||||||
|
req.environ['manila.context'], 'share', 'deny_access')
|
||||||
|
self.controller._check_for_access_rule_locks.assert_called_once_with(
|
||||||
|
context, body['deny_access'], body_data['access_id'], id
|
||||||
|
)
|
||||||
|
|
||||||
|
def test__check_for_access_rule_locks_no_locks(self):
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "get_all", mock.Mock(return_value=([], 0)))
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id,
|
||||||
|
version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access_id = 'fake_access_id'
|
||||||
|
share_id = 'fake_share_id'
|
||||||
|
|
||||||
|
self.controller._check_for_access_rule_locks(
|
||||||
|
context, {}, access_id, share_id)
|
||||||
|
|
||||||
|
delete_search_opts = {
|
||||||
|
'resource_id': access_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
resource_locks.API.get_all.assert_called_once_with(
|
||||||
|
context, search_opts=delete_search_opts, show_count=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test__check_for_access_rules_locks_too_many_locks(self):
|
||||||
|
locks = [{'id': f'lock_id_{i}'} for i in range(4)]
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "get_all",
|
||||||
|
mock.Mock(return_value=(locks, len(locks))))
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id,
|
||||||
|
version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access_id = 'fake_access_id'
|
||||||
|
share_id = 'fake_share_id'
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden,
|
||||||
|
self.controller._check_for_access_rule_locks,
|
||||||
|
context, {}, access_id, share_id)
|
||||||
|
|
||||||
|
delete_search_opts = {
|
||||||
|
'resource_id': access_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
resource_locks.API.get_all.assert_called_once_with(
|
||||||
|
context, search_opts=delete_search_opts, show_count=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test__check_for_access_rules_cant_manipulate_lock(self):
|
||||||
|
locks = [{
|
||||||
|
'id': 'fake_lock_id',
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}]
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "get_all",
|
||||||
|
mock.Mock(return_value=(locks, len(locks))))
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "ensure_context_can_delete_lock",
|
||||||
|
mock.Mock(side_effect=exception.NotAuthorized))
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id,
|
||||||
|
version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access_id = 'fake_access_id'
|
||||||
|
share_id = 'fake_share_id'
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden,
|
||||||
|
self.controller._check_for_access_rule_locks,
|
||||||
|
context, {'unrestrict': True}, access_id, share_id)
|
||||||
|
|
||||||
|
delete_search_opts = {
|
||||||
|
'resource_id': access_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
resource_locks.API.get_all.assert_called_once_with(
|
||||||
|
context, search_opts=delete_search_opts, show_count=True
|
||||||
|
)
|
||||||
|
(resource_locks.API.ensure_context_can_delete_lock
|
||||||
|
.assert_called_once_with(
|
||||||
|
context, locks[0]['id']))
|
||||||
|
|
||||||
|
def test__check_for_access_rules_locks_unauthorized(self):
|
||||||
|
locks = [{
|
||||||
|
'id': 'fake_lock_id',
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}]
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "get_all",
|
||||||
|
mock.Mock(return_value=(locks, len(locks))))
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "ensure_context_can_delete_lock",
|
||||||
|
mock.Mock(side_effect=exception.NotAuthorized))
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "delete",
|
||||||
|
mock.Mock(side_effect=exception.NotAuthorized))
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id,
|
||||||
|
version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access_id = 'fake_access_id'
|
||||||
|
share_id = 'fake_share_id'
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden,
|
||||||
|
self.controller._check_for_access_rule_locks,
|
||||||
|
context, {'unrestrict': True}, access_id, share_id
|
||||||
|
)
|
||||||
|
delete_search_opts = {
|
||||||
|
'resource_id': access_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}
|
||||||
|
resource_locks.API.get_all.assert_called_once_with(
|
||||||
|
context, search_opts=delete_search_opts, show_count=True
|
||||||
|
)
|
||||||
|
(resource_locks.API.ensure_context_can_delete_lock
|
||||||
|
.assert_called_once_with(
|
||||||
|
context, locks[0]['id']))
|
||||||
|
|
||||||
|
def test_check_for_access_rules_locks(self):
|
||||||
|
locks = [{
|
||||||
|
'id': 'fake_lock_id',
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}]
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "get_all",
|
||||||
|
mock.Mock(return_value=(locks, len(locks))))
|
||||||
|
self.mock_object(
|
||||||
|
resource_locks.API, "ensure_context_can_delete_lock")
|
||||||
|
self.mock_object(resource_locks.API, "delete")
|
||||||
|
|
||||||
|
req = fakes.HTTPRequest.blank('/tenant1/shares/%s/action' % id,
|
||||||
|
version='2.82')
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
access_id = 'fake_access_id'
|
||||||
|
share_id = 'fake_share_id'
|
||||||
|
|
||||||
|
self.controller._check_for_access_rule_locks(
|
||||||
|
context, {'unrestrict': True}, access_id, share_id)
|
||||||
|
|
||||||
|
delete_search_opts = {
|
||||||
|
'resource_id': access_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE
|
||||||
|
}
|
||||||
|
resource_locks.API.get_all.assert_called_once_with(
|
||||||
|
context, search_opts=delete_search_opts, show_count=True)
|
||||||
|
(resource_locks.API.ensure_context_can_delete_lock
|
||||||
|
.assert_called_once_with(
|
||||||
|
context, locks[0]['id']))
|
||||||
|
|
||||||
@ddt.data('_allow_access', '_deny_access')
|
@ddt.data('_allow_access', '_deny_access')
|
||||||
def test_allow_access_deny_access_policy_not_authorized(self, method):
|
def test_allow_access_deny_access_policy_not_authorized(self, method):
|
||||||
req = fakes.HTTPRequest.blank('/tenant1/shares/someuuid/action')
|
req = fakes.HTTPRequest.blank('/tenant1/shares/someuuid/action')
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
@ -46,37 +47,60 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@ddt.data(
|
@ddt.data(
|
||||||
test_utils.annotated('no_body_content', {}),
|
test_utils.annotated(
|
||||||
test_utils.annotated('invalid_body', {'share': 'somedata'}),
|
'no_body_content', {
|
||||||
|
'body': {},
|
||||||
|
'resource_type': 'share'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
test_utils.annotated(
|
||||||
|
'invalid_body', {
|
||||||
|
'body': {
|
||||||
|
'share': 'somedata'
|
||||||
|
},
|
||||||
|
'resource_type': 'share'
|
||||||
|
}
|
||||||
|
),
|
||||||
test_utils.annotated(
|
test_utils.annotated(
|
||||||
'invalid_action', {
|
'invalid_action', {
|
||||||
'resource_lock': {
|
'body': {
|
||||||
'resource_action': 'invalid_action',
|
'resource_lock': {
|
||||||
|
'resource_action': 'invalid_action',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
'resource_type': 'share'
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
test_utils.annotated(
|
test_utils.annotated(
|
||||||
'invalid_reason', {
|
'invalid_reason', {
|
||||||
'resource_lock': {
|
'body': {
|
||||||
'lock_reason': 'xyzzyspoon!' * 94,
|
'resource_lock': {
|
||||||
|
'lock_reason': 'xyzzyspoon!' * 94,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
'resource_type': 'share'
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
test_utils.annotated(
|
test_utils.annotated(
|
||||||
'disallowed_attributes', {
|
'disallowed_attributes', {
|
||||||
'resource_lock': {
|
'body': {
|
||||||
'lock_reason': 'the reason is you',
|
'resource_lock': {
|
||||||
'resource_action': 'delete',
|
'lock_reason': 'the reason is you',
|
||||||
'resource_id': uuidutils.generate_uuid(),
|
'resource_action': 'delete',
|
||||||
|
'resource_id': uuidutils.generate_uuid(),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
'resource_type': 'share'
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test__check_body_for_update_invalid(self, body):
|
@ddt.unpack
|
||||||
|
def test__check_body_for_update_invalid(self, body, resource_type):
|
||||||
|
current_lock = {'resource_type': resource_type}
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.controller._check_body,
|
self.controller._check_body,
|
||||||
body,
|
body,
|
||||||
for_update=True)
|
lock_to_update=current_lock)
|
||||||
|
|
||||||
@ddt.data(
|
@ddt.data(
|
||||||
test_utils.annotated('no_body_content', {}),
|
test_utils.annotated('no_body_content', {}),
|
||||||
@ -111,14 +135,6 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
test_utils.annotated(
|
|
||||||
'empty_resource_type', {
|
|
||||||
'resource_lock': {
|
|
||||||
'resource_id': uuidutils.generate_uuid(),
|
|
||||||
'resource_type': '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
def test__check_body_for_create_invalid(self, body):
|
def test__check_body_for_create_invalid(self, body):
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
@ -128,29 +144,43 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
@ddt.data(
|
@ddt.data(
|
||||||
test_utils.annotated(
|
test_utils.annotated(
|
||||||
'action_and_lock_reason', {
|
'action_and_lock_reason', {
|
||||||
'resource_lock': {
|
'body': {
|
||||||
'resource_action': 'delete',
|
'resource_lock': {
|
||||||
'lock_reason': 'the reason is you',
|
'resource_action': 'delete',
|
||||||
|
'lock_reason': 'the reason is you',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
'resource_type': 'share',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
test_utils.annotated(
|
test_utils.annotated(
|
||||||
'lock_reason', {
|
'lock_reason', {
|
||||||
'resource_lock': {
|
'body': {
|
||||||
'lock_reason': 'tienes razon',
|
'resource_lock': {
|
||||||
|
'lock_reason': 'tienes razon',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
'resource_type': 'share',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
test_utils.annotated(
|
test_utils.annotated(
|
||||||
'resource_action', {
|
'resource_action', {
|
||||||
'resource_lock': {
|
'body': {
|
||||||
'resource_action': 'delete',
|
'resource_lock': {
|
||||||
|
'resource_action': 'delete',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
'resource_type': 'access_rule',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test__check_body_for_update(self, body):
|
@ddt.unpack
|
||||||
result = self.controller._check_body(body, for_update=True)
|
def test__check_body_for_update(self, body, resource_type):
|
||||||
|
current_lock = copy.copy(body['resource_lock'])
|
||||||
|
current_lock['resource_type'] = resource_type
|
||||||
|
|
||||||
|
result = self.controller._check_body(
|
||||||
|
body, lock_to_update=current_lock)
|
||||||
|
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
@ -315,6 +345,27 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
self.req,
|
self.req,
|
||||||
body)
|
body)
|
||||||
|
|
||||||
|
def test_create_visibility_already_locked(self):
|
||||||
|
self.mock_object(self.controller, '_check_body')
|
||||||
|
resource_id = '27e14086-16e1-445b-ad32-b2ebb07225a8'
|
||||||
|
body = {
|
||||||
|
'resource_lock': {
|
||||||
|
'resource_id': resource_id,
|
||||||
|
'resource_type': 'share',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.mock_object(
|
||||||
|
self.controller.resource_locks_api,
|
||||||
|
'create',
|
||||||
|
mock.Mock(
|
||||||
|
side_effect=exception.ResourceVisibilityLockExists(
|
||||||
|
resource_id=resource_id))
|
||||||
|
)
|
||||||
|
self.assertRaises(webob.exc.HTTPConflict,
|
||||||
|
self.controller.create,
|
||||||
|
self.req,
|
||||||
|
body)
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
self.mock_object(self.controller, '_check_body')
|
self.mock_object(self.controller, '_check_body')
|
||||||
expected_lock = stubs.stub_lock(
|
expected_lock = stubs.stub_lock(
|
||||||
@ -344,11 +395,13 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
self.assertIn('links', actual_lock)
|
self.assertIn('links', actual_lock)
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
self.mock_object(self.controller, '_check_body')
|
|
||||||
expected_lock = stubs.stub_lock(
|
expected_lock = stubs.stub_lock(
|
||||||
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
||||||
lock_reason=None,
|
lock_reason=None,
|
||||||
)
|
)
|
||||||
|
self.mock_object(self.controller, '_check_body')
|
||||||
|
self.mock_object(self.controller.resource_locks_api, 'get',
|
||||||
|
mock.Mock(return_value=expected_lock))
|
||||||
self.mock_object(self.controller.resource_locks_api,
|
self.mock_object(self.controller.resource_locks_api,
|
||||||
'update',
|
'update',
|
||||||
mock.Mock(return_value=expected_lock))
|
mock.Mock(return_value=expected_lock))
|
||||||
@ -367,7 +420,7 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
|
|
||||||
self.controller.resource_locks_api.update.assert_called_once_with(
|
self.controller.resource_locks_api.update.assert_called_once_with(
|
||||||
utils.IsAMatcher(context.RequestContext),
|
utils.IsAMatcher(context.RequestContext),
|
||||||
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
expected_lock,
|
||||||
{'lock_reason': None}
|
{'lock_reason': None}
|
||||||
)
|
)
|
||||||
self.assertSubDictMatch(expected_lock, actual_lock)
|
self.assertSubDictMatch(expected_lock, actual_lock)
|
||||||
|
@ -19,6 +19,7 @@ import ddt
|
|||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from manila.api.v2 import share_accesses
|
from manila.api.v2 import share_accesses
|
||||||
|
from manila.common import constants
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila import policy
|
from manila import policy
|
||||||
from manila import test
|
from manila import test
|
||||||
@ -102,6 +103,76 @@ class ShareAccessesAPITest(test.TestCase):
|
|||||||
for key in summary_keys:
|
for key in summary_keys:
|
||||||
self.assertEqual(index_access[key], show_el[key])
|
self.assertEqual(index_access[key], show_el[key])
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
|
def test_list_accesses_restricted(self, restricted):
|
||||||
|
req = self._get_index_request(version='2.82')
|
||||||
|
rule_list = [{
|
||||||
|
'access_to': '0.0.0.0/0',
|
||||||
|
'id': 'fakeid',
|
||||||
|
'access_key': 'fake_key'
|
||||||
|
}]
|
||||||
|
self.mock_object(
|
||||||
|
self.controller.share_api, 'access_get_all',
|
||||||
|
mock.Mock(return_value=rule_list))
|
||||||
|
self.mock_object(
|
||||||
|
self.controller, '_is_rule_restricted',
|
||||||
|
mock.Mock(return_value=restricted))
|
||||||
|
|
||||||
|
index_result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertIn('access_list', index_result)
|
||||||
|
self.controller._is_rule_restricted.assert_called_once_with(
|
||||||
|
req.environ['manila.context'], rule_list[0]['id'])
|
||||||
|
if restricted:
|
||||||
|
for access in index_result['access_list']:
|
||||||
|
self.assertEqual('******', access['access_key'])
|
||||||
|
self.assertEqual('******', access['access_to'])
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
|
def test_show_restricted(self, restricted):
|
||||||
|
req = self._get_show_request(
|
||||||
|
version='2.82', use_admin_context=False)
|
||||||
|
self.mock_object(
|
||||||
|
self.controller, '_is_rule_restricted',
|
||||||
|
mock.Mock(return_value=restricted))
|
||||||
|
|
||||||
|
show_result = self.controller.show(req, self.access['id'])
|
||||||
|
|
||||||
|
expected_access_to = (
|
||||||
|
'******' if restricted else self.access['access_to'])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
expected_access_to, show_result['access']['access_to'])
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
|
def test__is_rule_restricted(self, is_rule_restricted):
|
||||||
|
req = self._get_show_request(
|
||||||
|
version='2.82', use_admin_context=False)
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
fake_lock = {
|
||||||
|
'lock_context': 'user',
|
||||||
|
'user_id': 'fake',
|
||||||
|
'project_id': 'fake',
|
||||||
|
'resource_id': 'fake',
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_DELETE,
|
||||||
|
'lock_reason': 'fake reason',
|
||||||
|
}
|
||||||
|
lock = fake_lock if is_rule_restricted else {}
|
||||||
|
locks = [lock]
|
||||||
|
|
||||||
|
self.mock_object(
|
||||||
|
self.controller.resource_locks_api, 'get_all',
|
||||||
|
mock.Mock(return_value=(locks, len(locks))))
|
||||||
|
self.mock_object(
|
||||||
|
self.controller.resource_locks_api, 'access_is_restricted',
|
||||||
|
mock.Mock(return_value=is_rule_restricted))
|
||||||
|
|
||||||
|
result_rule_restricted = self.controller._is_rule_restricted(
|
||||||
|
context, self.access['id'])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
is_rule_restricted, result_rule_restricted)
|
||||||
|
|
||||||
def test_list_accesses_share_not_found(self):
|
def test_list_accesses_share_not_found(self):
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exc.HTTPBadRequest,
|
exc.HTTPBadRequest,
|
||||||
@ -145,10 +216,12 @@ class ShareAccessesAPITest(test.TestCase):
|
|||||||
self.assertIs(False, share_being_checked['is_public'])
|
self.assertIs(False, share_being_checked['is_public'])
|
||||||
|
|
||||||
def test_show_access_not_found(self):
|
def test_show_access_not_found(self):
|
||||||
|
req = self._get_show_request('inexistent_id')
|
||||||
|
print(req.environ)
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exc.HTTPNotFound,
|
exc.HTTPNotFound,
|
||||||
self.controller.show,
|
self.controller.show,
|
||||||
self._get_show_request('inexistent_id'), 'inexistent_id')
|
req, 'inexistent_id')
|
||||||
|
|
||||||
@ddt.data('1.0', '2.0', '2.8', '2.44')
|
@ddt.data('1.0', '2.0', '2.8', '2.44')
|
||||||
def test_list_with_unsupported_version(self, version):
|
def test_list_with_unsupported_version(self, version):
|
||||||
|
@ -330,6 +330,23 @@ class ShareAccessDatabaseAPITestCase(test.TestCase):
|
|||||||
metadata = {}
|
metadata = {}
|
||||||
self.assertEqual(new_metadata, metadata)
|
self.assertEqual(new_metadata, metadata)
|
||||||
|
|
||||||
|
def test_share_access_get_with_context(self):
|
||||||
|
ctxt = context.RequestContext('demo', 'fake', False)
|
||||||
|
share = db_utils.create_share(project_id=ctxt.project_id)
|
||||||
|
rules = [db_utils.create_access(share_id=share['id'])]
|
||||||
|
|
||||||
|
result = db_api.share_access_get_with_context(ctxt, rules[0]['id'])
|
||||||
|
|
||||||
|
self.assertEqual(result['project_id'], ctxt.project_id)
|
||||||
|
|
||||||
|
def test_share_access_get_with_context_not_found(self):
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
exception.NotFound,
|
||||||
|
db_api.share_access_get_with_context,
|
||||||
|
self.ctxt,
|
||||||
|
'fake_rule_id')
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class ShareDatabaseAPITestCase(test.TestCase):
|
class ShareDatabaseAPITestCase(test.TestCase):
|
||||||
|
@ -124,6 +124,71 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
test_utils.annotated(
|
||||||
|
'service_manipulating_user_lock',
|
||||||
|
(context.RequestContext(
|
||||||
|
'fake', 'fake', is_admin=False,
|
||||||
|
service_roles=['service']),
|
||||||
|
'user',
|
||||||
|
'user_b'),
|
||||||
|
),
|
||||||
|
test_utils.annotated(
|
||||||
|
'admin_manipulating_user_lock',
|
||||||
|
(context.RequestContext('fake', 'fake', is_admin=True),
|
||||||
|
'admin',
|
||||||
|
'user_a'),
|
||||||
|
),
|
||||||
|
test_utils.annotated(
|
||||||
|
'user_manipulating_locks_they_own',
|
||||||
|
(context.RequestContext('user_a', 'fake', is_admin=False),
|
||||||
|
'user',
|
||||||
|
'user_a'),
|
||||||
|
),
|
||||||
|
test_utils.annotated(
|
||||||
|
'user_manipulating_other_users_lock',
|
||||||
|
(context.RequestContext('user_a', 'fake', is_admin=False),
|
||||||
|
'user',
|
||||||
|
'user_b'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_access_is_restricted(self, ctxt, lock_ctxt, lock_user):
|
||||||
|
resource_lock = {
|
||||||
|
'user_id': lock_user,
|
||||||
|
'lock_context': lock_ctxt
|
||||||
|
}
|
||||||
|
is_restricted = (
|
||||||
|
(not ctxt.is_admin and not ctxt.is_service)
|
||||||
|
and lock_user != ctxt.user_id)
|
||||||
|
expected_mock_policy = {}
|
||||||
|
if is_restricted:
|
||||||
|
expected_mock_policy['side_effect'] = exception.NotAuthorized
|
||||||
|
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
||||||
|
self.mock_object(policy, 'check_policy',
|
||||||
|
mock.Mock(**expected_mock_policy))
|
||||||
|
|
||||||
|
result = self.lock_api.access_is_restricted(
|
||||||
|
ctxt,
|
||||||
|
resource_lock
|
||||||
|
)
|
||||||
|
self.assertEqual(is_restricted, result)
|
||||||
|
|
||||||
|
def test_access_is_restricted_not_authorized(self):
|
||||||
|
resource_lock = {
|
||||||
|
'user_id': 'fakeuserid',
|
||||||
|
'lock_context': 'user'
|
||||||
|
}
|
||||||
|
ctxt = context.RequestContext('fake', 'fake')
|
||||||
|
self.mock_object(self.lock_api, '_check_allow_lock_manipulation',
|
||||||
|
mock.Mock(side_effect=exception.NotAuthorized()))
|
||||||
|
|
||||||
|
result = self.lock_api.access_is_restricted(
|
||||||
|
ctxt,
|
||||||
|
resource_lock
|
||||||
|
)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_get_all_all_projects_ignored(self):
|
def test_get_all_all_projects_ignored(self):
|
||||||
self.mock_object(policy, 'check_policy', mock.Mock(return_value=False))
|
self.mock_object(policy, 'check_policy', mock.Mock(return_value=False))
|
||||||
self.mock_object(self.lock_api.db, 'resource_lock_get_all',
|
self.mock_object(self.lock_api.db, 'resource_lock_get_all',
|
||||||
@ -257,15 +322,84 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
'project_id': 'fakeproject',
|
'project_id': 'fakeproject',
|
||||||
'lock_context': 'user',
|
'lock_context': 'user',
|
||||||
'lock_reason': None,
|
'lock_reason': None,
|
||||||
|
'resource_type': constants.SHARE_RESOURCE_TYPE
|
||||||
|
|
||||||
}
|
}
|
||||||
self.assertEqual(expected_create_arg, db_create_arg)
|
self.assertEqual(expected_create_arg, db_create_arg)
|
||||||
|
|
||||||
|
def test_create_access_show_lock(self):
|
||||||
|
self.mock_object(self.lock_api.db, 'resource_lock_create',
|
||||||
|
mock.Mock(return_value='created_obj'))
|
||||||
|
mock_access = {
|
||||||
|
'id': 'cacac01c-853d-47f3-afcb-da4484bd09a5',
|
||||||
|
'state': constants.STATUS_ACTIVE,
|
||||||
|
}
|
||||||
|
self.mock_object(self.lock_api.db, 'access_get',
|
||||||
|
mock.Mock(return_value=mock_access))
|
||||||
|
self.mock_object(self.lock_api.db, 'resource_lock_get_all',
|
||||||
|
mock.Mock(return_value=['', 0]))
|
||||||
|
self.mock_object(self.ctxt, 'elevated',
|
||||||
|
mock.Mock(return_value=self.ctxt))
|
||||||
|
|
||||||
|
result = self.lock_api.create(
|
||||||
|
self.ctxt,
|
||||||
|
resource_id='cacac01c-853d-47f3-afcb-da4484bd09a5',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_SHOW,
|
||||||
|
resource_type=constants.SHARE_ACCESS_RESOURCE_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual('created_obj', result)
|
||||||
|
db_create_arg = self.lock_api.db.resource_lock_create.call_args[0][1]
|
||||||
|
resource_id = 'cacac01c-853d-47f3-afcb-da4484bd09a5'
|
||||||
|
expected_create_arg = {
|
||||||
|
'resource_id': resource_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_SHOW,
|
||||||
|
'user_id': 'fakeuser',
|
||||||
|
'project_id': 'fakeproject',
|
||||||
|
'lock_context': 'user',
|
||||||
|
'lock_reason': None,
|
||||||
|
'resource_type': constants.SHARE_ACCESS_RESOURCE_TYPE
|
||||||
|
|
||||||
|
}
|
||||||
|
self.assertEqual(expected_create_arg, db_create_arg)
|
||||||
|
filters = {
|
||||||
|
'resource_id': resource_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_SHOW,
|
||||||
|
'all_projects': True
|
||||||
|
}
|
||||||
|
self.lock_api.db.resource_lock_get_all.assert_called_once_with(
|
||||||
|
self.ctxt, filters=filters)
|
||||||
|
|
||||||
|
def test_create_visibility_lock_lock_exists(self):
|
||||||
|
self.mock_object(self.lock_api.db, 'resource_lock_create',
|
||||||
|
mock.Mock(return_value='created_obj'))
|
||||||
|
self.mock_object(self.lock_api.db, 'resource_lock_get_all',
|
||||||
|
mock.Mock(return_value=['visibility_lock', 1]))
|
||||||
|
self.mock_object(self.ctxt, 'elevated',
|
||||||
|
mock.Mock(return_value=self.ctxt))
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
exception.ResourceVisibilityLockExists,
|
||||||
|
self.lock_api.create,
|
||||||
|
self.ctxt,
|
||||||
|
resource_id='cacac01c-853d-47f3-afcb-da4484bd09a5',
|
||||||
|
resource_action=constants.RESOURCE_ACTION_SHOW,
|
||||||
|
resource_type=constants.SHARE_ACCESS_RESOURCE_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_id = 'cacac01c-853d-47f3-afcb-da4484bd09a5'
|
||||||
|
filters = {
|
||||||
|
'resource_id': resource_id,
|
||||||
|
'resource_action': constants.RESOURCE_ACTION_SHOW,
|
||||||
|
'all_projects': True
|
||||||
|
}
|
||||||
|
self.lock_api.db.resource_lock_get_all.assert_called_once_with(
|
||||||
|
self.ctxt, filters=filters)
|
||||||
|
|
||||||
@ddt.data(True, False)
|
@ddt.data(True, False)
|
||||||
def test_update_lock_resource_not_allowed_with_policy_failure(
|
def test_update_lock_resource_not_allowed_with_policy_failure(
|
||||||
self, policy_fails):
|
self, policy_fails):
|
||||||
self.mock_object(self.lock_api.db, 'resource_lock_get', mock.Mock(
|
lock = {'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}
|
||||||
return_value={'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}))
|
|
||||||
if policy_fails:
|
if policy_fails:
|
||||||
self.mock_object(
|
self.mock_object(
|
||||||
policy,
|
policy,
|
||||||
@ -286,7 +420,7 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
self.assertRaises(exception.NotAuthorized,
|
self.assertRaises(exception.NotAuthorized,
|
||||||
self.lock_api.update,
|
self.lock_api.update,
|
||||||
self.ctxt,
|
self.ctxt,
|
||||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
lock,
|
||||||
{'foo': 'bar'})
|
{'foo': 'bar'})
|
||||||
|
|
||||||
@ddt.data(constants.STATUS_DELETING,
|
@ddt.data(constants.STATUS_DELETING,
|
||||||
@ -303,8 +437,6 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
'resource_action': 'something',
|
'resource_action': 'something',
|
||||||
'resource_type': 'share',
|
'resource_type': 'share',
|
||||||
}
|
}
|
||||||
self.mock_object(self.lock_api.db, 'resource_lock_get',
|
|
||||||
mock.Mock(return_value=lock))
|
|
||||||
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
||||||
self.mock_object(self.lock_api.db,
|
self.mock_object(self.lock_api.db,
|
||||||
'share_get',
|
'share_get',
|
||||||
@ -313,21 +445,20 @@ class ResourceLockApiTest(test.TestCase):
|
|||||||
self.assertRaises(exception.InvalidInput,
|
self.assertRaises(exception.InvalidInput,
|
||||||
self.lock_api.update,
|
self.lock_api.update,
|
||||||
self.ctxt,
|
self.ctxt,
|
||||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
lock,
|
||||||
{'resource_action': 'delete'})
|
{'resource_action': 'delete'})
|
||||||
|
|
||||||
self.lock_api.db.resource_lock_update.assert_not_called()
|
self.lock_api.db.resource_lock_update.assert_not_called()
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
self.mock_object(self.lock_api.db, 'resource_lock_get', mock.Mock(
|
|
||||||
return_value={'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}))
|
|
||||||
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
||||||
self.mock_object(self.lock_api.db, 'resource_lock_update',
|
self.mock_object(self.lock_api.db, 'resource_lock_update',
|
||||||
mock.Mock(return_value='updated_obj'))
|
mock.Mock(return_value='updated_obj'))
|
||||||
|
lock = {'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}
|
||||||
|
|
||||||
result = self.lock_api.update(
|
result = self.lock_api.update(
|
||||||
self.ctxt,
|
self.ctxt,
|
||||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
lock,
|
||||||
{'foo': 'bar'},
|
{'foo': 'bar'},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added the possibility to lock the deletion of access rules, as well as the
|
||||||
|
visibility of the sensitive fields `access_to` and `access_type` while
|
||||||
|
creating share access rules. When the visibility is restricted, only the
|
||||||
|
owner or more privileged users will be able to visualize the context of
|
||||||
|
the sensitive fields.
|
||||||
|
Both locks can also be imposed by the recently introduced resource locks
|
||||||
|
APIs.
|
||||||
|
- |
|
||||||
|
It is now possible to filter access rules based on the `access_to`,
|
||||||
|
`access_type`, `access_key` and `access_level` keys.
|
Loading…
Reference in New Issue
Block a user