Administer pgAdmin Users and Preferences Using the Command Line Interface (CLI). #2483

This commit is contained in:
Khushboo Vashi 2023-12-21 12:07:26 +05:30 committed by GitHub
parent a973c9c62c
commit 0d287df6dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 749 additions and 165 deletions

View File

@ -61,7 +61,7 @@ Exporting Servers
*****************
To export the servers defined in an installation, simply invoke ``setup.py`` with
the ``--dump-servers`` command line option, followed by the name (and if required,
the ``dump-servers`` command line option, followed by the name (and if required,
path) to the desired output file. By default, servers owned by the desktop mode
user will be dumped (pgadmin4@pgadmin.org by default - see the DESKTOP_USER
setting in ``config.py``). This can be overridden with the ``--user`` command
@ -73,28 +73,28 @@ For example:
.. code-block:: bash
/path/to/python /path/to/setup.py --dump-servers output_file.json
/path/to/python /path/to/setup.py dump-servers output_file.json
# or, to specify a non-default user name:
# or, to specify a non-default user name and auth source (the default is Internal):
/path/to/python /path/to/setup.py --dump-servers output_file.json --user user@example.com
/path/to/python /path/to/setup.py dump-servers output_file.json --user user@example.com --auth_source ldap
# to specify a pgAdmin config DB file:
/path/to/python /path/to/setup.py --dump-servers output_file.json --sqlite-path /path/to/pgadmin4.db
/path/to/python /path/to/setup.py dump-servers output_file.json --sqlite-path /path/to/pgadmin4.db
To export only certain servers, use the ``--servers`` option and list one or
To export only certain servers, use the ``--server`` option and list one or
more server IDs. For example:
.. code-block:: bash
/path/to/python /path/to/setup.py --dump-servers output_file.json --server 1 2 5
/path/to/python /path/to/setup.py dump-servers output_file.json --server 1 --server 2 --server 5
Importing Servers
*****************
To import the servers defined in a JSON file, simply invoke ``setup.py`` with
the ``--load-servers`` command line option, followed by the name (and if required,
the ``load-servers`` command line option, followed by the name (and if required,
path) of the JSON file containing the server definitions. Servers will be owned
by the desktop mode user (pgadmin4@pgadmin.org by default - see the DESKTOP_USER
setting in ``config.py``). This can be overridden with the ``--user`` command
@ -108,19 +108,19 @@ desktop mode. By default SQLITE_PATH setting in ``config.py`` is taken. For exam
.. code-block:: bash
/path/to/python /path/to/setup.py --load-servers input_file.json
/path/to/python /path/to/setup.py load-servers input_file.json
# or, to replace the list of servers with the newly imported one:
/path/to/python /path/to/setup.py --load-servers input_file.json --replace
/path/to/python /path/to/setup.py load-servers input_file.json --replace
# or, to specify a non-default user name to own the new servers:
# or, to specify a non-default user name and auth source (the default is Internal) to own the new servers:
/path/to/python /path/to/setup.py --load-servers input_file.json --user user@example.com
/path/to/python /path/to/setup.py load-servers input_file.json --user user@example.com
# to specify a pgAdmin config DB file:
/path/to/python /path/to/setup.py --load-servers input_file.json --sqlite-path /path/to/pgadmin4.db
/path/to/python /path/to/setup.py load-servers input_file.json --sqlite-path /path/to/pgadmin4.db
If any Servers are defined with a Server Group that is not already present in
the configuration database, the required Group will be created.

View File

@ -549,3 +549,40 @@ Use the fields on the *Options* panel to specify storage preferences.
* When the *Show hidden files and folders?* switch is set to *True*, the file
manager will display hidden files and folders.
Using 'setup.py' command line script
####################################
.. note:: To manage preferences using ``setup.py`` script, you must use
the Python interpreter that is normally used to run pgAdmin to ensure
that the required Python packages are available. In most packages, this
can be found in the Python Virtual Environment that can be found in the
installation directory. When using platform-native packages, the system
installation of Python may be the one used by pgAdmin.
Manage Preferences
******************
Get Preferences
***************
To get all the preferences listed, invoke ``setup.py`` with ``get-prefs`` command line option.
You can also get this mapping by hovering the individual preference in the Preference UI dialog.
.. code-block:: bash
/path/to/python /path/to/setup.py get-prefs
Save Preferences
****************
To save the preferences, invoke ``setup.py`` with ``set-prefs`` command line option, followed by username,
preference_key=value and auth_source. Multiple preference can be given too by a space separated.
If auth_source is not given, Internal authentication will be consider by default.
.. code-block:: bash
/path/to/python /path/to/setup.py set-prefs user1@gmail.com sqleditor:editor:comma_first=true
# to specify an auth_source
/path/to/python /path/to/setup.py set-prefs user1@gmail.com sqleditor:editor:comma_first=true --auth-source=ldap

View File

@ -75,3 +75,142 @@ users, but otherwise have the same capabilities as those with the *User* role.
* Click the *Help* button (?) to access online help.
* Click the *Close* button to save work. You will be prompted to return to the
dialog if your selections cannot be saved.
Using 'setup.py' command line script
####################################
.. note:: To manage users using ``setup.py`` script, you must use
the Python interpreter that is normally used to run pgAdmin to ensure
that the required Python packages are available. In most packages, this
can be found in the Python Virtual Environment that can be found in the
installation directory. When using platform-native packages, the system
installation of Python may be the one used by pgAdmin.
When using PIP wheel package to install pgadmin, all the commands can be used
without Python interpreter.
Some of the examples:
pgadmin4-cli add-user user1@gmail.com password --role 1
pgadmin4-cli get-prefs
Manage Users
*************
Add User
*********
To add user, invoke ``setup.py`` with ``add-user`` command line option, followed by
email and password. role and active will be optional fields.
.. code-block:: bash
/path/to/python /path/to/setup.py add-user user1@gmail.com password
# to specify a role, admin and non-admin users:
/path/to/python /path/to/setup.py add-user user1@gmail.com password --admin
/path/to/python /path/to/setup.py add-user user1@gmail.com password --nonadmin
# to specify user's status
/path/to/python /path/to/setup.py add-user user1@gmail.com password --active
/path/to/python /path/to/setup.py add-user user1@gmail.com password --inactive
Add External User
*****************
To add external authentication user, invoke ``setup.py`` with ``add-external-user`` command line option,
followed by email, password and authentication source. email, role and status will be optional fields.
.. code-block:: bash
/path/to/python /path/to/setup.py add-external-user user1@gmail.com ldap
# to specify an email:
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --email user1@gmail.com
# to specify a role, admin and non-admin user:
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --admin
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --nonadmin
# to specify user's status
/path/to/python /path/to/setup.py add-external-user user1@gmail.com ldap --active
/path/to/python /path/to/setup.py add-external-user user1@gmail.com ldap --inactive
Update User
***********
To update user, invoke ``setup.py`` with ``update-user`` command line option, followed by
email address. password, role and active are updatable fields.
.. code-block:: bash
/path/to/python /path/to/setup.py update-user user1@gmail.com --password new-password
# to specify a role, admin and non-admin user:
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --admin
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --nonadmin
# to specify user's status
/path/to/python /path/to/setup.py update-user user1@gmail.com password --active
/path/to/python /path/to/setup.py update-user user1@gmail.com password --inactive
Update External User
********************
To update the external user, invoke ``setup.py`` with ``update-external-user`` command line option,
followed by username and auth source. email, password, role and active are updatable fields.
.. code-block:: bash
# to change email address:
/path/to/python /path/to/setup.py update-external-user ldap ldapuser --email newemail@gmail.com
# to specify a role, admin and non-admin user:
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --admin
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --nonadmin
# to change user's status
/path/to/python /path/to/setup.py update-user ldap ldapuser --active
/path/to/python /path/to/setup.py update-user ldap ldapuser --inactive
Delete User
***********
To delete the user, invoke ``setup.py`` with ``delete-user`` command line option, followed by
username and auth_source. For Internal users, email adress will be used instead of username.
.. code-block:: bash
/path/to/python /path/to/setup.py delete-user user1@gmail.com --auth-source internal
/path/to/python /path/to/setup.py delete-user ldapuser --auth-source ldap
Get User
********
To get the user details, invoke ``setup.py`` with ``get-users`` command line option, followed by
username/email address.
.. code-block:: bash
# to list all the users:
/path/to/python /path/to/setup.py get-users
# to get the user's details:
/path/to/python /path/to/setup.py get-users --username user1@gmail.com
Output
******
Each command output can be seen in the json format too by adding --json command line option.

View File

@ -70,9 +70,9 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ]; then
# When running in Desktop mode, no user is created
# so we have to import servers anonymously
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
/venv/bin/python3 /pgadmin4/setup.py --load-servers "${PGADMIN_SERVER_JSON_FILE}"
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}"
else
/venv/bin/python3 /pgadmin4/setup.py --load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}"
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}"
fi
fi
fi

