diff --git a/spp_storage_backend/README.rst b/spp_storage_backend/README.rst index 9307d00a2..2a118ba44 100644 --- a/spp_storage_backend/README.rst +++ b/spp_storage_backend/README.rst @@ -115,6 +115,18 @@ Dependencies Changelog ========= +19.0.2.0.1 +~~~~~~~~~~ + +- fix(security): restrict storage backend credentials to storage + administrators. The S3 access/secret keys and Azure connection string + were readable by any internal user via RPC (``base.group_user`` had + model read and the fields had no field-level protection). The three + credential fields are now gated with + ``groups="spp_storage_backend.group_storage_admin"``, and the S3/Azure + client builders resolve them via ``sudo()`` so a non-admin can still + operate a backend without being able to read the secret. + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_storage_backend/__manifest__.py b/spp_storage_backend/__manifest__.py index 2c76ef29d..c0b12340d 100644 --- a/spp_storage_backend/__manifest__.py +++ b/spp_storage_backend/__manifest__.py @@ -4,7 +4,7 @@ "name": "OpenSPP Storage Backend", "summary": "Pluggable storage backend configuration for OpenSPP file storage", "category": "OpenSPP/Core", - "version": "19.0.2.0.0", + "version": "19.0.2.0.1", "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", "license": "LGPL-3", diff --git a/spp_storage_backend/models/storage_backend.py b/spp_storage_backend/models/storage_backend.py index 2404f5914..9aabeebcc 100644 --- a/spp_storage_backend/models/storage_backend.py +++ b/spp_storage_backend/models/storage_backend.py @@ -58,10 +58,12 @@ class StorageBackend(models.Model): ) s3_access_key = fields.Char( string="Access Key", + groups="spp_storage_backend.group_storage_admin", help="AWS Access Key ID or S3-compatible access key", ) s3_secret_key = fields.Char( string="Secret Key", + groups="spp_storage_backend.group_storage_admin", help="AWS Secret Access Key or S3-compatible secret key", ) s3_region = fields.Char( @@ -78,6 +80,7 @@ class StorageBackend(models.Model): # Azure Configuration azure_connection_string = fields.Char( string="Connection String", + groups="spp_storage_backend.group_storage_admin", help="Azure Storage connection string", ) azure_container = fields.Char( @@ -363,9 +366,14 @@ def _get_s3_client(self): except ImportError as e: raise UserError(_("boto3 library is not installed. Please install it to use S3 storage.")) from e + # Credentials are group-restricted (admin-only readable); resolve them + # server-side so an operating user can use the backend without being + # able to read the secret. The value is only used to build the client. + # nosemgrep: odoo-sudo-without-context - read admin-only credential fields to build the client + creds = self.sudo() config = { - "aws_access_key_id": self.s3_access_key, - "aws_secret_access_key": self.s3_secret_key, + "aws_access_key_id": creds.s3_access_key, + "aws_secret_access_key": creds.s3_secret_key, "region_name": self.s3_region, } @@ -443,6 +451,16 @@ def _test_s3_connection(self): # AZURE BACKEND IMPLEMENTATION # ======================================================================== + def _get_azure_connection_string(self): + """Resolve the group-restricted Azure connection string server-side. + + The field is admin-only readable; an operating user may use the backend + without being able to read the secret, so it is read via ``sudo()`` and + only used to build clients / SAS URLs (never returned to the caller). + """ + # nosemgrep: odoo-sudo-without-context - read admin-only credential field to build the client + return self.sudo().azure_connection_string + def _get_azure_client(self): """Get configured Azure Blob client.""" try: @@ -452,7 +470,7 @@ def _get_azure_client(self): _("azure-storage-blob library is not installed. Please install it to use Azure storage.") ) from e - service_client = BlobServiceClient.from_connection_string(self.azure_connection_string) + service_client = BlobServiceClient.from_connection_string(self._get_azure_connection_string()) return service_client.get_container_client(self.azure_container) def _store_azure(self, binary_data, path): @@ -497,8 +515,10 @@ def _get_azure_public_url(self, reference, expires_in): container_client = self._get_azure_client() blob_client = container_client.get_blob_client(reference) - # Extract account name and key from connection string - conn_parts = dict(item.split("=", 1) for item in self.azure_connection_string.split(";") if "=" in item) + # Extract account name and key from connection string (admin-only field + # resolved via sudo; used only to sign the SAS URL, not returned). + connection_string = self._get_azure_connection_string() + conn_parts = dict(item.split("=", 1) for item in connection_string.split(";") if "=" in item) account_name = conn_parts.get("AccountName") account_key = conn_parts.get("AccountKey") diff --git a/spp_storage_backend/readme/HISTORY.md b/spp_storage_backend/readme/HISTORY.md index 4aaf9afef..efd87404b 100644 --- a/spp_storage_backend/readme/HISTORY.md +++ b/spp_storage_backend/readme/HISTORY.md @@ -1,3 +1,13 @@ +### 19.0.2.0.1 + +- fix(security): restrict storage backend credentials to storage administrators. + The S3 access/secret keys and Azure connection string were readable by any + internal user via RPC (`base.group_user` had model read and the fields had no + field-level protection). The three credential fields are now gated with + `groups="spp_storage_backend.group_storage_admin"`, and the S3/Azure client + builders resolve them via `sudo()` so a non-admin can still operate a backend + without being able to read the secret. + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_storage_backend/static/description/index.html b/spp_storage_backend/static/description/index.html index 34a3c642c..4d09de8fb 100644 --- a/spp_storage_backend/static/description/index.html +++ b/spp_storage_backend/static/description/index.html @@ -487,6 +487,19 @@

Changelog

+

19.0.2.0.1

+ +
+

19.0.2.0.0