Browse Source

[ADD] base_export_security: Create module (#917)

* [ADD] base_export_security: Create module
* Add Export Rights security group, access rights
* Add user rights checks to prevent unauthorized exports and hide
  Export menu item from unauthorized users
* Add Export model for logging data export activity
* Add views, menu link for Export Logs
* Add data exports discussion channel, notifications of exports

* [IMP] base_export_security: Make requested changes
* Fix manifest website and depends
* Fix xml structure
* Rename model 'export' -> 'export.event'
* Simplify date calculation
* Use % instead of .format
* Avoid translating variables
* Assign recordsets to variables instead of ids
* Use (6, _, ids) command instead of (4, id, _)
* Clean up syntax redundancies
* Remove unnecessary sort
* Override export_data method with inheritance/super

* [FIX] base_export_security: Fix eslint errors
* Fix prefer-rest-params
* Fix no-prototype-builtins
* Fix comma-dangle
* Fix dot-location
* Fix unnecessary apply

* [IMP] base_export_security: Update readme
* Add three screenshots from PR to readme
pull/1029/merge
Brenton Hughes 7 years ago
committed by Dave Lasley
parent
commit
37dc920c5e
  1. 92
      base_export_security/README.rst
  2. 5
      base_export_security/__init__.py
  3. 25
      base_export_security/__manifest__.py
  4. 12
      base_export_security/data/export.xml
  5. 6
      base_export_security/models/__init__.py
  6. 26
      base_export_security/models/base.py
  7. 81
      base_export_security/models/export.py
  8. 13
      base_export_security/security/export_security.xml
  9. 2
      base_export_security/security/ir.model.access.csv
  10. BIN
      base_export_security/static/description/export_log.png
  11. BIN
      base_export_security/static/description/export_menu_item_hidden.png
  12. BIN
      base_export_security/static/description/export_notification.png
  13. 35
      base_export_security/static/src/js/base_export_security.js
  14. 71
      base_export_security/static/tests/js/base_export_security.js
  15. 5
      base_export_security/tests/__init__.py
  16. 123
      base_export_security/tests/test_export.py
  17. 90
      base_export_security/views/export_view.xml

92
base_export_security/README.rst

@ -0,0 +1,92 @@
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl
:alt: License: LGPL-3
===============
Export Security
===============
This module adds the following security features to Odoo's data exporting:
#. A security group for restricting users' ability to export data
#. A log of exports detailing the user responsible, as well as the model, records, and fields exported
#. A `#data exports` channel for export activity notifications
Screenshots
===========
Hidden Export Menu Item
-----------------------
.. image:: /base_export_security/static/description/export_menu_item_hidden.png?raw=true
:alt: Hidden Export Menu Item
:width: 600 px
Export Log
----------
.. image:: /base_export_security/static/description/export_log.png?raw=true
:alt: Export Log
:width: 600 px
Channel Notification
--------------------
.. image:: /base_export_security/static/description/export_notification.png?raw=true
:alt: Export Notification
:width: 500 px
Configuration
=============
To configure this module, you need to:
#. Add users who require data export rights to the `Export Rights` security group, found in Settings/Users (requires developer mode to be active)
#. Invite users who should receive export activity notifications to the `#data exports` notification channel
Usage
=====
To view detailed Export Logs, go to Settings/Data Exports/Export Logs
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/149/10.0
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Brent Hughes <brent.hughes@laslabs.com>
Do not contact contributors directly about support or help with technical issues.
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
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.
To contribute to this module, please visit https://odoo-community.org.

5
base_export_security/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import models

25
base_export_security/__manifest__.py

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
"name": "Export Security",
"summary": "Security features for Odoo exports",
"version": "10.0.1.0.0",
"category": "Extra Tools",
"website": "https://laslabs.com/",
"author": "LasLabs, Odoo Community Association (OCA)",
"license": "LGPL-3",
"application": False,
"installable": True,
"depends": [
"mail",
"web",
],
"data": [
"data/export.xml",
"security/export_security.xml",
"security/ir.model.access.csv",
"views/export_view.xml",
],
}