View File

@ -72,7 +72,7 @@ fi
# Run setup script first:
echo "Creating configuration database..."
if ! /usr/pgadmin4/venv/bin/python3 /usr/pgadmin4/web/setup.py;
if ! /usr/pgadmin4/venv/bin/python3 /usr/pgadmin4/web/setup.py setup-db;
then
echo "Error setting up server mode. Please examine the output above."
exit 1

View File

@ -100,7 +100,8 @@ setup(
},
entry_points={
'console_scripts': ['pgadmin4=pgadmin4.pgAdmin4:main'],
'console_scripts': ['pgadmin4=pgadmin4.pgAdmin4:main',
'pgadmin4-cli=pgadmin4.setup:main'],
},
)

View File

@ -60,3 +60,4 @@ keyring==24.*; python_version > '3.7'
keyring==23.*; python_version <= '3.7'
Werkzeug==2.3.*; python_version > '3.7'
Werkzeug==2.2.3; python_version <= '3.7'
typer[all]==0.9.*

View File

@ -105,7 +105,7 @@ app = create_app()
app.config['sessions'] = dict()
if setup_db_required:
setup.setup_db(app)
setup.setup_db()
# Authentication sources
if len(config.AUTHENTICATION_SOURCES) > 0:

View File

@ -216,6 +216,7 @@ def create_app(app_name=None):
app.config.from_object(config)
app.config.update(dict(PROPAGATE_EXCEPTIONS=True))
config.SETTINGS_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION
##########################################################################
# Setup logging and log the application startup
##########################################################################

