mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-07-04 11:33:52 -05:00
Port User Management dialog to React. Fixes #7345
This commit is contained in:
parent
3766fa7f0b
commit
271b6d91fc
27
docs/en_US/change_ownership.rst
Normal file
27
docs/en_US/change_ownership.rst
Normal 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.
|
|
@ -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 |
BIN
docs/en_US/images/change_ownership.png
Normal file
BIN
docs/en_US/images/change_ownership.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
docs/en_US/images/change_ownership_info.png
Normal file
BIN
docs/en_US/images/change_ownership_info.png
Normal file
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 |
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)=>{
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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={{}}
|
||||
|
|
|
@ -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, ''
|
||||
|
|
|
@ -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]);
|
||||
}
|
|
@ -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,
|
||||
};
|
Loading…
Reference in New Issue
Block a user