12
base_export_security/data/export.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo noupdate="1">
<record id="export_channel" model="mail.channel">
<field name="name">data exports</field>
<field name="public">private</field>
<field name="channel_partner_ids" eval="[(4, ref('base.partner_root'))]"/>
<field name="description">Notifications of data export activity</field>
</record>
</odoo>

6
base_export_security/models/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import base
from . import export

26
base_export_security/models/base.py

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, models
from odoo.exceptions import AccessError
class Base(models.AbstractModel):
_inherit = 'base'
@api.multi
def export_data(self, fields_to_export, raw_data=False):
if self.env.user.has_group('base_export_security.export_group'):
field_names = map(
lambda path_array: path_array[0], map(
models.fix_import_export_id_paths,
fields_to_export,
),
)
self.env['export.event'].log_export(self, field_names)
return super(Base, self).export_data(fields_to_export, raw_data)
else:
raise AccessError(
_('You do not have permission to export data'),
)

81
base_export_security/models/export.py

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, fields, models
class Export(models.Model):
_name = 'export.event'
_description = 'Data Export Record'
name = fields.Char()
model_id = fields.Many2one(
'ir.model',
string='Exported Model',
readonly=True,
)
user_id = fields.Many2one(
'res.users',
string='Exported by',
readonly=True,
)
field_ids = fields.Many2many(
'ir.model.fields',
string='Exported Fields',
readonly=True,
)
record_ids = fields.Many2many(
'ir.model.data',
string='Exported Records',
readonly=True,
)
@api.model
def log_export(self, recordset, field_names):
date = fields.Datetime.now()
model_name = recordset._name
model = self.env['ir.model'].search([('model', '=', model_name)])
user = self.env.user
name_data = {'date': date, 'model': model.name, 'user': user.name}
name = '%(date)s / %(model)s / %(user)s' % name_data
exported_fields = self.env['ir.model.fields'].search([
('model', '=', model_name),
('name', 'in', field_names),
])
records = self.env['ir.model.data'].search([
('model', '=', model_name),
('res_id', 'in', recordset.ids),
])
export = self.create({
'name': name,
'model_id': model.id,
'field_ids': [(6, 0, exported_fields.ids)],
'record_ids': [(6, 0, records.ids)],
'user_id': user.id,
})
export.sudo().post_notification()
return export
@api.multi
def post_notification(self):
channel = self.env.ref('base_export_security.export_channel')
field_labels = ', '.join(
self.field_ids.mapped('field_description'),
)
message_data = {
'records': len(self.record_ids),
'model': self.model_id.name,
'user': self.user_id.name,
'fields': field_labels,
}
message_body = _(
'%(records)d <b>%(model)s</b> records exported by <b>%(user)s'
'</b>.<br><b>Fields exported:</b> %(fields)s'
) % message_data
message = channel.message_post(
body=message_body,
message_type='notification',
subtype='mail.mt_comment',
)
return message

13
base_export_security/security/export_security.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="export_group" model="res.groups">
<field name="name">Export Rights</field>
<field name="comment">
The user will be able to export data.
</field>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
</odoo>

2
base_export_security/security/ir.model.access.csv

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
export_rights,export rights,model_export_event,base_export_security.export_group,1,1,1,1

BIN
base_export_security/static/description/export_log.png

After

Width: 1000  |  Height: 655  |  Size: 263 KiB

BIN
base_export_security/static/description/export_menu_item_hidden.png

After

Width: 1000  |  Height: 297  |  Size: 100 KiB

BIN
base_export_security/static/description/export_notification.png

After

Width: 1000  |  Height: 137  |  Size: 95 KiB

35
base_export_security/static/src/js/base_export_security.js

