mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Change server password feature
This commit is contained in:
parent
8e4e1640e7
commit
4816f5ed12
@ -19,7 +19,7 @@ from pgadmin.browser.utils import PGChildNodeView
|
|||||||
import traceback
|
import traceback
|
||||||
from flask.ext.babel import gettext
|
from flask.ext.babel import gettext
|
||||||
import pgadmin.browser.server_groups as sg
|
import pgadmin.browser.server_groups as sg
|
||||||
from pgadmin.utils.crypto import encrypt
|
from pgadmin.utils.crypto import encrypt, decrypt, pqencryptpassword
|
||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
from pgadmin.browser.server_groups.servers.types import ServerType
|
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||||
import config
|
import config
|
||||||
@ -195,7 +195,9 @@ class ServerNode(PGChildNodeView):
|
|||||||
[{'post': 'create_restore_point'}],
|
[{'post': 'create_restore_point'}],
|
||||||
'connect': [{
|
'connect': [{
|
||||||
'get': 'connect_status', 'post': 'connect', 'delete': 'disconnect'
|
'get': 'connect_status', 'post': 'connect', 'delete': 'disconnect'
|
||||||
}]
|
}],
|
||||||
|
'change_password': [{
|
||||||
|
'post': 'change_password'}]
|
||||||
})
|
})
|
||||||
|
|
||||||
def nodes(self, gid):
|
def nodes(self, gid):
|
||||||
@ -838,4 +840,100 @@ class ServerNode(PGChildNodeView):
|
|||||||
)
|
)
|
||||||
return internal_server_error(errormsg=str(e))
|
return internal_server_error(errormsg=str(e))
|
||||||
|
|
||||||
|
def change_password(self, gid, sid):
|
||||||
|
"""
|
||||||
|
This function is used to change the password of the
|
||||||
|
Database Server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gid: Group id
|
||||||
|
sid: Server id
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(request.form['data'])
|
||||||
|
if data and ('password' not in data or
|
||||||
|
data['password'] == '' or
|
||||||
|
'newPassword' not in data or
|
||||||
|
data['newPassword'] == '' or
|
||||||
|
'confirmPassword' not in data or
|
||||||
|
data['confirmPassword'] == ''):
|
||||||
|
return make_json_response(
|
||||||
|
status=400,
|
||||||
|
success=0,
|
||||||
|
errormsg=gettext(
|
||||||
|
"Couldn't find the required parameter(s)."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if data['newPassword'] != data['confirmPassword']:
|
||||||
|
return make_json_response(
|
||||||
|
status=200,
|
||||||
|
success=0,
|
||||||
|
errormsg=gettext(
|
||||||
|
"Passwords do not match."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch Server Details
|
||||||
|
server = Server.query.filter_by(id=sid).first()
|
||||||
|
if server is None:
|
||||||
|
return bad_request(gettext("Server not found."))
|
||||||
|
|
||||||
|
# Fetch User Details.
|
||||||
|
user = User.query.filter_by(id=current_user.id).first()
|
||||||
|
if user is None:
|
||||||
|
return unauthorized(gettext("Unauthorized request."))
|
||||||
|
|
||||||
|
from pgadmin.utils.driver import get_driver
|
||||||
|
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||||
|
conn = manager.connection()
|
||||||
|
|
||||||
|
decrypted_password = decrypt(manager.password, user.password)
|
||||||
|
|
||||||
|
if isinstance(decrypted_password, bytes):
|
||||||
|
decrypted_password = decrypted_password.decode()
|
||||||
|
|
||||||
|
password = data['password']
|
||||||
|
|
||||||
|
# Validate old password before setting new.
|
||||||
|
if password != decrypted_password:
|
||||||
|
return unauthorized(gettext("Incorrect password."))
|
||||||
|
|
||||||
|
# Hash new password before saving it.
|
||||||
|
password = pqencryptpassword(data['newPassword'], manager.user)
|
||||||
|
|
||||||
|
SQL = render_template("/".join([
|
||||||
|
'servers/sql',
|
||||||
|
'9.2_plus' if manager.version >= 90200 else '9.1_plus',
|
||||||
|
'change_password.sql'
|
||||||
|
]),
|
||||||
|
conn=conn, _=gettext,
|
||||||
|
user=manager.user, encrypted_password=password)
|
||||||
|
|
||||||
|
status, res = conn.execute_scalar(SQL)
|
||||||
|
|
||||||
|
if not status:
|
||||||
|
return internal_server_error(errormsg=res)
|
||||||
|
|
||||||
|
password = encrypt(data['newPassword'], user.password)
|
||||||
|
# Check if old password was stored in pgadmin4 sqlite database.
|
||||||
|
# If yes then update that password.
|
||||||
|
if server.password is not None:
|
||||||
|
setattr(server, 'password', password)
|
||||||
|
db.session.commit()
|
||||||
|
# Also update password in connection manager.
|
||||||
|
manager.password = password
|
||||||
|
manager.update_session()
|
||||||
|
|
||||||
|
return make_json_response(
|
||||||
|
status=200,
|
||||||
|
success=1,
|
||||||
|
info=gettext(
|
||||||
|
"Password changed successfully."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return internal_server_error(errormsg=str(e))
|
||||||
|
|
||||||
ServerNode.register_node_view(blueprint)
|
ServerNode.register_node_view(blueprint)
|
||||||
|
@ -18,3 +18,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
height: 1.3em;
|
height: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.change_password {
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
@ -48,6 +48,11 @@ function($, _, S, pgAdmin, pgBrowser, alertify) {
|
|||||||
applies: ['tools', 'context'], callback: 'restore_point',
|
applies: ['tools', 'context'], callback: 'restore_point',
|
||||||
category: 'restore', priority: 7, label: '{{ _('Add named restore point') }}',
|
category: 'restore', priority: 7, label: '{{ _('Add named restore point') }}',
|
||||||
icon: 'fa fa-anchor', enable : 'is_applicable'
|
icon: 'fa fa-anchor', enable : 'is_applicable'
|
||||||
|
},{
|
||||||
|
name: 'change_password', node: 'server', module: this,
|
||||||
|
applies: ['file'], callback: 'change_password',
|
||||||
|
label: '{{ _('Change Password...') }}',
|
||||||
|
icon: 'fa fa-lock', enable : 'is_connected'
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
pgBrowser.messages['PRIV_GRANTEE_NOT_SPECIFIED'] =
|
pgBrowser.messages['PRIV_GRANTEE_NOT_SPECIFIED'] =
|
||||||
@ -259,6 +264,163 @@ function($, _, S, pgAdmin, pgBrowser, alertify) {
|
|||||||
evt.cancel = false;
|
evt.cancel = false;
|
||||||
}
|
}
|
||||||
).set({'title':'Restore point name'});
|
).set({'title':'Restore point name'});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Change password */
|
||||||
|
change_password: function(args){
|
||||||
|
var input = args || {},
|
||||||
|
obj = this,
|
||||||
|
t = pgBrowser.tree,
|
||||||
|
i = input.item || t.selected(),
|
||||||
|
d = i && i.length == 1 ? t.itemData(i) : undefined,
|
||||||
|
node = d && pgBrowser.Nodes[d._type],
|
||||||
|
url = obj.generate_url(i, 'change_password', d, true);
|
||||||
|
|
||||||
|
if (!d)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(!alertify.changeServerPassword) {
|
||||||
|
var newPasswordModel = Backbone.Model.extend({
|
||||||
|
defaults: {
|
||||||
|
user_name: undefined,
|
||||||
|
password: undefined,
|
||||||
|
newPassword: undefined,
|
||||||
|
confirmPassword: undefined
|
||||||
|
},
|
||||||
|
validate: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
passwordChangeFields = [{
|
||||||
|
name: 'user_name', label: '{{ _('User') }}',
|
||||||
|
type: 'text', disabled: true, control: 'input'
|
||||||
|
},{
|
||||||
|
name: 'password', label: '{{ _('Current Password') }}',
|
||||||
|
type: 'password', disabled: false, control: 'input',
|
||||||
|
required: true
|
||||||
|
},{
|
||||||
|
name: 'newPassword', label: '{{ _('New Password') }}',
|
||||||
|
type: 'password', disabled: false, control: 'input',
|
||||||
|
required: true
|
||||||
|
},{
|
||||||
|
name: 'confirmPassword', label: '{{ _('Confirm Password') }}',
|
||||||
|
type: 'password', disabled: false, control: 'input',
|
||||||
|
required: true
|
||||||
|
}];
|
||||||
|
|
||||||
|
|
||||||
|
alertify.dialog('changeServerPassword' ,function factory() {
|
||||||
|
return {
|
||||||
|
main: function(params) {
|
||||||
|
var title = '{{ _('Change Password') }} ';
|
||||||
|
this.set('title', title);
|
||||||
|
this.user_name = params.user.name;
|
||||||
|
},
|
||||||
|
setup:function() {
|
||||||
|
return {
|
||||||
|
buttons: [{
|
||||||
|
text: '{{ _('Ok') }}', key: 27, className: 'btn btn-primary', attrs:{name:'submit'}
|
||||||
|
},{
|
||||||
|
text: '{{ _('Cancel') }}', key: 27, className: 'btn btn-danger', attrs:{name:'cancel'}
|
||||||
|
}],
|
||||||
|
// Set options for dialog
|
||||||
|
options: {
|
||||||
|
padding : !1,
|
||||||
|
overflow: !1,
|
||||||
|
model: 0,
|
||||||
|
resizable: true,
|
||||||
|
maximizable: true,
|
||||||
|
pinnable: false,
|
||||||
|
closableByDimmer: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
// triggered when the dialog is closed
|
||||||
|
onclose: function() {
|
||||||
|
if (this.view) {
|
||||||
|
this.view.remove({data: true, internal: true, silent: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: function() {
|
||||||
|
//console.log(this.get('server').user.name);
|
||||||
|
var self = this;
|
||||||
|
// Disable Backup button until user provides Filename
|
||||||
|
this.__internal.buttons[0].element.disabled = true;
|
||||||
|
var $container = $("<div class='change_password'></div>"),
|
||||||
|
newpasswordmodel = new newPasswordModel({'user_name': self.user_name});
|
||||||
|
|
||||||
|
var view = this.view = new Backform.Form({
|
||||||
|
el: $container,
|
||||||
|
model: newpasswordmodel,
|
||||||
|
fields: passwordChangeFields});
|
||||||
|
|
||||||
|
view.render();
|
||||||
|
|
||||||
|
this.elements.content.appendChild($container.get(0));
|
||||||
|
|
||||||
|
// Listen to model & if filename is provided then enable Backup button
|
||||||
|
this.view.model.on('change', function() {
|
||||||
|
var that = this,
|
||||||
|
password = this.get('password'),
|
||||||
|
newPassword = this.get('newPassword'),
|
||||||
|
confirmPassword = this.get('confirmPassword');
|
||||||
|
|
||||||
|
if (_.isUndefined(password) || _.isNull(password) || password == '' ||
|
||||||
|
_.isUndefined(newPassword) || _.isNull(newPassword) || newPassword == '' ||
|
||||||
|
_.isUndefined(confirmPassword) || _.isNull(confirmPassword) || confirmPassword == '') {
|
||||||
|
self.__internal.buttons[0].element.disabled = true;
|
||||||
|
} else if (newPassword != confirmPassword) {
|
||||||
|
self.__internal.buttons[0].element.disabled = true;
|
||||||
|
|
||||||
|
this.errorTimeout && clearTimeout(this.errorTimeout);
|
||||||
|
this.errorTimeout = setTimeout(function() {
|
||||||
|
that.errorModel.set('confirmPassword', '{{ _('Passwords do not match.') }}');
|
||||||
|
} ,400);
|
||||||
|
}else {
|
||||||
|
that.errorModel.clear();
|
||||||
|
self.__internal.buttons[0].element.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Callback functions when click on the buttons of the Alertify dialogs
|
||||||
|
callback: function(e) {
|
||||||
|
if (e.button.element.name == "submit") {
|
||||||
|
var self = this,
|
||||||
|
args = this.view.model.toJSON();
|
||||||
|
|
||||||
|
e.cancel = true;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method:'POST',
|
||||||
|
data:{'data': JSON.stringify(args) },
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
alertify.success(res.info);
|
||||||
|
self.close();
|
||||||
|
} else {
|
||||||
|
alertify.error(res.errormsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
try {
|
||||||
|
var err = $.parseJSON(xhr.responseText);
|
||||||
|
if (err.success == 0) {
|
||||||
|
alertify.error(err.errormsg);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
alertify.changeServerPassword(d).resizeTo('40%','52%');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
model: pgAdmin.Browser.Node.Model.extend({
|
model: pgAdmin.Browser.Node.Model.extend({
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
{# Change database server password #}
|
||||||
|
ALTER USER {{conn|qtIdent(user)}} WITH ENCRYPTED PASSWORD {{encrypted_password|qtLiteral}};
|
@ -0,0 +1,2 @@
|
|||||||
|
{# Change database server password #}
|
||||||
|
ALTER USER {{conn|qtIdent(user)}} WITH ENCRYPTED PASSWORD {{encrypted_password|qtLiteral}};
|
@ -12,6 +12,7 @@
|
|||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
|
|
||||||
padding_string = b'}'
|
padding_string = b'}'
|
||||||
|
|
||||||
@ -68,3 +69,39 @@ def pad(str):
|
|||||||
|
|
||||||
# Add padding to make key 32 bytes long
|
# Add padding to make key 32 bytes long
|
||||||
return str + ((32 - len(str) % 32) * padding_string)
|
return str + ((32 - len(str) % 32) * padding_string)
|
||||||
|
|
||||||
|
|
||||||
|
def pqencryptpassword(password, user):
|
||||||
|
|
||||||
|
"""
|
||||||
|
pqencryptpassword -- to encrypt a password
|
||||||
|
This is intended to be used by client applications that wish to send
|
||||||
|
commands like ALTER USER joe PASSWORD 'pwd'. The password need not
|
||||||
|
be sent in cleartext if it is encrypted on the client side. This is
|
||||||
|
good because it ensures the cleartext password won't end up in logs,
|
||||||
|
pg_stat displays, etc. We export the function so that clients won't
|
||||||
|
be dependent on low-level details like whether the enceyption is MD5
|
||||||
|
or something else.
|
||||||
|
|
||||||
|
Arguments are the cleartext password, and the SQL name of the user it
|
||||||
|
is for.
|
||||||
|
|
||||||
|
Return value is "md5" followed by a 32-hex-digit MD5 checksum..
|
||||||
|
|
||||||
|
Args:
|
||||||
|
password:
|
||||||
|
user:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
m = hashlib.md5()
|
||||||
|
|
||||||
|
# Place salt at the end because it may be known by users trying to crack
|
||||||
|
# the MD5 output.
|
||||||
|
|
||||||
|
m.update(password.encode())
|
||||||
|
m.update(user.encode())
|
||||||
|
|
||||||
|
return "md5" + m.hexdigest()
|
||||||
|
Loading…
Reference in New Issue
Block a user