View File

@ -185,7 +185,7 @@ class ServerGroupView(NodeView):
# This matches the behavior of
# web/pgadmin/utils/__init.py__#clear_database_servers
# called by the setup script when importing and replacing servers:
# `python setup.py --load-servers input_file.json --replace`
# `python setup.py load-servers input_file.json --replace`
sg = groups.first()
shared_servers = Server.query.filter_by(servergroup_id=gid,

View File

@ -48,8 +48,8 @@ class PreferencesModule(PgAdminModule):
'preferences.index',
'preferences.get_by_name',
'preferences.get_all',
'preferences.get_all_cli',
'preferences.update_pref'
]
@ -181,6 +181,28 @@ def preferences_s():
)
@blueprint.route("/get_all_cli", methods=["GET"], endpoint='get_all_cli')
def get_all_cli():
"""Fetch all preferences for caching."""
# Load Preferences
pref = Preferences.preferences()
res = {}
for m in pref:
if len(m['categories']):
for c in m['categories']:
for p in c['preferences']:
p['module'] = m['name']
res["{0}:{1}:{2}".format(m['label'], p['label'], c['label']
)] = "{0}:{1}:{2}".format(
p['module'],c['name'],p['name'])
return ajax_response(
response=res,
status=200
)
def get_data():
"""
Get preferences data.
@ -249,6 +271,26 @@ def save():
return response
def save_pref(data):
"""
Save a specific preference.
"""
if data['name'] in ['vw_edt_tab_title_placeholder',
'qt_tab_title_placeholder',
'debugger_tab_title_placeholder'] \
and data['value'].isspace():
data['value'] = ''
res, msg = Preferences.save_cli(
data['mid'], data['category_id'], data['id'], data['user_id'],
data['value'])
if not res:
return False
return True
@blueprint.route("/update", methods=["PUT"], endpoint="update_pref")
@login_required
def update():

View File

@ -378,6 +378,7 @@ export default function PreferencesComponent({ ...props }) {
if(field.visible && _.isNull(firstElement)) {
firstElement = field;
}
field.tooltip = item._parent._metadata.data.name + ':' + item._metadata.data.name + ':' + field.name;
});
setLoadTree(crypto.getRandomValues(new Uint16Array(1)));
initTreeTimeout = setTimeout(() => {
@ -598,6 +599,10 @@ export default function PreferencesComponent({ ...props }) {
window.open(url_for('help.static', { 'filename': 'preferences.html' }), 'pgadmin_help');
};
const onDialogHelpCli = () => {
window.open(url_for('preferences.get_all_cli'), 'pgadmin_help');
};
return (
<Box height={'100%'}>
<Box className={classes.root}>
@ -623,6 +628,7 @@ export default function PreferencesComponent({ ...props }) {
<Box className={classes.footer}>
<Box>
<PgIconButton data-test="dialog-help" onClick={onDialogHelp} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
<PgIconButton data-test="dialog-help-cli" onClick={onDialogHelpCli} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
</Box>
<Box className={classes.actionBtn} marginLeft="auto">
<DefaultButton className={classes.buttonMargin} onClick={() => { props.closeModal();}} startIcon={<CloseSharpIcon onClick={() => { props.closeModal();}} />}>

View File

@ -36,13 +36,13 @@ class ImportExportServersTestCase(BaseTestGenerator):
# Load the servers
os.system(
"python \"%s\" --load-servers \"%s\" 2> %s" %
"python \"%s\" load-servers \"%s\" 2> %s" %
(setup, os.path.join(path, "servers.json"), os.devnull)
)
# And dump them again
tf = tempfile.NamedTemporaryFile(delete=False)
os.system("python \"%s\" --dump-servers \"%s\" 2> %s" %
os.system("python \"%s\" dump-servers \"%s\" 2> %s" %
(setup, tf.name, os.devnull))
# Compare the JSON files, ignoring servers that exist in our

View File

@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Box, makeStyles, Tab, Tabs } from '@material-ui/core';
import { Box, makeStyles, Tab, Tabs, Tooltip } from '@material-ui/core';
import _ from 'lodash';
import PropTypes from 'prop-types';
import clsx from 'clsx';
@ -359,6 +359,10 @@ export default function FormView({
]}
/>;
if(field.tooltip) {
currentControl = <Tooltip title={field.tooltip} aria-label={field.tooltip}>{currentControl}</Tooltip>;
}
if(field.isFullTab && field.helpMessage) {
currentControl = (<React.Fragment key={`coll-${field.id}`}>
<FormNote key={`note-${field.id}`} text={field.helpMessage}/>

View File

@ -212,7 +212,7 @@ const ALLOWED_PROPS_FIELD_COMMON = [
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName', 'hidden',
'withContainer', 'controlGridBasis', 'hasCheckbox', 'treeData'
'withContainer', 'controlGridBasis', 'hasCheckbox', 'treeData', 'title'
];
const ALLOWED_PROPS_FIELD_FORM = [

View File

@ -27,7 +27,7 @@ const useStyles = makeStyles((theme) => ({
}
}));
export default function KeyboardShortcuts({ value, onChange, fields }) {
export default function KeyboardShortcuts({ value, onChange, fields, title }) {
const classes = useStyles();
const keyCid = _.uniqueId('c');
const keyhelpid = `h${keyCid}`;
@ -87,11 +87,11 @@ export default function KeyboardShortcuts({ value, onChange, fields }) {
<Typography id={keyLabel}>{element.label}</Typography>
</Grid>
<Grid item lg={8} md={8} sm={8} xs={12}>
<InputText id={keyCid} helpid={keyhelpid} type='text' value={value?.key?.char} controlProps={
<InputText id={keyCid} helpid={keyhelpid} value={value?.key?.char} controlProps={
{
onKeyDown: onKeyDown,
}
}></InputText>
} title={title} />
</Grid>
</Grid>;
} else if (element.name == 'shift') {
@ -129,5 +129,6 @@ KeyboardShortcuts.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
controlProps: PropTypes.object,
fields: PropTypes.array
fields: PropTypes.array,
title: PropTypes.string
};

View File

@ -552,10 +552,12 @@ def update_user(uid, data):
# Username and email can not be changed for internal users
if usr.auth_source == INTERNAL:
non_editable_params = ('username', 'email')
else:
non_editable_params = ('username',)
for f in non_editable_params:
if f in data:
return False, _("'{0}' is not allowed to modify.").format(f)
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)
@ -580,6 +582,7 @@ 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)

View File

@ -24,7 +24,7 @@ from threading import Lock
from .paths import get_storage_directory
from .preferences import Preferences
from pgadmin.utils.constants import UTILITIES_ARRAY, USER_NOT_FOUND, \
MY_STORAGE, ACCESS_DENIED_MESSAGE
MY_STORAGE, ACCESS_DENIED_MESSAGE, INTERNAL
from pgadmin.utils.ajax import make_json_response
from pgadmin.model import db, User, ServerGroup, Server
from urllib.parse import unquote
@ -439,10 +439,11 @@ def add_value(attr_dict, key, value):
def dump_database_servers(output_file, selected_servers,
dump_user=current_user, from_setup=False):
dump_user=current_user, from_setup=False,
auth_source=INTERNAL):
"""Dump the server groups and servers.
"""
user = _does_user_exist(dump_user, from_setup)
user = _does_user_exist(dump_user, from_setup, auth_source)
if user is None:
return False, USER_NOT_FOUND % dump_user
@ -456,7 +457,10 @@ def dump_database_servers(output_file, selected_servers,
servers = Server.query.filter_by(user_id=user_id).all()
server_dict = {}
for server in servers:
if selected_servers is None or str(server.id) in selected_servers:
if selected_servers is None or (
isinstance(selected_servers, list) and len(selected_servers) == 0)\
or str(server.id) in selected_servers\
or server.id in selected_servers:
# Get the group name
group_name = ServerGroup.query.filter_by(
user_id=user_id, id=server.servergroup_id).first().name
@ -592,10 +596,11 @@ def validate_json_data(data, is_admin):
def load_database_servers(input_file, selected_servers,
load_user=current_user, from_setup=False):
load_user=current_user, from_setup=False,
auth_source=INTERNAL):
"""Load server groups and servers.
"""
user = _does_user_exist(load_user, from_setup)
user = _does_user_exist(load_user, from_setup, auth_source)
if user is None:
return False, USER_NOT_FOUND % load_user
@ -745,10 +750,11 @@ def load_database_servers(input_file, selected_servers,
return True, msg
def clear_database_servers(load_user=current_user, from_setup=False):
def clear_database_servers(load_user=current_user, from_setup=False,
auth_source=INTERNAL):
"""Clear groups and servers configurations.
"""
user = _does_user_exist(load_user, from_setup)
user = _does_user_exist(load_user, from_setup, auth_source)
if user is None:
return False
@ -782,14 +788,16 @@ def clear_database_servers(load_user=current_user, from_setup=False):
return False, error_msg
def _does_user_exist(user, from_setup):
def _does_user_exist(user, from_setup, auth_source=INTERNAL):
"""
This function will check user is exist or not. If exist then return
"""
if isinstance(user, User):
user = user.email
user = user.username
auth_source = user.auth_source
new_user = User.query.filter_by(email=user).first()
new_user = User.query.filter_by(username=user,
auth_source=auth_source).first()
if new_user is None:
print(USER_NOT_FOUND % user)

View File

@ -41,6 +41,7 @@ class _PGCSRFProtect(CSRFProtect):
'pgadmin.authenticate.login',
'pgadmin.tools.erd.panel',
'pgadmin.tools.psql.panel',
'pgadmin.preferences.get_all_cli',
]
for exempt in exempt_views:

View File

@ -594,6 +594,34 @@ class Preferences():
return None
@classmethod
def save_cli(cls, mid, cid, pid, user_id, value):
"""
save
Update the value for the preference in the configuration database.
:param mid: Module ID
:param cid: Category ID
:param pid: Preference ID
:param value: Value for the options
"""
pref = UserPrefTable.query.filter_by(
pid=pid
).filter_by(uid=user_id).first()
value = "{}".format(value)
if pref is None:
pref = UserPrefTable(
uid=user_id, pid=pid, value=value
)
db.session.add(pref)
else:
pref.value = value
db.session.commit()
return True, None
@classmethod
def save(cls, mid, cid, pid, value):
"""

