Allow restricting access rules fields and deletion
Access rules rules allow API will now take three additional parameters: - lock_visibility: when True, only services, administrators and the same user will be able to see the content of ``access_to`` and access_key. - lock_deletion: when True, the access rule will be locked for deletion. Only services, administrators or the user that placed the lock will be able to drop the access rule. - lock_reason: a reason for the lock. This parameter should only be provided in the presence of at least one of the former parameters. In order to delete an access rule that is currently locked, the requester will need to specify ``unrestrict=True`` in the request. In case a service placed the restrictions, only the own service or the system administrator will be able to release it. This change also implements filters to the access list API. It is now possible to filter access rules based on `access_to`, `access_type`, `access_level` and `access_key`. DocImpact Change-Id: Iea422c9d6bc99a81cd88c5f4b7055d6a1cf97fdc
This commit is contained in:
parent
f641577d8a
commit
0f82690ddd
@ -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``.
|
||||||
@ -3917,6 +3933,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