Port User Management dialog to React. Fixes #7345

This commit is contained in:
Akshay Joshi 2022-08-11 10:25:52 +05:30
parent 3766fa7f0b
commit 271b6d91fc
17 changed files with 751 additions and 1225 deletions

View File

@ -0,0 +1,27 @@
.. _change_ownership:
************************************
`Change Ownership Dialog`:index:
************************************
Use the *Change Ownership* dialog to change the ownership of the shared servers.
This dialog will appear if a user has been deleted from
:ref:`User Management <user_management>` and owned some shared servers.
Choose the user who will own the shared servers from the drop-down.
.. image:: images/change_ownership.png
:alt: Change ownership dialog
:align: center
Click the *Change* button to change the ownership.
The shared servers owned by the user will be deleted if the user is not
selected from the drop-down.
.. image:: images/change_ownership_info.png
:alt: Change ownership dialog
:align: center
Click the *Change* button to change the ownership; click *Close* to
exit the dialog.

View File

@ -35,6 +35,7 @@ Mode is pre-configured for security.
login
mfa
user_management
change_ownership
change_user_password
restore_locked_user
ldap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -15,6 +15,7 @@ Housekeeping
************
| `Issue #7344 <https://redmine.postgresql.org/issues/7344>`_ - Port Role Reassign dialog to React.
| `Issue #7345 <https://redmine.postgresql.org/issues/7345>`_ - Port User Management dialog to React.
| `Issue #7462 <https://redmine.postgresql.org/issues/7462>`_ - Remove the SQL files for the unsupported versions of the database server.
| `Issue #7567 <https://redmine.postgresql.org/issues/7567>`_ - Port About dialog to React.
| `Issue #7568 <https://redmine.postgresql.org/issues/7568>`_ - Port change user password and 2FA dialog to React.

View File

@ -35,12 +35,13 @@ To add a user, click the Add (+) button at the top right corner.
Provide information about the new pgAdmin role in the row:
* Use the drop-down list box next to *Authentication source* field to select the
type of authentication that should be used for the user. If LDAP
authentication is not enabled for pgAdmin, then *Authentication source* field
is disabled.
type of authentication that should be used for the user. If authentication
source is only 'internal' then *Authentication source* field
is disabled. Supported *Authentication source* are internal, ldap, kerberos,
oauth2 and webserver.
* Click in the *Username* field, and provide a username for the user. This field
is enabled only if you select *ldap* as authentication source. If you select
*internal* as authentication source, your email address is displayed in the
is enabled only when you select authentication source except *internal*. If you
select *internal* as authentication source, your email address is displayed in the
username field.
* Click in the *Email* field, and provide an email address for the user.
* Use the drop-down list box next to *Role* to select whether a user is an
@ -54,15 +55,19 @@ Provide information about the new pgAdmin role in the row:
active; the default is *Yes*. Use this switch to disable account activity
without deleting an account.
* Use the *New password* field to provide the password associated with the user
specified in the *Email* field. This field is disabled if you select *ldap*
as authentication source since LDAP password is not stored in the pgAdmin database.
specified in the *Email* field. This field is disabled if you select any
authentication source except *internal*.
* Re-enter the password in the *Confirm password* field. This field is disabled
if you select *ldap* as authentication source.
* Move the *Locked* switch to the *True* position if you want to lock the account;
the default is *False*. This functionality is useful when a user is locked by trying unsuccessful login attempts.
the default is *False*. This functionality is useful when a user is locked by
trying unsuccessful login attempts.
To discard a user, and revoke access to pgAdmin, click the trash icon to the
left of the row and confirm deletion in the *Delete user?* dialog.
left of the row and confirm deletion in the *Delete user?* dialog. If the user
has created some shared servers, then the :ref:`Change Ownership <change_ownership>`
dialog will appear to change the ownership of a shared server.
Users with the *Administrator* role are able to add, edit and remove pgAdmin
users, but otherwise have the same capabilities as those with the *User* role.

View File

