Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:manila
User: [email protected]
Usertags: pu
Hi,
I have uploaded manila 20.0.0-3+deb13u2 to Trixie p-u to address
2 issues:
- CVE-2026-71195
- a missing sudoers rule
[ Reason ]
CVE-2026-71195 / OSSA-2026-046: Manila allows cross-project
public-share and export-metadata changes.
The missing sudoers rule in debian/manila-common.sudoers:
+manila ALL = (root) NOPASSWD: /usr/bin/privsep-helper *
which was preventing Manila to issue root-needing commands
through rootwrap.
[ Impact ]
Many things not working due to missing sudoers rights, plus
the CVE.
[ Tests ]
The attached debdiff shows a lot of new tests ran at package
build and autopkgtest.
[ Risks ]
As this is tested in my CI, minimal risks.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
Please approve manila/20.0.0-3+deb13u2 into Trixie p-u.
Cheers,
Thomas Goirand (zigo)
diff -Nru manila-20.0.0/debian/changelog manila-20.0.0/debian/changelog
--- manila-20.0.0/debian/changelog 2026-08-20 17:26:40.000000000 +0200
+++ manila-20.0.0/debian/changelog 2026-10-15 23:15:31.000000000 +0200
@@ -1,3 +1,18 @@
+manila (1:20.0.0-3+deb13u2) trixie; urgency=medium
+
+ * Add /usr/bin/privsep-helper to the sudoers rules: manila now uses
+ oslo.privsep ("sudo privsep-helper") instead of rootwrap to run
+ privileged commands, and the share service runs as the manila user.
+ Without this, the LVM driver initialization fails with
+ FailedToDropPrivileges and manila-share never becomes available to
+ the scheduler.
+ * CVE-2026-71195 / OSSA-2026-046: Manila allows cross-project
+ public-share and export-metadata changes. Applied upstream patch
+ "Fix policy control for public shares" (Closes: #1151439):
+ - CVE-2026-71195_Fix_policy_control_for_public_shares.patch
+
+ -- Thomas Goirand <[email protected]> Thu, 15 Oct 2026 23:15:31 +0200
+
manila (1:20.0.0-3+deb13u1) trixie; urgency=medium
* OSSN-0103: resource-lock list trusts a foreign project_id filter. Applied
diff -Nru manila-20.0.0/debian/manila-common.sudoers
manila-20.0.0/debian/manila-common.sudoers
--- manila-20.0.0/debian/manila-common.sudoers 2026-08-20 17:26:40.000000000
+0200
+++ manila-20.0.0/debian/manila-common.sudoers 2026-10-15 23:15:31.000000000
+0200
@@ -1,3 +1,4 @@
Defaults:manila !requiretty
manila ALL = (root) NOPASSWD: /usr/bin/manila-rootwrap
/etc/manila/rootwrap.conf *
+manila ALL = (root) NOPASSWD: /usr/bin/privsep-helper *
diff -Nru
manila-20.0.0/debian/patches/CVE-2026-71195_Fix_policy_control_for_public_shares.patch
manila-20.0.0/debian/patches/CVE-2026-71195_Fix_policy_control_for_public_shares.patch
---
manila-20.0.0/debian/patches/CVE-2026-71195_Fix_policy_control_for_public_shares.patch
1970-01-01 01:00:00.000000000 +0100
+++
manila-20.0.0/debian/patches/CVE-2026-71195_Fix_policy_control_for_public_shares.patch
2026-10-15 23:15:31.000000000 +0200
@@ -0,0 +1,720 @@
+Description: Fix policy control for public shares
+ A public share is visible to all tenants, but users other than the owner
+ should not be able to modify it: several operations skipped the ownership
+ policy check for public shares. Enforce ownership checks when manipulating
+ access rules, creating backups or replicas, and soft deleting or restoring
+ public shares. Export location metadata operations are now scoped to the
+ share in the request URL; export locations that do not belong to that
+ share now return 404 (CVE-2026-71195).
+Origin: upstream, https://review.opendev.org/c/openstack/manila/+/1011483
+Change-Id: Ie283dce157f9c90f5d2ab38c1e74daa7df3ac118
+Bug: https://bugs.launchpad.net/bugs/2161412
+Bug: https://bugs.launchpad.net/bugs/2168962
+Bug-Debian: https://bugs.debian.org/1151439
+Author: Carlos da Silva <[email protected]>
+Date: Fri, 7 Aug 2026 20:55:09 -0300
+Last-Update: 2026-10-20
+
+
+Index: manila/manila/api/v1/shares.py
+===================================================================
+--- manila.orig/manila/api/v1/shares.py
++++ manila/manila/api/v1/shares.py
+@@ -552,7 +552,7 @@ class ShareMixin(object):
+ access_data = body.get('allow_access', body.get('os-allow_access'))
+ if not enable_metadata:
+ access_data.pop('metadata', None)
+- share = self.share_api.get(context, id)
++ share = self.share_api.get(context, id, for_modification=True)
+
+ if share.get('is_soft_deleted'):
+ msg = _("Cannot allow access for share '%s' "
+@@ -666,7 +666,7 @@ class ShareMixin(object):
+
+ 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, for_modification=True)
+
+ if share.get('is_soft_deleted'):
+ msg = _("Cannot deny access for share '%s' "
+Index: manila/manila/api/v2/metadata.py
+===================================================================
+--- manila.orig/manila/api/v2/metadata.py
++++ manila/manila/api/v2/metadata.py
+@@ -92,7 +92,10 @@ class MetadataController(object):
+ get_res_method = getattr(
+ db, self.resource_get[self.resource_name])
+ if parent_id is not None:
+- kwargs["parent_id"] = parent_id
++ if self.resource_name == 'share_export_location':
++ kwargs["share_id"] = parent_id
++ else:
++ kwargs["parent_id"] = parent_id
+ res = get_res_method(context, resource_id, **kwargs)
+
+ if self.resource_name not in ["share_export_location"]:
+Index: manila/manila/api/v2/share_accesses.py
+===================================================================
+--- manila.orig/manila/api/v2/share_accesses.py
++++ manila/manila/api/v2/share_accesses.py
+@@ -168,7 +168,8 @@ class ShareAccessesController(wsgi.Contr
+ if access_level == share_access.access_level:
+ return self._view_builder.view(req, share_access)
+
+- share = self.share_api.get(context, share_access.share_id)
++ share = self.share_api.get(
++ context, share_access.share_id, for_modification=True)
+ values = {
+ 'access_level': access_level,
+ }
+Index: manila/manila/api/v2/share_backups.py
+===================================================================
+--- manila.orig/manila/api/v2/share_backups.py
++++ manila/manila/api/v2/share_backups.py
+@@ -158,7 +158,8 @@ class ShareBackupController(wsgi.Control
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+- share = self.share_api.get(context, share_id)
++ share = self.share_api.get(
++ context, share_id, for_modification=True)
+ except exception.NotFound:
+ msg = _("No share exists with ID %s.")
+ raise exc.HTTPBadRequest(explanation=msg % share_id)
+Index: manila/manila/api/v2/share_export_locations.py
+===================================================================
+--- manila.orig/manila/api/v2/share_export_locations.py
++++ manila/manila/api/v2/share_export_locations.py
+@@ -24,6 +24,7 @@ from manila.db import api as db_api
+ from manila import exception
+ from manila.i18n import _
+ from manila import policy
++from manila.share import api as share_api
+
+ LOG = log.getLogger(__name__)
+ CONF = cfg.CONF
+@@ -40,12 +41,13 @@ class ShareExportLocationController(wsgi
+ self._conf_admin_only_metadata_keys = getattr(
+ CONF, 'admin_only_el_metadata', []
+ )
++ self.share_api = share_api.API()
+
+- def _verify_share(self, context, share_id):
++ def _verify_share(self, context, share_id, for_modification=False):
+ try:
+- share = db_api.share_get(context, share_id)
+- if not share['is_public']:
+- policy.check_policy(context, 'share', 'get', share)
++ self.share_api.get(
++ context, share_id, for_modification=for_modification
++ )
+ except exception.NotFound:
+ msg = _("Share '%s' not found.") % share_id
+ raise exc.HTTPNotFound(explanation=msg)
+@@ -141,7 +143,7 @@ class ShareExportLocationController(wsgi
+ """Returns the list of metadata for a given share export location."""
+ context = req.environ['manila.context']
+ self._verify_share(context, share_id)
+- return self._index_metadata(req, resource_id)
++ return self._index_metadata(req, resource_id, parent_id=share_id)
+
+ @wsgi.Controller.api_version("2.87")
+ @wsgi.Controller.authorize("update_metadata")
+@@ -152,8 +154,9 @@ class ShareExportLocationController(wsgi
+ delete=False)
+ body['metadata'] = _metadata
+ context = req.environ['manila.context']
+- self._verify_share(context, share_id)
+- return self._create_metadata(req, resource_id, body)
++ self._verify_share(context, share_id, for_modification=True)
++ return self._create_metadata(
++ req, resource_id, body, parent_id=share_id)
+
+ @wsgi.Controller.api_version("2.87")
+ @wsgi.Controller.authorize("update_metadata")
+@@ -163,8 +166,9 @@ class ShareExportLocationController(wsgi
+ body['metadata'])
+ body['metadata'] = _metadata
+ context = req.environ['manila.context']
+- self._verify_share(context, share_id)
+- return self._update_all_metadata(req, resource_id, body)
++ self._verify_share(context, share_id, for_modification=True)
++ return self._update_all_metadata(
++ req, resource_id, body, parent_id=share_id)
+
+ @wsgi.Controller.api_version("2.87")
+ @wsgi.Controller.authorize("update_metadata")
+@@ -175,8 +179,9 @@ class ShareExportLocationController(wsgi
+ delete=False)
+ body['metadata'] = _metadata
+ context = req.environ['manila.context']
+- self._verify_share(context, share_id)
+- return self._update_metadata_item(req, resource_id, body, key)
++ self._verify_share(context, share_id, for_modification=True)
++ return self._update_metadata_item(
++ req, resource_id, body, key, parent_id=share_id)
+
+ @wsgi.Controller.api_version("2.87")
+ @wsgi.Controller.authorize("get_metadata")
+@@ -184,15 +189,17 @@ class ShareExportLocationController(wsgi
+ """Show metadata for a given share export location."""
+ context = req.environ['manila.context']
+ self._verify_share(context, share_id)
+- return self._show_metadata(req, resource_id, key)
++ return self._show_metadata(
++ req, resource_id, key, parent_id=share_id)
+
+ @wsgi.Controller.api_version("2.87")
+ @wsgi.Controller.authorize("delete_metadata")
+ def delete_metadata(self, req, share_id, resource_id, key):
+ """Delete metadata for a given share export location."""
+ context = req.environ['manila.context']
+- self._verify_share(context, share_id)
+- return self._delete_metadata(req, resource_id, key)
++ self._verify_share(context, share_id, for_modification=True)
++ return self._delete_metadata(
++ req, resource_id, key, parent_id=share_id)
+
+
+ def create_resource():
+Index: manila/manila/api/v2/share_replicas.py
+===================================================================
+--- manila.orig/manila/api/v2/share_replicas.py
++++ manila/manila/api/v2/share_replicas.py
+@@ -166,7 +166,8 @@ class ShareReplicationController(wsgi.Co
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+- share_ref = db.share_get(context, share_id)
++ share_ref = self.share_api.get(
++ context, share_id, for_modification=True)
+ except exception.NotFound:
+ msg = _("No share exists with ID %s.")
+ raise exc.HTTPNotFound(explanation=msg % share_id)
+Index: manila/manila/api/v2/shares.py
+===================================================================
+--- manila.orig/manila/api/v2/shares.py
++++ manila/manila/api/v2/shares.py
+@@ -280,7 +280,7 @@ class ShareController(wsgi.Controller,
+ LOG.debug("Soft delete share with id: %s", id, context=context)
+
+ try:
+- share = self.share_api.get(context, id)
++ share = self.share_api.get(context, id, for_modification=True)
+ self.share_api.soft_delete(context, share)
+ except exception.NotFound:
+ raise exc.HTTPNotFound()
+@@ -303,7 +303,7 @@ class ShareController(wsgi.Controller,
+ LOG.debug("Restore share with id: %s", id, context=context)
+
+ try:
+- share = self.share_api.get(context, id)
++ share = self.share_api.get(context, id, for_modification=True)
+ except exception.NotFound:
+ msg = _("No share exists with ID %s.")
+ raise exc.HTTPNotFound(explanation=msg % id)
+Index: manila/manila/share/api.py
+===================================================================
+--- manila.orig/manila/share/api.py
++++ manila/manila/share/api.py
+@@ -2296,11 +2296,13 @@ class API(base.Base):
+ def snapshot_update(self, context, snapshot, fields):
+ return self.db.share_snapshot_update(context, snapshot['id'], fields)
+
+- def get(self, context, share_id):
++ def get(self, context, share_id, for_modification=False):
+ share = self.db.share_get(context, share_id)
+- if not share['is_public']:
++ share_is_public = share['is_public']
++ if not share_is_public or for_modification:
+ authorized = policy.check_policy(
+- context, 'share', 'get', share, do_raise=False)
++ context, 'share', 'get', share,
++ do_raise=share_is_public and for_modification)
+ if not authorized:
+ raise exception.NotFound()
+ if share['status'] in (
+Index: manila/manila/tests/api/v2/test_metadata.py
+===================================================================
+--- manila.orig/manila/tests/api/v2/test_metadata.py
++++ manila/manila/tests/api/v2/test_metadata.py
+@@ -17,6 +17,7 @@ import webob
+
+ from manila.api.v2 import metadata
+ from manila import context
++from manila import db
+ from manila import exception
+ from manila import policy
+ from manila import test
+@@ -81,6 +82,46 @@ class MetadataAPITest(test.TestCase):
+ policy_resource_obj = mock_policy_check.call_args[0][3]
+ self.assertEqual(share_obj['id'], policy_resource_obj['id'])
+
++ def test__get_resource_export_location_scoped_to_share(self):
++ # Export location lookups must be scoped to the share in the
++ # request URL, so that the metadata of an export location
++ # belonging to a different share cannot be retrieved.
++ self.controller.resource_name = 'share_export_location'
++ fake_context = context.RequestContext('fake', 'fake')
++ export_location = {'id': 'fake-el-uuid'}
++ mock_get_by_uuid = self.mock_object(
++ db, 'export_location_get_by_uuid',
++ mock.Mock(return_value=export_location))
++
++ res = self.controller._get_resource(
++ fake_context, 'fake-el-uuid', for_modification=True,
++ parent_id='fake-share-id')
++
++ self.assertEqual(export_location, res)
++ mock_get_by_uuid.assert_called_once_with(
++ fake_context, 'fake-el-uuid', share_id='fake-share-id')
++ self.mock_policy_check.assert_not_called()
++
++ def test__get_resource_export_location_not_in_share_not_found(self):
++ # An export location that does not belong to the share in the
++ # request URL must not be accessible, regardless of the type
++ # of the share it belongs to.
++ self.controller.resource_name = 'share_export_location'
++ fake_context = context.RequestContext('fake', 'fake')
++ self.mock_object(
++ db, 'export_location_get_by_uuid',
++ mock.Mock(side_effect=exception.ExportLocationNotFound(
++ uuid='fake-el-uuid')))
++
++ self.assertRaises(
++ webob.exc.HTTPNotFound,
++ self.controller._get_resource,
++ fake_context,
++ 'fake-el-uuid',
++ for_modification=True,
++ parent_id='fake-share-id',
++ )
++
+ def test_create_index_metadata(self):
+ url = self._get_request()
+ body = {'metadata': {'test_key1': 'test_v1', 'test_key2': 'test_v2'}}
+Index: manila/manila/tests/api/v2/test_share_accesses.py
+===================================================================
+--- manila.orig/manila/tests/api/v2/test_share_accesses.py
++++ manila/manila/tests/api/v2/test_share_accesses.py
+@@ -15,7 +15,6 @@
+
+ from unittest import mock
+
+-import copy
+ import ddt
+ from webob import exc
+
+@@ -247,17 +246,55 @@ class ShareAccessesAPITest(test.TestCase
+ return req
+
+ def test_update_access_level(self):
+- update_share_access = copy.deepcopy(self.access)
+- update_share_access.update({'access_level': 'ro'})
+- self.mock_object(
++ fake_access = mock.Mock(
++ access_level='rw', share_id=self.share['id'])
++ updated_access = mock.Mock(
++ access_level='ro', share_id=self.share['id'])
++ mock_get_access = self.mock_object(
++ self.controller, '_get_share_access',
++ mock.Mock(return_value=fake_access))
++ mock_get = self.mock_object(
++ self.controller.share_api, 'get',
++ mock.Mock(return_value=self.share))
++ mock_update = self.mock_object(
+ self.controller.share_api, 'update_access',
+- mock.Mock(return_value=update_share_access))
++ mock.Mock(return_value=updated_access))
++ mock_view = self.mock_object(
++ self.controller._view_builder, 'view',
++ mock.Mock(return_value={'access': {'access_level': 'ro'}}))
++
++ body = {'update_access': {'access_level': 'ro'}}
++ req = self._get_update_request()
++ context = req.environ['manila.context']
++ ret = self.controller.update(req, self.access['id'], body=body)
++
++ self.assertEqual('ro', ret['access']['access_level'])
++ mock_get_access.assert_called_once_with(
++ context, self.access['id'])
++ mock_get.assert_called_once_with(
++ context, self.share['id'], for_modification=True)
++ mock_update.assert_called_once_with(
++ context, self.share, fake_access, {'access_level': 'ro'})
++ mock_view.assert_called_once_with(req, updated_access)
++
++ def test_update_policy_not_authorized(self):
++ fake_access = mock.Mock(
++ access_level='rw', share_id=self.share['id'])
++ self.mock_object(
++ self.controller, '_get_share_access',
++ mock.Mock(return_value=fake_access))
++ self.mock_object(
++ self.controller.share_api, 'get',
++ mock.Mock(side_effect=exception.PolicyNotAuthorized(
++ action='share:get')))
+
+ body = {'update_access': {'access_level': 'ro'}}
+- url = self._get_update_request()
+- ret = self.controller.update(url, self.access['id'], body=body)
+- self.assertEqual(update_share_access['access_level'],
+- ret['access']['access_level'])
++ req = self._get_update_request()
++
++ self.assertRaises(
++ exception.PolicyNotAuthorized,
++ self.controller.update,
++ req, self.access['id'], body=body)
+
+ def test_update_access_level_invalid_access_level(self):
+ body = {'access': {'access_level': 'fake_access'}}
+Index: manila/manila/tests/api/v2/test_share_backups.py
+===================================================================
+--- manila.orig/manila/tests/api/v2/test_share_backups.py
++++ manila/manila/tests/api/v2/test_share_backups.py
+@@ -362,19 +362,38 @@ class ShareBackupsApiTest(test.TestCase)
+
+ def test_create(self):
+ fake_backup, expected_backup = self._get_fake_backup()
++ share_id = 'FAKE_SHAREID'
+ body = {
+ 'share_backup': {
+- 'share_id': 'FAKE_SHAREID',
++ 'share_id': share_id,
+ }
+ }
+- self.mock_object(share.API, 'get',
+- mock.Mock(return_value={'id': 'FAKE_SHAREID'}))
++ mock_get = self.mock_object(
++ share.API, 'get', mock.Mock(return_value={'id': share_id}))
+ self.mock_object(share.API, 'create_share_backup',
+ mock.Mock(return_value=fake_backup))
+
+ req = self.backups_req
+ res_dict = self.controller.create(req, body)
+ self.assertEqual(expected_backup, res_dict['share_backup'])
++ mock_get.assert_called_once_with(
++ self.backups_req.environ['manila.context'], share_id,
++ for_modification=True)
++
++ def test_create_policy_not_authorized(self):
++ body = {
++ 'share_backup': {
++ 'share_id': 'FAKE_SHAREID',
++ }
++ }
++ self.mock_object(
++ share.API, 'get',
++ mock.Mock(side_effect=exception.PolicyNotAuthorized(
++ action='share:get')))
++
++ self.assertRaises(exception.PolicyNotAuthorized,
++ self.controller.create,
++ self.backups_req, body)
+
+ def test_delete_invalid_backup(self):
+ fake_exception = exception.ShareBackupNotFound(
+Index: manila/manila/tests/api/v2/test_share_export_locations.py
+===================================================================
+--- manila.orig/manila/tests/api/v2/test_share_export_locations.py
++++ manila/manila/tests/api/v2/test_share_export_locations.py
+@@ -307,7 +307,8 @@ class ShareExportLocationsAPITest(test.T
+ self.assertEqual(body, res)
+ mock_validate.assert_called_once_with(req, el_id, body['metadata'],
+ delete=False)
+- mock_create.assert_called_once_with(req, el_id, body)
++ mock_create.assert_called_once_with(
++ req, el_id, body, parent_id=self.share['id'])
+
+ def test_update_all_metadata(self):
+ index_result = self.controller.index(self.req, self.share['id'])
+@@ -329,7 +330,34 @@ class ShareExportLocationsAPITest(test.T
+ body)
+ self.assertEqual(body, res)
+ mock_validate.assert_called_once_with(req, el_id, body['metadata'])
+- mock_update.assert_called_once_with(req, el_id, body)
++ mock_update.assert_called_once_with(
++ req, el_id, body, parent_id=self.share['id'])
++
++ def test__verify_share_public_share_skips_policy_for_read(self):
++ public_share = db_utils.create_share(is_public=True)
++
++ self.controller._verify_share(
++ self.ctxt['user'], public_share['id'])
++
++ self.mock_policy_check.assert_not_called()
++
++ def
test__verify_share_public_share_enforces_policy_for_modification(self):
++ public_share = db_utils.create_share(is_public=True)
++
++ self.controller._verify_share(
++ self.ctxt['user'], public_share['id'], for_modification=True)
++
++ self.mock_policy_check.assert_called_once_with(
++ self.ctxt['user'], 'share', 'get', mock.ANY, do_raise=True)
++
++ def test__verify_share_private_share_always_enforces_policy(self):
++ private_share = db_utils.create_share(is_public=False)
++
++ self.controller._verify_share(
++ self.ctxt['user'], private_share['id'])
++
++ self.mock_policy_check.assert_called_once_with(
++ self.ctxt['user'], 'share', 'get', mock.ANY, do_raise=False)
+
+ def test_delete_metadata(self):
+ index_result = self.controller.index(self.req, self.share['id'])
+@@ -343,4 +371,5 @@ class ShareExportLocationsAPITest(test.T
+ version="2.87", use_admin_context=True)
+ self.controller.delete_metadata(req, self.share['id'], el_id,
+ 'fake_key')
+- mock_delete.assert_called_once_with(req, el_id, 'fake_key')
++ mock_delete.assert_called_once_with(
++ req, el_id, 'fake_key', parent_id=self.share['id'])
+Index: manila/manila/tests/api/v2/test_share_replicas.py
+===================================================================
+--- manila.orig/manila/tests/api/v2/test_share_replicas.py
++++ manila/manila/tests/api/v2/test_share_replicas.py
+@@ -378,6 +378,43 @@ class ShareReplicasApiTest(test.TestCase
+ self.mock_policy_check.assert_called_once_with(
+ self.member_context, self.resource_name, 'create')
+
++ def test_create_share_from_other_project_not_found(self):
++ # A share that belongs to another project is not visible to the
++ # caller, so the ownership lookup must fail with 404.
++ body = {
++ 'share_replica': {
++ 'share_id': 'FAKE_SHAREID',
++ 'availability_zone': 'FAKE_AZ'
++ }
++ }
++ self.mock_object(share.API, 'get',
++ mock.Mock(side_effect=exception.NotFound))
++
++ self.assertRaises(exc.HTTPNotFound,
++ self.controller.create,
++ self.replicas_req, body)
++ self.mock_policy_check.assert_called_once_with(
++ self.member_context, self.resource_name, 'create')
++
++ def test_create_share_from_other_project_not_authorized(self):
++ # A public share that belongs to another project: the ownership
++ # policy check inside the share lookup must raise
++ # PolicyNotAuthorized, which the WSGI layer converts to 403.
++ body = {
++ 'share_replica': {
++ 'share_id': 'FAKE_SHAREID',
++ 'availability_zone': 'FAKE_AZ'
++ }
++ }
++ self.mock_object(share.API, 'get', mock.Mock(
++ side_effect=exception.PolicyNotAuthorized(action='share:get')))
++
++ self.assertRaises(exception.PolicyNotAuthorized,
++ self.controller.create,
++ self.replicas_req, body)
++ self.mock_policy_check.assert_called_once_with(
++ self.member_context, self.resource_name, 'create')
++
+ def test_create_has_been_soft_deleted(self):
+ share_ref = fake_share.fake_share(is_soft_deleted=True)
+ body = {
+@@ -389,7 +426,7 @@ class ShareReplicasApiTest(test.TestCase
+ mock__view_builder_call = self.mock_object(
+ share_replicas.replication_view.ReplicationViewBuilder,
+ 'detail_list')
+- self.mock_object(share_replicas.db, 'share_get',
++ self.mock_object(share.API, 'get',
+ mock.Mock(return_value=share_ref))
+
+ self.assertRaises(exc.HTTPForbidden,
+@@ -415,7 +452,7 @@ class ShareReplicasApiTest(test.TestCase
+ mock__view_builder_call = self.mock_object(
+ share_replicas.replication_view.ReplicationViewBuilder,
+ 'detail_list')
+- self.mock_object(share_replicas.db, 'share_get',
++ self.mock_object(share.API, 'get',
+ mock.Mock(return_value=fake_replica))
+ self.mock_object(share_replicas.db, 'share_network_get',
+ mock.Mock(side_effect=exception.ShareNetworkNotFound(
+@@ -444,7 +481,7 @@ class ShareReplicasApiTest(test.TestCase
+ }
+ }
+ exc_args = {'id': 'xyz', 'reason': 'abc'}
+- self.mock_object(share_replicas.db, 'share_get',
++ self.mock_object(share.API, 'get',
+ mock.Mock(return_value=fake_replica))
+ self.mock_object(share.API, 'create_share_replica',
+ mock.Mock(side_effect=exception_type(**exc_args)))
+@@ -483,7 +520,7 @@ class ShareReplicasApiTest(test.TestCase
+ else:
+ share_network = db_utils.create_share_network()
+
+- self.mock_object(share_replicas.db, 'share_get',
++ self.mock_object(share.API, 'get',
+ mock.Mock(return_value=fake_replica))
+ self.mock_object(share.API, 'create_share_replica',
+ mock.Mock(return_value=fake_replica))
+Index: manila/manila/tests/api/v2/test_shares.py
+===================================================================
+--- manila.orig/manila/tests/api/v2/test_shares.py
++++ manila/manila/tests/api/v2/test_shares.py
+@@ -1627,8 +1627,14 @@ class ShareAPITest(test.TestCase):
+ req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
+ version='2.69')
+ body = {"soft_delete": None}
+- resp = self.controller.share_soft_delete(req, 1, body)
++ mock_get = self.mock_object(
++ share_api.API, 'get', mock.Mock(return_value=self.share))
++
++ resp = self.controller.share_soft_delete(req, 1, body=body)
++
+ self.assertEqual(202, resp.status_int)
++ mock_get.assert_called_once_with(
++ req.environ['manila.context'], 1, for_modification=True)
+
+ def test_share_soft_delete_has_been_soft_deleted_already(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
+@@ -1661,10 +1667,15 @@ class ShareAPITest(test.TestCase):
+ req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
+ version='2.69')
+ body = {"restore": None}
+- self.mock_object(share_api.API, 'get',
+- mock.Mock(return_value=self.share_in_recycle_bin))
+- resp = self.controller.share_restore(req, 1, body)
++ mock_get = self.mock_object(
++ share_api.API, 'get',
++ mock.Mock(return_value=self.share_in_recycle_bin))
++
++ resp = self.controller.share_restore(req, 1, body=body)
++
+ self.assertEqual(202, resp.status_int)
++ mock_get.assert_called_once_with(
++ req.environ['manila.context'], 1, for_modification=True)
+
+ def test_share_restore_with_deleting_status(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
+@@ -1675,7 +1686,35 @@ class ShareAPITest(test.TestCase):
+ mock.Mock(return_value=self.share_in_recycle_bin_is_deleting))
+ self.assertRaises(
+ webob.exc.HTTPForbidden, self.controller.share_restore,
+- req, 1, body)
++ req, 1, body=body)
++
++ def test_share_soft_delete_policy_not_authorized(self):
++ req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
++ version='2.69')
++ body = {"soft_delete": None}
++ self.mock_object(
++ share_api.API, 'get',
++ mock.Mock(side_effect=exception.PolicyNotAuthorized(
++ action='share:get')))
++
++ self.assertRaises(
++ exception.PolicyNotAuthorized,
++ self.controller.share_soft_delete,
++ req, 1, body=body)
++
++ def test_share_restore_policy_not_authorized(self):
++ req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
++ version='2.69')
++ body = {"restore": None}
++ self.mock_object(
++ share_api.API, 'get',
++ mock.Mock(side_effect=exception.PolicyNotAuthorized(
++ action='share:get')))
++
++ self.assertRaises(
++ exception.PolicyNotAuthorized,
++ self.controller.share_restore,
++ req, 1, body=body)
+
+ def test_share_delete(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/shares/1')
+Index: manila/manila/tests/share/test_api.py
+===================================================================
+--- manila.orig/manila/tests/share/test_api.py
++++ manila/manila/tests/share/test_api.py
+@@ -3098,6 +3098,64 @@ class ShareAPITestCase(test.TestCase):
+ ctx, 'share', 'get', share, do_raise=False)
+ db_api.share_get.assert_called_once_with(ctx, share['id'])
+
++ def test_get_public_share_skips_policy_check(self):
++ fake_share = db_utils.create_share(is_public=True)
++ self.mock_object(db_api, 'share_get',
++ mock.Mock(return_value=fake_share))
++
++ result = self.api.get(self.context, 'fakeid')
++
++ self.assertEqual(fake_share, result)
++ share_api.policy.check_policy.assert_not_called()
++
++ def test_get_public_share_for_modification_checks_policy(self):
++ fake_share = db_utils.create_share(is_public=True)
++ share_api.policy.check_policy.return_value = True
++ self.mock_object(db_api, 'share_get',
++ mock.Mock(return_value=fake_share))
++
++ result = self.api.get(
++ self.context, 'fakeid', for_modification=True)
++
++ self.assertEqual(fake_share, result)
++ share_api.policy.check_policy.assert_called_once_with(
++ self.context, 'share', 'get', fake_share, do_raise=True)
++
++ def test_get_public_share_for_modification_not_authorized(self):
++ fake_share = db_utils.create_share(
++ is_public=True,
++ project_id='df6d65cc1f8946ba86be06b8140ec4b3')
++ self.mock_object(
++ share_api.policy, 'check_policy',
++ mock.Mock(side_effect=exception.PolicyNotAuthorized(
++ action='share:get')))
++ self.mock_object(db_api, 'share_get',
++ mock.Mock(return_value=fake_share))
++ ctx = context.RequestContext('fake_uid', 'fake_pid', is_admin=False)
++
++ self.assertRaises(exception.PolicyNotAuthorized,
++ self.api.get,
++ ctx,
++ fake_share['id'],
++ for_modification=True)
++
++ def test_get_private_share_for_modification_not_authorized(self):
++ fake_share = db_utils.create_share(
++ is_public=False,
++ project_id='df6d65cc1f8946ba86be06b8140ec4b3')
++ share_api.policy.check_policy.return_value = False
++ self.mock_object(db_api, 'share_get',
++ mock.Mock(return_value=fake_share))
++ ctx = context.RequestContext('fake_uid', 'fake_pid', is_admin=False)
++
++ self.assertRaises(exception.NotFound,
++ self.api.get,
++ ctx,
++ fake_share['id'],
++ for_modification=True)
++ share_api.policy.check_policy.assert_called_once_with(
++ ctx, 'share', 'get', fake_share, do_raise=False)
++
+ @mock.patch.object(db_api, 'share_snapshot_get_all_by_project',
+ mock.Mock())
+ def test_get_all_snapshots_admin_not_all_tenants(self):
+Index:
manila/releasenotes/notes/bug-2161412-enforce-policy-checks-on-public-shares-6cb60f669bb20f6c.yaml
+===================================================================
+--- /dev/null
++++
manila/releasenotes/notes/bug-2161412-enforce-policy-checks-on-public-shares-6cb60f669bb20f6c.yaml
+@@ -0,0 +1,11 @@
++---
++security:
++ - |
++ Any user was able to manipulate the access rules, create backups or
++ replicas, soft delete or restore public shares they did not own,
++ and read or manipulate the export location metadata of shares they
++ did not own. These operations now enforce ownership, and export
++ location metadata is scoped to the share in the request URL. For
++ details, see `Launchpad bug #2161412
++ <https://bugs.launchpad.net/manila/+bug/2161412>`_ and `Launchpad
++ bug #2168962 <https://bugs.launchpad.net/manila/+bug/2168962>`_.
diff -Nru manila-20.0.0/debian/patches/series
manila-20.0.0/debian/patches/series
--- manila-20.0.0/debian/patches/series 2026-08-20 17:26:40.000000000 +0200
+++ manila-20.0.0/debian/patches/series 2026-10-15 23:15:31.000000000 +0200
@@ -1,2 +1,3 @@
install-missing-files.patch
OSSN-0103_Prevent_unauthorized_resource_locks_search.patch
+CVE-2026-71195_Fix_policy_control_for_public_shares.patch