[IMP] sql_request_abstract black, isort

This commit is contained in:
alfadil
2020-03-05 15:59:30 +03:00
committed by Sylvain LE GAL
parent 53e1f298d3
commit dfab6e248a
4 changed files with 100 additions and 89 deletions

View File

@@ -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,
} }

View File

@@ -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

View File

@@ -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>

View File

@@ -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>