@ -144,7 +144,7 @@ window.onload = function(e){
<ul class="dropdown-menu dropdown-menu-right" role="menu">
{% if auth_only_internal %}
<li>
<a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.Browser.UserManagement.change_password(
<a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.UserManagement.change_password(
'{{ url_for('browser.change_password') }}'
)">
{{ _('Change Password') }}
@ -154,14 +154,14 @@ window.onload = function(e){
{% endif %}
{% if mfa_enabled is defined and mfa_enabled is true %}
<li>
<a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.Browser.UserManagement.show_mfa(
<a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.UserManagement.show_mfa(
'{{ login_url("mfa.register", next_url="internal") }}'
)">{{ _('Two-Factor Authentication') }}</a>
</li>
<li class="dropdown-divider"></li>
{% endif %}
{% if is_admin %}
<li><a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.Browser.UserManagement.show_users()">{{ _('Users') }}</a></li>
<li><a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.UserManagement.show_users()">{{ _('Users') }}</a></li>
<li class="dropdown-divider"></li>
{% endif %}
<li><a class="dropdown-item" role="menuitem" href="{{ logout_url }}">{{ _('Logout') }}</a></li>

View File

@ -301,7 +301,7 @@ export function showChangeOwnership() {
userList = arguments[1],
noOfSharedServers = arguments[2],
deletedUser = arguments[3],
destroyUserManagement = arguments[4];
deleteUserRow = arguments[4];
// Render Preferences component
Notify.showModal(title, (onClose) => {
@ -314,24 +314,15 @@ export function showChangeOwnership() {
return new Promise((resolve, reject)=>{
if (data.newUser == '') {
api.delete(url_for('user_management.user', {uid: deletedUser['uid']}))
.then(() => {
Notify.success(gettext('User deleted.'));
onClose();
destroyUserManagement();
resolve();
})
.catch((err)=>{
Notify.error(err);
reject(err);
});
deleteUserRow();
onClose();
} else {
let newData = {'new_owner': `${data.newUser}`, 'old_owner': `${deletedUser['uid']}`};
let newData = {'new_owner': `${data.newUser}`, 'old_owner': `${deletedUser['id']}`};
api.post(url_for('user_management.change_owner'), newData)
.then(({data: respData})=>{
Notify.success(gettext(respData.info));
onClose();
destroyUserManagement();
deleteUserRow();
resolve(respData.data);
})
.catch((err)=>{

View File

@ -17,7 +17,7 @@ import AddIcon from '@material-ui/icons/AddOutlined';
import { MappedCellControl } from './MappedControl';
import EditRoundedIcon from '@material-ui/icons/EditRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
import { useTable, useFlexLayout, useResizeColumns, useSortBy, useExpanded } from 'react-table';
import { useTable, useFlexLayout, useResizeColumns, useSortBy, useExpanded, useGlobalFilter } from 'react-table';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import _ from 'lodash';
@ -30,6 +30,7 @@ import { evalFunc } from 'sources/utils';
import { DepListenerContext } from './DepListener';
import { useIsMounted } from '../custom_hooks';
import Notify from '../helpers/Notifier';
import { InputText } from '../components/FormComponents';
const useStyles = makeStyles((theme)=>({
grid: {
@ -225,11 +226,26 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
</div>, depsMap);
}
export function DataGridHeader({label, canAdd, onAddClick}) {
export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTextChange}) {
const classes = useStyles();
const [searchText, setSearchText] = useState('');
return (
<Box className={classes.gridHeader}>
{ label &&
<Box className={classes.gridHeaderText}>{label}</Box>
}
{ canSearch &&
<Box className={classes.gridHeaderText} width={'100%'}>
<InputText value={searchText}
onChange={(value)=>{
onSearchTextChange(value);
setSearchText(value);
}}
placeholder={'Search'}>
</InputText>
</Box>
}
<Box className={classes.gridControls}>
{canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
</Box>
@ -240,6 +256,8 @@ DataGridHeader.propTypes = {
label: PropTypes.string,
canAdd: PropTypes.bool,
onAddClick: PropTypes.func,
canSearch: PropTypes.bool,
onSearchTextChange: PropTypes.func,
};
export default function DataGridView({
@ -303,21 +321,27 @@ export default function DataGridView({
return (
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />}
onClick={()=>{
Notify.confirm(
props.customDeleteTitle || gettext('Delete Row'),
props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
function() {
dataDispatch({
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
path: accessPath,
value: row.index,
});
return true;
},
function() {
return true;
}
);
const deleteRow = ()=> {
dataDispatch({
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
path: accessPath,
value: row.index,
});
return true;
};
if (props.onDelete){
props.onDelete(row.original || {}, deleteRow);
} else {
Notify.confirm(
props.customDeleteTitle || gettext('Delete Row'),
props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
deleteRow,
function() {
return true;
}
);
}
}} className={classes.gridRowButton} disabled={!canDeleteRow} />
);
}
@ -425,6 +449,7 @@ export default function DataGridView({
}), []);
let tablePlugins = [
useGlobalFilter,
useFlexLayout,
useResizeColumns,
useSortBy,
@ -437,10 +462,11 @@ export default function DataGridView({
headerGroups,
rows,
prepareRow,
setGlobalFilter,
} = useTable(
{
columns,
data: value || [],
data: value,
defaultColumn,
manualSortBy: true,
autoResetSortBy: false,
@ -476,7 +502,12 @@ export default function DataGridView({
return (
<Box className={containerClassName}>
<Box className={classes.grid}>
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick} />}
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick}
canSearch={props.canSearch}
onSearchTextChange={(value)=>{
setGlobalFilter(value || undefined);
}}
/>}
<div {...getTableProps()} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()} className={classes.tableContentWidth}>
@ -524,4 +555,6 @@ DataGridView.propTypes = {
]),
customDeleteTitle: PropTypes.string,
customDeleteMsg: PropTypes.string,
canSearch: PropTypes.bool,
onDelete: PropTypes.func,
};

View File

@ -268,10 +268,10 @@ export default function FormView({
}
const props = {
key: field.id, value: value[field.id], viewHelperProps: viewHelperProps,
key: field.id, value: value[field.id] || [], viewHelperProps: viewHelperProps,
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
containerClassName: classes.controlRow, ...field, canAdd: canAdd, canEdit: canEdit, canDelete: canDelete,
visible: visible, canAddRow: canAddRow,
visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch
};
if(CustomControl) {

View File

@ -155,7 +155,7 @@ function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, v
case 'text':
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
case 'password':
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
return <InputText name={name} value={value} onChange={onTextChange} {...props} type='password'/>;
case 'select':
return <InputSelect name={name} value={value} onChange={onTextChange} optionsLoaded={optionsLoadedRerender} {...props}/>;
case 'switch':

View File

@ -886,7 +886,7 @@ function SchemaPropertiesView({
key={field.id}
viewHelperProps={viewHelperProps}
name={field.id}
value={origData[field.id]}
value={origData[field.id] || []}
schema={field.schema}
accessPath={[field.id]}
formErr={{}}

View File

@ -10,10 +10,8 @@
"""Implements pgAdmin4 User Management Utility"""
import simplejson as json
import re
from flask import render_template, request, \
url_for, Response, abort, current_app, session
Response, abort, current_app, session
from flask_babel import gettext as _
from flask_security import login_required, roles_required, current_user
from flask_security.utils import encrypt_password
@ -25,7 +23,7 @@ from pgadmin.utils.ajax import make_response as ajax_response, \
make_json_response, bad_request, internal_server_error, forbidden
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\
SUPPORTED_AUTH_SOURCES, KERBEROS, LDAP
SUPPORTED_AUTH_SOURCES
from pgadmin.utils.validation_utils import validate_email
from pgadmin.model import db, Role, User, UserPreference, Server, \
ServerGroup, Process, Setting, roles_users, SharedServer
@ -59,12 +57,11 @@ class UserManagementModule(PgAdminModule):
"""
return [
'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.auth_sources', 'user_management.auth_sources',
'user_management.users', 'user_management.user',
current_app.login_manager.login_view,
'user_management.auth_sources', 'user_management.change_owner',
'user_management.shared_servers', 'user_management.admin_users',
'user_management.change_owner',
'user_management.save'
]
@ -74,55 +71,6 @@ blueprint = UserManagementModule(
)
def validate_password(data, new_data):
"""
Check password new and confirm password match. If both passwords are not
match raise exception.
:param data: Data.
:param new_data: new data dict.
"""
if ('newPassword' in data and data['newPassword'] != "" and
'confirmPassword' in data and data['confirmPassword'] != ""):
if data['newPassword'] == data['confirmPassword']:
new_data['password'] = encrypt_password(data['newPassword'])
else:
raise InternalServerError(_("Passwords do not match."))
def validate_user(data):
new_data = dict()
validate_password(data, new_data)
if 'email' in data and data['email'] and data['email'] != "":
if validate_email(data['email']):
new_data['email'] = data['email']
else:
raise InternalServerError(_("Invalid email address."))
if 'role' in data and data['role'] != "":
new_data['roles'] = int(data['role'])
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']
if 'locked' in data and type(data['locked']) == bool:
new_data['locked'] = data['locked']
if data['locked']:
new_data['login_attempts'] = config.MAX_LOGIN_ATTEMPTS
else:
new_data['login_attempts'] = 0
return new_data
@blueprint.route("/")
@login_required
def index():
@ -223,148 +171,6 @@ def user(uid):
)
@blueprint.route('/user/', methods=['POST'], endpoint='create_user')
@roles_required('Administrator')
def create():
"""
Returns:
"""
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
status, res = create_user(data)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res,
status=200
)
def _create_new_user(new_data):
"""
Create new user.
:param new_data: Data from user creation.
:return: Return new created user.
"""
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
else INTERNAL
username = new_data['username'] if \
'username' in new_data and auth_source != \
INTERNAL 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
usr = User(username=username,
email=email,
roles=new_data['roles'],
active=new_data['active'],
password=password,
auth_source=auth_source)
db.session.add(usr)
db.session.commit()
# Add default server group for new user.
server_group = ServerGroup(user_id=usr.id, name="Servers")
db.session.add(server_group)
db.session.commit()
return usr
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 False, _("Missing field: '{0}'").format(f)
try:
new_data = validate_user(data)
if 'roles' in new_data:
new_data['roles'] = [Role.query.get(new_data['roles'])]
except Exception as e:
return False, str(e)
try:
usr = _create_new_user(new_data)
except Exception as e:
return False, str(e)
return True, {
'id': usr.id,
'username': usr.username,
'email': usr.email,
'active': usr.active,
'role': usr.roles[0].id,
'locked': usr.locked
}
@blueprint.route(
'/user/<int:uid>', methods=['DELETE'], endpoint='delete_user'
)
@roles_required('Administrator')
def delete(uid):
"""
Args:
uid:
Returns:
"""
usr = User.query.get(uid)
if not usr:
abort(404)
try:
server_groups = ServerGroup.query.filter_by(user_id=uid).all()
sg = [server_group.id for server_group in server_groups]
Setting.query.filter_by(user_id=uid).delete()
UserPreference.query.filter_by(uid=uid).delete()
Server.query.filter_by(user_id=uid).delete()
ServerGroup.query.filter_by(user_id=uid).delete()
Process.query.filter_by(user_id=uid).delete()
# Delete Shared servers for current user.
SharedServer.query.filter_by(user_id=uid).delete()
SharedServer.query.filter(SharedServer.servergroup_id.in_(sg)).delete(
synchronize_session=False)
# Finally delete user
db.session.delete(usr)
db.session.commit()
return make_json_response(
success=1,
info=_("User deleted."),
data={}
)
except Exception as e:
return internal_server_error(errormsg=str(e))
@blueprint.route('/change_owner', methods=['POST'], endpoint='change_owner')
@roles_required('Administrator')
def change_owner():
@ -430,9 +236,6 @@ def change_owner():
server_group.user_id = data['new_owner']
db.session.commit()
# Delete old owner records.
delete(data['old_owner'])
return make_json_response(
success=1,
info=_("Owner changed successfully."),
@ -537,72 +340,6 @@ def admin_users(uid=None):
)
@blueprint.route('/user/<int:uid>', methods=['PUT'], endpoint='update_user')
@roles_required('Administrator')
def update(uid):
"""
Args:
uid:
Returns:
"""
usr = User.query.get(uid)
if not usr:
abort(404)
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
# Username and email can not be changed for internal users
if usr.auth_source == INTERNAL:
non_editable_params = ('username', 'email')
for f in non_editable_params:
if f in data:
return forbidden(
errmsg=_(
"'{0}' is not allowed to modify."
).format(f)
)
try:
new_data = validate_user(data)
if 'roles' in new_data:
new_data['roles'] = [Role.query.get(new_data['roles'])]
except Exception as e:
return bad_request(errormsg=_(str(e)))
try:
for k, v in new_data.items():
setattr(usr, k, v)
db.session.commit()
res = {'id': usr.id,
'username': usr.username,
'email': usr.email,
'active': usr.active,
'role': usr.roles[0].id,
'auth_source': usr.auth_source,
'locked': usr.locked
}
return ajax_response(
response=res,
status=200
)
except Exception as e:
return internal_server_error(errormsg=str(e))
@blueprint.route(
'/role/', methods=['GET'], defaults={'rid': None}, endpoint='roles'
)
@ -650,3 +387,221 @@ def auth_sources():
response=sources,
status=200
)
@blueprint.route('/save', methods=['POST'], endpoint='save')
@roles_required('Administrator')
def save():
"""
This function is used to add/update/delete users.
"""
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
try:
# Create Users
if 'added' in data:
for item in data['added']:
status, res = create_user(item)
if not status:
return internal_server_error(errormsg=res)
# Modify Users
if 'changed' in data:
for item in data['changed']:
status, res = update_user(item['id'], item)
if not status:
return internal_server_error(errormsg=res)
# Delete Users
if 'deleted' in data:
for item in data['deleted']:
status, res = delete_user(item['id'])
if not status:
return internal_server_error(errormsg=res)
except Exception as e:
return internal_server_error(errormsg=str(e))
return ajax_response(
status=200
)
def validate_password(data, new_data):
"""
Check password new and confirm password match. If both passwords are not
match raise exception.
:param data: Data.
:param new_data: new data dict.
"""
if ('newPassword' in data and data['newPassword'] != "" and
'confirmPassword' in data and data['confirmPassword'] != ""):
if data['newPassword'] == data['confirmPassword']:
new_data['password'] = encrypt_password(data['newPassword'])
else:
raise InternalServerError(_("Passwords do not match."))
def validate_user(data):
new_data = dict()
validate_password(data, new_data)
if 'email' in data and data['email'] and data['email'] != "":
if validate_email(data['email']):
new_data['email'] = data['email']
else:
raise InternalServerError(_("Invalid email address."))
if 'role' in data and data['role'] != "":
new_data['roles'] = int(data['role'])
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']
if 'locked' in data and type(data['locked']) == bool:
new_data['locked'] = data['locked']
if data['locked']:
new_data['login_attempts'] = config.MAX_LOGIN_ATTEMPTS
else:
new_data['login_attempts'] = 0
return new_data
def _create_new_user(new_data):
"""
Create new user.
:param new_data: Data from user creation.
:return: Return new created user.
"""
auth_source = new_data['auth_source'] if 'auth_source' in new_data \
else INTERNAL
username = new_data['username'] if \
'username' in new_data and auth_source != \
INTERNAL 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
usr = User(username=username,
email=email,
roles=new_data['roles'],
active=new_data['active'],
password=password,
auth_source=auth_source)
db.session.add(usr)
db.session.commit()
# Add default server group for new user.
server_group = ServerGroup(user_id=usr.id, name="Servers")
db.session.add(server_group)
db.session.commit()
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 False, _("Missing field: '{0}'").format(f)
try:
new_data = validate_user(data)
if 'roles' in new_data:
new_data['roles'] = [Role.query.get(new_data['roles'])]
except Exception as e:
return False, str(e)
try:
_create_new_user(new_data)
except Exception as e:
return False, str(e)
return True, ''
def update_user(uid, data):
"""
This function is used to update the users.
"""
usr = User.query.get(uid)
if not usr:
return False, _("Unable to update user '{0}'").format(uid)
# Username and email can not be changed for internal users
if usr.auth_source == INTERNAL:
non_editable_params = ('username', 'email')
for f in non_editable_params:
if f in data:
return False, _("'{0}' is not allowed to modify.").format(f)
try:
new_data = validate_user(data)
if 'roles' in new_data:
new_data['roles'] = [Role.query.get(new_data['roles'])]
except Exception as e:
return False, str(e)
try:
for k, v in new_data.items():
setattr(usr, k, v)
db.session.commit()
except Exception as e:
return False, str(e)
return True, ''
def delete_user(uid):
"""
This function is used to delete the users
"""
usr = User.query.get(uid)
if not usr:
return False, _("Unable to update user '{0}'").format(uid)
try:
server_groups = ServerGroup.query.filter_by(user_id=uid).all()
sg = [server_group.id for server_group in server_groups]
Setting.query.filter_by(user_id=uid).delete()
UserPreference.query.filter_by(uid=uid).delete()
Server.query.filter_by(user_id=uid).delete()
ServerGroup.query.filter_by(user_id=uid).delete()
Process.query.filter_by(user_id=uid).delete()
# Delete Shared servers for current user.
SharedServer.query.filter_by(user_id=uid).delete()
SharedServer.query.filter(SharedServer.servergroup_id.in_(sg)).delete(
synchronize_session=False)
# Finally delete user
db.session.delete(usr)
db.session.commit()
except Exception as e:
return False, str(e)
return True, ''

View File

@ -0,0 +1,381 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import { makeStyles } from '@material-ui/core';
import SchemaView from '../../../../static/js/SchemaView';
import BaseUISchema from '../../../../static/js/SchemaView/base_schema.ui';
import pgAdmin from 'sources/pgadmin';
import Theme from 'sources/Theme';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';
import getApiInstance from '../../../../static/js/api_instance';
import authConstant from 'pgadmin.browser.constants';
import current_user from 'pgadmin.user_management.current_user';
import { isEmptyString } from '../../../../static/js/validators';
import Notify from '../../../../static/js/helpers/Notifier';
import { showChangeOwnership } from '../../../../static/js/Dialogs/index';
class UserManagementCollection extends BaseUISchema {
constructor(authSources, roleOptions) {
super({
id: undefined,
username: undefined,
email: undefined,
active: true,
role: '2',
newPassword: undefined,
confirmPassword: undefined,
auth_source: authConstant['INTERNAL']
});
this.authOnlyInternal = (current_user['auth_sources'].length == 1 &&
current_user['auth_sources'].includes(authConstant['INTERNAL'])) ? true : false;
this.authSources = authSources;
this.roleOptions = roleOptions;
}
get idAttribute() {
return 'id';
}
isUserNameEnabled(state) {
if (this.authOnlyInternal || state.auth_source == authConstant['INTERNAL']) {
return false;
}
return true;
}
isEditable(state) {
return state.id != current_user['id'];
}
get baseFields() {
let obj = this;
return [
{
id: 'auth_source', label: gettext('Authentication source'), cell: 'select',
options: obj.authSources, minWidth: 110, width: 110,
controlProps: {
allowClear: false,
openOnEnter: false,
first_empty: false,
},
visible: function() {
if (obj.authOnlyInternal)
return false;
return true;
},
editable: function(state) {
return (obj.isNew(state) && !obj.authOnlyInternal);
}
}, {
id: 'username', label: gettext('Username'), cell: 'text',
minWidth: 90, width: 90,
deps: ['auth_source'],
depChange: (state)=>{
if (obj.isUserNameEnabled(state) && obj.isNew(state) && !isEmptyString(obj.username)) {
state.username = undefined;
}
},
editable: (state)=> {
return obj.isUserNameEnabled(state);
}
}, {
id: 'email', label: gettext('Email'), cell: 'text',
minWidth: 90, width: 90, deps: ['id'],
editable: (state)=> {
if (obj.isNew(state))
return true;
if (obj.isEditable(state) && state.auth_source != authConstant['INTERNAL'])
return true;
return false;
}
}, {
id: 'role', label: gettext('Role'), cell: 'select',
options: obj.roleOptions, minWidth: 95, width: 95,
controlProps: {
allowClear: false,
openOnEnter: false,
first_empty: false,
},
editable: (state)=> {
return obj.isEditable(state);
}
}, {
id: 'active', label: gettext('Active'), cell: 'switch', width: 60, disableResizing: true,
editable: (state)=> {
return obj.isEditable(state);
}
}, {
id: 'newPassword', label: gettext('New password'), cell: 'password',
minWidth: 90, width: 90, deps: ['auth_source'],
editable: (state)=> {
return obj.isEditable(state) && state.auth_source == authConstant['INTERNAL'];
}
}, {
id: 'confirmPassword', label: gettext('Confirm password'), cell: 'password',
minWidth: 90, width: 90, deps: ['auth_source'],
editable: (state)=> {
return obj.isEditable(state) && state.auth_source == authConstant['INTERNAL'];
}
}, {
id: 'locked', label: gettext('Locked'), cell: 'switch', width: 60, disableResizing: true,
editable: (state)=> {
return obj.isEditable(state);
}
}
];
}
validate(state, setError) {
let msg = undefined;
let obj = this;
if (obj.isUserNameEnabled(state) && isEmptyString(state.username)) {
msg = gettext('Username cannot be empty.');
setError('username', msg);
return true;
} else {
setError('username', null);
}
if (state.auth_source == authConstant['INTERNAL']) {
let 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 (isEmptyString(state.email)) {
msg = gettext('Email cannot be empty.');
setError('email', msg);
return true;
} else if (!email_filter.test(state.email)) {
msg = gettext('Invalid email address: %s.', state.email);
setError('email', msg);
return true;
} else {
setError('email', null);
}
// TODO: Check for duplicate email address errmsg = gettext('The email address %s already exists.'
if (obj.isNew(state) && isEmptyString(state.newPassword)) {
msg = gettext('Password cannot be empty for user %s.', state.email);
setError('newPassword', msg);
return true;
} else if (state.newPassword?.length < 6) {
msg = gettext('Password must be at least 6 characters for user %s.', state.email);
setError('newPassword', msg);
return true;
} else {
setError('newPassword', null);
}
if (obj.isNew(state) && isEmptyString(state.confirmPassword)) {
msg = gettext('Confirm Password cannot be empty for user %s.', state.email);
setError('confirmPassword', msg);
return true;
} else {
setError('confirmPassword', null);
}
if (state.newPassword !== state.confirmPassword) {
msg = gettext('Passwords do not match for user %s.', state.email);
setError('confirmPassword', msg);
return true;
} else {
setError('confirmPassword', null);
}
}
return false;
}
}
class UserManagementSchema extends BaseUISchema {
constructor(authSources, roleOptions) {
super();
this.userManagementCollObj = new UserManagementCollection(authSources, roleOptions);
}
deleteUser(deleteRow) {
Notify.confirm(
gettext('Delete user?'),
gettext('Are you sure you wish to delete this user?'),
deleteRow,
function() {
return true;
}
);
}
get baseFields() {
let obj = this;
const api = getApiInstance();
return [
{
id: 'userManagement', label: '', type: 'collection', schema: obj.userManagementCollObj,
canAdd: true, canDelete: true, isFullTab: true, group: 'temp_user',
canDeleteRow: (row)=>{
if (row['id'] == current_user['id'])
return false;
return true;
},
onDelete: (row, deleteRow)=> {
let deletedUser = {'id': row['id'], 'name': !isEmptyString(row['email']) ? row['email'] : row['username']};
api.get(url_for('user_management.shared_servers', {'uid': row['id']}))
.then((res)=>{
if (res.data?.data?.shared_servers > 0) {
api.get(url_for('user_management.admin_users', {'uid': row['id']}))
.then((result)=>{
showChangeOwnership(gettext('Change ownership'),
result?.data?.data?.result?.data,
res?.data?.data?.shared_servers,
deletedUser,
deleteRow
);
})
.catch((err)=>{
Notify.error(err);
});
} else {
obj.deleteUser(deleteRow);
}
})
.catch((err)=>{
Notify.error(err);
obj.deleteUser(deleteRow);
});
},
canSearch: true
},
];
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
padding: 0,
},
}));
function UserManagementDialog({onClose}) {
const classes = useStyles();
const [authSources, setAuthSources] = React.useState([]);
const [roles, setRoles] = React.useState([]);
const api = getApiInstance();
React.useEffect(async ()=>{
try {
api.get(url_for('user_management.auth_sources'))
.then(res=>{
setAuthSources(res.data);
})
.catch((err)=>{
Notify.error(err);
});
api.get(url_for('user_management.roles'))
.then(res=>{
setRoles(res.data);
})
.catch((err)=>{
Notify.error(err);
});
} catch (error) {
Notify.error(error);
}
}, []);
const onSaveClick = (_isNew, changeData)=>{
return new Promise((resolve, reject)=>{
try {
api.post(url_for('user_management.save'), changeData['userManagement'])
.then(()=>{
Notify.success('Users Saved Successfully');
})
.catch((err)=>{
Notify.error(err);
});
resolve();
onClose();
} catch (error) {
reject(error);
}
});
};
const authSourcesOptions = authSources.map((m)=>({
label: m.label,
value: m.value,
}));
if(authSourcesOptions.length <= 0) {
return <></>;
}
const roleOptions = roles.map((m)=>({
label: m.name,
value: m.id,
}));
if(roleOptions.length <= 0) {
return <></>;
}
const onDialogHelp = () => {
window.open(url_for('help.static', { 'filename': 'user_management.html' }), 'pgadmin_help');
};
return <SchemaView
formType={'dialog'}
getInitData={()=>{ return new Promise((resolve, reject)=>{
api.get(url_for('user_management.users'))
.then((res)=>{
resolve({userManagement:res.data});
})
.catch((err)=>{
reject(err);
});
}); }}
schema={new UserManagementSchema(authSourcesOptions, roleOptions)}
viewHelperProps={{
mode: 'edit',
}}
onSave={onSaveClick}
onClose={onClose}
onHelp={onDialogHelp}
hasSQL={false}
disableSqlHelp={true}
isTabView={false}
formClassName={classes.root}
/>;
}
UserManagementDialog.propTypes = {
onClose: PropTypes.func
};
export default function showUserManagement() {
pgAdmin.Browser.Node.registerUtilityPanel();
let panel = pgAdmin.Browser.Node.addUtilityPanel(980, pgAdmin.Browser.stdH.md),
j = panel.$container.find('.obj_properties').first();
panel.title(gettext('User Management'));
const onClose = ()=> {
ReactDOM.unmountComponentAtNode(j[0]);
panel.close();
};
ReactDOM.render(
<Theme>
<UserManagementDialog
onClose={onClose}
/>
</Theme>, j[0]);
}

View File

@ -7,914 +7,46 @@
//
//////////////////////////////////////////////////////////////
import Notify from '../../../../static/js/helpers/Notifier';
import { showChangeOwnership, showUrlDialog } from '../../../../static/js/Dialogs/index';
import pgAdmin from 'sources/pgadmin';
import gettext from 'sources/gettext';
import { showUrlDialog } from '../../../../static/js/Dialogs/index';
import showUserManagement from './UserManagementDialog';
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs',
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node', 'pgadmin.backform',
'pgadmin.user_management.current_user', 'sources/utils', 'pgadmin.browser.constants',
'backgrid.select.all', 'backgrid.filter',
], function(
gettext, url_for, $, _, alertify, pgBrowser, Backbone, Backgrid, Backform,
pgNode, pgBackform, userInfo, commonUtils, pgConst,
) {
// if module is already initialized, refer to that.
if (pgBrowser.UserManagement) {
return pgBrowser.UserManagement;
class UserManagement {
static instance;
static getInstance(...args) {
if (!UserManagement.instance) {
UserManagement.instance = new UserManagement(...args);
}
return UserManagement.instance;
}
var USERURL = url_for('user_management.users'),
ROLEURL = url_for('user_management.roles'),
SOURCEURL = url_for('user_management.auth_sources'),
DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'],
LDAP = pgConst['LDAP'],
KERBEROS = pgConst['KERBEROS'],
OAUTH2 = pgConst['OAUTH2'],
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false,
userFilter = function(collection) {
return (new Backgrid.Extension.ClientSideFilter({
collection: collection,
placeholder: gettext('Filter by email'),
// How long to wait after typing has stopped before searching can start
wait: 150,
}));
};
// 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();
this.render();
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)
return;
this.initialized = true;
return this;
},
// Callback to draw change password Dialog.
change_password: function(url) {
showUrlDialog(gettext('Change Password'), url, 'change_user_password.html', undefined, pgBrowser.stdH.lg);
},
show_mfa: function(url) {
showUrlDialog(gettext('Authentication'), url, 'mfa.html', 1200, 680);
},
is_editable: function(m) {
if (m instanceof Backbone.Collection) {
return true;
}
return (m.get('id') != userInfo['id']);
},
// Callback to draw User Management Dialog.
show_users: function() {
if (!userInfo['is_admin']) return;
var self = this,
Roles = [],
Sources = [];
var UserModel = pgBrowser.Node.Model.extend({
idAttribute: 'id',
urlRoot: USERURL,
defaults: {
id: undefined,
username: undefined,
email: undefined,
active: true,
role: '2',
newPassword: undefined,
confirmPassword: undefined,
auth_source: DEFAULT_AUTH_SOURCE,
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;
}
return (m.isNew() && !m.get('authOnlyInternal'));
},
}, {
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') == DEFAULT_AUTH_SOURCE) {
if (m.isNew() && m.get('username') != undefined && m.get('username') != '') {
setTimeout( function() {
m.set('username', undefined);
}, 10);
}
return false;
}
return true;
},
disabled: false,
}, {
id: 'email',
label: gettext('Email'),
type: 'text',
cell: Backgrid.Extension.StringDepCell,
cellHeaderClasses: 'width_percent_30',
deps: ['id'],
editable: function(m) {
if (!m.get('authOnlyInternal')) return true;
if (m instanceof Backbone.Collection) {
return false;
}
// Disable email edit for existing user.
if (m.isNew()) {
return true;
}
return false;
},
}, {
id: 'role',
label: gettext('Role'),
type: 'text',
control: 'Select2',
cellHeaderClasses: 'width_percent_20',
cell: 'select2',
select2: {
allowClear: false,
openOnEnter: false,
first_empty: false,
},
options: function(controlOrCell) {
var options = [];
if (controlOrCell instanceof Backform.Control) {
// This is be backform select2 control
_.each(Roles, function(role) {
options.push({
label: role.name,
value: role.id.toString(),
});
});
} else {
// This must be backgrid select2 cell
_.each(Roles, function(role) {
options.push([role.name, role.id.toString()]);
});
}
return options;
},
editable: function(m) {
return self.is_editable(m);
},
}, {
id: 'active',
label: gettext('Active'),
type: 'switch',
cell: 'switch',
cellHeaderClasses: 'width_percent_10',
sortable: false,
editable: function(m) {
return self.is_editable(m);
},
}, {
id: 'newPassword',
label: gettext('New password'),
type: 'password',
disabled: false,
control: 'input',
cell: PasswordDepCell,
cellHeaderClasses: 'width_percent_20',
deps: ['auth_source'],
sortable: false,
editable: function(m) {
return (m.get('auth_source') == DEFAULT_AUTH_SOURCE);
},
}, {
id: 'confirmPassword',
label: gettext('Confirm password'),
type: 'password',
disabled: false,
control: 'input',
cell: PasswordDepCell,
cellHeaderClasses: 'width_percent_20',
deps: ['auth_source'],
sortable: false,
editable: function(m) {
return (m.get('auth_source') == DEFAULT_AUTH_SOURCE);
},
},{
id: 'locked',
label: gettext('Locked'),
type: 'switch',
cell: 'switch',
disabled: false,
sortable: false,
editable: function (m){
return (m.get('id') != userInfo['id']);
},
}],
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 (this.get('auth_source') == DEFAULT_AUTH_SOURCE && ('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.');
this.errorModel.set('email', errmsg);
return errmsg;
} else if (!!this.get('email') && !email_filter.test(this.get('email'))) {
errmsg = gettext('Invalid email address: %s.',
this.get('email')
);
this.errorModel.set('email', errmsg);
return errmsg;
} else if (!!this.get('email') && this.collection.nonFilter.where({
'email': this.get('email'), 'auth_source': DEFAULT_AUTH_SOURCE,
}).length > 1) {
errmsg = gettext('The email address %s already exists.',
this.get('email')
);
this.errorModel.set('email', errmsg);
return errmsg;
} else {
this.errorModel.unset('email');
}
if ('role' in changedAttrs && (_.isUndefined(this.get('role')) ||
_.isNull(this.get('role')) ||
String(this.get('role')).replace(/^\s+|\s+$/g, '') == '')) {
errmsg = gettext('Role cannot be empty for user %s.',
(this.get('email') || '')
);
this.errorModel.set('role', errmsg);
return errmsg;
} else {
this.errorModel.unset('role');
}
if (this.get('auth_source') == DEFAULT_AUTH_SOURCE) {
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') || '')
);
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') || '')
);
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');
}
} 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.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');
}
}
} else {
if (!!this.get('username') && (this.collection.nonFilter.where({
'username': this.get('username'), 'auth_source': LDAP,
}).length > 1) || (this.collection.nonFilter.where({
'username': this.get('username'), 'auth_source': KERBEROS,
}).length > 1) || (this.collection.nonFilter.where({
'username': this.get('username'), 'auth_source': OAUTH2,
}).length > 1)) {
errmsg = gettext('The username %s already exists.',
this.get('username')
);
this.errorModel.set('username', errmsg);
return errmsg;
}
}
return null;
},
}),
gridSchema = Backform.generateGridColumnsFromModel(
null, UserModel, 'edit'),
deleteUserCell = Backgrid.Extension.DeleteCell.extend({
raiseError: function() {
Notify.error(
gettext('Error during deleting user.')
);
},
changeOwnership: function(res, uid) {
let self = this,
url = url_for('user_management.admin_users', {'uid': uid});
const destroyUserManagement = ()=>{
alertify.UserManagement().destroy();
};
$.ajax({
url: url,
headers: {
'Cache-Control' : 'no-cache',
},
}).done(function (result) {
showChangeOwnership(gettext('Change ownership'),
result.data.result.data,
res['data'].shared_servers,
{uid: uid, name: self.model.get('username')},
destroyUserManagement
);
}).fail(function(e) {
let msg = '';
if(e.status == 404) {
msg = 'Unable to find url.';
} else {
msg = e.responseJSON.errormsg;
}
Notify.error(msg);
});
},
deleteUser: function() {
let self = this;
Notify.confirm(
gettext('Delete user?'),
gettext('Are you sure you wish to delete this user?'),
function() {
self.model.destroy({
wait: true,
success: function() {
Notify.success(gettext('User deleted.'));
},
error: self.raiseError,
});
},
function() {
return true;
}
);
},
deleteRow: function(e) {
var self = this;
e.preventDefault();
if (self.model.get('id') == userInfo['id']) {
Notify.alert(
gettext('Cannot delete user.'),
gettext('Cannot delete currently logged in user.'),
function() {
return true;
}
);
return true;
}
// We will check if row is deletable or not
var canDeleteRow = (!_.isUndefined(this.column.get('canDeleteRow')) &&
_.isFunction(this.column.get('canDeleteRow'))) ?
Backgrid.callByNeed(this.column.get('canDeleteRow'),
this.column, this.model) : true;
if (canDeleteRow) {
if (self.model.isNew()) {
self.model.destroy();
} else {
if(self.model.get('role') == 1){
$.ajax({
url: url_for('user_management.shared_servers', {'uid': self.model.get('id'),
}),
method: 'GET',
async: false,
})
.done(function(res) {
if(res['data'].shared_servers > 0) {
self.changeOwnership(res, self.model.get('id'));
} else {
self.deleteUser();
}
})
.fail(function() {
self.deleteUser();
});
} else {
self.deleteUser();
}
}
} else {
Notify.alert(
gettext('Error'),
gettext('This user cannot be deleted.'),
function() {
return true;
}
);
}
},
});
gridSchema.columns.unshift({
name: 'pg-backform-delete',
label: '',
cell: deleteUserCell,
editable: false,
cell_priority: -1,
canDeleteRow: true,
});
// Users Management dialog code here
if (!alertify.UserManagement) {
alertify.dialog('UserManagement', function factory() {
return {
main: function(title) {
this.set('title', title);
},
build: function() {
alertify.pgDialogBuild.apply(this);
},
setup: function() {
return {
buttons: [{
text: '',
key: 112,
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs: {
name: 'dialog_help',
type: 'button',
label: gettext('Users'),
url: url_for(
'help.static', {
'filename': 'user_management.html',
}),
},
}, {
text: gettext('Close'),
key: 27,
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button user_management_pg-alertify-button',
attrs: {
name: 'close',
type: 'button',
},
}],
// Set options for dialog
options: {
title: gettext('User Management'),
//disable both padding and overflow control.
padding: !1,
overflow: !1,
modal: false,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
closable: false,
},
};
},
hooks: {
// Triggered when the dialog is closed
onclose: function() {
if (this.view) {
// clear our backform model/view
this.view.remove({
data: true,
internal: true,
silent: true,
});
this.$content.remove();
}
},
},
prepare: function() {
var footerTpl = _.template([
'<div class="pg-prop-footer" style="visibility:hidden;">',
' <div class="pg-prop-status-bar">',
' <div class="error-in-footer"> ',
' <div class="d-flex px-2 py-1"> ',
' <div class="pr-2"> ',
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ',
' </div> ',
' <div class="alert-text" role="status"></div> ',
' <div class="ml-auto close-error-bar"> ',
' <a class="close-error fa fa-times text-danger"></a> ',
' </div> ',
' </div> ',
' </div> ',
' </div>',
'</div>',
].join('\n')),
$statusBar = $(footerTpl()),
UserRow = Backgrid.Row.extend({
userInvalidClass: 'bg-user-invalid',
initialize: function() {
Backgrid.Row.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'pgadmin:user:invalid', this.userInvalid);
this.listenTo(this.model, 'pgadmin:user:valid', this.userValid);
},
userInvalid: function() {
$(this.el).removeClass('new');
$(this.el).addClass(this.userInvalidClass);
},
userValid: function() {
$(this.el).removeClass(this.userInvalidClass);
},
}),
UserCollection = Backbone.Collection.extend({
model: UserModel,
url: USERURL,
initialize: function() {
Backbone.Collection.prototype.initialize.apply(this, arguments);
var self = this;
self.changedUser = null;
self.invalidUsers = {};
self.nonFilter = this;
self.on('add', self.onModelAdd);
self.on('remove', self.onModelRemove);
self.on('pgadmin-session:model:invalid', function(msg, m) {
self.invalidUsers[m.cid] = msg;
m.trigger('pgadmin:user:invalid', m);
$statusBar.find('.alert-text').html(msg);
$statusBar.css('visibility', 'visible');
});
self.on('pgadmin-session:model:valid', function(m) {
delete self.invalidUsers[m.cid];
m.trigger('pgadmin:user:valid', m);
this.updateErrorMsg();
this.saveUser(m);
});
},
onModelAdd: function(m) {
// Start tracking changes.
m.startNewSession();
},
onModelRemove: function(m) {
delete this.invalidUsers[m.cid];
this.updateErrorMsg();
},
updateErrorMsg: function() {
var self = this,
msg = null;
for (var key in self.invalidUsers) {
msg = self.invalidUsers[key];
if (msg) {
break;
}
}
if (msg) {
$statusBar.find('.alert-text').html(msg);
$statusBar.css('visibility', 'visible');
} else {
$statusBar.find('.alert-text').empty();
$statusBar.css('visibility', 'hidden');
}
},
saveUser: function(m) {
var d = m.toJSON(true);
if((m.isNew() && (m.get('auth_source') == LDAP || m.get('auth_source') == KERBEROS || m.get('auth_source') == OAUTH2) && (!m.get('username') || !m.get('auth_source') || !m.get('role')))
|| (m.isNew() && m.get('auth_source') == DEFAULT_AUTH_SOURCE && (!m.get('email') || !m.get('role') ||
!m.get('newPassword') || !m.get('confirmPassword') || m.get('newPassword') != m.get('confirmPassword')))
|| (!m.isNew() && m.get('newPassword') != m.get('confirmPassword'))) {
// For old user password change is in progress and user model is valid but admin has not added
// both the passwords so return without saving.
return false;
}
if (m.sessChanged() && d && !_.isEmpty(d)) {
m.stopSession();
m.save({}, {
attrs: d,
wait: true,
success: function() {
// User created/updated on server now start new session for this user.
let temp_auth_sources = m.get('auth_source');
m.set({
'newPassword': undefined,
'confirmPassword': undefined,
'auth_source': undefined,
});
// It's a heck to re-render the Auth Source control.
m.set({
'auth_source': temp_auth_sources,
});
m.startNewSession();
Notify.success(gettext('User \'%s\' saved.',
m.get('username')
));
},
error: function(res, jqxhr) {
m.startNewSession();
Notify.error(
gettext('Error saving user: \'%s\'',
jqxhr.responseJSON.errormsg
)
);
},
});
}
},
}),
userCollection = this.userCollection = new UserCollection(),
header =
`<div class="navtab-inline-controls pgadmin-controls">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text fa fa-search" id="labelSearch"></span>
</div>
<input type="search" class="form-control" id="txtGridSearch" placeholder="` + gettext('Search') + '" aria-label="' + gettext('Search') + `" aria-describedby="labelSearch" />
</div>
<button id="btn_add" type="button" class="btn btn-secondary btn-navtab-inline add" title="` + gettext('Add') + `">
<span class="fa fa-plus "></span>
</button>
</div>
</div>
</div>`,
headerTpl = _.template(header),
data = {
canAdd: true,
add_title: gettext('Add new user'),
},
$gridBody = $('<div></div>', {
class: 'user_container flex-grow-1',
});
$.ajax({
url: ROLEURL,
method: 'GET',
async: false,
})
.done(function(res) {
Roles = res;
})
.fail(function() {
setTimeout(function() {
Notify.alert(
gettext('Error'),
gettext('Cannot load user roles.')
);
}, 100);
});
$.ajax({
url: SOURCEURL,
method: 'GET',
async: false,
})
.done(function(res) {
Sources = res;
})
.fail(function() {
setTimeout(function() {
Notify.alert(
gettext('Error'),
gettext('Cannot load user Sources.')
);
}, 100);
});
var view = this.view = new Backgrid.Grid({
row: UserRow,
columns: gridSchema.columns,
collection: userCollection,
className: 'backgrid table table-bordered table-noouter-border table-bottom-border table-hover',
});
$gridBody.append(view.render().$el[0]);
this.$content = $('<div class=\'user_management object subnode subnode-noouter-border d-flex flex-column\'></div>').append(
headerTpl(data)).append($gridBody).append($statusBar);
this.elements.content.appendChild(this.$content[0]);
// Render Search Filter
userCollection.nonFilter = userFilter(userCollection).setCustomSearchBox($('#txtGridSearch')).shadowCollection;
userCollection.fetch();
this.$content.find('a.close-error').on('click',() => {
$statusBar.find('.alert-text').empty();
$statusBar.css('visibility', 'hidden');
});
this.$content.find('button.add').first().on('click',(e) => {
e.preventDefault();
// There should be only one empty row.
let anyNew = false;
for(const [idx, model] of userCollection.models.entries()) {
if(model.isNew()) {
let row = view.body.rows[idx].$el;
row.addClass('new');
$(row).pgMakeVisible('backgrid');
$(row).find('.email').trigger('click');
anyNew = true;
}
}
if(!anyNew) {
$(view.body.$el.find($('tr.new'))).removeClass('new');
var m = new(UserModel)(null, {
handler: userCollection,
top: userCollection,
collection: userCollection,
});
userCollection.add(m);
var newRow = view.body.rows[userCollection.indexOf(m)].$el;
newRow.addClass('new');
$(newRow).pgMakeVisible('backgrid');
$(newRow).find('.email').trigger('click');
}
return false;
});
commonUtils.findAndSetFocus(this.$content);
},
callback: function(e) {
if (e.button.element.name == 'dialog_help') {
e.cancel = true;
pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
null, null);
return;
}
if (e.button.element.name == 'close') {
var self = this;
if (!_.all(this.userCollection.pluck('id')) || !_.isEmpty(this.userCollection.invalidUsers)) {
e.cancel = true;
Notify.confirm(
gettext('Discard unsaved changes?'),
gettext('Are you sure you want to close the dialog? Any unsaved changes will be lost.'),
function() {
self.close();
return true;
},
function() {
// Do nothing.
return true;
}
);
}
}
},
};
});
}
alertify.UserManagement(true).resizeTo(pgBrowser.stdW.lg, pgBrowser.stdH.md);
},
};
return pgBrowser.UserManagement;
});
init() {
if (this.initialized)
return;
this.initialized = true;
}
// This is a callback function to show change user dialog.
change_password(url) {
showUrlDialog(gettext('Change Password'), url, 'change_user_password.html', undefined, pgAdmin.Browser.stdH.lg);
}
// This is a callback function to show 2FA dialog.
show_mfa(url) {
showUrlDialog(gettext('Authentication'), url, 'mfa.html', 1200, 680);
}
// This is a callback function to show user management dialog.
show_users() {
showUserManagement();
}
}
pgAdmin.UserManagement = UserManagement.getInstance();
module.exports = {
UserManagement: UserManagement,
};