diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index df30d51c9c2..d8978e5bab1 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -325,6 +325,9 @@ class Lead(models.Model): # Set date_open to today if it is an opp default = default or {} default['date_open'] = fields.Datetime.now() if self.type == 'opportunity' else False + # Do not assign to an archived user + if not self.user_id.active: + default['user_id'] = False return super(Lead, self.with_context(context)).copy(default=default) @api.model diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 8afab6fba39..c422e613d8d 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -514,6 +514,8 @@ class HrExpenseSheet(models.Model): @api.multi def refuse_expenses(self, reason): + if not self.user_has_groups('hr_expense.group_hr_expense_user'): + raise UserError(_("Only HR Officers can refuse expenses")) self.write({'state': 'cancel'}) for sheet in self: body = (_("Your Expense %s has been refused.
") % (sheet.name, reason)) @@ -521,6 +523,8 @@ class HrExpenseSheet(models.Model): @api.multi def approve_expense_sheets(self): + if not self.user_has_groups('hr_expense.group_hr_expense_user'): + raise UserError(_("Only HR Officers can approve expenses")) self.write({'state': 'approve', 'responsible_id': self.env.user.id}) @api.multi diff --git a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py index 49adf3fed7f..d0c980f9f5f 100644 --- a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py @@ -165,7 +165,7 @@ class hr_timesheet_sheet_sheet_day(models.Model): ON r.user_id = u.id LEFT JOIN res_partner p ON u.partner_id = p.id - WHERE check_out IS NOT NULL + WHERE a.check_out IS NOT NULL group by (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id, timezone )) AS foo GROUP BY name, sheet_id, timezone diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py index 517ded468cd..62a5225cfe5 100644 --- a/addons/mail/controllers/bus.py +++ b/addons/mail/controllers/bus.py @@ -36,18 +36,16 @@ class MailChatController(BusController): # -------------------------- @route('/mail/chat_post', type="json", auth="none") def mail_chat_post(self, uuid, message_content, **kwargs): - request_uid = self._default_request_uid() # find the author from the user session, which can be None author_id = False # message_post accept 'False' author_id, but not 'None' if request.session.uid: author_id = request.env['res.users'].sudo().browse(request.session.uid).partner_id.id # post a message without adding followers to the channel. email_from=False avoid to get author from email data - mail_channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1) - message = mail_channel.sudo(request_uid).with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', content_subtype='plaintext', **kwargs) + mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1) + message = mail_channel.sudo().with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', content_subtype='plaintext') return message and message.id or False @route(['/mail/chat_history'], type="json", auth="none") def mail_chat_history(self, uuid, last_id=False, limit=20): - request_uid = self._default_request_uid() - channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1) - return channel.sudo(request_uid).channel_fetch_message(last_id, limit) + channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1) + return channel.sudo().channel_fetch_message(last_id, limit) diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 581267d4435..b655ccf2fbb 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -3,7 +3,7 @@ from datetime import date, datetime, timedelta -from odoo import api, fields, models +from odoo import api, exceptions, fields, models, _ class MailActivityType(models.Model): @@ -128,20 +128,65 @@ class MailActivity(models.Model): def _onchange_recommended_activity_type_id(self): self.activity_type_id = self.recommended_activity_type_id + @api.multi + def _check_access(self, operation): + """ Rule to access activities + + * create: check write rights on related document; + * write: rule OR write rights on document; + * unlink: rule OR write rights on document; + """ + self.check_access_rights(operation, raise_exception=True) # will raise an AccessError + + if operation in ('write', 'unlink'): + try: + self.check_access_rule(operation) + except exceptions.AccessError: + pass + else: + return + + doc_operation = 'read' if operation == 'read' else 'write' + activity_to_documents = dict() + for activity in self.sudo(): + activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id) + for model, res_ids in activity_to_documents.items(): + self.env[model].check_access_rights(doc_operation, raise_exception=True) + try: + self.env[model].browse(res_ids).check_access_rule(doc_operation) + except exceptions.AccessError: + raise exceptions.AccessError( + _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % + (self._description, operation)) + @api.model def create(self, values): - activity = super(MailActivity, self).create(values) - self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) - return activity + # already compute default values to be sure those are computed using the current user + values_w_defaults = self.default_get(self._fields.keys()) + values_w_defaults.update(values) + + # continue as sudo because activities are somewhat protected + activity = super(MailActivity, self.sudo()).create(values_w_defaults) + activity_user = activity.sudo(self.env.user) + activity_user._check_access('create') + self.env[activity_user.res_model].browse(activity_user.res_id).message_subscribe(partner_ids=[activity_user.user_id.partner_id.id]) + return activity_user @api.multi def write(self, values): - res = super(MailActivity, self).write(values) + self._check_access('write') + res = super(MailActivity, self.sudo()).write(values) + if values.get('user_id'): for activity in self: self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) return res + @api.multi + def unlink(self): + self._check_access('unlink') + return super(MailActivity, self.sudo()).unlink() + @api.multi def action_done(self): """ Wrapper without feedback because web button add context as diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 5bc4678848d..3194eb15033 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -98,6 +98,7 @@ class Message(models.Model): tracking_value_ids = fields.One2many( 'mail.tracking.value', 'mail_message_id', string='Tracking values', + groups="base.group_no_one", help='Tracked values are stored in a separate model. This field allow to reconstruct ' 'the tracking and to generate statistics on the model.') # mail gateway @@ -281,7 +282,7 @@ class Message(models.Model): # 1. Aggregate partners (author_id and partner_ids), attachments and tracking values partners = self.env['res.partner'].sudo() attachments = self.env['ir.attachment'] - trackings = self.env['mail.tracking.value'] + message_ids = message_tree.keys() for key, message in message_tree.iteritems(): if message.author_id: partners |= message.author_id @@ -293,8 +294,6 @@ class Message(models.Model): partners |= message.needaction_partner_ids if message.attachment_ids: attachments |= message.attachment_ids - if message.tracking_value_ids: - trackings |= message.tracking_value_ids # Read partners as SUPERUSER -> message being browsed as SUPERUSER it is already the case partners_names = partners.name_get() partner_tree = dict((partner[0], partner) for partner in partners_names) @@ -309,13 +308,18 @@ class Message(models.Model): }) for attachment in attachments_data) # 3. Tracking values - tracking_tree = dict((tracking.id, { - 'id': tracking.id, - 'changed_field': tracking.field_desc, - 'old_value': tracking.get_old_display_value()[0], - 'new_value': tracking.get_new_display_value()[0], - 'field_type': tracking.field_type, - }) for tracking in trackings) + tracking_values = self.env['mail.tracking.value'].sudo().search([('mail_message_id', 'in', message_ids)]) + message_to_tracking = dict() + tracking_tree = dict.fromkeys(tracking_values.ids, False) + for tracking in tracking_values: + message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id) + tracking_tree[tracking.id] = { + 'id': tracking.id, + 'changed_field': tracking.field_desc, + 'old_value': tracking.get_old_display_value()[0], + 'new_value': tracking.get_new_display_value()[0], + 'field_type': tracking.field_type, + } # 4. Update message dictionaries for message_dict in messages: @@ -342,9 +346,9 @@ class Message(models.Model): if attachment.id in attachments_tree: attachment_ids.append(attachments_tree[attachment.id]) tracking_value_ids = [] - for tracking_value in message.tracking_value_ids: - if tracking_value.id in tracking_tree: - tracking_value_ids.append(tracking_tree[tracking_value.id]) + for tracking_value_id in message_to_tracking.get(message_id, list()): + if tracking_value_id in tracking_tree: + tracking_value_ids.append(tracking_tree[tracking_value_id]) message_dict.update({ 'author_id': author, @@ -745,7 +749,12 @@ class Message(models.Model): return '%s%s alt="%s"' % (data_to_url[key], match.group(3), name) values['body'] = _image_dataurl.sub(base64_to_boundary, values['body']) + # delegate creation of tracking after the create as sudo to avoid access rights issues + tracking_values_cmd = values.pop('tracking_value_ids', False) message = super(Message, self).create(values) + if tracking_values_cmd: + message.sudo().write({'tracking_value_ids': tracking_values_cmd}) + message._invalidate_documents() if not self.env.context.get('message_create_from_mail_mail'): diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index b07b7c8b118..9990d9bca31 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -63,7 +63,7 @@ class Partner(models.Model): record_name = message.record_name tracking = [] - for tracking_value in message.tracking_value_ids: + for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]): tracking.append((tracking_value.field_desc, tracking_value.get_old_display_value()[0], tracking_value.get_new_display_value()[0])) diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 0bd1697ca87..5040f812d73 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -23,9 +23,9 @@ access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,1 access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1 access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0 access_mail_message_subtype_user,mail.message.subtype.user,model_mail_message_subtype,,1,1,1,1 -access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,1,0,0,0 -access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,1,1,1,1 -access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,1,1,1,1 +access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,0,0,0,0 +access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,0,0,0,0 +access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,0,0,0,0 access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking_value,base.group_system,1,1,1,1 access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1 access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1 @@ -35,7 +35,7 @@ access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1, access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0 access_mail_test_user,mail.test.all,model_mail_test,base.group_user,1,1,1,1 access_mail_test_portal,mail.test.all,model_mail_test,base.group_portal,1,1,0,0 -access_mail_activity_all,mail.activity.all,model_mail_activity,,1,0,0,0 +access_mail_activity_all,mail.activity.all,model_mail_activity,,0,0,0,0 access_mail_activity_user,mail.activity.user,model_mail_activity,base.group_user,1,1,1,1 -access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,1,0,0,0 -access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,1,1,1 \ No newline at end of file +access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,0,0,0,0 +access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,1,1,1 diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index d5a5b5422c9..b9b13d9de3b 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -40,5 +40,16 @@ + + mail.activity: user: own only + + [('user_id', '=', user.id)] + + + + + + + diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 4226f54dc34..7b67bfcbb40 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -293,9 +293,11 @@ class WebsiteSlides(http.Controller): @http.route(['/slides/add_slide'], type='json', auth='user', methods=['POST'], website=True) def create_slide(self, *args, **post): - file_size = len(post['datas']) * 3 / 4; # base64 - if (file_size / 1024.0 / 1024.0) > 25: - return {'error': _('File is too big. File size cannot exceed 25MB')} + # check the size only when we upload a file. + if post.get('datas'): + file_size = len(post['datas']) * 3 / 4 # base64 + if (file_size / 1024.0 / 1024.0) > 25: + return {'error': _('File is too big. File size cannot exceed 25MB')} values = dict((fname, post[fname]) for fname in [ 'name', 'url', 'tag_ids', 'slide_type', 'channel_id', diff --git a/doc/cla/individual/remi-filament.md b/doc/cla/individual/remi-filament.md new file mode 100644 index 00000000000..b57242f471f --- /dev/null +++ b/doc/cla/individual/remi-filament.md @@ -0,0 +1,11 @@ +France, 2017-11-06 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Rémi CAZENAVE remi@le-filament.com https://github.com/remi-filament diff --git a/odoo/addons/base/ir/ir_cron.py b/odoo/addons/base/ir/ir_cron.py index 17b92cae609..bcb2df904a5 100644 --- a/odoo/addons/base/ir/ir_cron.py +++ b/odoo/addons/base/ir/ir_cron.py @@ -17,6 +17,13 @@ _logger = logging.getLogger(__name__) BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version'] +class BadVersion(Exception): + pass + +class BadModuleState(Exception): + pass + + _intervalTypes = { 'days': lambda interval: relativedelta(days=interval), 'hours': lambda interval: relativedelta(hours=interval), @@ -141,33 +148,102 @@ class ir_cron(models.Model): cron_cr.commit() @classmethod - def _acquire_job(cls, db_name): - # TODO remove 'check' argument from addons/base_automation/base_automation.py - """ Try to process one cron job. + def _process_jobs(cls, db_name): + """ Try to process all cron jobs. This selects in database all the jobs that should be processed. It then tries to lock each of them and, if it succeeds, run the cron job (if it doesn't succeed, it means the job was already locked to be taken care of by another thread) and return. - If a job was processed, returns True, otherwise returns False. + :raise BadVersion: if the version is different from the worker's + :raise BadModuleState: if modules are to install/upgrade/remove """ db = odoo.sql_db.db_connect(db_name) threading.current_thread().dbname = db_name - jobs = [] try: with db.cursor() as cr: - # Make sure the database we poll has the same version as the code of base - cr.execute("SELECT 1 FROM ir_module_module WHERE name=%s AND latest_version=%s", ('base', BASE_VERSION)) - if cr.fetchone(): - # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. - cr.execute("""SELECT * FROM ir_cron - WHERE numbercall != 0 - AND active AND nextcall <= (now() at time zone 'UTC') - ORDER BY priority""") - jobs = cr.dictfetchall() - else: - _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) + # Make sure the database has the same version as the code of + # base and that no module must be installed/upgraded/removed + cr.execute("SELECT latest_version FROM ir_module_module WHERE name=%s", ['base']) + (version,) = cr.fetchone() + cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %']) + (changes,) = cr.fetchone() + if not version or changes: + raise BadModuleState() + elif version != BASE_VERSION: + raise BadVersion() + # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. + cr.execute("""SELECT * FROM ir_cron + WHERE numbercall != 0 + AND active AND nextcall <= (now() at time zone 'UTC') + ORDER BY priority""") + jobs = cr.dictfetchall() + + for job in jobs: + lock_cr = db.cursor() + try: + # Try to grab an exclusive lock on the job row from within the task transaction + # Restrict to the same conditions as for the search since the job may have already + # been run by an other thread when cron is running in multi thread + lock_cr.execute("""SELECT * + FROM ir_cron + WHERE numbercall != 0 + AND active + AND nextcall <= (now() at time zone 'UTC') + AND id=%s + FOR UPDATE NOWAIT""", + (job['id'],), log_exceptions=False) + + locked_job = lock_cr.fetchone() + if not locked_job: + _logger.debug("Job `%s` already executed by another process/thread. skipping it", job['cron_name']) + continue + # Got the lock on the job row, run its code + _logger.debug('Starting job `%s`.', job['cron_name']) + job_cr = db.cursor() + try: + registry = odoo.registry(db_name) + registry[cls._name]._process_job(job_cr, job, lock_cr) + except Exception: + _logger.exception('Unexpected exception while processing cron job %r', job) + finally: + job_cr.close() + + except psycopg2.OperationalError as e: + if e.pgcode == '55P03': + # Class 55: Object not in prerequisite state; 55P03: lock_not_available + _logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['cron_name']) + continue + else: + # Unexpected OperationalError + raise + finally: + # we're exiting due to an exception while acquiring the lock + lock_cr.close() + + finally: + if hasattr(threading.current_thread(), 'dbname'): + del threading.current_thread().dbname + + @classmethod + def _acquire_job(cls, db_name): + """ Try to process all cron jobs. + + This selects in database all the jobs that should be processed. It then + tries to lock each of them and, if it succeeds, run the cron job (if it + doesn't succeed, it means the job was already locked to be taken care + of by another thread) and return. + + This method hides most exceptions related to the database's version, the + modules' state, and such. + """ + try: + cls._process_jobs(db_name) + except BadVersion: + _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) + except BadModuleState: + _logger.warning('Skipping database %s because of modules to install/upgrade/remove.', db_name) except psycopg2.ProgrammingError as e: if e.pgcode == '42P01': # Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table @@ -178,51 +254,6 @@ class ir_cron(models.Model): except Exception: _logger.warning('Exception in cron:', exc_info=True) - for job in jobs: - lock_cr = db.cursor() - try: - # Try to grab an exclusive lock on the job row from within the task transaction - # Restrict to the same conditions as for the search since the job may have already - # been run by an other thread when cron is running in multi thread - lock_cr.execute("""SELECT * - FROM ir_cron - WHERE numbercall != 0 - AND active - AND nextcall <= (now() at time zone 'UTC') - AND id=%s - FOR UPDATE NOWAIT""", - (job['id'],), log_exceptions=False) - - locked_job = lock_cr.fetchone() - if not locked_job: - _logger.debug("Job `%s` already executed by another process/thread. skipping it", job['cron_name']) - continue - # Got the lock on the job row, run its code - _logger.debug('Starting job `%s`.', job['cron_name']) - job_cr = db.cursor() - try: - registry = odoo.registry(db_name) - registry[cls._name]._process_job(job_cr, job, lock_cr) - except Exception: - _logger.exception('Unexpected exception while processing cron job %r', job) - finally: - job_cr.close() - - except psycopg2.OperationalError as e: - if e.pgcode == '55P03': - # Class 55: Object not in prerequisite state; 55P03: lock_not_available - _logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['cron_name']) - continue - else: - # Unexpected OperationalError - raise - finally: - # we're exiting due to an exception while acquiring the lock - lock_cr.close() - - if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect - del threading.current_thread().dbname - @api.multi def _try_lock(self): """Try to grab a dummy exclusive write-lock to the rows with the given ids, diff --git a/odoo/addons/base/ir/report_ir_model.xml b/odoo/addons/base/ir/report_ir_model.xml index 284767a3247..8dbbf9aba2f 100644 --- a/odoo/addons/base/ir/report_ir_model.xml +++ b/odoo/addons/base/ir/report_ir_model.xml @@ -3,7 +3,7 @@