From 061ca2a8f1596c0cca3af6507b5dba92f17e66b7 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 27 Apr 2026 02:08:56 +0000 Subject: [PATCH] [ADD] attachment_mimetype_restriction --- attachment_mimetype_restriction/README.rst | 121 +++++ attachment_mimetype_restriction/__init__.py | 2 + .../__manifest__.py | 18 + .../controllers/__init__.py | 1 + .../controllers/main.py | 28 ++ .../models/__init__.py | 5 + .../models/ir_attachment.py | 79 +++ .../models/ir_model.py | 16 + .../models/mail_thread.py | 120 +++++ .../models/res_company.py | 10 + .../models/res_config_settings.py | 14 + .../readme/CONFIGURE.rst | 17 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 9 + .../static/description/index.html | 459 ++++++++++++++++++ .../tests/__init__.py | 1 + .../test_attachment_mimetype_restriction.py | 85 ++++ .../views/ir_model_views.xml | 22 + .../views/res_config_settings_views.xml | 36 ++ .../addons/attachment_mimetype_restriction | 1 + .../attachment_mimetype_restriction/setup.py | 6 + 21 files changed, 1052 insertions(+) create mode 100644 attachment_mimetype_restriction/README.rst create mode 100644 attachment_mimetype_restriction/__init__.py create mode 100644 attachment_mimetype_restriction/__manifest__.py create mode 100644 attachment_mimetype_restriction/controllers/__init__.py create mode 100644 attachment_mimetype_restriction/controllers/main.py create mode 100644 attachment_mimetype_restriction/models/__init__.py create mode 100644 attachment_mimetype_restriction/models/ir_attachment.py create mode 100644 attachment_mimetype_restriction/models/ir_model.py create mode 100644 attachment_mimetype_restriction/models/mail_thread.py create mode 100644 attachment_mimetype_restriction/models/res_company.py create mode 100644 attachment_mimetype_restriction/models/res_config_settings.py create mode 100644 attachment_mimetype_restriction/readme/CONFIGURE.rst create mode 100644 attachment_mimetype_restriction/readme/CONTRIBUTORS.rst create mode 100644 attachment_mimetype_restriction/readme/DESCRIPTION.rst create mode 100644 attachment_mimetype_restriction/static/description/index.html create mode 100644 attachment_mimetype_restriction/tests/__init__.py create mode 100644 attachment_mimetype_restriction/tests/test_attachment_mimetype_restriction.py create mode 100644 attachment_mimetype_restriction/views/ir_model_views.xml create mode 100644 attachment_mimetype_restriction/views/res_config_settings_views.xml create mode 120000 setup/attachment_mimetype_restriction/odoo/addons/attachment_mimetype_restriction create mode 100644 setup/attachment_mimetype_restriction/setup.py diff --git a/attachment_mimetype_restriction/README.rst b/attachment_mimetype_restriction/README.rst new file mode 100644 index 0000000000..d69afcb710 --- /dev/null +++ b/attachment_mimetype_restriction/README.rst @@ -0,0 +1,121 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================================ +Attachment MIME Type Restriction +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:97078c2e3e231052fbfc80c9c3181fd9223d0fa5d7e0c7b6b8b1b22ef6932c25 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/15.0/attachment_mimetype_restriction + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-15-0/social-15-0-attachment_mimetype_restriction + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module restricts attachment uploads to an explicit allowlist of MIME types +using content-based detection rather than filename extensions. Only configured +MIME types are accepted; everything else is rejected. Leaving the allowlist +empty disables the restriction and allows all file types. + +For incoming emails, the email itself is always accepted, but any attachments +whose MIME type is not in the allowlist are stripped out before the message is +saved. A security notice is then posted on the related record listing the +removed files, so users can see what was filtered. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +**Global Configuration (Company-wide):** + +#. Go to Settings → General Settings +#. In the "Allowed Attachment Types" field, enter comma-separated MIME types +#. Example: ``image/png,application/pdf`` +#. Leave empty to allow all file types + +**Per-Model Configuration (Optional):** + +#. Go to Settings → Technical → Database Structure → Models +#. Select a model (e.g., "Contact" for res.partner) +#. In the "Allowed Attachment Types" field, enter comma-separated MIME types +#. Empty value = use global config; set value = override global config + +**Configuration Hierarchy:** + +Per-model settings override global settings when defined. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Quartile + +Contributors +~~~~~~~~~~~~ + +- Quartile \<\> + - Aung Ko Ko Lin + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-yostashiro| image:: https://github.com/yostashiro.png?size=40px + :target: https://github.com/yostashiro + :alt: yostashiro +.. |maintainer-aungkokolin1997| image:: https://github.com/aungkokolin1997.png?size=40px + :target: https://github.com/aungkokolin1997 + :alt: aungkokolin1997 + +Current `maintainers `__: + +|maintainer-yostashiro| |maintainer-aungkokolin1997| + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/attachment_mimetype_restriction/__init__.py b/attachment_mimetype_restriction/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/attachment_mimetype_restriction/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/attachment_mimetype_restriction/__manifest__.py b/attachment_mimetype_restriction/__manifest__.py new file mode 100644 index 0000000000..a5da356872 --- /dev/null +++ b/attachment_mimetype_restriction/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Attachment MIME Type Restriction", + "summary": "Restrict attachment uploads to an allowlist of MIME types", + "version": "15.0.1.0.0", + "category": "Social", + "website": "https://github.com/OCA/social", + "author": "Quartile, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["base", "mail"], + "data": [ + "views/ir_model_views.xml", + "views/res_config_settings_views.xml", + ], + "maintainers": ["yostashiro", "aungkokolin1997"], +} diff --git a/attachment_mimetype_restriction/controllers/__init__.py b/attachment_mimetype_restriction/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/attachment_mimetype_restriction/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/attachment_mimetype_restriction/controllers/main.py b/attachment_mimetype_restriction/controllers/main.py new file mode 100644 index 0000000000..267298cf14 --- /dev/null +++ b/attachment_mimetype_restriction/controllers/main.py @@ -0,0 +1,28 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import json + +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request + +from odoo.addons.mail.controllers.discuss import DiscussController + + +class DiscussControllerExtended(DiscussController): + @http.route("/mail/attachment/upload", methods=["POST"], type="http", auth="public") + def mail_attachment_upload( + self, ufile, thread_id, thread_model, is_pending=False, **kwargs + ): + try: + return super().mail_attachment_upload( + ufile, thread_id, thread_model, is_pending, **kwargs + ) + except ValidationError as e: + error_msg = str(e.args[0]) if e.args else str(e) + attachmentData = {"error": error_msg} + return request.make_response( + data=json.dumps(attachmentData), + headers=[("Content-Type", "application/json")], + ) diff --git a/attachment_mimetype_restriction/models/__init__.py b/attachment_mimetype_restriction/models/__init__.py new file mode 100644 index 0000000000..ea4a0bc5ed --- /dev/null +++ b/attachment_mimetype_restriction/models/__init__.py @@ -0,0 +1,5 @@ +from . import ir_attachment +from . import ir_model +from . import mail_thread +from . import res_company +from . import res_config_settings diff --git a/attachment_mimetype_restriction/models/ir_attachment.py b/attachment_mimetype_restriction/models/ir_attachment.py new file mode 100644 index 0000000000..f55a487d99 --- /dev/null +++ b/attachment_mimetype_restriction/models/ir_attachment.py @@ -0,0 +1,79 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, models +from odoo.exceptions import ValidationError + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + @api.model + def _get_allowed_mimetypes(self, company_id, res_model=None): + if res_model: + model = self.env["ir.model"].search([("model", "=", res_model)], limit=1) + if model and model.attachment_allowed_mimetypes: + return [ + mt.strip().lower() + for mt in model.attachment_allowed_mimetypes.split(",") + if mt.strip() + ] + company = self.env["res.company"].browse(company_id) + global_mimetypes = company.attachment_allowed_mimetypes + if not global_mimetypes: + return [] + return [mt.strip().lower() for mt in global_mimetypes.split(",") if mt.strip()] + + @api.model + def _resolve_attachment_company_id(self, vals): + company_id = vals.get("company_id") + if company_id: + return company_id + res_model = vals.get("res_model") + res_id = vals.get("res_id") + if res_model and res_id and res_model in self.env: + record = self.env[res_model].browse(res_id).exists() + if record and "company_id" in record._fields and record.company_id: + return record.company_id.id + return self.env.company.id + + def _validate_mimetype_from_vals(self, vals): + if self.env.context.get("install_mode"): + return + # Skip runtime asset bundles (compiled JS/CSS). Their /web/assets/ url + # is only set in a follow-up write() after create(), so detect them by + # the create-time signature: res_model='ir.ui.view' + public=True. + if vals.get("res_model") == "ir.ui.view" and vals.get("public"): + return + mimetype = self._compute_mimetype(vals) + res_model = vals.get("res_model") + company_id = self._resolve_attachment_company_id(vals) + allowed_mimetypes = self._get_allowed_mimetypes(company_id, res_model) + if not allowed_mimetypes: + return + if mimetype.lower() not in allowed_mimetypes: + raise ValidationError(_("File type '%s' is not allowed.") % mimetype) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + self._validate_mimetype_from_vals(vals) + return super().create(vals_list) + + def write(self, vals): + fields_to_check = ["datas", "raw", "mimetype", "res_model", "company_id"] + if any(key in vals for key in fields_to_check): + for record in self: + check_vals = { + "datas": vals.get("datas"), + "raw": vals.get("raw"), + "mimetype": vals.get("mimetype", record.mimetype), + "res_model": vals.get("res_model", record.res_model), + "res_id": vals.get("res_id", record.res_id), + "company_id": vals.get( + "company_id", + record.company_id.id if record.company_id else False, + ), + } + self._validate_mimetype_from_vals(check_vals) + return super().write(vals) diff --git a/attachment_mimetype_restriction/models/ir_model.py b/attachment_mimetype_restriction/models/ir_model.py new file mode 100644 index 0000000000..64e71e920e --- /dev/null +++ b/attachment_mimetype_restriction/models/ir_model.py @@ -0,0 +1,16 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class IrModel(models.Model): + _inherit = "ir.model" + + attachment_allowed_mimetypes = fields.Char( + string="Allowed Attachment Types", + help="Comma-separated list of allowed MIME types for attachments on this " + "model. Leave empty to use company's global configuration. " + "Example: image/png,application/pdf. " + "This configuration applies globally to all companies.", + ) diff --git a/attachment_mimetype_restriction/models/mail_thread.py b/attachment_mimetype_restriction/models/mail_thread.py new file mode 100644 index 0000000000..678d8c340b --- /dev/null +++ b/attachment_mimetype_restriction/models/mail_thread.py @@ -0,0 +1,120 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import base64 +import logging + +from markupsafe import escape + +from odoo import _, models + +_logger = logging.getLogger(__name__) + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + def _evaluate_attachment_against_allowlist( + self, name, content, info, model, res_id, company_id + ): + try: + if isinstance(content, str): + encoding = info and info.get("encoding") + try: + content_bytes = content.encode(encoding or "utf-8") + except UnicodeEncodeError: + _logger.debug( + "Encoding '%s' failed for attachment '%s'; " + "retrying as utf-8", + encoding, + name, + ) + content_bytes = content.encode("utf-8") + else: + content_bytes = content + temp_vals = { + "name": name, + "datas": base64.b64encode(content_bytes), + "res_model": model, + "res_id": res_id, + "company_id": company_id, + } + mimetype = self.env["ir.attachment"]._compute_mimetype(temp_vals) + allowed_mimetypes = self.env["ir.attachment"]._get_allowed_mimetypes( + company_id, model + ) + except Exception as e: + _logger.warning( + "Pre-validation failed for attachment '%s' (%s); blocking by default", + name, + e, + ) + return {"name": name, "mimetype": _("could not be analyzed")} + if mimetype and allowed_mimetypes and mimetype.lower() not in allowed_mimetypes: + return {"name": name, "mimetype": mimetype} + return None + + def _message_post_process_attachments( + self, attachments, attachment_ids, message_values + ): + model = message_values.get("model") + res_id = message_values.get("res_id") + target_record = None + if model and res_id and model in self.env: + target_record = self.env[model].browse(res_id).exists() or None + if ( + target_record + and "company_id" in target_record._fields + and target_record.company_id + ): + company_id = target_record.company_id.id + else: + company_id = self.env.company.id + blocked_attachments_info = [] + if attachments: + filtered_attachments = [] + for attachment in attachments: + if len(attachment) == 2: + name, content = attachment + info = {} + elif len(attachment) == 3: + name, content, info = attachment + else: + continue + blocked_info = self._evaluate_attachment_against_allowlist( + name, content, info, model, res_id, company_id + ) + if blocked_info: + blocked_attachments_info.append(blocked_info) + continue + filtered_attachments.append(attachment) + attachments = filtered_attachments + result = super()._message_post_process_attachments( + attachments, attachment_ids, message_values + ) + if blocked_attachments_info and target_record: + blocked_list = [] + for blocked in blocked_attachments_info: + blocked_list.append( + f"• {escape(blocked['name'])} " + f"({escape(blocked['mimetype'])})" + ) + notification_body = ( + '
' + "

