Added LDAP authentication support. Fixes #2186

This commit is contained in:
Khushboo Vashi
2020-04-06 15:57:05 +05:30
committed by Akshay Joshi
parent 8ceeb39268
commit f77aa3284f
26 changed files with 1243 additions and 165 deletions

View File

@@ -74,7 +74,8 @@ class UserManagementModule(PgAdminModule):
'user_management.roles', 'user_management.role',
'user_management.update_user', 'user_management.delete_user',
'user_management.create_user', 'user_management.users',
'user_management.user', current_app.login_manager.login_view
'user_management.user', current_app.login_manager.login_view,
'user_management.auth_sources', 'user_management.auth_sources'
]
@@ -100,7 +101,7 @@ def validate_user(data):
else:
raise Exception(_("Passwords do not match."))
if 'email' in data and data['email'] != "":
if 'email' in data and data['email'] and data['email'] != "":
if email_filter.match(data['email']):
new_data['email'] = data['email']
else:
@@ -112,6 +113,12 @@ def validate_user(data):
if 'active' in data and data['active'] != "":
new_data['active'] = data['active']
if 'username' in data and data['username'] != "":
new_data['username'] = data['username']
if 'auth_source' in data and data['auth_source'] != "":
new_data['auth_source'] = data['auth_source']
return new_data
@@ -140,6 +147,7 @@ def script():
@pgCSRFProtect.exempt
@login_required
def current_user_info():
return Response(
response=render_template(
"user_management/js/current_user.js",
@@ -148,13 +156,15 @@ def current_user_info():
user_id=current_user.id,
email=current_user.email,
name=(
current_user.email.split('@')[0] if config.SERVER_MODE is True
current_user.username.split('@')[0] if
config.SERVER_MODE is True
else 'postgres'
),
allow_save_password='true' if config.ALLOW_SAVE_PASSWORD
else 'false',
allow_save_tunnel_password='true'
if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false',
auth_sources=config.AUTHENTICATION_SOURCES,
),
status=200,
mimetype="application/javascript"
@@ -180,9 +190,11 @@ def user(uid):
u = User.query.get(uid)
res = {'id': u.id,
'username': u.username,
'email': u.email,
'active': u.active,
'role': u.roles[0].id
'role': u.roles[0].id,
'auth_source': u.auth_source
}
else:
users = User.query.all()
@@ -190,9 +202,11 @@ def user(uid):
users_data = []
for u in users:
users_data.append({'id': u.id,
'username': u.username,
'email': u.email,
'active': u.active,
'role': u.roles[0].id
'role': u.roles[0].id,
'auth_source': u.auth_source
})
res = users_data
@@ -215,11 +229,29 @@ def create():
request.data, encoding='utf-8'
)
for f in ('email', 'role', 'active', 'newPassword', 'confirmPassword'):
status, res = create_user(data)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res,
status=200
)
def create_user(data):
if 'auth_source' in data and data['auth_source'] != 'internal':
req_params = ('username', 'role', 'active', 'auth_source')
else:
req_params = ('email', 'role', 'active', 'newPassword',
'confirmPassword')
for f in req_params:
if f in data and data[f] != '':
continue
else:
return bad_request(errormsg=_("Missing field: '{0}'".format(f)))
return False, _("Missing field: '{0}'".format(f))
try:
new_data = validate_user(data)
@@ -228,13 +260,23 @@ def create():
new_data['roles'] = [Role.query.get(new_data['roles'])]
except Exception as e:
return bad_request(errormsg=_(str(e)))
return False, str(e)
try:
usr = User(email=new_data['email'],
username = new_data['username'] if 'username' in new_data \
else new_data['email']
email = new_data['email'] if 'email' in new_data else None
password = new_data['password'] if 'password' in new_data else None
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
else current_app.PGADMIN_DEFAULT_AUTH_SOURCE
usr = User(username=username,
email=email,
roles=new_data['roles'],
active=new_data['active'],
password=new_data['password'])
password=password,
auth_source=auth_source)
db.session.add(usr)
db.session.commit()
# Add default server group for new user.
@@ -242,18 +284,15 @@ def create():
db.session.add(server_group)
db.session.commit()
except Exception as e:
return internal_server_error(errormsg=str(e))
return False, str(e)
res = {'id': usr.id,
'email': usr.email,
'active': usr.active,
'role': usr.roles[0].id
}
return ajax_response(
response=res,
status=200
)
return True, {
'id': usr.id,
'username': usr.username,
'email': usr.email,
'active': usr.active,
'role': usr.roles[0].id
}
@blueprint.route(
@@ -337,9 +376,11 @@ def update(uid):
db.session.commit()
res = {'id': usr.id,
'username': usr.username,
'email': usr.email,
'active': usr.active,
'role': usr.roles[0].id
'role': usr.roles[0].id,
'auth_source': usr.auth_source
}
return ajax_response(
@@ -384,3 +425,17 @@ def role(rid):
response=res,
status=200
)
@blueprint.route(
'/auth_sources/', methods=['GET'], endpoint='auth_sources'
)
def auth_sources():
sources = []
for source in current_app.PGADMIN_SUPPORTED_AUTH_SOURCE:
sources.append({'label': source, 'value': source})
return ajax_response(
response=sources,
status=200
)

View File

@@ -9,12 +9,12 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs',
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node',
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node', 'pgadmin.backform',
'pgadmin.user_management.current_user',
'backgrid.select.all', 'backgrid.filter',
], function(
gettext, url_for, $, _, alertify, pgBrowser, Backbone, Backgrid, Backform,
pgNode, userInfo
pgNode, pgBackform, userInfo
) {
// if module is already initialized, refer to that.
@@ -24,6 +24,8 @@ define([
var USERURL = url_for('user_management.users'),
ROLEURL = url_for('user_management.roles'),
SOURCEURL = url_for('user_management.auth_sources'),
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes('internal')) ? true : false,
userFilter = function(collection) {
return (new Backgrid.Extension.ClientSideFilter({
collection: collection,
@@ -33,6 +35,41 @@ define([
}));
};
// Integer Cell for Columns Length and Precision
var PasswordDepCell = Backgrid.Extension.PasswordDepCell =
Backgrid.Extension.PasswordCell.extend({
initialize: function() {
Backgrid.Extension.PasswordCell.prototype.initialize.apply(this, arguments);
Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments);
},
dependentChanged: function () {
this.$el.empty();
var model = this.model,
column = this.column,
editable = this.column.get('editable'),
is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
if (is_editable){ this.$el.addClass('editable'); }
else { this.$el.removeClass('editable'); }
this.delegateEvents();
return this;
},
render: function() {
Backgrid.NumberCell.prototype.render.apply(this, arguments);
var model = this.model,
column = this.column,
editable = this.column.get('editable'),
is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
if (is_editable){ this.$el.addClass('editable'); }
else { this.$el.removeClass('editable'); }
return this;
},
remove: Backgrid.Extension.DependentCell.prototype.remove,
});
pgBrowser.UserManagement = {
init: function() {
if (this.initialized)
@@ -235,20 +272,67 @@ define([
// Callback to draw User Management Dialog.
show_users: function() {
if (!userInfo['is_admin']) return;
var Roles = [];
var Roles = [],
Sources = [];
var UserModel = pgBrowser.Node.Model.extend({
idAttribute: 'id',
urlRoot: USERURL,
defaults: {
id: undefined,
username: undefined,
email: undefined,
active: true,
role: undefined,
newPassword: undefined,
confirmPassword: undefined,
auth_source: 'internal',
authOnlyInternal: AUTH_ONLY_INTERNAL,
},
schema: [{
id: 'auth_source',
label: gettext('Authentication Source'),
type: 'text',
control: 'Select2',
url: url_for('user_management.auth_sources'),
cellHeaderClasses: 'width_percent_30',
visible: function(m) {
if (m.get('authOnlyInternal')) return false;
return true;
},
disabled: false,
cell: 'Select2',
select2: {
allowClear: false,
openOnEnter: false,
first_empty: false,
},
options: function() {
return Sources;
},
editable: function(m) {
if (m instanceof Backbone.Collection) {
return true;
}
if (m.isNew() && !m.get('authOnlyInternal')) {
return true;
} else {
return false;
}
},
}, {
id: 'username',
label: gettext('Username'),
type: 'text',
cell: Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_30',
deps: ['auth_source'],
editable: function(m) {
if (m.get('authOnlyInternal') || m.get('auth_source') == 'internal') return false;
return true;
},
disabled: false,
}, {
id: 'email',
label: gettext('Email'),
type: 'text',
@@ -256,6 +340,8 @@ define([
cellHeaderClasses: 'width_percent_30',
deps: ['id'],
editable: function(m) {
if (!m.get('authOnlyInternal')) return true;
if (m instanceof Backbone.Collection) {
return false;
}
@@ -328,23 +414,39 @@ define([
type: 'password',
disabled: false,
control: 'input',
cell: 'password',
cell: PasswordDepCell,
cellHeaderClasses: 'width_percent_20',
deps: ['auth_source'],
editable: function(m) {
if (m.get('auth_source') == 'internal') {
return true;
} else {
return false;
}
},
}, {
id: 'confirmPassword',
label: gettext('Confirm password'),
type: 'password',
disabled: false,
control: 'input',
cell: 'password',
cell: PasswordDepCell,
cellHeaderClasses: 'width_percent_20',
deps: ['auth_source'],
editable: function(m) {
if (m.get('auth_source') == 'internal') {
return true;
} else {
return false;
}
},
}],
validate: function() {
var errmsg = null,
changedAttrs = this.changed || {},
email_filter = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
if (('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
if (this.get('auth_source') == 'internal' && ('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
_.isNull(this.get('email')) ||
String(this.get('email')).replace(/^\s+|\s+$/g, '') == '')) {
errmsg = gettext('Email address cannot be empty.');
@@ -358,9 +460,8 @@ define([
this.errorModel.set('email', errmsg);
return errmsg;
} else if (!!this.get('email') && this.collection.where({
'email': this.get('email'),
'email': this.get('email'), 'auth_source': 'internal',
}).length > 1) {
errmsg = gettext('The email address %s already exists.',
this.get('email')
);
@@ -385,111 +486,113 @@ define([
this.errorModel.unset('role');
}
if (this.isNew()) {
// Password is compulsory for new user.
if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
_.isNull(this.get('newPassword')) ||
this.get('newPassword') == '')) {
if (this.get('auth_source') == 'internal') {
if (this.isNew()) {
// Password is compulsory for new user.
if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
_.isNull(this.get('newPassword')) ||
this.get('newPassword') == '')) {
errmsg = gettext('Password cannot be empty for user %s.',
(this.get('email') || '')
);
errmsg = gettext('Password cannot be empty for user %s.',
(this.get('email') || '')
);
this.errorModel.set('newPassword', errmsg);
return errmsg;
} else if (!_.isUndefined(this.get('newPassword')) &&
!_.isNull(this.get('newPassword')) &&
this.get('newPassword').length < 6) {
this.errorModel.set('newPassword', errmsg);
return errmsg;
} else if (!_.isUndefined(this.get('newPassword')) &&
!_.isNull(this.get('newPassword')) &&
this.get('newPassword').length < 6) {
errmsg = gettext('Password must be at least 6 characters for user %s.',
(this.get('email') || '')
);
errmsg = gettext('Password must be at least 6 characters for user %s.',
(this.get('email') || '')
);
this.errorModel.set('newPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('newPassword');
}
if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
_.isNull(this.get('confirmPassword')) ||
this.get('confirmPassword') == '')) {
errmsg = gettext('Confirm Password cannot be empty for user %s.',
(this.get('email') || '')
);
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('confirmPassword');
}
if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
this.get('newPassword') != this.get('confirmPassword')) {
errmsg = gettext('Passwords do not match for user %s.',
(this.get('email') || '')
);
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('confirmPassword');
}
this.errorModel.set('newPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('newPassword');
}
if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
this.get('newPassword') == '') &&
((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
this.get('confirmPassword') == ''))) {
if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
this.errorModel.unset('newPassword');
if (this.get('newPassword') == '') {
this.set({
'newPassword': undefined,
});
}
this.errorModel.unset('confirmPassword');
if (this.get('confirmPassword') == '') {
this.set({
'confirmPassword': undefined,
});
}
} else if (!_.isUndefined(this.get('newPassword')) &&
!_.isNull(this.get('newPassword')) &&
!this.get('newPassword') == '' &&
this.get('newPassword').length < 6) {
errmsg = gettext('Password must be at least 6 characters for user %s.',
(this.get('email') || '')
);
this.errorModel.set('newPassword', errmsg);
return errmsg;
} else if (_.isUndefined(this.get('confirmPassword')) ||
_.isNull(this.get('confirmPassword')) ||
this.get('confirmPassword') == '')) {
this.get('confirmPassword') == '') {
errmsg = gettext('Confirm Password cannot be empty for user %s.',
(this.get('email') || '')
);
errmsg = gettext('Confirm Password cannot be empty for user %s.',
(this.get('email') || '')
);
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('confirmPassword');
}
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
this.get('newPassword') != this.get('confirmPassword')) {
if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
this.get('newPassword') != this.get('confirmPassword')) {
errmsg = gettext('Passwords do not match for user %s.',
(this.get('email') || '')
);
errmsg = gettext('Passwords do not match for user %s.',
(this.get('email') || '')
);
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('confirmPassword');
}
} else {
if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
this.get('newPassword') == '') &&
((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
this.get('confirmPassword') == ''))) {
this.errorModel.unset('newPassword');
if (this.get('newPassword') == '') {
this.set({
'newPassword': undefined,
});
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('newPassword');
this.errorModel.unset('confirmPassword');
}
this.errorModel.unset('confirmPassword');
if (this.get('confirmPassword') == '') {
this.set({
'confirmPassword': undefined,
});
}
} else if (!_.isUndefined(this.get('newPassword')) &&
!_.isNull(this.get('newPassword')) &&
!this.get('newPassword') == '' &&
this.get('newPassword').length < 6) {
errmsg = gettext('Password must be at least 6 characters for user %s.',
(this.get('email') || '')
);
this.errorModel.set('newPassword', errmsg);
return errmsg;
} else if (_.isUndefined(this.get('confirmPassword')) ||
_.isNull(this.get('confirmPassword')) ||
this.get('confirmPassword') == '') {
errmsg = gettext('Confirm Password cannot be empty for user %s.',
(this.get('email') || '')
);
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
this.get('newPassword') != this.get('confirmPassword')) {
errmsg = gettext('Passwords do not match for user %s.',
(this.get('email') || '')
);
this.errorModel.set('confirmPassword', errmsg);
return errmsg;
} else {
this.errorModel.unset('newPassword');
this.errorModel.unset('confirmPassword');
}
}
return null;
@@ -716,7 +819,10 @@ define([
saveUser: function(m) {
var d = m.toJSON(true);
if (m.isNew() && (!m.get('email') || !m.get('role') ||
if(m.isNew() && m.get('authOnlyInternal') === false &&
(!m.get('username') || !m.get('auth_source') || !m.get('role')) ) {
return false;
} else if (m.isNew() && m.get('authOnlyInternal') === true && (!m.get('email') || !m.get('role') ||
!m.get('newPassword') || !m.get('confirmPassword') ||
m.get('newPassword') != m.get('confirmPassword'))) {
// New user model is valid but partially filled so return without saving.
@@ -741,7 +847,7 @@ define([
m.startNewSession();
alertify.success(gettext('User \'%s\' saved.',
m.get('email')
m.get('username')
));
},
error: function(res, jqxhr) {
@@ -797,6 +903,23 @@ define([
}, 100);
});
$.ajax({
url: SOURCEURL,
method: 'GET',
async: false,
})
.done(function(res) {
Sources = res;
})
.fail(function() {
setTimeout(function() {
alertify.alert(
gettext('Error'),
gettext('Cannot load user Sources.')
);
}, 100);
});
var view = this.view = new Backgrid.Grid({
row: UserRow,
columns: gridSchema.columns,

View File

@@ -14,6 +14,7 @@ define('pgadmin.user_management.current_user', [], function() {
'is_admin': {{ is_admin }},
'name': '{{ name }}',
'allow_save_password': {{ allow_save_password }},
'allow_save_tunnel_password': {{ allow_save_tunnel_password }}
'allow_save_tunnel_password': {{ allow_save_tunnel_password }},
'auth_sources': {{ auth_sources }}
}
});