@ -0,0 +1,35 @@
/* Copyright 2017 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */
odoo.define('base_export_security', function(require){
'use strict';
var Model = require('web.Model');
var ListView = require('web.ListView');
var core = require('web.core');
ListView.include({
render_sidebar: function($node) {
var exportLabel = core._t('Export');
var users = new Model('res.users');
var res = this._super($node);
if (this.sidebar && Object.prototype.hasOwnProperty.call(this.sidebar.items, 'other')) {
users.call('has_group', ['base_export_security.export_group']).then(function(result){
if(!result){
var filteredItems = this.sidebar.items.other.filter(
function(item){
return item.label !== exportLabel;
}
);
this.sidebar.items.other = filteredItems;
this.sidebar.redraw();
}
}.bind(this));
}
return res;
}
});
});

71
base_export_security/static/tests/js/base_export_security.js

@ -0,0 +1,71 @@
/* Copyright 2017 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */
odoo.define_section('base_export_security',
['web.core', 'web.data', 'web.data_manager', 'web.ListView'],
function(test, mock) {
'use strict';
function setup (authorization) {
mock.add('test.model:read', function () {
return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }];
});
mock.add('res.users:has_group', function () {
return authorization;
});
}
function fields_view_get () {
return {
type: 'tree',
fields: {
a: {type: 'char', string: 'A'},
b: {type: 'char', string: 'B'},
c: {type: 'char', string: 'C'}
},
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>'
};
}
function labelCount (exportLabel) {
var $fix = $('#qunit-fixture');
var exportItems = $fix.find('.btn-group a[data-section="other"]').filter(
function(i, item){
return item.text.trim() === exportLabel;
}
);
return exportItems.length;
}
function renderView (data, data_manager, ListView) {
var $fix = $('#qunit-fixture');
var dataset = new data.DataSetStatic(null, 'test.model', null, [1]);
var fields_view = data_manager._postprocess_fvg(fields_view_get());
var listView = new ListView({}, dataset, fields_view, {sidebar: true});
listView.appendTo($fix).then(listView.render_sidebar($fix));
}
test('It should display the Export menu item to authorized users',
function(assert, core, data, data_manager, ListView) {
var exportLabel = core._t('Export');
setup(true);
renderView(data, data_manager, ListView);
assert.strictEqual(labelCount(exportLabel), 1);
}
);
test('It should not display the Export menu item to unauthorized users',
function(assert, core, data, data_manager, ListView) {
var exportLabel = core._t('Export');
setup(false);
renderView(data, data_manager, ListView);
assert.strictEqual(labelCount(exportLabel), 0);
}
);
});

5
base_export_security/tests/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_export

