mirror of
https://github.com/OCA/reporting-engine.git
synced 2025-02-16 16:30:38 +02:00
[IMP] sql_request_abstract black, isort
This commit is contained in:
@@ -3,20 +3,18 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
{
|
{
|
||||||
'name': 'SQL Request Abstract',
|
"name": "SQL Request Abstract",
|
||||||
'version': '12.0.1.0.1',
|
"version": "13.0.1.0.0",
|
||||||
'author': 'GRAP,Akretion,Odoo Community Association (OCA)',
|
"author": "GRAP,Akretion,Odoo Community Association (OCA)",
|
||||||
'website': 'https://www.odoo-community.org',
|
"website": "https://www.odoo-community.org",
|
||||||
'license': 'AGPL-3',
|
"license": "AGPL-3",
|
||||||
'category': 'Tools',
|
"category": "Tools",
|
||||||
'summary': 'Abstract Model to manage SQL Requests',
|
"summary": "Abstract Model to manage SQL Requests",
|
||||||
'depends': [
|
"depends": ["base"],
|
||||||
'base',
|
"data": [
|
||||||
|
"security/ir_module_category.xml",
|
||||||
|
"security/res_groups.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
],
|
],
|
||||||
'data': [
|
"installable": True,
|
||||||
'security/ir_module_category.xml',
|
|
||||||
'security/res_groups.xml',
|
|
||||||
'security/ir.model.access.csv',
|
|
||||||
],
|
|
||||||
'installable': True,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
|
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import base64
|
|
||||||
from psycopg2 import ProgrammingError
|
from psycopg2 import ProgrammingError
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
@@ -17,8 +18,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SQLRequestMixin(models.AbstractModel):
|
class SQLRequestMixin(models.AbstractModel):
|
||||||
_name = 'sql.request.mixin'
|
_name = "sql.request.mixin"
|
||||||
_description = 'SQL Request Mixin'
|
_description = "SQL Request Mixin"
|
||||||
|
|
||||||
_clean_query_enabled = True
|
_clean_query_enabled = True
|
||||||
|
|
||||||
@@ -30,58 +31,68 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
|
|
||||||
_sql_request_users_relation = False
|
_sql_request_users_relation = False
|
||||||
|
|
||||||
STATE_SELECTION = [
|
STATE_SELECTION = [("draft", "Draft"), ("sql_valid", "SQL Valid")]
|
||||||
('draft', 'Draft'),
|
|
||||||
('sql_valid', 'SQL Valid'),
|
|
||||||
]
|
|
||||||
|
|
||||||
PROHIBITED_WORDS = [
|
PROHIBITED_WORDS = [
|
||||||
'delete',
|
"delete",
|
||||||
'drop',
|
"drop",
|
||||||
'insert',
|
"insert",
|
||||||
'alter',
|
"alter",
|
||||||
'truncate',
|
"truncate",
|
||||||
'execute',
|
"execute",
|
||||||
'create',
|
"create",
|
||||||
'update',
|
"update",
|
||||||
'ir_config_parameter',
|
"ir_config_parameter",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Default Section
|
# Default Section
|
||||||
@api.model
|
@api.model
|
||||||
def _default_group_ids(self):
|
def _default_group_ids(self):
|
||||||
ir_model_obj = self.env['ir.model.data']
|
ir_model_obj = self.env["ir.model.data"]
|
||||||
return [ir_model_obj.xmlid_to_res_id(
|
return [
|
||||||
'sql_request_abstract.group_sql_request_user')]
|
ir_model_obj.xmlid_to_res_id("sql_request_abstract.group_sql_request_user")
|
||||||
|
]
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_user_ids(self):
|
def _default_user_ids(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Columns Section
|
# Columns Section
|
||||||
name = fields.Char('Name', required=True)
|
name = fields.Char("Name", required=True)
|
||||||
|
|
||||||
query = fields.Text(
|
query = fields.Text(
|
||||||
string='Query', required=True, help="You can't use the following words"
|
string="Query",
|
||||||
": DELETE, DROP, CREATE, INSERT, ALTER, TRUNCATE, EXECUTE, UPDATE.")
|
required=True,
|
||||||
|
help="You can't use the following words"
|
||||||
|
": DELETE, DROP, CREATE, INSERT, ALTER, TRUNCATE, EXECUTE, UPDATE.",
|
||||||
|
)
|
||||||
|
|
||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
string='State', selection=STATE_SELECTION, default='draft',
|
string="State",
|
||||||
|
selection=STATE_SELECTION,
|
||||||
|
default="draft",
|
||||||
help="State of the Request:\n"
|
help="State of the Request:\n"
|
||||||
" * 'Draft': Not tested\n"
|
" * 'Draft': Not tested\n"
|
||||||
" * 'SQL Valid': SQL Request has been checked and is valid")
|
" * 'SQL Valid': SQL Request has been checked and is valid",
|
||||||
|
)
|
||||||
|
|
||||||
group_ids = fields.Many2many(
|
group_ids = fields.Many2many(
|
||||||
comodel_name='res.groups', string='Allowed Groups',
|
comodel_name="res.groups",
|
||||||
|
string="Allowed Groups",
|
||||||
relation=_sql_request_groups_relation,
|
relation=_sql_request_groups_relation,
|
||||||
column1='sql_id', column2='group_id',
|
column1="sql_id",
|
||||||
default=_default_group_ids)
|
column2="group_id",
|
||||||
|
default=_default_group_ids,
|
||||||
|
)
|
||||||
|
|
||||||
user_ids = fields.Many2many(
|
user_ids = fields.Many2many(
|
||||||
comodel_name='res.users', string='Allowed Users',
|
comodel_name="res.users",
|
||||||
|
string="Allowed Users",
|
||||||
relation=_sql_request_users_relation,
|
relation=_sql_request_users_relation,
|
||||||
column1='sql_id', column2='user_id',
|
column1="sql_id",
|
||||||
default=_default_user_ids)
|
column2="user_id",
|
||||||
|
default=_default_user_ids,
|
||||||
|
)
|
||||||
|
|
||||||
# Action Section
|
# Action Section
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -93,17 +104,22 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
item._check_prohibited_words()
|
item._check_prohibited_words()
|
||||||
if item._check_execution_enabled:
|
if item._check_execution_enabled:
|
||||||
item._check_execution()
|
item._check_execution()
|
||||||
item.state = 'sql_valid'
|
item.state = "sql_valid"
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def button_set_draft(self):
|
def button_set_draft(self):
|
||||||
self.write({'state': 'draft'})
|
self.write({"state": "draft"})
|
||||||
|
|
||||||
# API Section
|
# API Section
|
||||||
@api.multi
|
@api.multi
|
||||||
def _execute_sql_request(
|
def _execute_sql_request(
|
||||||
self, params=None, mode='fetchall', rollback=True,
|
self,
|
||||||
view_name=False, copy_options="CSV HEADER DELIMITER ';'"):
|
params=None,
|
||||||
|
mode="fetchall",
|
||||||
|
rollback=True,
|
||||||
|
view_name=False,
|
||||||
|
copy_options="CSV HEADER DELIMITER ';'",
|
||||||
|
):
|
||||||
"""Execute a SQL request on the current database.
|
"""Execute a SQL request on the current database.
|
||||||
|
|
||||||
??? This function checks before if the user has the
|
??? This function checks before if the user has the
|
||||||
@@ -139,12 +155,11 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
res = False
|
res = False
|
||||||
# Check if the request is in a valid state
|
# Check if the request is in a valid state
|
||||||
if self.state == 'draft':
|
if self.state == "draft":
|
||||||
raise UserError(_(
|
raise UserError(_("It is not allowed to execute a not checked request."))
|
||||||
"It is not allowed to execute a not checked request."))
|
|
||||||
|
|
||||||
# Disable rollback if a creation of a view is asked
|
# Disable rollback if a creation of a view is asked
|
||||||
if mode in ('view', 'materialized_view'):
|
if mode in ("view", "materialized_view"):
|
||||||
rollback = False
|
rollback = False
|
||||||
|
|
||||||
# pylint: disable=sql-injection
|
# pylint: disable=sql-injection
|
||||||
@@ -154,31 +169,31 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
query = self.query
|
query = self.query
|
||||||
query = query
|
query = query
|
||||||
|
|
||||||
if mode in ('fetchone', 'fetchall'):
|
if mode in ("fetchone", "fetchall"):
|
||||||
pass
|
pass
|
||||||
elif mode == 'stdout':
|
elif mode == "stdout":
|
||||||
query = "COPY (%s) TO STDOUT WITH %s" % (query, copy_options)
|
query = "COPY ({}) TO STDOUT WITH {}".format(query, copy_options)
|
||||||
elif mode in 'view':
|
elif mode in "view":
|
||||||
query = "CREATE VIEW %s AS (%s);" % (query, view_name)
|
query = "CREATE VIEW {} AS ({});".format(query, view_name)
|
||||||
elif mode in 'materialized_view':
|
elif mode in "materialized_view":
|
||||||
self._check_materialized_view_available()
|
self._check_materialized_view_available()
|
||||||
query = "CREATE MATERIALIZED VIEW %s AS (%s);" % (query, view_name)
|
query = "CREATE MATERIALIZED VIEW {} AS ({});".format(query, view_name)
|
||||||
else:
|
else:
|
||||||
raise UserError(_("Unimplemented mode : '%s'" % mode))
|
raise UserError(_("Unimplemented mode : '%s'" % mode))
|
||||||
|
|
||||||
if rollback:
|
if rollback:
|
||||||
rollback_name = self._create_savepoint()
|
rollback_name = self._create_savepoint()
|
||||||
try:
|
try:
|
||||||
if mode == 'stdout':
|
if mode == "stdout":
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
self.env.cr.copy_expert(query, output)
|
self.env.cr.copy_expert(query, output)
|
||||||
res = base64.b64encode(output.getvalue())
|
res = base64.b64encode(output.getvalue())
|
||||||
output.close()
|
output.close()
|
||||||
else:
|
else:
|
||||||
self.env.cr.execute(query)
|
self.env.cr.execute(query)
|
||||||
if mode == 'fetchall':
|
if mode == "fetchall":
|
||||||
res = self.env.cr.fetchall()
|
res = self.env.cr.fetchall()
|
||||||
elif mode == 'fetchone':
|
elif mode == "fetchone":
|
||||||
res = self.env.cr.fetchone()
|
res = self.env.cr.fetchone()
|
||||||
finally:
|
finally:
|
||||||
self._rollback_savepoint(rollback_name)
|
self._rollback_savepoint(rollback_name)
|
||||||
@@ -188,8 +203,7 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
# Private Section
|
# Private Section
|
||||||
@api.model
|
@api.model
|
||||||
def _create_savepoint(self):
|
def _create_savepoint(self):
|
||||||
rollback_name = '%s_%s' % (
|
rollback_name = "{}_{}".format(self._name.replace(".", "_"), uuid.uuid1().hex)
|
||||||
self._name.replace('.', '_'), uuid.uuid1().hex)
|
|
||||||
# pylint: disable=sql-injection
|
# pylint: disable=sql-injection
|
||||||
req = "SAVEPOINT %s" % (rollback_name)
|
req = "SAVEPOINT %s" % (rollback_name)
|
||||||
self.env.cr.execute(req)
|
self.env.cr.execute(req)
|
||||||
@@ -204,18 +218,22 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
@api.model
|
@api.model
|
||||||
def _check_materialized_view_available(self):
|
def _check_materialized_view_available(self):
|
||||||
self.env.cr.execute("SHOW server_version;")
|
self.env.cr.execute("SHOW server_version;")
|
||||||
res = self.env.cr.fetchone()[0].split('.')
|
res = self.env.cr.fetchone()[0].split(".")
|
||||||
minor_version = float('.'.join(res[:2]))
|
minor_version = float(".".join(res[:2]))
|
||||||
if minor_version < 9.3:
|
if minor_version < 9.3:
|
||||||
raise UserError(_(
|
raise UserError(
|
||||||
"Materialized View requires PostgreSQL 9.3 or greater but"
|
_(
|
||||||
" PostgreSQL %s is currently installed.") % (minor_version))
|
"Materialized View requires PostgreSQL 9.3 or greater but"
|
||||||
|
" PostgreSQL %s is currently installed."
|
||||||
|
)
|
||||||
|
% (minor_version)
|
||||||
|
)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _clean_query(self):
|
def _clean_query(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
query = self.query.strip()
|
query = self.query.strip()
|
||||||
while query[-1] == ';':
|
while query[-1] == ";":
|
||||||
query = query[:-1]
|
query = query[:-1]
|
||||||
self.query = query
|
self.query = query
|
||||||
|
|
||||||
@@ -226,12 +244,16 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
query = self.query.lower()
|
query = self.query.lower()
|
||||||
for word in self.PROHIBITED_WORDS:
|
for word in self.PROHIBITED_WORDS:
|
||||||
expr = r'\b%s\b' % word
|
expr = r"\b%s\b" % word
|
||||||
is_not_safe = re.search(expr, query)
|
is_not_safe = re.search(expr, query)
|
||||||
if is_not_safe:
|
if is_not_safe:
|
||||||
raise UserError(_(
|
raise UserError(
|
||||||
"The query is not allowed because it contains unsafe word"
|
_(
|
||||||
" '%s'") % (word))
|
"The query is not allowed because it contains unsafe word"
|
||||||
|
" '%s'"
|
||||||
|
)
|
||||||
|
% (word)
|
||||||
|
)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _check_execution(self):
|
def _check_execution(self):
|
||||||
@@ -246,8 +268,7 @@ class SQLRequestMixin(models.AbstractModel):
|
|||||||
res = self._hook_executed_request()
|
res = self._hook_executed_request()
|
||||||
except ProgrammingError as e:
|
except ProgrammingError as e:
|
||||||
logger.exception("Failed query: %s", query)
|
logger.exception("Failed query: %s", query)
|
||||||
raise UserError(
|
raise UserError(_("The SQL query is not valid:\n\n %s") % e)
|
||||||
_("The SQL query is not valid:\n\n %s") % e)
|
|
||||||
finally:
|
finally:
|
||||||
self._rollback_savepoint(rollback_name)
|
self._rollback_savepoint(rollback_name)
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<record model="ir.module.category" id="category_sql_abstract">
|
<record model="ir.module.category" id="category_sql_abstract">
|
||||||
<field name="name">SQL Request</field>
|
<field name="name">SQL Request</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
|
Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
|
||||||
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<record model="res.groups" id="group_sql_request_user">
|
<record model="res.groups" id="group_sql_request_user">
|
||||||
<field name="name">User</field>
|
<field name="name">User</field>
|
||||||
<field name="category_id" ref="category_sql_abstract" />
|
<field name="category_id" ref="category_sql_abstract" />
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record model="res.groups" id="group_sql_request_manager">
|
<record model="res.groups" id="group_sql_request_manager">
|
||||||
<field name="name">Manager</field>
|
<field name="name">Manager</field>
|
||||||
<field name="category_id" ref="category_sql_abstract" />
|
<field name="category_id" ref="category_sql_abstract" />
|
||||||
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
|
<field name="users" eval="[(4, ref('base.user_admin'))]" />
|
||||||
<field name="implied_ids" eval="[(4, ref('group_sql_request_user'))]" />
|
<field name="implied_ids" eval="[(4, ref('group_sql_request_user'))]" />
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Reference in New Issue
Block a user