Version in base suite: 20.0.0-3 Version in overlay suite: 20.0.0-3+deb13u1 Base version: manila_20.0.0-3+deb13u1 Target version: manila_20.0.0-3+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/m/manila/manila_20.0.0-3+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/m/manila/manila_20.0.0-3+deb13u2.dsc changelog | 15 manila-common.sudoers | 1 patches/CVE-2026-71195_Fix_policy_control_for_public_shares.patch | 720 ++++++++++ patches/series | 1 4 files changed, 737 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpw8i97c8f/manila_20.0.0-3+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpw8i97c8f/manila_20.0.0-3+deb13u2.dsc: no acceptable signature found diff -Nru manila-20.0.0/debian/changelog manila-20.0.0/debian/changelog --- manila-20.0.0/debian/changelog 2026-08-20 15:26:40.000000000 +0000 +++ manila-20.0.0/debian/changelog 2026-10-15 21:15:31.000000000 +0000 @@ -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 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 15:26:40.000000000 +0000 +++ manila-20.0.0/debian/manila-common.sudoers 2026-10-15 21:15:31.000000000 +0000 @@ -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 00:00:00.000000000 +0000 +++ manila-20.0.0/debian/patches/CVE-2026-71195_Fix_policy_control_for_public_shares.patch 2026-10-15 21:15:31.000000000 +0000 @@ -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 +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 ++ `_ and `Launchpad ++ 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 15:26:40.000000000 +0000 +++ manila-20.0.0/debian/patches/series 2026-10-15 21:15:31.000000000 +0000 @@ -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