123
base_export_security/tests/test_export.py

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import mock
from odoo import _
from odoo.exceptions import AccessError
from odoo.tests.common import HttpCase, TransactionCase
class TestExport(TransactionCase):
def setUp(self):
super(TestExport, self).setUp()
export_group = self.env.ref('base_export_security.export_group')
self.authorized_user = self.env['res.users'].create({
'login': 'exporttestuser',
'partner_id': self.env['res.partner'].create({
'name': "Export Test User"
}).id,
'groups_id': [(4, export_group.id, 0)],
})
self.unauthorized_user = self.env['res.users'].create({
'login': 'unauthorizedexporttestuser',
'partner_id': self.env['res.partner'].create({
'name': "Unauthorized Export Test User"
}).id,
})
self.model_name = 'ir.module.module'
self.recordset = self.env[self.model_name].search([])
self.field_names = ['name', 'id']
self.model = self.env['ir.model'].search([
('model', '=', self.model_name),
])
self.records = self.env['ir.model.data'].search([
('model', '=', self.model_name),
('res_id', 'in', self.recordset.ids),
])
self.fields = self.env['ir.model.fields'].search([
('model', '=', self.model_name),
('name', 'in', self.field_names),
])
def test_log_export(self):
"""It should create a new Export record with correct data"""
log = self.env['export.event'].sudo(self.authorized_user).log_export(
self.recordset,
self.field_names,
)
self.assertEqual(
[log.model_id, log.field_ids, log.record_ids, log.user_id],
[self.model, self.fields, self.records, self.authorized_user],
'Log not created properly',
)
def test_log_export_posts_notification(self):
"""It should call post_notification method"""
post_notification_mock = mock.MagicMock()
self.env['export.event']._patch_method(
'post_notification',
post_notification_mock,
)
self.env['export.event'].sudo(self.authorized_user).log_export(
self.recordset,
self.field_names,
)
post_notification_mock.assert_called_once_with()
self.env['export.event']._revert_method('post_notification')
def test_post_notification(self):
"""It should post a notification with appropriate data
to the #data export channel"""
export = self.env['export.event'].create({
'model_id': self.model.id,
'field_ids': [(4, self.fields.ids)],
'record_ids': [(4, self.records.ids)],
'user_id': self.authorized_user.id,
})
message = export.sudo().post_notification()
field_labels = ', '.join(
self.fields.sorted().mapped('field_description'),
)
message_data = {
'records': len(self.records.ids),
'model': self.model.name,
'user': self.authorized_user.name,
'fields': field_labels,
}
message_body = _(
'<p>%(records)d <b>%(model)s</b> records exported by <b>%(user)s'
'</b>.<br><b>Fields exported:</b> %(fields)s</p>'
) % message_data
self.assertEqual(
[message.body, message.message_type, message.model],
[message_body, 'notification', 'mail.channel'],
'Message not posted properly',
)
def test_export_data_access(self):
"""It should raise AccessError if user does not have export rights"""
with self.assertRaises(AccessError):
self.env[self.model_name].sudo(
self.unauthorized_user
).export_data(self, None)
def test_export_data_calls_log_export(self):
"""It should call log_export if user has export rights"""
log_export_mock = mock.MagicMock()
self.env['export.event']._patch_method('log_export', log_export_mock)
model = self.env[self.model_name]
model.sudo(self.authorized_user).export_data(self.field_names)
log_export_mock.assert_called_once_with(model, self.field_names)
self.env['export.event']._revert_method('log_export')
class TestJS(HttpCase):
def test_export_visibility(self):
"""Test visibility of export menu item"""
self.phantom_js(
"/web/tests?debug=assets&module=base_export_security",
"",
login="admin",
)

90
base_export_security/views/export_view.xml

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="export_view_form" model="ir.ui.view">
<field name="name">Export Log</field>
<field name="model">export.event</field>
<field name="arch" type="xml">
<form string="Export Log" create="false">
<sheet>
<group>
<field name="name"/>
<field name="model_id"/>
<field name="user_id"/>
<field name="create_date"
string="Export Date"
readonly="1"/>
</group>
<notebook>
<page string="Exported Fields">
<field name="field_ids"/>
</page>
<page string="Exported Records">
<field name="record_ids"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="export_view_tree" model="ir.ui.view">
<field name="name">Export Logs</field>
<field name="model">export.event</field>
<field name="arch" type="xml">
<tree string="Export Logs" create="false" default_order='create_date desc'>
<field name="model_id"/>
<field name="user_id"/>
<field name="create_date" string="Export Date"/>
</tree>
</field>
</record>
<record id="export_view_search" model="ir.ui.view">
<field name="name">Export Logs Search</field>
<field name="model">export.event</field>
<field name="arch" type="xml">
<search string="Export Logs">
<field name="user_id"/>
<field name="create_date" string="Export Date"/>
<field name="model_id"/>
<field name="field_ids"/>
<field name="record_ids"/>
</search>
</field>
</record>
<record id="export_action_log" model="ir.actions.act_window">
<field name="name">Export Logs</field>
<field name="res_model">export.event</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="export_view_search"/>
</record>
<menuitem id="export_category"
name="Data Exports"
parent="base.menu_administration"/>
<menuitem id="export_menu"
name="Export Logs"
sequence="7"
parent="export_category"
action="export_action_log"/>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr=".">
<script type="text/javascript"
src="/base_export_security/static/src/js/base_export_security.js"/>
</xpath>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="application/javascript"
src="/base_export_security/static/tests/js/base_export_security.js"/>
</xpath>
</template>
</odoo>
Loading…
Cancel
Save