⚠️ Security Notice: Blocked Attachments

" + "

The following attachment(s) from the email above were " + "blocked:

" + "

" + "
".join(blocked_list) + "

" + "

These file types are not allowed by your organization's " + "security policy.

" + "
" + ) + try: + target_record.sudo().message_post( + body=notification_body, + message_type="notification", + subtype_xmlid="mail.mt_note", + ) + except Exception as e: + _logger.warning("Could not post blocked attachment notification: %s", e) + return result diff --git a/attachment_mimetype_restriction/models/res_company.py b/attachment_mimetype_restriction/models/res_company.py new file mode 100644 index 0000000000..92363edb76 --- /dev/null +++ b/attachment_mimetype_restriction/models/res_company.py @@ -0,0 +1,10 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + attachment_allowed_mimetypes = fields.Char(string="Allowed Attachment Types") diff --git a/attachment_mimetype_restriction/models/res_config_settings.py b/attachment_mimetype_restriction/models/res_config_settings.py new file mode 100644 index 0000000000..ec266af1ea --- /dev/null +++ b/attachment_mimetype_restriction/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + attachment_allowed_mimetypes = fields.Char( + related="company_id.attachment_allowed_mimetypes", + string="Allowed Attachment Types", + readonly=False, + ) diff --git a/attachment_mimetype_restriction/readme/CONFIGURE.rst b/attachment_mimetype_restriction/readme/CONFIGURE.rst new file mode 100644 index 0000000000..0b4aa63f76 --- /dev/null +++ b/attachment_mimetype_restriction/readme/CONFIGURE.rst @@ -0,0 +1,17 @@ +**Global Configuration (Company-wide):** + +#. Go to Settings → General Settings +#. In the "Allowed Attachment Types" field, enter comma-separated MIME types +#. Example: ``image/png,application/pdf`` +#. Leave empty to allow all file types + +**Per-Model Configuration (Optional):** + +#. Go to Settings → Technical → Database Structure → Models +#. Select a model (e.g., "Contact" for res.partner) +#. In the "Allowed Attachment Types" field, enter comma-separated MIME types +#. Empty value = use global config; set value = override global config + +**Configuration Hierarchy:** + +Per-model settings override global settings when defined. diff --git a/attachment_mimetype_restriction/readme/CONTRIBUTORS.rst b/attachment_mimetype_restriction/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..a8a2dfb2ee --- /dev/null +++ b/attachment_mimetype_restriction/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +- Quartile \<\> + - Aung Ko Ko Lin diff --git a/attachment_mimetype_restriction/readme/DESCRIPTION.rst b/attachment_mimetype_restriction/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..ff02f78f72 --- /dev/null +++ b/attachment_mimetype_restriction/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +This module restricts attachment uploads to an explicit allowlist of MIME types +using content-based detection rather than filename extensions. Only configured +MIME types are accepted; everything else is rejected. Leaving the allowlist +empty disables the restriction and allows all file types. + +For incoming emails, the email itself is always accepted, but any attachments +whose MIME type is not in the allowlist are stripped out before the message is +saved. A security notice is then posted on the related record listing the +removed files, so users can see what was filtered. diff --git a/attachment_mimetype_restriction/static/description/index.html b/attachment_mimetype_restriction/static/description/index.html new file mode 100644 index 0000000000..6212cb9f37 --- /dev/null +++ b/attachment_mimetype_restriction/static/description/index.html @@ -0,0 +1,459 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Attachment MIME Type Restriction

