Added support for schema level restriction. Fixes #5583

Allow user to edit the connection properties when the database server
is already connected.
This commit is contained in:
Nikhil Mohite 2020-06-30 19:15:23 +05:30 committed by Akshay Joshi
parent 4c05287677
commit c873218c32
15 changed files with 492 additions and 214 deletions

View File

@ -97,6 +97,19 @@ Follow these steps to add additional parameter value definitions; to discard a
parameter, click the trash icon to the left of the row and confirm deletion in
the *Delete Row* popup.
Click the *Advanced* tab to continue.
.. image:: images/database_advanced.png
:alt: Database dialog advanced tab
:align: center
Use the *Advanced* tab to set advanced parameters for the database.
* Use *Schema restriction* field to provide a SQL restriction that will be used
against the pg_namespace table to limit the schemas that you see.
For example, you might enter: *public* so that only *public* are shown in
the pgAdmin browser.Separate entries with a comma or tab as you type.
Click the *SQL* tab to continue.
Your entries in the *Database* dialog generate a SQL command (see an example

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #5583 <https://redmine.postgresql.org/issues/5583>`_ - Added support for schema level restriction.
Housekeeping
************

View File

@ -0,0 +1,32 @@
"""empty message
Revision ID: 84700139beb0
Revises: d39482714a2e
Create Date: 2020-06-24 15:53:56.489518
"""
from pgadmin.model import db
# revision identifiers, used by Alembic.
revision = '84700139beb0'
down_revision = 'd39482714a2e'
branch_labels = None
depends_on = None
def upgrade():
db.engine.execute("""
CREATE TABLE "database" (
"id" INTEGER NOT NULL,
"schema_res" TEXT,
"server" INTEGER NOT NULL,
PRIMARY KEY("id","server"),
FOREIGN KEY("server") REFERENCES "server"("id")
);
""")
def downgrade():
pass

View File

@ -536,7 +536,7 @@ class ServerNode(PGChildNodeView):
if connected:
for arg in (
'host', 'hostaddr', 'port', 'db', 'username', 'sslmode',
'hostaddr', 'db', 'sslmode',
'role', 'service'
):
if arg in data:
@ -1016,6 +1016,7 @@ class ServerNode(PGChildNodeView):
# Connect the Server
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
manager.update(server)
conn = manager.connection()
# Get enc key

View File

@ -32,7 +32,7 @@ from pgadmin.utils.driver import get_driver
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.model import Server
from pgadmin.model import db, Server, Database
class DatabaseModule(CollectionNodeModule):
@ -423,6 +423,11 @@ class DatabaseView(PGChildNodeView):
)
status, res1 = self.conn.execute_dict(SQL)
database = Database.query.filter_by(id=did, server=sid).first()
if database:
result['schema_res'] = database.schema_res.split(
',') if database.schema_res else []
if not status:
return internal_server_error(errormsg=res1)
@ -611,6 +616,11 @@ class DatabaseView(PGChildNodeView):
return internal_server_error(errormsg=res)
response = res['rows'][0]
# Add database entry into database table with schema_restrictions.
database = Database(id=response['did'], server=sid,
schema_res=','.join(data['schema_res']))
db.session.add(database)
db.session.commit()
return jsonify(
node=self.blueprint.generate_browser_node(
@ -627,53 +637,27 @@ class DatabaseView(PGChildNodeView):
)
)
@check_precondition(action='update')
def update(self, gid, sid, did):
"""Update the database."""
@staticmethod
def _update_db_schema_res(data, did, sid):
database = Database.query.filter_by(id=did, server=sid).first()
if 'schema_res' in data:
if database:
data['schema_res'] = ','.join(data['schema_res'])
setattr(database, 'schema_res', data['schema_res'])
else:
database_obj = Database(id=did, server=sid,
schema_res=','.join(
data['schema_res']))
db.session.add(database_obj)
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
# Generic connection for offline updates
conn = self.manager.connection(conn_id='db_offline_update')
status, errmsg = conn.connect()
if not status:
current_app.logger.error(
"Could not create database connection for offline updates\n"
"Err: {0}".format(errmsg)
)
return internal_server_error(errmsg)
if did is not None:
# Fetch the name of database for comparison
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'nodes.sql']),
did=did, conn=self.conn, last_system_oid=0
)
)
if not status:
return internal_server_error(errormsg=rset)
if len(rset['rows']) == 0:
return gone(
_('Could not find the database on the server.')
)
data['old_name'] = (rset['rows'][0])['name']
if 'name' not in data:
data['name'] = data['old_name']
# Release any existing connection from connection manager
# to perform offline operation
self.manager.release(did=did)
def _check_rename_db_or_change_table_space(self, data, conn, all_ids):
for action in ["rename_database", "tablespace"]:
SQL = self.get_offline_sql(gid, sid, data, did, action)
SQL = SQL.strip('\n').strip(' ')
if SQL and SQL != "":
status, msg = conn.execute_scalar(SQL)
sql = self.get_offline_sql(all_ids['gid'], all_ids['sid'], data,
all_ids['did'], action)
sql = sql.strip('\n').strip(' ')
if sql and sql != "":
status, msg = conn.execute_scalar(sql)
if not status:
# In case of error from server while rename it,
# reconnect to the database with old name again.
@ -684,13 +668,38 @@ class DatabaseView(PGChildNodeView):
if not status:
current_app.logger.error(
'Could not reconnected to database(#{0}).\n'
'Error: {1}'.format(did, errmsg)
'Error: {1}'.format(all_ids['did'], errmsg)
)
return internal_server_error(errormsg=msg)
return True, msg
QueryHistory.update_history_dbname(
current_user.id, sid, data['old_name'], data['name'])
# Make connection for database again
current_user.id, all_ids['sid'], data['old_name'],
data['name'])
return False, ''
def _fetch_db_details(self, data, did):
if did is not None:
# Fetch the name of database for comparison
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'nodes.sql']),
did=did, conn=self.conn, last_system_oid=0
)
)
if not status:
return True, rset
if len(rset['rows']) == 0:
return gone(
_('Could not find the database on the server.')
)
data['old_name'] = (rset['rows'][0])['name']
if 'name' not in data:
data['name'] = data['old_name']
return False, ''
def _reconnect_connect_db(self, data, did):
if self._db['datallowconn']:
self.conn = self.manager.connection(
database=data['name'], auto_reconnect=True
@ -702,12 +711,70 @@ class DatabaseView(PGChildNodeView):
'Could not connected to database(#{0}).\n'
'Error: {1}'.format(did, errmsg)
)
return True, errmsg
return False, ''
def _commit_db_changes(self, res, can_drop):
if self.manager.db == res['name']:
can_drop = False
try:
db.session.commit()
except Exception as e:
current_app.logger.exception(e)
return True, e.message, False
return False, '', can_drop
def _get_data_from_request(self):
return request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
@check_precondition(action='update')
def update(self, gid, sid, did):
"""Update the database."""
data = self._get_data_from_request()
# Update schema restriction in db object.
DatabaseView._update_db_schema_res(data, did, sid)
# Generic connection for offline updates
conn = self.manager.connection(conn_id='db_offline_update')
status, errmsg = conn.connect()
if not status:
current_app.logger.error(
"Could not create database connection for offline updates\n"
"Err: {0}".format(errmsg)
)
return internal_server_error(errmsg)
SQL = self.get_online_sql(gid, sid, data, did)
SQL = SQL.strip('\n').strip(' ')
if SQL and SQL != "":
status, msg = self.conn.execute_scalar(SQL)
fetching_error, err_msg = self._fetch_db_details(data, did)
if fetching_error:
return internal_server_error(errormsg=err_msg)
# Release any existing connection from connection manager
# to perform offline operation
self.manager.release(did=did)
all_ids = {
'gid': gid,
'sid': sid,
'did': did
}
is_error, errmsg = self._check_rename_db_or_change_table_space(data,
conn,
all_ids)
if is_error:
return internal_server_error(errmsg)
# Make connection for database again
connection_error, errmsg = self._reconnect_connect_db(data, did)
if connection_error:
return internal_server_error(errmsg)
sql = self.get_online_sql(gid, sid, data, did)
sql = sql.strip('\n').strip(' ')
if sql and sql != "":
status, msg = self.conn.execute_scalar(sql)
if not status:
return internal_server_error(errormsg=msg)
@ -733,9 +800,15 @@ class DatabaseView(PGChildNodeView):
res = rset['rows'][0]
can_drop = can_dis_conn = True
if self.manager.db == res['name']:
can_drop = can_dis_conn = False
can_drop = True
error, errmsg, is_can_drop = self._commit_db_changes(res, can_drop)
if error:
return make_json_response(
success=0,
errormsg=errmsg
)
can_drop = can_dis_conn = is_can_drop
return jsonify(
node=self.blueprint.generate_browser_node(

View File

@ -24,6 +24,7 @@ from pgadmin.utils.ajax import make_json_response, internal_server_error, \
make_response as ajax_response, gone, bad_request
from pgadmin.utils.driver import get_driver
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.model import Database
"""
This module is responsible for generating two nodes
@ -384,10 +385,21 @@ class SchemaView(PGChildNodeView):
Returns:
JSON of available schema nodes
"""
database = Database.query.filter_by(id=did, server=sid).first()
param = None
if database:
schema_restrictions = database.schema_res
if schema_restrictions:
schema_res = ",".join(
["'%s'"] * len(schema_restrictions.split(',')))
param = schema_res % (tuple(schema_restrictions.split(',')))
SQL = render_template(
"/".join([self.template_path, 'sql/properties.sql']),
_=gettext,
show_sysobj=self.blueprint.show_system_objects
show_sysobj=self.blueprint.show_system_objects,
schema_restrictions=param
)
status, res = self.conn.execute_dict(SQL)
@ -413,11 +425,22 @@ class SchemaView(PGChildNodeView):
JSON of available schema child nodes
"""
res = []
database = Database.query.filter_by(id=did, server=sid).first()
param = None
if database:
schema_restrictions = database.schema_res
if schema_restrictions:
schema_res = ",".join(
["'%s'"] * len(schema_restrictions.split(',')))
param = schema_res % (tuple(schema_restrictions.split(',')))
SQL = render_template(
"/".join([self.template_path, 'sql/nodes.sql']),
show_sysobj=self.blueprint.show_system_objects,
_=gettext,
scid=scid
scid=scid,
schema_restrictions=param
)
status, rset = self.conn.execute_2darray(SQL)
@ -428,10 +451,9 @@ class SchemaView(PGChildNodeView):
if scid is not None:
if len(rset['rows']) == 0:
return gone(gettext("""
Could not find the schema in the database.
It may have been removed by another user.
"""))
return gone(gettext(
"""Could not find the schema in the database.
It may have been removed by another user."""))
row = rset['rows'][0]
return make_json_response(
data=self.blueprint.generate_browser_node(
@ -896,10 +918,9 @@ It may have been removed by another user.
return internal_server_error(errormsg=res)
if len(res['rows']) == 0:
return gone(gettext("""
Could not find the schema in the database.
It may have been removed by another user.
"""))
return gone(gettext(
"""Could not find the schema in the database.
It may have been removed by another user."""))
data = res['rows'][0]
backend_support_keywords = kwargs.copy()

View File

@ -17,4 +17,10 @@ WHERE
NOT (
{{ CATALOGS.LIST('nsp') }}
)
{% if schema_restrictions %}
AND
nsp.nspname in ({{schema_restrictions}})
{% endif %}
ORDER BY nspname;

View File

@ -50,4 +50,8 @@ WHERE
NOT (
{{ CATALOGS.LIST('nsp') }}
)
{% if schema_restrictions %}
AND
nsp.nspname in ({{schema_restrictions}})
{% endif %}
ORDER BY 1, nspname;

View File

@ -46,6 +46,7 @@ define('pgadmin.node.database', [
node_image: function() {
return 'pg-icon-database';
},
width: '700px',
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@ -297,6 +298,7 @@ define('pgadmin.node.database', [
defseqacl: [],
is_template: false,
deftypeacl: [],
schema_res:'',
},
// Default values!
@ -310,7 +312,8 @@ define('pgadmin.node.database', [
pgBrowser.Node.Model.prototype.initialize.apply(this, arguments);
},
schema: [{
schema: [
{
id: 'name', label: gettext('Database'), cell: 'string',
editable: false, type: 'text',
},{
@ -453,6 +456,44 @@ define('pgadmin.node.database', [
mode: ['edit', 'create'], min_version: 90200,
},
],
},{
type: 'collection', group: gettext('Advanced'),
},
{
id: 'schema_res', label: gettext('Schema restriction'),
type: 'select2', group: gettext('Advanced'),
mode: ['properties', 'edit', 'create'],
select2: {
multiple: true, allowClear: false, tags: true,
tokenSeparators: [','], first_empty: false,
selectOnClose: true, emptyOptions: true,
},
control: Backform.Select2Control.extend({
onChange: function() {
Backform.Select2Control.prototype.onChange.apply(this, arguments);
if (!this.model || !(
this.model.changed &&
this.model.get('oid') !== undefined
)) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.schema_res != this.model.changed.schema_res)
{
this.model.inform_text = gettext(
'Please refresh the Schemas node to make changes to the schema restriction take effect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},
{
id: 'note', label: gettext('Note: Changes to the schema restriction will require the Schemas node in the browser to be refreshed before they will be shown.'),
group: gettext('Advanced'), type: 'help',
mode: ['edit', 'create'],
},
],
validate: function() {

View File

@ -41,7 +41,8 @@ class DatabasesUpdateTestCase(BaseTestGenerator):
try:
data = {
"comments": "This is db update comment",
"id": self.db_id
"id": self.db_id,
"schema_res": ["public"]
}
response = self.tester.put(
self.url + str(utils.SERVER_GROUP) + '/' + str(

View File

@ -73,7 +73,8 @@ def get_db_data(db_owner):
"name": "db_add_%s" % str(uuid.uuid4())[1: 8],
"privileges": [],
"securities": [],
"variables": []
"variables": [],
"schema_res": ["public", "sample"]
}
return data

View File

@ -39,7 +39,6 @@ define('pgadmin.node.server', [
}],
validate: function() {
this.errorModel.clear();
if (_.isUndefined(this.get('label')) ||
_.isNull(this.get('label')) ||
String(this.get('label')).replace(/^\s+|\s+$/g, '') == '') {
@ -66,7 +65,6 @@ define('pgadmin.node.server', [
return d && d.connected;
},
Init: function() {
/* Avoid multiple registration of same menus */
if (this.initialized)
return;
@ -785,16 +783,70 @@ define('pgadmin.node.server', [
mode: ['properties', 'edit', 'create'],
},{
id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
mode: ['properties', 'edit', 'create'],
control: Backform.InputControl.extend({
onChange: function() {
Backform.InputControl.prototype.onChange.apply(this, arguments);
if (!this.model || !this.model.changed) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.host != this.model.changed.host && !this.model.isNew() && this.model.get('connected'))
{
this.model.inform_text = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},{
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected', min: 1, max: 65535,
mode: ['properties', 'edit', 'create'], min: 1, max: 65535,
control: Backform.InputControl.extend({
onChange: function() {
Backform.InputControl.prototype.onChange.apply(this, arguments);
if (!this.model || !this.model.changed) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.port != this.model.changed.port && !this.model.isNew() && this.model.get('connected'))
{
this.model.inform_text = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},{
id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
},{
id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
mode: ['properties', 'edit', 'create'],
control: Backform.InputControl.extend({
onChange: function() {
Backform.InputControl.prototype.onChange.apply(this, arguments);
if (!this.model || !this.model.changed) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.username != this.model.changed.username && !this.model.isNew() && this.model.get('connected'))
{
this.model.inform_text = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},{
id: 'password', label: gettext('Password'), type: 'password', maxlength: '2000',
group: gettext('Connection'), control: 'input', mode: ['create'], deps: ['connect_now'],

View File

@ -1339,6 +1339,21 @@ define('pgadmin.browser.node', [
}
}.bind(panel),
informBeforeAttributeChange = function(ok_callback) {
var j = this.$container.find('.obj_properties').first();
view = j && j.data('obj-view');
if (view && view.model && !_.isUndefined(view.model.inform_text) && !_.isNull(view.model.inform_text)) {
Alertify.alert(
gettext('Warning'),
gettext(view.model.inform_text)
);
}
ok_callback();
return true;
}.bind(panel),
onSave = function(view, saveBtn) {
var m = view.model,
d = m.toJSON(true),
@ -1535,9 +1550,11 @@ define('pgadmin.browser.node', [
warnBeforeAttributeChange.call(
panel,
function() {
informBeforeAttributeChange.call(panel, function(){
setTimeout(function() {
onSave.call(this, view, btn);
}, 0);
});
}
);
});

View File

@ -290,3 +290,18 @@ class QueryHistoryModel(db.Model):
dbname = db.Column(db.String(), nullable=False, primary_key=True)
query_info = db.Column(db.String(), nullable=False)
last_updated_flag = db.Column(db.String(), nullable=False)
class Database(db.Model):
"""
Define a Database.
"""
__tablename__ = 'database'
id = db.Column(db.Integer, primary_key=True)
schema_res = db.Column(db.String(256), nullable=True)
server = db.Column(
db.Integer,
db.ForeignKey('server.id'),
nullable=False,
primary_key=True
)