Limit access to database from the same series as the server
The last series to which the database was updated is stored in ir.configuration. With this information, the list of databases is filtered for the same series. Remote access to a database is restricted to databases available in the list. Closes #13791
This commit is contained in:
1 parent
1168597b27
commit
1edefbeae5
12 files changed
+100
-49
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Limit access to database from the same series
|
||||
* Fill wizard state view instance with default value
|
||||
* Replace passlib by pwdlib
|
||||
* Add size limit for RPC arguments
|
||||
|
||||
@@ -59,11 +59,13 @@ Database
|
||||
|
||||
Initialize the database schema.
|
||||
|
||||
.. method:: Database.test([hostname])
|
||||
.. method:: Database.test([hostname[, series]])
|
||||
|
||||
Return if the database is a Tryton database.
|
||||
If ``hostname`` is set, it checks also if it has the same configured
|
||||
hostname.
|
||||
If ``series`` is ``True``, it checks also if it has the same series from the
|
||||
last update.
|
||||
|
||||
.. method:: Database.nextid(connection, table[, count])
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ except ImportError:
|
||||
requests_utils = None
|
||||
|
||||
__version__ = "7.5.0"
|
||||
__series__ = '.'.join(__version__.split('.')[:2])
|
||||
|
||||
os.environ.setdefault(
|
||||
'TRYTOND_APPNAME',
|
||||
|
||||
@@ -8,7 +8,7 @@ from getpass import getpass
|
||||
|
||||
from sql import Literal, Table
|
||||
|
||||
from trytond import backend
|
||||
from trytond import __series__, backend
|
||||
from trytond.config import config
|
||||
from trytond.pool import Pool
|
||||
from trytond.sendmail import send_test_email
|
||||
@@ -129,6 +129,9 @@ def run(options):
|
||||
User = pool.get('res.user')
|
||||
Configuration = pool.get('ir.configuration')
|
||||
configuration = Configuration(1)
|
||||
if init[db_name]:
|
||||
configuration.series = __series__
|
||||
|
||||
with inactive_records():
|
||||
admin, = User.search([('login', '=', 'admin')])
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class DatabaseInterface(object):
|
||||
def init(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def test(self, hostname=None):
|
||||
def test(self, hostname=None, series=False):
|
||||
'''
|
||||
Test if it is a Tryton database.
|
||||
'''
|
||||
|
||||
@@ -27,8 +27,8 @@ from psycopg2 import DataError as DatabaseDataError
|
||||
from psycopg2 import IntegrityError as DatabaseIntegrityError
|
||||
from psycopg2 import InterfaceError
|
||||
from psycopg2 import OperationalError as DatabaseOperationalError
|
||||
from psycopg2 import ProgrammingError
|
||||
from psycopg2.errors import QueryCanceled as DatabaseTimeoutError
|
||||
from psycopg2.errors import UndefinedColumn
|
||||
from psycopg2.extras import register_default_json, register_default_jsonb
|
||||
from sql import Cast, Flavor, For, Literal, Table
|
||||
from sql.aggregate import Count
|
||||
@@ -36,6 +36,7 @@ from sql.conditionals import Coalesce
|
||||
from sql.functions import Function
|
||||
from sql.operators import BinaryOperator, Concat
|
||||
|
||||
from trytond import __series__
|
||||
from trytond.backend.database import DatabaseInterface, SQLType
|
||||
from trytond.config import config, parse_uri
|
||||
from trytond.sql.operators import RangeOperator
|
||||
@@ -345,18 +346,19 @@ class Database(DatabaseInterface):
|
||||
if res and abs(timestamp - now) < timeout:
|
||||
return res
|
||||
|
||||
res = []
|
||||
connection = self.get_connection()
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT datname FROM pg_database '
|
||||
'WHERE datistemplate = false ORDER BY datname')
|
||||
res = []
|
||||
for db_name, in cursor:
|
||||
try:
|
||||
conn = connect(**self._connection_params(db_name))
|
||||
try:
|
||||
with conn:
|
||||
if self._test(conn, hostname=hostname):
|
||||
if self._test(
|
||||
conn, hostname=hostname, series=True):
|
||||
res.append(db_name)
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -401,36 +403,38 @@ class Database(DatabaseInterface):
|
||||
finally:
|
||||
self.put_connection(connection)
|
||||
|
||||
def test(self, hostname=None):
|
||||
def test(self, hostname=None, series=False):
|
||||
try:
|
||||
connection = self.get_connection()
|
||||
except Exception:
|
||||
logger.debug('Test failed for "%s"', self.name, exc_info=True)
|
||||
return False
|
||||
try:
|
||||
return self._test(connection, hostname=hostname)
|
||||
return self._test(connection, hostname=hostname, series=series)
|
||||
finally:
|
||||
self.put_connection(connection, close=True)
|
||||
|
||||
@classmethod
|
||||
def _test(cls, connection, hostname=None):
|
||||
def _test(cls, connection, hostname=None, series=False):
|
||||
cursor = connection.cursor()
|
||||
tables = ('ir_model', 'ir_model_field', 'ir_ui_view', 'ir_ui_menu',
|
||||
'res_user', 'res_group', 'ir_module', 'ir_module_dependency',
|
||||
'ir_translation', 'ir_lang', 'ir_configuration')
|
||||
cursor.execute('SELECT table_name FROM information_schema.tables '
|
||||
'WHERE table_name IN %s', (tables,))
|
||||
if len(cursor.fetchall()) != len(tables):
|
||||
cursor.execute(
|
||||
'SELECT table_name FROM information_schema.tables '
|
||||
'WHERE table_name = %s', ('ir_configuration',))
|
||||
if not cursor.rowcount:
|
||||
return False
|
||||
if hostname:
|
||||
try:
|
||||
cursor.execute(
|
||||
'SELECT hostname FROM ir_configuration')
|
||||
try:
|
||||
if series:
|
||||
cursor.execute('SELECT series FROM ir_configuration')
|
||||
config_series = {s for s, in cursor if s}
|
||||
if config_series and __series__ not in config_series:
|
||||
return False
|
||||
if hostname:
|
||||
cursor.execute('SELECT hostname FROM ir_configuration')
|
||||
hostnames = {h for h, in cursor if h}
|
||||
if hostnames and hostname not in hostnames:
|
||||
return False
|
||||
except ProgrammingError:
|
||||
pass
|
||||
except UndefinedColumn:
|
||||
return False
|
||||
return True
|
||||
|
||||
def nextid(self, connection, table, count=1):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
CREATE TABLE ir_configuration (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL
|
||||
CONSTRAINT ir_configuration_id_positive CHECK(id >= 0),
|
||||
series VARCHAR,
|
||||
language VARCHAR,
|
||||
hostname VARCHAR,
|
||||
PRIMARY KEY(id)
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import random
|
||||
import sqlite3 as sqlite
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
@@ -23,6 +24,7 @@ from sql.functions import (
|
||||
Substring, Trim)
|
||||
from sql.operators import Equal
|
||||
|
||||
from trytond import __series__
|
||||
from trytond.backend.database import DatabaseInterface, SQLType
|
||||
from trytond.config import config, parse_uri
|
||||
from trytond.tools import safe_join
|
||||
@@ -438,6 +440,8 @@ class Database(DatabaseInterface):
|
||||
|
||||
_local = threading.local()
|
||||
_conn = None
|
||||
_list_cache = {}
|
||||
_list_cache_timestamp = {}
|
||||
flavor = Flavor(
|
||||
paramstyle='qmark', function_mapping=MAPPING, null_ordering=False,
|
||||
max_limit=-1)
|
||||
@@ -599,24 +603,33 @@ class Database(DatabaseInterface):
|
||||
with sqlite.connect(path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.close()
|
||||
cls._list_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def drop(cls, connection, database_name):
|
||||
if database_name == ':memory:':
|
||||
cls._local.memory_database._conn = None
|
||||
return
|
||||
if os.sep in database_name:
|
||||
return
|
||||
file = os.path.join(
|
||||
config.get('database', 'path'), database_name + '.sqlite')
|
||||
os.remove(file)
|
||||
for suffix in ['-shm', '-wal']:
|
||||
try:
|
||||
os.remove(file + suffix)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if database_name == ':memory:':
|
||||
cls._local.memory_database._conn = None
|
||||
else:
|
||||
file = os.path.join(
|
||||
config.get('database', 'path'), database_name + '.sqlite')
|
||||
os.remove(file)
|
||||
for suffix in ['-shm', '-wal']:
|
||||
try:
|
||||
os.remove(file + suffix)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
cls._list_cache.clear()
|
||||
|
||||
def list(self, hostname=None):
|
||||
now = time.time()
|
||||
timeout = config.getint('session', 'timeout')
|
||||
res = self.__class__._list_cache.get(hostname)
|
||||
timestamp = self.__class__._list_cache_timestamp.get(hostname, now)
|
||||
if res and abs(timestamp - now) < timeout:
|
||||
return res
|
||||
|
||||
res = []
|
||||
listdir = [':memory:']
|
||||
try:
|
||||
@@ -635,9 +648,12 @@ class Database(DatabaseInterface):
|
||||
logger.debug(
|
||||
'Test failed for "%s"', db_name, exc_info=True)
|
||||
continue
|
||||
if database.test(hostname=hostname):
|
||||
if database.test(hostname=hostname, series=True):
|
||||
res.append(db_name)
|
||||
database.close()
|
||||
|
||||
self.__class__._list_cache[hostname] = res
|
||||
self.__class__._list_cache_timestamp[hostname] = now
|
||||
return res
|
||||
|
||||
def init(self):
|
||||
@@ -673,28 +689,29 @@ class Database(DatabaseInterface):
|
||||
cursor.execute(*insert)
|
||||
conn.commit()
|
||||
|
||||
def test(self, hostname=None):
|
||||
Flavor.set(self.flavor)
|
||||
tables = ['ir_model', 'ir_model_field', 'ir_ui_view', 'ir_ui_menu',
|
||||
'res_user', 'res_group', 'ir_module', 'ir_module_dependency',
|
||||
'ir_translation', 'ir_lang', 'ir_configuration']
|
||||
sqlite_master = Table('sqlite_master')
|
||||
select = sqlite_master.select(sqlite_master.name)
|
||||
select.where = sqlite_master.type == 'table'
|
||||
select.where &= sqlite_master.name.in_(tables)
|
||||
def test(self, hostname=None, series=False):
|
||||
with self._conn as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(*select)
|
||||
cursor.execute(
|
||||
'SELECT name FROM sqlite_master '
|
||||
'WHERE type = ? AND name = ?',
|
||||
('table', 'ir_configuration'))
|
||||
except Exception:
|
||||
return False
|
||||
if len(cursor.fetchall()) != len(tables):
|
||||
if not cursor.fetchall():
|
||||
return False
|
||||
if hostname:
|
||||
configuration = Table('ir_configuration')
|
||||
if series:
|
||||
try:
|
||||
cursor.execute(*configuration.select(
|
||||
configuration.hostname))
|
||||
cursor.execute('SELECT series FROM ir_configuration')
|
||||
except Exception:
|
||||
return False
|
||||
config_series = {s for s, in cursor if s}
|
||||
if config_series and __series__ not in config_series:
|
||||
return False
|
||||
if hostname:
|
||||
try:
|
||||
cursor.execute('SELECT hostname FROM ir_configuration')
|
||||
except Exception:
|
||||
return False
|
||||
hostnames = {h for h, in cursor if h}
|
||||
|
||||
@@ -7,6 +7,7 @@ from trytond.model import ModelSingleton, ModelSQL, fields
|
||||
|
||||
class Configuration(ModelSingleton, ModelSQL):
|
||||
__name__ = 'ir.configuration'
|
||||
series = fields.Char("Series")
|
||||
language = fields.Char('language')
|
||||
hostname = fields.Char("Hostname", strip=False)
|
||||
_get_language_cache = Cache('ir_configuration.get_language')
|
||||
|
||||
@@ -13,6 +13,7 @@ from sql.functions import CurrentTimestamp
|
||||
|
||||
import trytond.convert as convert
|
||||
import trytond.tools as tools
|
||||
from trytond import __series__
|
||||
from trytond.config import config
|
||||
from trytond.const import MODULES_GROUP
|
||||
from trytond.exceptions import MissingDependenciesException
|
||||
@@ -22,6 +23,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
ir_module = Table('ir_module')
|
||||
ir_model_data = Table('ir_model_data')
|
||||
ir_configuration = Table('ir_configuration')
|
||||
|
||||
MODULES_PATH = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
@@ -288,6 +290,8 @@ def load_module_graph(graph, pool, update=None, lang=None, indexes=None):
|
||||
model._update_sql_indexes(concurrently=concurrently)
|
||||
|
||||
if update:
|
||||
cursor.execute(*ir_configuration.update(
|
||||
[ir_configuration.series], [__series__]))
|
||||
if indexes or indexes is None:
|
||||
create_indexes(concurrently=False)
|
||||
else:
|
||||
|
||||
@@ -19,7 +19,7 @@ from werkzeug.wrappers import Request as _Request
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from trytond import backend, security
|
||||
from trytond.config import config
|
||||
from trytond.config import config, get_hostname
|
||||
from trytond.exceptions import RateLimitException, UserError, UserWarning
|
||||
from trytond.pool import Pool
|
||||
from trytond.tools import cached_property
|
||||
@@ -191,6 +191,12 @@ def with_pool(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, database_name, *args, **kwargs):
|
||||
database_list = Pool.database_list()
|
||||
if database_name not in database_list:
|
||||
with Transaction().start(None, 0, readonly=True) as transaction:
|
||||
hostname = get_hostname(request.host)
|
||||
db_list = transaction.database.list(hostname=hostname)
|
||||
if database_name not in db_list:
|
||||
abort(HTTPStatus.NOT_FOUND)
|
||||
pool = Pool(database_name)
|
||||
if database_name not in database_list:
|
||||
with Transaction().start(database_name, 0, readonly=True):
|
||||
|
||||
@@ -5,6 +5,13 @@ import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
try:
|
||||
from http import HTTPStatus
|
||||
except ImportError:
|
||||
from http import client as HTTPStatus
|
||||
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from trytond import backend
|
||||
from trytond.config import config
|
||||
from trytond.exceptions import LoginException, RateLimitException
|
||||
@@ -16,6 +23,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def _get_pool(dbname):
|
||||
database_list = Pool.database_list()
|
||||
if dbname not in database_list:
|
||||
db_list = Transaction().database.list()
|
||||
if dbname not in db_list:
|
||||
abort(HTTPStatus.NOT_FOUND)
|
||||
pool = Pool(dbname)
|
||||
if dbname not in database_list:
|
||||
pool.init()
|
||||
|
||||
Reference in new issue
Block a user