diff --git a/addons/website/models/mixins.py b/addons/website/models/mixins.py
index 92f2f45fc2d2cd3d67bba3588a139fbb6278ceab..1d46b55c3bd66df849d65ab093a8bb31bc00586e 100644
--- a/addons/website/models/mixins.py
+++ b/addons/website/models/mixins.py
@@ -4,9 +4,10 @@
 import logging
 
 
-from odoo import api, fields, models
+from odoo import api, fields, models, _
 from odoo.http import request
 from odoo.osv import expression
+from odoo.exceptions import AccessError
 
 logger = logging.getLogger(__name__)
 
@@ -120,6 +121,7 @@ class WebsitePublishedMixin(models.AbstractModel):
 
     website_published = fields.Boolean('Visible on current website', related='is_published', readonly=False)
     is_published = fields.Boolean('Is published', copy=False)
+    can_publish = fields.Boolean('Can publish', compute='_compute_can_publish')
     website_url = fields.Char('Website URL', compute='_compute_website_url', help='The full URL to access the document through the website.')
 
     @api.multi
@@ -144,9 +146,40 @@ class WebsitePublishedMixin(models.AbstractModel):
             'target': 'self',
         }
 
+    @api.model_create_multi
+    def create(self, vals_list):
+        records = super(WebsitePublishedMixin, self).create(vals_list)
+
+        is_publish_modified = any('website_published' in values for values in vals_list)
+        if is_publish_modified and not all(record.can_publish for record in records):
+            raise AccessError(self._get_can_publish_error_message())
+
+        return records
+
+    @api.multi
+    def write(self, values):
+        if 'website_published' in values and not all(record.can_publish for record in self):
+            raise AccessError(self._get_can_publish_error_message())
+
+        return super(WebsitePublishedMixin, self).write(values)
+
     def create_and_get_website_url(self, **kwargs):
         return self.create(kwargs).website_url
 
+    @api.multi
+    def _compute_can_publish(self):
+        """ This method can be overridden if you need more complex rights management than just 'website_publisher'
+        The publish widget will be hidden and the user won't be able to change the 'website_published' value
+        if this method sets can_publish False """
+        for record in self:
+            record.can_publish = True
+
+    @api.model
+    def _get_can_publish_error_message(self):
+        """ Override this method to customize the error message shown when the user doesn't
+        have the rights to publish/unpublish. """
+        return _("You do not have the rights to publish/unpublish")
+
 
 class WebsitePublishedMultiMixin(WebsitePublishedMixin):
 
diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml
index 1c65928f56bed149c646b9d7c03574b15f0104f9..1e2c6fa7dfbc5f3ee7b75412241ff9349133646a 100644
--- a/addons/website/views/website_navbar_templates.xml
+++ b/addons/website/views/website_navbar_templates.xml
@@ -64,7 +64,7 @@
                 </ul>
 
                 <ul class="o_menu_systray d-none d-md-block" groups="website.group_website_publisher">
-                    <li t-if="'website_published' in main_object.fields_get()" t-attf-class="js_publish_management #{main_object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="main_object.id" t-att-data-object="main_object._name" t-att-data-controller="publish_controller">
+                    <li t-if="'website_published' in main_object.fields_get() and ('can_publish' not in main_object.fields_get() or main_object.can_publish)" t-attf-class="js_publish_management #{main_object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="main_object.id" t-att-data-object="main_object._name" t-att-data-controller="publish_controller">
                         <label class="o_switch o_switch_danger js_publish_btn" for="id">
                             <input type="checkbox" t-att-checked="main_object.website_published" id="id"/>
                             <span/>