View File

@ -55,7 +55,7 @@ describe('KeyboardShortcuts', () => {
fields={fields}
controlProps={{
extraprop: 'test',
keyDown: onChange
'keydown': onChange
}}
onChange={onChange}
/>);

View File

@ -23,6 +23,7 @@ import threading
import time
import unittest
import asyncio
from selenium.webdriver.firefox.options import Options as FirefoxOptions
if sys.platform == "win32":
@ -89,9 +90,6 @@ if pgadmin_credentials and \
os.environ['PGADMIN_SETUP_PASSWORD'] = str(pgadmin_credentials[
'login_password'])
# Execute the setup file
exec(open("setup.py").read())
# Get the config database schema version. We store this in pgadmin.model
# as it turns out that putting it in the config files isn't a great idea
from pgadmin.model import SCHEMA_VERSION
@ -110,7 +108,6 @@ config.CONSOLE_LOG_LEVEL = WARNING
# Create the app
from pgAdmin4 import app
# app = create_app()
app.app_context().push()
app.PGADMIN_INT_KEY = ''

View File

@ -10,9 +10,16 @@
"""Perform the initial setup of the application, by creating the auth
and settings database."""
import argparse
import os
import sys
import typer
from rich.console import Console
from rich.table import Table
from rich import box, print
import json as jsonlib
console = Console()
app = typer.Typer()
# We need to include the root directory in sys.path to ensure that we can
# find everything we need when running in the standalone runtime.
@ -29,69 +36,446 @@ if 'SERVER_MODE' in globals():
else:
builtins.SERVER_MODE = None
from pgadmin.model import db, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
from pgadmin.model import db, Version, User,\
SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
from pgadmin import create_app
from pgadmin.utils import clear_database_servers, dump_database_servers,\
load_database_servers
from pgadmin.setup import db_upgrade, create_app_data_directory
from typing import Optional, List
from typing_extensions import Annotated
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL, LDAP, OAUTH2,\
KERBEROS, WEBSERVER
from pgadmin.tools.user_management import create_user, delete_user, update_user
from enum import Enum
app = typer.Typer(pretty_exceptions_show_locals=False)
def dump_servers(args):
"""Dump the server groups and servers.
class ManageServers:
Args:
args (ArgParser): The parsed command line options
"""
@app.command()
def dump_servers(output_file: str, user: Optional[str] = None,
auth_source: Optional[str] = INTERNAL,
sqlite_path: Optional[str] = None,
server: List[int] = None):
"""Dump the server groups and servers. """
# What user?
if args.user is not None:
dump_user = args.user
else:
dump_user = config.DESKTOP_USER
# What user?
dump_user = user if user is not None else config.DESKTOP_USER
# And the sqlite path
if args.sqlite_path is not None:
config.SQLITE_PATH = args.sqlite_path
# And the sqlite path
if sqlite_path is not None:
config.SQLITE_PATH = sqlite_path
print('----------')
print('Dumping servers with:')
print('User:', dump_user)
print('SQLite pgAdmin config:', config.SQLITE_PATH)
print('----------')
print('----------')
print('Dumping servers with:')
print('User:', dump_user)
print('SQLite pgAdmin config:', config.SQLITE_PATH)
print('----------')
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
dump_database_servers(args.dump_servers, args.servers, dump_user, True)
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
dump_database_servers(output_file, server, dump_user, True,
auth_source)
@app.command()
def load_servers(input_file: str, user: Optional[str] = None,
auth_source: Optional[str] = INTERNAL,
sqlite_path: Optional[str] = None,
replace: Optional[bool] = False
):
"""Load server groups and servers."""
# What user?
load_user = user if user is not None else config.DESKTOP_USER
# And the sqlite path
if sqlite_path is not None:
config.SQLITE_PATH = sqlite_path
print('----------')
print('Loading servers with:')
print('User:', load_user)
print('SQLite pgAdmin config:', config.SQLITE_PATH)
print('----------')
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
if replace:
clear_database_servers(load_user, True, auth_source)
load_database_servers(input_file, None, load_user, True,
auth_source)
def load_servers(args):
"""Load server groups and servers.
Args:
args (ArgParser): The parsed command line options
"""
# What user?
load_user = args.user if args.user is not None else config.DESKTOP_USER
# And the sqlite path
if args.sqlite_path is not None:
config.SQLITE_PATH = args.sqlite_path
print('----------')
print('Loading servers with:')
print('User:', load_user)
print('SQLite pgAdmin config:', config.SQLITE_PATH)
print('----------')
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
load_database_servers(args.load_servers, None, load_user, True)
class AuthExtTypes(str, Enum):
oauth2 = OAUTH2
ldap = LDAP
kerberos = KERBEROS
webserver = WEBSERVER
def setup_db(app):
# Enum class can not be extended
class AuthType(str, Enum):
oauth2 = OAUTH2
ldap = LDAP
kerberos = KERBEROS
webserver = WEBSERVER
internal = INTERNAL
class ManageUsers:
@app.command()
def add_user(email: str, password: str,
role: Annotated[Optional[bool], typer.Option(
"--admin/--nonadmin")] = False,
active: Annotated[Optional[bool],
typer.Option("--active/--inactive")] = True,
console: Optional[bool] = True,
json: Optional[bool] = False
):
"""Add Internal user. """
data = {
'email': email,
'role': 1 if role else 2,
'active': active,
'auth_source': INTERNAL,
'newPassword': password,
'confirmPassword': password,
}
ManageUsers.create_user(data, console, json)
@app.command()
def add_external_user(username: str,
auth_source: AuthExtTypes = AuthExtTypes.oauth2,
email: Optional[str] = None,
role: Annotated[Optional[bool],
typer.Option(
"--admin/--nonadmin")] = False,
active: Annotated[Optional[bool],
typer.Option(
"--active/--inactive")] = True,
console: Optional[bool] = True,
json: Optional[bool] = False
):
"""Add external user, other than Internal like
Ldap, Ouath2, Kerberos, Webserver. """
data = {
'username': username,
'email': email,
'role': 1 if role else 2,
'active': active,
'auth_source': auth_source
}
ManageUsers.create_user(data, console, json)
@app.command()
def delete_user(username: str, auth_source: AuthType = AuthType.internal):
"""Delete the user. """
delete = typer.confirm("Are you sure you want to delete it?")
if delete:
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
uid = ManageUsers.get_user(username=username,
auth_source=auth_source)
if not uid:
print("User not found")
else:
status, msg = delete_user(uid)
if status:
print('User deleted successfully.')
else:
print('Something went wrong. ' + str(msg))
@app.command()
def update_user(email: str,
password: Optional[str] = None,
role: Annotated[Optional[bool],
typer.Option("--admin/--nonadmin"
)] = None,
active: Annotated[Optional[bool],
typer.Option("--active/--inactive"
)] = None,
console: Optional[bool] = True,
json: Optional[bool] = False
):
"""Update internal user."""
data = dict()
if password:
if len(password) < 6:
print("Password must be at least 6 characters long.")
exit()
data['password'] = password
if role is not None:
data['role'] = 1 if role else 2
if active is not None:
data['active'] = active
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
uid = ManageUsers.get_user(username=email,
auth_source=INTERNAL)
if not uid:
print("User not found")
else:
status, msg = update_user(uid, data)
if status:
_user = ManageUsers.get_users(username=email,
auth_source=INTERNAL,
console=False)
ManageUsers.display_user(_user[0], console, json)
else:
print('Something went wrong. ' + str(msg))
@app.command()
def get_users(username:Optional[str] = None,
auth_source: AuthType = None,
console:Optional[bool] = True,
json:Optional[bool] = False
):
"""Get user(s) details."""
app = create_app(config.APP_NAME + '-cli')
usr = None
with app.test_request_context():
if username and auth_source:
users = User.query.filter_by(username=username,
auth_source=auth_source)
elif not username and auth_source:
users = User.query.filter_by(auth_source=auth_source)
elif username and not auth_source:
users = User.query.filter_by(username=username)
else:
users = User.query.all()
users_data = []
for u in users:
_data = {'id': u.id,
'username': u.username,
'email': u.email,
'active': u.active,
'role': u.roles[0].id,
'auth_source': u.auth_source,
'locked': u.locked
}
users_data.append(_data)
if console:
ManageUsers.display_user(_data, False, json)
if not console:
return users_data
@app.command()
def update_external_user(username: str,
auth_source: AuthExtTypes = AuthExtTypes.oauth2,
email: Optional[str] = None,
role: Annotated[Optional[bool],
typer.Option("--admin/--nonadmin"
)] = None,
active: Annotated[
Optional[bool],
typer.Option("--active/--inactive")] = None,
console: Optional[bool] = True,
json: Optional[bool] = False
):
"""Update external users other than Internal like
Ldap, Ouath2, Kerberos, Webserver."""
data = dict()
if email:
data['email'] = email
if role is not None:
data['role'] = 1 if role else 2
if active is not None:
data['active'] = active
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
uid = ManageUsers.get_user(username=username,
auth_source=auth_source)
if not uid:
print("User not found")
else:
status, msg = update_user(uid, data)
if status:
_user = ManageUsers.get_users(username=username,
auth_source=auth_source,
console=False)
ManageUsers.display_user(_user[0], console, json)
else:
print('Something went wrong. ' + str(msg))
def create_user(data, console, json):
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
username = data['username'] if 'username' in data else\
data['email']
uid = ManageUsers.get_user(username=username,
auth_source=data['auth_source'])
if uid:
print("User already exists.")
exit()
if 'newPassword' in data and len(data['newPassword']) < 6:
print("Password must be at least 6 characters long.")
exit()
status, msg = create_user(data)
if status:
ManageUsers.display_user(data, console, json)
else:
print('Something went wrong. ' + str(msg))
def get_user(username=None, auth_source=INTERNAL):
app = create_app(config.APP_NAME + '-cli')
usr = None
with app.test_request_context():
usr = User.query.filter_by(username=username,
auth_source=auth_source).first()
if not usr:
return None
return usr.id
def display_user(data, _console, _json):
if _json:
json_formatted_str = jsonlib.dumps(data, indent=0)
console.print(json_formatted_str)
else:
table = Table(title="User Details", box=box.ASCII)
table.add_column("Field", style="green")
table.add_column("Value", style="green")
if 'username' in data:
table.add_row("Username", data['username'])
if 'email' in data:
table.add_row("Email", data['email'])
table.add_row("auth_source", data['auth_source'])
table.add_row("role",
"Admin" if data['role'] and data['role'] != 2 else
"Non-admin")
table.add_row("active",
'True' if data['active'] else 'False')
console.print(table)
class ManagePreferences:
def get_user(username=None, auth_source=INTERNAL):
app = create_app(config.APP_NAME + '-cli')
usr = None
with app.test_request_context():
usr = User.query.filter_by(username=username,
auth_source=auth_source).first()
if not usr:
return None
return usr.id
@app.command()
def get_prefs(id: Optional[bool] = None, json: Optional[bool] = False):
"""Get Preferences List."""
app = create_app(config.APP_NAME + '-cli')
table = Table(title="Pref Details", box=box.ASCII)
table.add_column("Preference", style="green")
with app.app_context():
from pgadmin.preferences import save_pref
from pgadmin.utils.preferences import Preferences
from pgadmin.model import db, Preferences as PrefTable, \
ModulePreference as ModulePrefTable, \
UserPreference as UserPrefTable, \
PreferenceCategory as PrefCategoryTbl
module_prefs = ModulePrefTable.query.all()
cat_prefs = PrefCategoryTbl.query.all()
prefs = PrefTable.query.all()
if id:
all_preferences = {}
else:
all_preferences = []
for i in module_prefs:
for j in cat_prefs:
if i.id == j.mid:
for k in prefs:
if k.cid == j.id:
if id:
all_preferences["{0}:{1}:{2}".format(
i.name, j.name, k.name)
] = "{0}:{1}:{2}".format(i.id, j.id, k.id)
else:
table.add_row("{0}:{1}:{2}".format(
i.name, j.name, k.name))
all_preferences.append(
"{0}:{1}:{2}".format(
i.name, j.name, k.name)
)
if id:
return all_preferences
else:
if json:
json_formatted_str = jsonlib.dumps(
{"Preferences": all_preferences},
indent=0)
console.print(json_formatted_str)
else:
console.print(table)
@app.command()
def set_prefs(username, pref_options: List[str],
auth_source: AuthType = AuthType.internal,
json: Optional[bool] = False):
"""Set User preferences."""
user_id = ManagePreferences.get_user(username, auth_source)
app = create_app(config.APP_NAME + '-cli')
table = Table(title="Pref Details", box=box.ASCII)
table.add_column("Preference", style="green")
if not user_id:
print("User not found.")
return
prefs = ManagePreferences.get_prefs(True)
app = create_app(config.APP_NAME + '-cli')
with app.app_context():
from pgadmin.preferences import save_pref
for opt in pref_options:
val = opt.split("=")
final_opt = val[0].split(":")
val = val[1]
f = ":".join(final_opt)
if f in prefs:
ids = prefs[f].split(":")
save_pref({
'mid': ids[0],
'category_id': ids[1],
'id': ids[2],
'name': final_opt[2],
'user_id': user_id,
'value': val})
_row = {
'mid': ids[0],
'category_id': ids[1],
'id': ids[2],
'name': final_opt[2],
'user_id': user_id,
'value': val}
if json:
json_formatted_str = jsonlib.dumps(_row, indent=0)
console.print(json_formatted_str)
else:
table.add_row(jsonlib.dumps(_row))
if not json:
console.print(table)
@app.command()
def setup_db():
"""Setup the configuration database."""
app = create_app()
create_app_data_directory(config)
print("pgAdmin 4 - Application Initialisation")
@ -148,74 +532,5 @@ def setup_db(app):
run_migration_for_sqlite()
def clear_servers():
"""Clear groups and servers configurations.
Args:
args (ArgParser): The parsed command line options
"""
# What user?
load_user = args.user if args.user is not None else config.DESKTOP_USER
# And the sqlite path
if args.sqlite_path is not None:
config.SQLITE_PATH = args.sqlite_path
app = create_app(config.APP_NAME + '-cli')
with app.app_context():
clear_database_servers(load_user, True)
if __name__ == '__main__':
# Configuration settings
parser = argparse.ArgumentParser(description='Setup the pgAdmin config DB')
exp_group = parser.add_argument_group('Dump server config')
exp_group.add_argument('--dump-servers', metavar="OUTPUT_FILE",
help='Dump the servers in the DB', required=False)
exp_group.add_argument('--servers', metavar="SERVERS", nargs='*',
help='One or more servers to dump', required=False)
imp_group = parser.add_argument_group('Load server config')
imp_group.add_argument('--load-servers', metavar="INPUT_FILE",
help='Load servers into the DB', required=False)
imp_group.add_argument('--replace', dest='replace', action='store_true',
help='replace server configurations',
required=False)
imp_group.set_defaults(replace=False)
# Common args
parser.add_argument('--sqlite-path', metavar="PATH",
help='Dump/load with the specified pgAdmin config DB'
' file. This is particularly helpful when there'
' are multiple pgAdmin configurations. It is also'
' recommended to use this option when running'
' pgAdmin in desktop mode.', required=False)
parser.add_argument('--user', metavar="USER_NAME",
help='Dump/load servers for the specified username',
required=False)
args, extra = parser.parse_known_args()
config.SETTINGS_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION
if "PGADMIN_TESTING_MODE" in os.environ and \
os.environ["PGADMIN_TESTING_MODE"] == "1":
config.SQLITE_PATH = config.TEST_SQLITE_PATH
# What to do?
if args.dump_servers is not None:
try:
dump_servers(args)
except Exception as e:
print(str(e))
elif args.load_servers is not None:
try:
if args.replace:
clear_servers()
load_servers(args)
except Exception as e:
print(str(e))
else:
app = create_app()
setup_db(app)
if __name__ == "__main__":
app()