mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-07-07 04:53:25 -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
|
login
|
||||||
mfa
|
mfa
|
||||||
user_management
|
user_management
|
||||||
|
change_ownership
|
||||||
change_user_password
|
change_user_password
|
||||||
restore_locked_user
|
restore_locked_user
|
||||||
ldap
|
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 #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 #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 #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.
|
| `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:
|
Provide information about the new pgAdmin role in the row:
|
||||||
|
|
||||||
* Use the drop-down list box next to *Authentication source* field to select the
|
* 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
|
type of authentication that should be used for the user. If authentication
|
||||||
authentication is not enabled for pgAdmin, then *Authentication source* field
|
source is only 'internal' then *Authentication source* field
|
||||||
is disabled.
|
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
|
* 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
|
is enabled only when you select authentication source except *internal*. If you
|
||||||
*internal* as authentication source, your email address is displayed in the
|
select *internal* as authentication source, your email address is displayed in the
|
||||||
username field.
|
username field.
|
||||||
* Click in the *Email* field, and provide an email address for the user.
|
* 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
|
* 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
|
active; the default is *Yes*. Use this switch to disable account activity
|
||||||
without deleting an account.
|
without deleting an account.
|
||||||
* Use the *New password* field to provide the password associated with the user
|
* 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*
|
specified in the *Email* field. This field is disabled if you select any
|
||||||
as authentication source since LDAP password is not stored in the pgAdmin database.
|
authentication source except *internal*.
|
||||||
* Re-enter the password in the *Confirm password* field. This field is disabled
|
* Re-enter the password in the *Confirm password* field. This field is disabled
|
||||||
if you select *ldap* as authentication source.
|
if you select *ldap* as authentication source.
|
||||||
* Move the *Locked* switch to the *True* position if you want to lock the account;
|
* 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
|
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 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.
|
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">
|
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||||
{% if auth_only_internal %}
|
{% if auth_only_internal %}
|
||||||
<li>
|
<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') }}'
|
'{{ url_for('browser.change_password') }}'
|
||||||
)">
|
)">
|
||||||
{{ _('Change Password') }}
|
{{ _('Change Password') }}
|
||||||
|
@ -154,14 +154,14 @@ window.onload = function(e){
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if mfa_enabled is defined and mfa_enabled is true %}
|
{% if mfa_enabled is defined and mfa_enabled is true %}
|
||||||
<li>
|
<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") }}'
|
'{{ login_url("mfa.register", next_url="internal") }}'
|
||||||
)">{{ _('Two-Factor Authentication') }}</a>
|
)">{{ _('Two-Factor Authentication') }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown-divider"></li>
|
<li class="dropdown-divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_admin %}
|
{% 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>
|
<li class="dropdown-divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a class="dropdown-item" role="menuitem" href="{{ logout_url }}">{{ _('Logout') }}</a></li>
|
<li><a class="dropdown-item" role="menuitem" href="{{ logout_url }}">{{ _('Logout') }}</a></li>
|
||||||
|
|
|
@ -301,7 +301,7 @@ export function showChangeOwnership() {
|
||||||
userList = arguments[1],
|
userList = arguments[1],
|
||||||
noOfSharedServers = arguments[2],
|
noOfSharedServers = arguments[2],
|
||||||
deletedUser = arguments[3],
|
deletedUser = arguments[3],
|
||||||
destroyUserManagement = arguments[4];
|
deleteUserRow = arguments[4];
|
||||||
|
|
||||||
// Render Preferences component
|
// Render Preferences component
|
||||||
Notify.showModal(title, (onClose) => {
|
Notify.showModal(title, (onClose) => {
|
||||||
|
@ -314,24 +314,15 @@ export function showChangeOwnership() {
|
||||||
|
|
||||||
return new Promise((resolve, reject)=>{
|
return new Promise((resolve, reject)=>{
|
||||||
if (data.newUser == '') {
|
if (data.newUser == '') {
|
||||||
api.delete(url_for('user_management.user', {uid: deletedUser['uid']}))
|
deleteUserRow();
|
||||||
.then(() => {
|
onClose();
|
||||||
Notify.success(gettext('User deleted.'));
|
|
||||||
onClose();
|
|
||||||
destroyUserManagement();
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
Notify.error(err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
} else {
|
} 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)
|
api.post(url_for('user_management.change_owner'), newData)
|
||||||
.then(({data: respData})=>{
|
.then(({data: respData})=>{
|
||||||
Notify.success(gettext(respData.info));
|
Notify.success(gettext(respData.info));
|
||||||
onClose();
|
onClose();
|
||||||
destroyUserManagement();
|
deleteUserRow();
|
||||||
resolve(respData.data);
|
resolve(respData.data);
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
|
|
@ -17,7 +17,7 @@ import AddIcon from '@material-ui/icons/AddOutlined';
|
||||||
import { MappedCellControl } from './MappedControl';
|
import { MappedCellControl } from './MappedControl';
|
||||||
import EditRoundedIcon from '@material-ui/icons/EditRounded';
|
import EditRoundedIcon from '@material-ui/icons/EditRounded';
|
||||||
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
|
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 clsx from 'clsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
@ -30,6 +30,7 @@ import { evalFunc } from 'sources/utils';
|
||||||
import { DepListenerContext } from './DepListener';
|
import { DepListenerContext } from './DepListener';
|
||||||
import { useIsMounted } from '../custom_hooks';
|
import { useIsMounted } from '../custom_hooks';
|
||||||
import Notify from '../helpers/Notifier';
|
import Notify from '../helpers/Notifier';
|
||||||
|
import { InputText } from '../components/FormComponents';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme)=>({
|
const useStyles = makeStyles((theme)=>({
|
||||||
grid: {
|
grid: {
|
||||||
|
@ -225,11 +226,26 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
|
||||||
</div>, depsMap);
|
</div>, depsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataGridHeader({label, canAdd, onAddClick}) {
|
export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTextChange}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.gridHeader}>
|
<Box className={classes.gridHeader}>
|
||||||
|
{ label &&
|
||||||
<Box className={classes.gridHeaderText}>{label}</Box>
|
<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}>
|
<Box className={classes.gridControls}>
|
||||||
{canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
|
{canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -240,6 +256,8 @@ DataGridHeader.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
canAdd: PropTypes.bool,
|
canAdd: PropTypes.bool,
|
||||||
onAddClick: PropTypes.func,
|
onAddClick: PropTypes.func,
|
||||||
|
canSearch: PropTypes.bool,
|
||||||
|
onSearchTextChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DataGridView({
|
export default function DataGridView({
|
||||||
|
@ -303,21 +321,27 @@ export default function DataGridView({
|
||||||
return (
|
return (
|
||||||
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />}
|
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />}
|
||||||
onClick={()=>{
|
onClick={()=>{
|
||||||
Notify.confirm(
|
const deleteRow = ()=> {
|
||||||
props.customDeleteTitle || gettext('Delete Row'),
|
dataDispatch({
|
||||||
props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
|
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
||||||
function() {
|
path: accessPath,
|
||||||
dataDispatch({
|
value: row.index,
|
||||||
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
});
|
||||||
path: accessPath,
|
return true;
|
||||||
value: row.index,
|
};
|
||||||
});
|
|
||||||
return true;
|
if (props.onDelete){
|
||||||
},
|
props.onDelete(row.original || {}, deleteRow);
|
||||||
function() {
|
} else {
|
||||||
return true;
|
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} />
|
}} className={classes.gridRowButton} disabled={!canDeleteRow} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -425,6 +449,7 @@ export default function DataGridView({
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
let tablePlugins = [
|
let tablePlugins = [
|
||||||
|
useGlobalFilter,
|
||||||
useFlexLayout,
|
useFlexLayout,
|
||||||
useResizeColumns,
|
useResizeColumns,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
|
@ -437,10 +462,11 @@ export default function DataGridView({
|
||||||
headerGroups,
|
headerGroups,
|
||||||
rows,
|
rows,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
|
setGlobalFilter,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
data: value || [],
|
data: value,
|
||||||
defaultColumn,
|
defaultColumn,
|
||||||
manualSortBy: true,
|
manualSortBy: true,
|
||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
|
@ -476,7 +502,12 @@ export default function DataGridView({
|
||||||
return (
|
return (
|
||||||
<Box className={containerClassName}>
|
<Box className={containerClassName}>
|
||||||
<Box className={classes.grid}>
|
<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}>
|
<div {...getTableProps()} className={classes.table}>
|
||||||
<DataTableHeader headerGroups={headerGroups} />
|
<DataTableHeader headerGroups={headerGroups} />
|
||||||
<div {...getTableBodyProps()} className={classes.tableContentWidth}>
|
<div {...getTableBodyProps()} className={classes.tableContentWidth}>
|
||||||
|
@ -524,4 +555,6 @@ DataGridView.propTypes = {
|
||||||
]),
|
]),
|
||||||
customDeleteTitle: PropTypes.string,
|
customDeleteTitle: PropTypes.string,
|
||||||
customDeleteMsg: PropTypes.string,
|
customDeleteMsg: PropTypes.string,
|
||||||
|
canSearch: PropTypes.bool,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -268,10 +268,10 @@ export default function FormView({
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = {
|
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,
|
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
|
||||||
containerClassName: classes.controlRow, ...field, canAdd: canAdd, canEdit: canEdit, canDelete: canDelete,
|
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) {
|
if(CustomControl) {
|
||||||
|
|
|
@ -155,7 +155,7 @@ function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, v
|
||||||
case 'text':
|
case 'text':
|
||||||
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
|
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||||
case 'password':
|
case 'password':
|
||||||
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
|
return <InputText name={name} value={value} onChange={onTextChange} {...props} type='password'/>;
|
||||||
case 'select':
|
case 'select':
|
||||||
return <InputSelect name={name} value={value} onChange={onTextChange} optionsLoaded={optionsLoadedRerender} {...props}/>;
|
return <InputSelect name={name} value={value} onChange={onTextChange} optionsLoaded={optionsLoadedRerender} {...props}/>;
|
||||||
case 'switch':
|
case 'switch':
|
||||||
|
|
|
@ -886,7 +886,7 @@ function SchemaPropertiesView({
|
||||||
key={field.id}
|
key={field.id}
|
||||||
viewHelperProps={viewHelperProps}
|
viewHelperProps={viewHelperProps}
|
||||||
name={field.id}
|
name={field.id}
|
||||||
value={origData[field.id]}
|
value={origData[field.id] || []}
|
||||||
schema={field.schema}
|
schema={field.schema}
|
||||||
accessPath={[field.id]}
|
accessPath={[field.id]}
|
||||||
formErr={{}}
|
formErr={{}}
|
||||||
|
|
|
@ -10,10 +10,8 @@
|
||||||
"""Implements pgAdmin4 User Management Utility"""
|
"""Implements pgAdmin4 User Management Utility"""
|
||||||
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
import re
|
|
||||||
|
|
||||||
from flask import render_template, request, \
|
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_babel import gettext as _
|
||||||
from flask_security import login_required, roles_required, current_user
|
from flask_security import login_required, roles_required, current_user
|
||||||
from flask_security.utils import encrypt_password
|
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
|
make_json_response, bad_request, internal_server_error, forbidden
|
||||||
from pgadmin.utils.csrf import pgCSRFProtect
|
from pgadmin.utils.csrf import pgCSRFProtect
|
||||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\
|
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.utils.validation_utils import validate_email
|
||||||
from pgadmin.model import db, Role, User, UserPreference, Server, \
|
from pgadmin.model import db, Role, User, UserPreference, Server, \
|
||||||
ServerGroup, Process, Setting, roles_users, SharedServer
|
ServerGroup, Process, Setting, roles_users, SharedServer
|
||||||
|
@ -59,12 +57,11 @@ class UserManagementModule(PgAdminModule):
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
'user_management.roles', 'user_management.role',
|
'user_management.roles', 'user_management.role',
|
||||||
'user_management.update_user', 'user_management.delete_user',
|
'user_management.users', 'user_management.user',
|
||||||
'user_management.create_user', 'user_management.users',
|
current_app.login_manager.login_view,
|
||||||
'user_management.user', current_app.login_manager.login_view,
|
'user_management.auth_sources', 'user_management.change_owner',
|
||||||
'user_management.auth_sources', 'user_management.auth_sources',
|
|
||||||
'user_management.shared_servers', 'user_management.admin_users',
|
'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("/")
|
@blueprint.route("/")
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
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')
|
@blueprint.route('/change_owner', methods=['POST'], endpoint='change_owner')
|
||||||
@roles_required('Administrator')
|
@roles_required('Administrator')
|
||||||
def change_owner():
|
def change_owner():
|
||||||
|
@ -430,9 +236,6 @@ def change_owner():
|
||||||
server_group.user_id = data['new_owner']
|
server_group.user_id = data['new_owner']
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# Delete old owner records.
|
|
||||||
delete(data['old_owner'])
|
|
||||||
|
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
success=1,
|
success=1,
|
||||||
info=_("Owner changed successfully."),
|
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(
|
@blueprint.route(
|
||||||
'/role/', methods=['GET'], defaults={'rid': None}, endpoint='roles'
|
'/role/', methods=['GET'], defaults={'rid': None}, endpoint='roles'
|
||||||
)
|
)
|
||||||
|
@ -650,3 +387,221 @@ def auth_sources():
|
||||||
response=sources,
|
response=sources,
|
||||||
status=200
|
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 pgAdmin from 'sources/pgadmin';
|
||||||
import { showChangeOwnership, showUrlDialog } from '../../../../static/js/Dialogs/index';
|
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.
|
class UserManagement {
|
||||||
if (pgBrowser.UserManagement) {
|
static instance;
|
||||||
return pgBrowser.UserManagement;
|
|
||||||
|
static getInstance(...args) {
|
||||||
|
if (!UserManagement.instance) {
|
||||||
|
UserManagement.instance = new UserManagement(...args);
|
||||||
|
}
|
||||||
|
return UserManagement.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
var USERURL = url_for('user_management.users'),
|
init() {
|
||||||
ROLEURL = url_for('user_management.roles'),
|
if (this.initialized)
|
||||||
SOURCEURL = url_for('user_management.auth_sources'),
|
return;
|
||||||
DEFAULT_AUTH_SOURCE = pgConst['INTERNAL'],
|
this.initialized = true;
|
||||||
LDAP = pgConst['LDAP'],
|
}
|
||||||
KERBEROS = pgConst['KERBEROS'],
|
|
||||||
OAUTH2 = pgConst['OAUTH2'],
|
// This is a callback function to show change user dialog.
|
||||||
AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length == 1 && userInfo['auth_sources'].includes(DEFAULT_AUTH_SOURCE)) ? true : false,
|
change_password(url) {
|
||||||
userFilter = function(collection) {
|
showUrlDialog(gettext('Change Password'), url, 'change_user_password.html', undefined, pgAdmin.Browser.stdH.lg);
|
||||||
return (new Backgrid.Extension.ClientSideFilter({
|
}
|
||||||
collection: collection,
|
|
||||||
placeholder: gettext('Filter by email'),
|
// This is a callback function to show 2FA dialog.
|
||||||
// How long to wait after typing has stopped before searching can start
|
show_mfa(url) {
|
||||||
wait: 150,
|
showUrlDialog(gettext('Authentication'), url, 'mfa.html', 1200, 680);
|
||||||
}));
|
}
|
||||||
};
|
|
||||||
|
// This is a callback function to show user management dialog.
|
||||||
// Integer Cell for Columns Length and Precision
|
show_users() {
|
||||||
var PasswordDepCell = Backgrid.Extension.PasswordDepCell =
|
showUserManagement();
|
||||||
Backgrid.Extension.PasswordCell.extend({
|
}
|
||||||
initialize: function() {
|
}
|
||||||
Backgrid.Extension.PasswordCell.prototype.initialize.apply(this, arguments);
|
|
||||||
Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments);
|
pgAdmin.UserManagement = UserManagement.getInstance();
|
||||||
},
|
|
||||||
dependentChanged: function () {
|
module.exports = {
|
||||||
this.$el.empty();
|
UserManagement: UserManagement,
|
||||||
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;
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user