+ +

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runboat

+

This module restricts attachment uploads to an explicit allowlist of MIME types +using content-based detection rather than filename extensions. Only configured +MIME types are accepted; everything else is rejected. Leaving the allowlist +empty disables the restriction and allows all file types.

+

For incoming emails, the email itself is always accepted, but any attachments +whose MIME type is not in the allowlist are stripped out before the message is +saved. A security notice is then posted on the related record listing the +removed files, so users can see what was filtered.

+

Table of contents

+ +
+

Configuration

+

Global Configuration (Company-wide):

+
    +
  1. Go to Settings → General Settings
  2. +
  3. In the “Allowed Attachment Types” field, enter comma-separated MIME types
  4. +
  5. Example: image/png,application/pdf
  6. +
  7. Leave empty to allow all file types
  8. +
+

Per-Model Configuration (Optional):

+
    +
  1. Go to Settings → Technical → Database Structure → Models
  2. +
  3. Select a model (e.g., “Contact” for res.partner)
  4. +
  5. In the “Allowed Attachment Types” field, enter comma-separated MIME types
  6. +
  7. Empty value = use global config; set value = override global config
  8. +
+

Configuration Hierarchy:

+

Per-model settings override global settings when defined.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

yostashiro aungkokolin1997

+

This module is part of the OCA/social project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/attachment_mimetype_restriction/tests/__init__.py b/attachment_mimetype_restriction/tests/__init__.py new file mode 100644 index 0000000000..661037d221 --- /dev/null +++ b/attachment_mimetype_restriction/tests/__init__.py @@ -0,0 +1 @@ +from . import test_attachment_mimetype_restriction diff --git a/attachment_mimetype_restriction/tests/test_attachment_mimetype_restriction.py b/attachment_mimetype_restriction/tests/test_attachment_mimetype_restriction.py new file mode 100644 index 0000000000..ccd444c5e4 --- /dev/null +++ b/attachment_mimetype_restriction/tests/test_attachment_mimetype_restriction.py @@ -0,0 +1,85 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import base64 + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestAttachmentMimetypeRestriction(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Attachment = cls.env["ir.attachment"] + cls.IrModel = cls.env["ir.model"] + cls.company = cls.env.company + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.partner_model = cls.IrModel.search([("model", "=", "res.partner")]) + + def _create_text_attachment(self, **overrides): + vals = { + "name": "test_file.txt", + "datas": base64.b64encode(b"test data"), + } + vals.update(overrides) + return self.Attachment.create(vals) + + def test_non_allowed_mimetype_blocked(self): + self.company.attachment_allowed_mimetypes = "image/png" + with self.assertRaises(ValidationError) as cm: + self._create_text_attachment() + self.assertIn("text/plain", str(cm.exception)) + + def test_allowed_mimetype_create(self): + self.company.attachment_allowed_mimetypes = "text/plain,application/pdf" + attachment = self._create_text_attachment() + self.assertEqual(attachment.mimetype, "text/plain") + + def test_empty_config_allows_all(self): + self.company.attachment_allowed_mimetypes = "" + self.assertTrue(self._create_text_attachment()) + + def test_per_model_overrides_global(self): + self.company.attachment_allowed_mimetypes = "image/png" + self.partner_model.attachment_allowed_mimetypes = "text/plain" + attachment = self._create_text_attachment( + res_model="res.partner", res_id=self.partner.id + ) + self.assertTrue(attachment) + + def test_per_model_empty_falls_through_to_global(self): + self.company.attachment_allowed_mimetypes = "image/png" + self.partner_model.attachment_allowed_mimetypes = "" + with self.assertRaises(ValidationError): + self._create_text_attachment( + res_model="res.partner", res_id=self.partner.id + ) + + def test_write_revalidates_on_datas_change(self): + self.company.attachment_allowed_mimetypes = "text/plain" + attachment = self._create_text_attachment() + self.company.attachment_allowed_mimetypes = "image/png" + with self.assertRaises(ValidationError): + attachment.write({"datas": base64.b64encode(b"updated content")}) + + def test_message_post_filters_blocked_attachments(self): + self.company.attachment_allowed_mimetypes = "text/html" + self.partner.message_post( + body="

Email

", + attachments=[ + ("blocked.txt", b"blocked content"), + ("allowed.html", b"allowed"), + ], + ) + attachments = self.Attachment.search( + [("res_model", "=", "res.partner"), ("res_id", "=", self.partner.id)] + ) + self.assertEqual(attachments.mapped("name"), ["allowed.html"]) + notice = ( + self.env["mail.message"] + .search([("res_id", "=", self.partner.id), ("model", "=", "res.partner")]) + .filtered(lambda m: "Security Notice" in m.body) + ) + self.assertEqual(len(notice), 1) + self.assertIn("blocked.txt", notice.body) diff --git a/attachment_mimetype_restriction/views/ir_model_views.xml b/attachment_mimetype_restriction/views/ir_model_views.xml new file mode 100644 index 0000000000..0ca0559c70 --- /dev/null +++ b/attachment_mimetype_restriction/views/ir_model_views.xml @@ -0,0 +1,22 @@ + + + + ir.model.form.inherit.attachment.mimetype + ir.model + + + + + + + + + + diff --git a/attachment_mimetype_restriction/views/res_config_settings_views.xml b/attachment_mimetype_restriction/views/res_config_settings_views.xml new file mode 100644 index 0000000000..d3f334629d --- /dev/null +++ b/attachment_mimetype_restriction/views/res_config_settings_views.xml @@ -0,0 +1,36 @@ + + + + res.config.settings.view.form.inherit.attachment_mimetype + res.config.settings + + + +
+
+
+
+
+ + + + diff --git a/setup/attachment_mimetype_restriction/odoo/addons/attachment_mimetype_restriction b/setup/attachment_mimetype_restriction/odoo/addons/attachment_mimetype_restriction new file mode 120000 index 0000000000..d4b5595247 --- /dev/null +++ b/setup/attachment_mimetype_restriction/odoo/addons/attachment_mimetype_restriction @@ -0,0 +1 @@ +../../../../attachment_mimetype_restriction \ No newline at end of file diff --git a/setup/attachment_mimetype_restriction/setup.py b/setup/attachment_mimetype_restriction/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/attachment_mimetype_restriction/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)