Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c10c322d1 | ||
|
|
aad2dfd725 | ||
|
|
af888027a4 | ||
|
|
86a8b165ab | ||
|
|
bdbca57332 | ||
|
|
dda6d0e3a8 | ||
|
|
411d718fa7 | ||
|
|
d17616eda8 | ||
|
|
c6a6d46283 | ||
|
|
4330a688aa | ||
|
|
96e6528243 | ||
|
|
ba9ff51aaa | ||
|
|
4eb899c92c | ||
|
|
6469b69565 | ||
|
|
208541cc48 | ||
|
|
9923eefcab | ||
|
|
32d194691e | ||
|
|
d336c1e786 | ||
|
|
7f230d05fa | ||
|
|
fcb3db7f54 | ||
|
|
2f211dc04c | ||
|
|
a11d289bd9 | ||
|
|
a50a553b0a | ||
|
|
e7eaef0dc0 | ||
|
|
849c679fc6 | ||
|
|
f43c781e98 | ||
|
|
119a99375d | ||
|
|
ff0fc9e7eb | ||
|
|
aec3ea117f | ||
|
|
ddc371adf7 | ||
|
|
0fad04de85 | ||
|
|
1f7194924f | ||
|
|
d78a7cc3bf | ||
|
|
657529280e | ||
|
|
d55ffe405b | ||
|
|
4eb184739a | ||
|
|
d57acce354 | ||
|
|
dc61039e93 | ||
|
|
9b29bc2033 | ||
|
|
504775de80 | ||
|
|
044355c5e0 | ||
|
|
85366aa128 | ||
|
|
6f4f28def7 | ||
|
|
fb9ce563fb | ||
|
|
3b36dd3964 | ||
|
|
bee80fe943 | ||
|
|
ccfbd2c457 | ||
|
|
1518b0828d | ||
|
|
93206710f7 | ||
|
|
2b0d44a33f | ||
|
|
fffc3cc242 | ||
|
|
adbb249604 | ||
|
|
64a232bc85 | ||
|
|
435752b83c | ||
|
|
30a8903374 | ||
|
|
24485fe964 | ||
|
|
13badc62c3 | ||
|
|
cf53953d9a | ||
|
|
d112dc3b96 | ||
|
|
b70d4c9857 | ||
|
|
3294e74197 | ||
|
|
79e490c5fa | ||
|
|
e4edcf2253 | ||
|
|
4ddb16f47a | ||
|
|
a3a0537277 | ||
|
|
9a76ed80bb | ||
|
|
872d5ac0b3 | ||
|
|
d59fcf3459 | ||
|
|
d8a078af53 | ||
|
|
9bb96360dd | ||
|
|
2228575564 | ||
|
|
5dd0de9e0f | ||
|
|
aa3dc389e4 | ||
|
|
92bb092718 | ||
|
|
3598840203 | ||
|
|
792441ab95 | ||
|
|
2c626d1181 | ||
|
|
278e812293 | ||
|
|
1d51a1a943 | ||
|
|
bf649420b7 | ||
|
|
1c93f93a6a | ||
|
|
3ba887001a | ||
|
|
257654849b | ||
|
|
d7f462c22c | ||
|
|
6296016b67 | ||
|
|
e3472103bb | ||
|
|
990b4f5e54 | ||
|
|
74b1a3bcc4 | ||
|
|
01c2d12b51 | ||
|
|
e5cf456fae | ||
|
|
da55da4d32 | ||
|
|
b301bdcecd | ||
|
|
23bd3359c1 | ||
|
|
0bc4edbab5 | ||
|
|
34aa360830 | ||
|
|
7e39fc8477 | ||
|
|
8f74b2e6d4 | ||
|
|
c8cb744ffd | ||
|
|
b3aa78c0a5 | ||
|
|
78e3b67eb1 | ||
|
|
c8bd75c9a8 | ||
|
|
a0e6da0772 | ||
|
|
8ca765478f | ||
|
|
f49c967bfd | ||
|
|
0a539c32d9 | ||
|
|
395ff36322 | ||
|
|
00a44a5d76 | ||
|
|
3ecdc57a59 | ||
|
|
08b0f6fe7b | ||
|
|
6d18741f1a | ||
|
|
dc47bf48f7 | ||
|
|
5b231ddd3f | ||
|
|
3e48c21fe5 | ||
|
|
4cc74c13f7 | ||
|
|
2cf5d39a01 | ||
|
|
f4112c9a0a | ||
|
|
6f69120af5 | ||
|
|
cf6d7f3b89 | ||
|
|
b78fc884e3 | ||
|
|
caafc6bb1b | ||
|
|
e9e52905cc | ||
|
|
cae00a1dc6 | ||
|
|
5dd63ca16b | ||
|
|
97c90f1e69 | ||
|
|
bcd48a92b9 | ||
|
|
6d0d387f53 | ||
|
|
3995ba9a95 | ||
|
|
4bb6b72377 | ||
|
|
080fcc1cfb | ||
|
|
93e2f23a48 | ||
|
|
965a27db39 | ||
|
|
7b377ff3da | ||
|
|
2dedb6e69a | ||
|
|
b15f15960f | ||
|
|
50f78bf210 | ||
|
|
0842f36f16 | ||
|
|
5002419611 | ||
|
|
f2756a3dcf | ||
|
|
2715932464 | ||
|
|
b366d210dc | ||
|
|
fbbc12a0a3 | ||
|
|
9039080214 | ||
|
|
f52584eef8 | ||
|
|
5abd9e3c5b | ||
|
|
3e05f7a14f |
No files matched your search
@@ -21,7 +21,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgver: [13, 14, 15, 16, 17]
|
||||
pgver: [14, 15, 16, 17, 18]
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgver: [13, 14, 15, 16, 17, 18]
|
||||
pgver: [14, 15, 16, 17, 18]
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
|
||||
@@ -32,7 +32,18 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest]
|
||||
pgver: [13, 14, 15, 16, 17]
|
||||
pgver: [14, 15, 16, 17, 18]
|
||||
postgisver: [34]
|
||||
exclude:
|
||||
- pgver: 18
|
||||
postgisver: 34
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
pgver: 18
|
||||
postgisver: 36
|
||||
- os: windows-latest
|
||||
pgver: 18
|
||||
postgisver: 36
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -53,7 +64,7 @@ jobs:
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev edb-as${{ matrix.pgver }}-server edb-as${{ matrix.pgver }}-server-pldebugger edb-as${{ matrix.pgver }}-postgis34
|
||||
sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev edb-as${{ matrix.pgver }}-server edb-as${{ matrix.pgver }}-server-pldebugger edb-as${{ matrix.pgver }}-postgis${{ matrix.postgisver }}
|
||||
|
||||
- name: Install pgagent on Linux
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' && matrix.pgver <= 16 }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-22.04, windows-latest]
|
||||
pgver: [13, 14, 15, 16, 17, 18]
|
||||
pgver: [14, 15, 16, 17, 18]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
||||
@@ -59,5 +59,6 @@ auditpy.txt
|
||||
web/coverage/
|
||||
web/.coverage
|
||||
web/regression/.coverage
|
||||
.worktree/
|
||||
web/regression/covhtml/
|
||||
web/regression/htmlcov/
|
||||
@@ -159,13 +159,15 @@ RUN apk update && apk upgrade && \
|
||||
bash \
|
||||
postfix \
|
||||
krb5-libs \
|
||||
libcurl \
|
||||
libjpeg-turbo \
|
||||
shadow \
|
||||
sudo \
|
||||
tzdata \
|
||||
libedit \
|
||||
libldap \
|
||||
libcap && \
|
||||
libcap \
|
||||
su-exec && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
# Copy in the Python packages
|
||||
@@ -173,7 +175,7 @@ COPY --from=env-builder /venv /venv
|
||||
|
||||
# Copy in the tools
|
||||
COPY --from=tool-builder /usr/local/pgsql /usr/local/
|
||||
COPY --from=pg18-builder /usr/local/lib/libpq.so.5.18 /usr/lib/liblz4.so.1.10.0 /usr/lib/
|
||||
COPY --from=pg18-builder /usr/local/lib/libpq.so.5.18 /usr/local/lib/libpq-oauth-18.so /usr/lib/liblz4.so.1.10.0 /usr/lib/
|
||||
|
||||
RUN ln -s libpq.so.5.18 /usr/lib/libpq.so.5 && \
|
||||
ln -s libpq.so.5.18 /usr/lib/libpq.so && \
|
||||
@@ -197,7 +199,7 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
|
||||
useradd -r -u 5050 -g root -s /sbin/nologin pgadmin && \
|
||||
mkdir -p /run/pgadmin /var/lib/pgadmin && \
|
||||
chown pgadmin:root /run/pgadmin /var/lib/pgadmin && \
|
||||
chmod g=u /var/lib/pgadmin && \
|
||||
chmod g=u /run/pgadmin /var/lib/pgadmin && \
|
||||
touch /pgadmin4/config_distro.py && \
|
||||
chown pgadmin:root /pgadmin4/config_distro.py && \
|
||||
chmod g=u /pgadmin4/config_distro.py && \
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
.. _ai_tools:
|
||||
|
||||
*******************
|
||||
`AI Reports`:index:
|
||||
*******************
|
||||
|
||||
**AI Reports** is a feature that provides AI-powered database analysis and insights
|
||||
using Large Language Models (LLMs). Use the *Tools → AI Reports* menu to access
|
||||
the various AI-powered reports.
|
||||
|
||||
The AI Reports feature allows you to:
|
||||
|
||||
* Generate security reports to identify potential security vulnerabilities and configuration issues.
|
||||
|
||||
* Create performance reports with optimization recommendations for queries and configurations.
|
||||
|
||||
* Perform design reviews to analyze database schema structure and suggest improvements.
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
Before using AI Reports, you must:
|
||||
|
||||
1. Ensure AI features are enabled in the server configuration (set ``LLM_ENABLED`` to ``True`` in ``config.py``).
|
||||
|
||||
2. Configure an LLM provider in :ref:`Preferences → AI <preferences>`.
|
||||
|
||||
**Note:**
|
||||
|
||||
* AI Reports using cloud providers (Anthropic, OpenAI) require an active internet connection.
|
||||
Local providers (Ollama, Docker Model Runner) do not require internet access.
|
||||
|
||||
* API usage may incur costs depending on your LLM provider's pricing model.
|
||||
Local providers (Ollama, Docker Model Runner) are free to use.
|
||||
|
||||
* The quality and accuracy of reports depend on the LLM provider and model configured.
|
||||
|
||||
|
||||
Configuring AI Reports
|
||||
**********************
|
||||
|
||||
To configure AI Reports, navigate to *File → Preferences → AI* (or click the *Settings*
|
||||
button and select *AI*).
|
||||
|
||||
.. image:: images/preferences_ai.png
|
||||
:alt: AI preferences
|
||||
:align: center
|
||||
|
||||
Select your preferred LLM provider from the dropdown:
|
||||
|
||||
**Anthropic**
|
||||
Use Claude models from Anthropic, or any Anthropic-compatible API provider.
|
||||
|
||||
* **API URL**: Custom API endpoint URL (leave empty for default: https://api.anthropic.com/v1).
|
||||
* **API Key File**: Path to a file containing your Anthropic API key (obtain from https://console.anthropic.com/). This path refers to the filesystem where the pgAdmin server is running (e.g., inside the container if using Docker). The ``~`` prefix is expanded to the home directory of the user running the pgAdmin server process. Optional when using a custom URL with a provider that does not require authentication.
|
||||
* **Model**: Select from available Claude models (e.g., claude-sonnet-4-20250514).
|
||||
|
||||
**OpenAI**
|
||||
Use GPT models from OpenAI, or any OpenAI-compatible API provider (e.g.,
|
||||
LiteLLM, LM Studio, EXO, or other local inference servers).
|
||||
|
||||
* **API URL**: Custom API endpoint URL (leave empty for default: https://api.openai.com/v1). Include the ``/v1`` path prefix if required by your provider.
|
||||
* **API Key File**: Path to a file containing your OpenAI API key (obtain from https://platform.openai.com/). This path refers to the filesystem where the pgAdmin server is running (e.g., inside the container if using Docker). The ``~`` prefix is expanded to the home directory of the user running the pgAdmin server process. Optional when using a custom URL with a provider that does not require authentication.
|
||||
* **Model**: Select from available GPT models (e.g., gpt-4).
|
||||
|
||||
**Ollama**
|
||||
Use locally-hosted open-source models via Ollama. Requires a running Ollama instance.
|
||||
|
||||
* **API URL**: The URL of your Ollama server (default: http://localhost:11434).
|
||||
* **Model**: Enter the name of the Ollama model to use (e.g., llama2, mistral).
|
||||
|
||||
**Docker Model Runner**
|
||||
Use models running in Docker Desktop's built-in model runner (available in Docker Desktop 4.40+).
|
||||
No API key is required.
|
||||
|
||||
* **API URL**: The URL of the Docker Model Runner API (default: http://localhost:12434).
|
||||
* **Model**: Select from available models or enter a custom model name.
|
||||
|
||||
.. note:: You can also use the *OpenAI* provider with a custom API URL for any
|
||||
OpenAI-compatible endpoint, including Docker Model Runner and other local
|
||||
inference servers.
|
||||
|
||||
After configuring your provider, click *Save* to apply the changes.
|
||||
|
||||
|
||||
Security Reports
|
||||
****************
|
||||
|
||||
Security Reports analyze your PostgreSQL server, database, or schema for potential
|
||||
security vulnerabilities and configuration issues.
|
||||
|
||||
To generate a security report:
|
||||
|
||||
1. In the *Browser* tree, select a server, database, or schema.
|
||||
|
||||
2. Choose *Tools → AI Reports → Security* from the menu, or right-click the
|
||||
object and select *Security* from the context menu.
|
||||
|
||||
3. The report will be generated and displayed in a new tab.
|
||||
|
||||
.. image:: images/ai_security_report.png
|
||||
:alt: AI security report
|
||||
:align: center
|
||||
|
||||
**Security Report Scope:**
|
||||
|
||||
* **Server Level**: Analyzes server configuration, authentication settings, roles, and permissions.
|
||||
|
||||
* **Database Level**: Reviews database-specific security settings, roles with database access, and object permissions.
|
||||
|
||||
* **Schema Level**: Examines schema permissions, object ownership, and access controls.
|
||||
|
||||
Each report includes:
|
||||
|
||||
* **Security Findings**: Identified vulnerabilities or security concerns.
|
||||
|
||||
* **Risk Assessment**: Severity levels for each finding (Critical, High, Medium, Low).
|
||||
|
||||
* **Recommendations**: Specific actions to remediate security issues.
|
||||
|
||||
* **Best Practices**: General security recommendations for PostgreSQL.
|
||||
|
||||
|
||||
Performance Reports
|
||||
*******************
|
||||
|
||||
Performance Reports analyze query performance, configuration settings, and provide
|
||||
optimization recommendations.
|
||||
|
||||
To generate a performance report:
|
||||
|
||||
1. In the *Browser* tree, select a server or database.
|
||||
|
||||
2. Choose *Tools → AI Reports → Performance* from the menu, or right-click the
|
||||
object and select *Performance* from the context menu.
|
||||
|
||||
3. The report will be generated and displayed in a new tab.
|
||||
|
||||
**Performance Report Scope:**
|
||||
|
||||
* **Server Level**: Analyzes server configuration parameters, resource utilization, and overall server performance metrics.
|
||||
|
||||
* **Database Level**: Reviews database-specific configuration, query performance, index usage, and table statistics.
|
||||
|
||||
Each report includes:
|
||||
|
||||
* **Performance Metrics**: Key performance indicators and statistics.
|
||||
|
||||
* **Configuration Analysis**: Review of relevant configuration parameters.
|
||||
|
||||
* **Query Optimization**: Recommendations for improving slow queries.
|
||||
|
||||
* **Index Recommendations**: Suggestions for adding, removing, or modifying indexes.
|
||||
|
||||
* **Capacity Planning**: Resource utilization trends and recommendations.
|
||||
|
||||
|
||||
Design Review Reports
|
||||
*********************
|
||||
|
||||
Design Review Reports analyze your database schema structure and suggest
|
||||
improvements for normalization, naming conventions, and best practices.
|
||||
|
||||
To generate a design review report:
|
||||
|
||||
1. In the *Browser* tree, select a database or schema.
|
||||
|
||||
2. Choose *Tools → AI Reports → Design* from the menu, or right-click the
|
||||
object and select *Design* from the context menu.
|
||||
|
||||
3. The report will be generated and displayed in a new tab.
|
||||
|
||||
**Design Review Scope:**
|
||||
|
||||
* **Database Level**: Reviews overall database structure, schema organization, and cross-schema dependencies.
|
||||
|
||||
* **Schema Level**: Analyzes tables, views, functions, and other objects within the schema.
|
||||
|
||||
Each report includes:
|
||||
|
||||
* **Schema Structure Analysis**: Review of table structures, relationships, and constraints.
|
||||
|
||||
* **Normalization Review**: Recommendations for database normalization (1NF, 2NF, 3NF, etc.).
|
||||
|
||||
* **Naming Conventions**: Suggestions for consistent naming patterns.
|
||||
|
||||
* **Data Type Usage**: Review of data type choices and recommendations.
|
||||
|
||||
* **Index Design**: Analysis of indexing strategy.
|
||||
|
||||
* **Best Practices**: General PostgreSQL schema design recommendations.
|
||||
|
||||
|
||||
Working with Reports
|
||||
********************
|
||||
|
||||
All AI reports are displayed in a dedicated panel with the following features:
|
||||
|
||||
**Report Display**
|
||||
Reports are formatted as Markdown and rendered with syntax highlighting for SQL code.
|
||||
|
||||
**Toolbar Actions**
|
||||
|
||||
* **Stop** - Cancel the current report generation. This is useful if the report
|
||||
is taking too long or if you want to change parameters.
|
||||
|
||||
* **Regenerate** - Generate a new report for the same object. Useful when you
|
||||
want to get a fresh analysis or if data has changed.
|
||||
|
||||
* **Download** - Download the report as a Markdown (.md) file. The filename
|
||||
includes the report type, object name, and date for easy identification.
|
||||
|
||||
**Multiple Reports**
|
||||
You can generate and view multiple reports simultaneously. Each report opens in
|
||||
a new tab, allowing you to compare reports across different servers, databases,
|
||||
or schemas.
|
||||
|
||||
**Report Management**
|
||||
Each report tab can be closed individually by clicking the *X* in the tab.
|
||||
Panel titles show the object name and report type for easy identification.
|
||||
|
||||
**Copying Content**
|
||||
You can select and copy text from reports to use in documentation or share with
|
||||
your team.
|
||||
|
||||
|
||||
Troubleshooting
|
||||
***************
|
||||
|
||||
**"AI features are disabled in the server configuration"**
|
||||
The administrator has disabled AI features on the server. Contact your
|
||||
pgAdmin administrator to enable the ``LLM_ENABLED`` configuration option.
|
||||
|
||||
**"Please configure an LLM provider in Preferences"**
|
||||
You need to configure an LLM provider before using AI Reports. See *Configuring AI Reports* above.
|
||||
|
||||
**"Please connect to the server/database first"**
|
||||
You must establish a connection to the server or database before generating reports.
|
||||
|
||||
**API Connection Errors**
|
||||
* Verify your API key is correct (for Anthropic and OpenAI).
|
||||
* Check your internet connection (for cloud providers).
|
||||
* For Ollama, ensure the Ollama server is running and accessible.
|
||||
* For Docker Model Runner, ensure Docker Desktop 4.40+ is running with the model runner enabled.
|
||||
* Check that your firewall allows connections to the LLM provider's API.
|
||||
|
||||
**Report Generation Fails**
|
||||
* Check the pgAdmin logs for detailed error messages.
|
||||
* Verify the database connection is still active.
|
||||
* Ensure the selected model is available for your account/subscription.
|
||||
@@ -17,3 +17,4 @@ PL/SQL code.
|
||||
schema_diff
|
||||
erd_tool
|
||||
psql_tool
|
||||
ai_tools
|
||||
|
After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 247 KiB |
@@ -51,6 +51,28 @@ Use the fields in the *Definition* tab to define the index:
|
||||
* Use the drop-down listbox next to *Depends on extensions* to select the extension
|
||||
that this index depends on (for example, edbspl). If set, dropping the extension
|
||||
will automatically drop the index as well.
|
||||
* Move the switch next to *Only Table?* to the *Yes* position to create the index
|
||||
only on the parent table without recursing to its partitions. This button will be
|
||||
enabled for partition tables only. The default is *No*.
|
||||
* Move the switch next to *Unique?* to the *Yes* position to check for duplicate values
|
||||
in the table when the index is created and when data is added. The default is *No*.
|
||||
* Move the switch next to *NULLs not distinct?* to the *Yes* position to treat null values as not distinct.
|
||||
The default is *No*. This option is available only on PostgreSQL 15 and above.
|
||||
* Move the *Clustered?* switch to the *Yes* position to instruct the server to
|
||||
cluster the table.
|
||||
* Move the *Concurrent build?* switch to the *Yes* position to build the index
|
||||
without taking any locks that prevent concurrent inserts, updates, or deletes
|
||||
on the table.
|
||||
* Use the *Constraint* field to provide a constraint expression; a constraint
|
||||
expression limits the entries in the index to those rows that satisfy the
|
||||
constraint.
|
||||
|
||||
Click the *With* tab to continue.
|
||||
|
||||
.. image:: images/index_with.png
|
||||
:alt: Index dialog with tab
|
||||
:align: center
|
||||
|
||||
* Use the *Fill Factor* field to specify a fill factor for the index. The fill
|
||||
factor specifies how full the selected method will try to fill each index
|
||||
page.
|
||||
@@ -66,18 +88,6 @@ Use the fields in the *Definition* tab to define the index:
|
||||
The default is *Yes*.
|
||||
* Move the switch next to *Autosummarize* to the *Yes* position to define whether a summarization run is
|
||||
queued for the previous page range whenever an insertion is detected on the next one. The default is *No*
|
||||
* Move the switch next to *Unique?* to the *Yes* position to check for duplicate values
|
||||
in the table when the index is created and when data is added. The default is *No*.
|
||||
* Move the switch next to *NULLs not distinct?* to the *Yes* position to treat null values as not distinct.
|
||||
The default is *No*. This option is available only on PostgreSQL 15 and above.
|
||||
* Move the *Clustered?* switch to the *Yes* position to instruct the server to
|
||||
cluster the table.
|
||||
* Move the *Concurrent build?* switch to the *Yes* position to build the index
|
||||
without taking any locks that prevent concurrent inserts, updates, or deletes
|
||||
on the table.
|
||||
* Use the *Constraint* field to provide a constraint expression; a constraint
|
||||
expression limits the entries in the index to those rows that satisfy the
|
||||
constraint.
|
||||
|
||||
Click the *Columns* tab to continue.
|
||||
|
||||
|
||||
@@ -110,3 +110,28 @@ There are 3 ways to configure LDAP:
|
||||
"LDAP_BIND_PASSWORD", "Password for simple bind.
|
||||
Specify the value if you have set the LDAP_BIND_USER parameter."
|
||||
|
||||
|
||||
Brute-force protection
|
||||
**********************
|
||||
|
||||
pgAdmin's built-in account-lockout setting (``MAX_LOGIN_ATTEMPTS``) applies
|
||||
only to accounts using the ``INTERNAL`` authentication source. It does
|
||||
**not** rate-limit or lock LDAP-authenticated logins.
|
||||
|
||||
When LDAP authentication is enabled, brute-force protection for LDAP
|
||||
credentials is the responsibility of:
|
||||
|
||||
- **The directory.** Configure account-lockout policy on the LDAP server
|
||||
itself (for example, OpenLDAP's ``ppolicy`` overlay, Active Directory's
|
||||
account-lockout GPO, or the equivalent on FreeIPA / 389 Directory
|
||||
Server). The directory is the credential authority and is the correct
|
||||
place to enforce per-account lockout.
|
||||
- **The reverse proxy or WAF in front of pgAdmin.** Configure
|
||||
request-rate limiting on the login endpoint (for example, ``limit_req``
|
||||
in nginx, ``stick-table`` rules in HAProxy, or the equivalent in your
|
||||
WAF) to bound login-attempt volume per source IP. This is independent
|
||||
of pgAdmin and protects against attackers who would otherwise reach the
|
||||
LDAP server through pgAdmin.
|
||||
|
||||
See :ref:`Avoiding a bruteforce attack <login>` for the equivalent note
|
||||
on the login page.
|
||||
@@ -60,6 +60,18 @@ asking you to contact the LDAP administrator to recover your LDAP password.
|
||||
Avoiding a bruteforce attack
|
||||
****************************
|
||||
|
||||
You have the possibility to lock an account by setting ``MAX_LOGIN_ATTEMPTS``
|
||||
once it has reached the maximum number of login attempts.
|
||||
You can disable this feature by setting the value to zero.
|
||||
For accounts using the ``INTERNAL`` authentication source, pgAdmin locks
|
||||
the account after ``MAX_LOGIN_ATTEMPTS`` consecutive failed logins. The
|
||||
default is 3; set it to zero to disable.
|
||||
|
||||
For accounts authenticated against an external identity provider
|
||||
(``LDAP``, ``OAUTH2``, ``KERBEROS``, ``WEBSERVER``), brute-force protection
|
||||
is **not** handled by pgAdmin and ``MAX_LOGIN_ATTEMPTS`` does not apply.
|
||||
Operators using these sources should:
|
||||
|
||||
- Configure account-lockout policy on the upstream provider (for example,
|
||||
Active Directory's account-lockout GPO, OpenLDAP's ``ppolicy`` overlay,
|
||||
or the OAuth2 provider's rate limits).
|
||||
- Configure request rate-limiting (for example, ``limit_req`` in nginx)
|
||||
on the reverse proxy in front of pgAdmin to bound login-attempt volume
|
||||
per source IP.
|
||||
@@ -156,6 +156,12 @@ Use the *Tools* menu to access the following options (in alphabetical order):
|
||||
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Search Objects...* | Click to open the :ref:`Search Objects... <search_objects>` and start searching any kind of objects in a database. |
|
||||
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| *AI Reports* | Click to access a submenu with AI-powered analysis options (requires :ref:`AI configuration <ai_tools>`): |
|
||||
| | |
|
||||
| | - *Security Report* - Generate an AI-powered security analysis for the selected server, database, or schema. |
|
||||
| | - *Performance Report* - Generate an AI-powered performance analysis for the selected server or database. |
|
||||
| | - *Design Report* - Generate an AI-powered design review for the selected database or schema. |
|
||||
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| *Add named restore point* | Click to open the :ref:`Add named restore point... <add_restore_point_dialog>` dialog to take a point-in-time snapshot of the current |
|
||||
| | server state. |
|
||||
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
@@ -27,6 +27,83 @@ The left pane of the *Preferences* tab displays a tree control; each node of
|
||||
the tree control provides access to options that are related to the node under
|
||||
which they are displayed.
|
||||
|
||||
The AI Node
|
||||
***********
|
||||
|
||||
Use preferences found in the *AI* node of the tree control to configure
|
||||
AI-powered features and LLM (Large Language Model) providers.
|
||||
|
||||
.. image:: images/preferences_ai.png
|
||||
:alt: Preferences AI section
|
||||
:align: center
|
||||
|
||||
**Note:** AI features must be enabled in the server configuration (``LLM_ENABLED = True``
|
||||
in ``config.py``) for these preferences to be available.
|
||||
|
||||
Use the fields on the *AI* panel to configure your LLM provider:
|
||||
|
||||
* Use the *Default Provider* drop-down to select your LLM provider. Options include:
|
||||
*Anthropic*, *OpenAI*, *Ollama*, or *Docker Model Runner*.
|
||||
|
||||
**Anthropic Settings:**
|
||||
|
||||
* Use the *API URL* field to set a custom API endpoint URL. Leave empty to use
|
||||
the default Anthropic API (``https://api.anthropic.com/v1``). Set a custom URL
|
||||
to use an Anthropic-compatible API provider.
|
||||
|
||||
* Use the *API Key File* field to specify the path to a file containing your
|
||||
Anthropic API key. This path refers to the filesystem where the pgAdmin
|
||||
server is running (e.g., inside the container if using Docker). The ``~``
|
||||
prefix is expanded to the home directory of the user running the pgAdmin
|
||||
server process. The API key may be optional when using a custom API URL
|
||||
with a provider that does not require authentication.
|
||||
|
||||
* Use the *Model* field to select from the available Claude models. Click the
|
||||
refresh button to fetch the latest available models from your configured
|
||||
endpoint.
|
||||
|
||||
**OpenAI Settings:**
|
||||
|
||||
* Use the *API URL* field to set a custom API endpoint URL. Leave empty to use
|
||||
the default OpenAI API (``https://api.openai.com/v1``). Set a custom URL to
|
||||
use any OpenAI-compatible API provider (e.g., LiteLLM, LM Studio, EXO).
|
||||
Include the ``/v1`` path prefix if required by your provider
|
||||
(e.g., ``http://localhost:1234/v1``).
|
||||
|
||||
* Use the *API Key File* field to specify the path to a file containing your
|
||||
OpenAI API key. This path refers to the filesystem where the pgAdmin
|
||||
server is running (e.g., inside the container if using Docker). The ``~``
|
||||
prefix is expanded to the home directory of the user running the pgAdmin
|
||||
server process. The API key may be optional when using a custom API URL
|
||||
with a provider that does not require authentication.
|
||||
|
||||
* Use the *Model* field to select from the available GPT models. Click the
|
||||
refresh button to fetch the latest available models from your configured
|
||||
endpoint.
|
||||
|
||||
**Ollama Settings:**
|
||||
|
||||
* Use the *API URL* field to specify the Ollama server URL
|
||||
(default: ``http://localhost:11434``).
|
||||
|
||||
* Use the *Model* field to select from the available models or enter a custom
|
||||
model name (e.g., ``llama2``, ``mistral``). Click the refresh button to fetch
|
||||
the latest available models from your Ollama server.
|
||||
|
||||
**Docker Model Runner Settings:**
|
||||
|
||||
* Use the *API URL* field to specify the Docker Model Runner API URL
|
||||
(default: ``http://localhost:12434``). Available in Docker Desktop 4.40+.
|
||||
|
||||
* Use the *Model* field to select from the available models or enter a custom
|
||||
model name. Click the refresh button to fetch the latest available models
|
||||
from your Docker Model Runner.
|
||||
|
||||
.. note:: You can also use the *OpenAI* provider with a custom API URL for any
|
||||
OpenAI-compatible endpoint, including Docker Model Runner, LM Studio, EXO,
|
||||
and other local inference servers. This can be useful when you want to use
|
||||
a provider that isn't explicitly listed but supports the OpenAI API format.
|
||||
|
||||
The Browser Node
|
||||
****************
|
||||
|
||||
@@ -392,11 +469,17 @@ Use the fields on the *File Downloads* panel to manage file downloads related pr
|
||||
|
||||
* When the *Automatically open downloaded files?* switch is set to *True*
|
||||
the downloaded file will automatically open in the system's default
|
||||
application associated with that file type.
|
||||
application associated with that file type. **Note:** This option is applicable and
|
||||
visible only in desktop mode.
|
||||
|
||||
* When the *Enable binary data download?* switch is set to *True*,
|
||||
binary data can be downloaded from the result grid. Default is set to *False*
|
||||
to prevent excessive memory usage on the server.
|
||||
|
||||
* When the *Prompt for the download location?* switch is set to *True*
|
||||
a prompt will appear after clicking the download button, allowing you
|
||||
to choose the download location.
|
||||
to choose the download location. **Note:** This option is applicable and
|
||||
visible only in desktop mode.
|
||||
|
||||
**Note:** File Downloads related settings are applicable and visible only in desktop mode.
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ The Query Tool features two panels:
|
||||
|
||||
* The upper panel displays the *SQL Editor*. You can use the panel to enter,
|
||||
edit, or execute a query or a script. It also shows the *History* tab which can be used
|
||||
to view the queries that have been executed in the session, and a *Scratch Pad*
|
||||
which can be used to hold text snippets during editing. If the Scratch Pad is
|
||||
to view the queries that have been executed in the session, a *Scratch Pad*
|
||||
which can be used to hold text snippets during editing, and an *AI Assistant*
|
||||
tab for generating SQL from natural language (when AI is configured). If the Scratch Pad is
|
||||
closed, it can be re-opened (or additional ones opened) by right-clicking in
|
||||
the SQL Editor and other panels and adding a new panel.
|
||||
* The lower panel displays the *Data Output* panel. The tabbed panel displays
|
||||
@@ -201,6 +202,49 @@ can be adjusted in ``config_local.py`` or ``config_system.py`` (see the
|
||||
`MAX_QUERY_HIST_STORED` value. See the :ref:`Deployment <deployment>` section
|
||||
for more information.
|
||||
|
||||
AI Assistant Panel
|
||||
******************
|
||||
|
||||
The *AI Assistant* tab provides a chat-style interface for generating SQL queries
|
||||
from natural language descriptions. This feature requires an AI provider to be
|
||||
configured in *Preferences > AI*. For configuration details, see the
|
||||
:ref:`preferences` documentation.
|
||||
|
||||
.. image:: images/query_ai_assistant.png
|
||||
:alt: Query tool AI Assistant panel
|
||||
:align: center
|
||||
|
||||
To use the AI Assistant:
|
||||
|
||||
1. Click on the *AI Assistant* tab in the upper panel, or use the *AI Assistant*
|
||||
toolbar button.
|
||||
2. Type a description of the SQL query you need in natural language.
|
||||
3. Press Enter or click the send button to submit your request.
|
||||
4. The AI will analyze your database schema and generate appropriate SQL.
|
||||
|
||||
The AI Assistant displays conversations with your messages and AI responses. When
|
||||
the AI generates SQL, it appears in a syntax-highlighted code block with action
|
||||
buttons:
|
||||
|
||||
* **Insert** - Insert the SQL at the current cursor position in the SQL Editor.
|
||||
* **Replace** - Replace all content in the SQL Editor with the generated SQL.
|
||||
* **Copy** - Copy the SQL to the clipboard.
|
||||
|
||||
The AI Assistant maintains conversation context, allowing you to refine queries
|
||||
iteratively. For example, you can ask for a query and then follow up with
|
||||
"also add a filter for active users" to modify the previous result.
|
||||
|
||||
**Tips for effective use:**
|
||||
|
||||
* Be specific about table and column names if you know them.
|
||||
* Describe the desired output format (e.g., "show count by category").
|
||||
* For complex queries, break down requirements step by step.
|
||||
* Use the *Clear* button to start a fresh conversation.
|
||||
|
||||
**Note:** The AI Assistant uses database schema inspection tools to understand
|
||||
your database structure. It supports SELECT, INSERT, UPDATE, DELETE, and DDL
|
||||
statements. All generated queries should be reviewed before execution.
|
||||
|
||||
The Data Output Panel
|
||||
*********************
|
||||
|
||||
@@ -335,6 +379,44 @@ If planner mis-estimated number of rows (actual vs planned) by
|
||||
:alt: Query tool explain plan statistics
|
||||
:align: center
|
||||
|
||||
* AI Insights
|
||||
|
||||
The *AI Insights* tab provides AI-powered analysis of query execution plans,
|
||||
identifying performance bottlenecks and suggesting optimizations. This tab is
|
||||
only available when an AI provider is configured in *Preferences > AI*.
|
||||
|
||||
.. image:: images/query_explain_ai_insights.png
|
||||
:alt: Query tool explain plan AI insights
|
||||
:align: center
|
||||
|
||||
When you switch to the AI Insights tab, the AI analyzes the execution plan and
|
||||
provides:
|
||||
|
||||
**Performance Bottlenecks** - Issues identified in the query plan, such as:
|
||||
|
||||
* Sequential scans on large tables that could benefit from indexes
|
||||
* Significant differences between estimated and actual row counts
|
||||
* Expensive sort or hash operations
|
||||
* Nested loops with high iteration counts
|
||||
|
||||
**Recommendations** - Concrete suggestions to improve query performance:
|
||||
|
||||
* Index creation statements with appropriate columns
|
||||
* ANALYZE commands to update table statistics
|
||||
* Configuration parameter adjustments
|
||||
* Query restructuring suggestions
|
||||
|
||||
Each recommendation that includes SQL (such as CREATE INDEX statements) has
|
||||
action buttons to *Copy* the SQL to the clipboard or *Insert* it into the
|
||||
Query Editor.
|
||||
|
||||
Click the *Regenerate* button to request a fresh analysis of the current plan.
|
||||
|
||||
**Note:** AI analysis is generated on-demand when you first click the AI Insights
|
||||
tab or when a new explain plan is generated while the tab is active. The analysis
|
||||
provides guidance but all suggested changes should be carefully evaluated before
|
||||
applying to production databases.
|
||||
|
||||
Messages Panel
|
||||
**************
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ notes for it.
|
||||
:maxdepth: 1
|
||||
|
||||
|
||||
release_notes_9_15
|
||||
release_notes_9_14
|
||||
release_notes_9_13
|
||||
release_notes_9_12
|
||||
release_notes_9_11
|
||||
release_notes_9_10
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
************
|
||||
Version 9.13
|
||||
************
|
||||
|
||||
Release date: 2026-03-05
|
||||
|
||||
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.12.
|
||||
|
||||
Supported Database Servers
|
||||
**************************
|
||||
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
|
||||
|
||||
**EDB Advanced Server**: 13, 14, 15, 16, 17 and 18
|
||||
|
||||
Bundled PostgreSQL Utilities
|
||||
****************************
|
||||
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.0
|
||||
|
||||
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #229 <https://github.com/pgadmin-org/pgadmin4/issues/229>`_ - Allow users to customize "OF TYPE" columns during table creation.
|
||||
| `Issue #5578 <https://github.com/pgadmin-org/pgadmin4/issues/5578>`_ - No FK are shown in diagram created from existing tables in the ERD Tool.
|
||||
| `Issue #6386 <https://github.com/pgadmin-org/pgadmin4/issues/6386>`_ - Add support for 'ONLY' in Index creation dialog.
|
||||
| `Issue #8198 <https://github.com/pgadmin-org/pgadmin4/issues/8198>`_ - Allow "drag-n-drop" for only user chosen tables, and show relations between them.
|
||||
| `Issue #9229 <https://github.com/pgadmin-org/pgadmin4/issues/9229>`_ - Load predefined users from a JSON file through command line.
|
||||
| `Issue #9641 <https://github.com/pgadmin-org/pgadmin4/issues/9641>`_ - Core LLM integration infrastructure, AI reports for security, schema, and performance, AI chat for the Query Tool, and AI Insights for EXPLAIN.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Issue #7578 <https://github.com/pgadmin-org/pgadmin4/issues/7578>`_ - Fixed an issue where the 'Quote strings only' configuration was ignored when downloading the result set.
|
||||
| `Issue #8988 <https://github.com/pgadmin-org/pgadmin4/issues/8988>`_ - Fixed an issue where tools settings changed by the users were not restored on application relaunch.
|
||||
| `Issue #9258 <https://github.com/pgadmin-org/pgadmin4/issues/9258>`_ - Fixed an issue where modifying a shared server incorrectly updated the original server details.
|
||||
| `Issue #9484 <https://github.com/pgadmin-org/pgadmin4/issues/9484>`_ - Fixed an issue where a long name in ERD table node was not breaking into multiple lines.
|
||||
| `Issue #9486 <https://github.com/pgadmin-org/pgadmin4/issues/9486>`_ - Fixed an issue where column comments were not displayed in the SQL tab for materialised views.
|
||||
| `Issue #9572 <https://github.com/pgadmin-org/pgadmin4/issues/9572>`_ - Fix an issue where deployment of helm chart crashing with operation not permitted.
|
||||
| `Issue #9583 <https://github.com/pgadmin-org/pgadmin4/issues/9583>`_ - Fix translation compilation.
|
||||
| `Issue #9649 <https://github.com/pgadmin-org/pgadmin4/issues/9649>`_ - Fix broken checkbox selection in backup dialog objects tree.
|
||||
| `Issue #9651 <https://github.com/pgadmin-org/pgadmin4/issues/9651>`_ - Fixed an issue in file dialog where rename was not working.
|
||||
@@ -0,0 +1,52 @@
|
||||
************
|
||||
Version 9.14
|
||||
************
|
||||
|
||||
Release date: 2026-04-02
|
||||
|
||||
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.13.
|
||||
|
||||
Supported Database Servers
|
||||
**************************
|
||||
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
|
||||
|
||||
**EDB Advanced Server**: 13, 14, 15, 16, 17 and 18
|
||||
|
||||
Bundled PostgreSQL Utilities
|
||||
****************************
|
||||
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.2
|
||||
|
||||
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #4011 <https://github.com/pgadmin-org/pgadmin4/issues/4011>`_ - Added support to download binary data from result grid.
|
||||
| `Issue #9703 <https://github.com/pgadmin-org/pgadmin4/issues/9703>`_ - Added support for custom LLM provider URLs for OpenAI and Anthropic, allowing use of OpenAI-compatible providers such as LM Studio, EXO, and LiteLLM.
|
||||
| `Issue #9709 <https://github.com/pgadmin-org/pgadmin4/issues/9709>`_ - Fixed an issue where AI features (AI Assistant tab, AI Reports menus, and AI Preferences) were visible in the UI even when LLM_ENABLED is set to False.
|
||||
| `Issue #9738 <https://github.com/pgadmin-org/pgadmin4/issues/9738>`_ - Allow copying of text from the AI Assistant chat panel.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Issue #8992 <https://github.com/pgadmin-org/pgadmin4/issues/8992>`_ - Fixed an issue where selecting all in the Query Tool's Messages tab would select the entire page content.
|
||||
| `Issue #9279 <https://github.com/pgadmin-org/pgadmin4/issues/9279>`_ - Fixed an issue where OAuth2 authentication fails with 'object has no attribute' if OAUTH2_AUTO_CREATE_USER is False.
|
||||
| `Issue #9392 <https://github.com/pgadmin-org/pgadmin4/issues/9392>`_ - Ensure that the Geometry Viewer refreshes when re-running queries or switching geometry columns, preventing stale data from being displayed.
|
||||
| `Issue #9457 <https://github.com/pgadmin-org/pgadmin4/issues/9457>`_ - Fixed Process Watcher garbled text on Windows with non-UTF-8 locales.
|
||||
| `Issue #9570 <https://github.com/pgadmin-org/pgadmin4/issues/9570>`_ - Fixed an issue where ALT+F5 for executing a query in the Query Tool shows a crosshair cursor icon for rectangular selection.
|
||||
| `Issue #9648 <https://github.com/pgadmin-org/pgadmin4/issues/9648>`_ - Fixed an issue where the default fillfactor value for B-tree indexes was incorrect.
|
||||
| `Issue #9694 <https://github.com/pgadmin-org/pgadmin4/issues/9694>`_ - Fixed an issue where AI Reports are grayed out after setting an API key by auto-selecting the default provider.
|
||||
| `Issue #9696 <https://github.com/pgadmin-org/pgadmin4/issues/9696>`_ - Fixed an issue where AI Assistant does not notify that No API Key or Provider is Set.
|
||||
| `Issue #9702 <https://github.com/pgadmin-org/pgadmin4/issues/9702>`_ - Fixed misleading AI activity messages that could be mistaken for actual database operations.
|
||||
| `Issue #9719 <https://github.com/pgadmin-org/pgadmin4/issues/9719>`_ - Fixed an issue where AI Reports fail with OpenAI models that do not support the temperature parameter.
|
||||
| `Issue #9721 <https://github.com/pgadmin-org/pgadmin4/issues/9721>`_ - Fixed an issue where permissions page is not completely accessible on full scroll.
|
||||
| `Issue #9729 <https://github.com/pgadmin-org/pgadmin4/issues/9729>`_ - Fixed an issue where some LLM models would not use database tools in the AI assistant, instead returning text descriptions of tool calls.
|
||||
| `Issue #9732 <https://github.com/pgadmin-org/pgadmin4/issues/9732>`_ - Improve the AI Assistant user prompt to be more descriptive of the actual functionality.
|
||||
| `Issue #9734 <https://github.com/pgadmin-org/pgadmin4/issues/9734>`_ - Fixed an issue where LLM responses are not streamed or rendered properly in the AI Assistant.
|
||||
| `Issue #9736 <https://github.com/pgadmin-org/pgadmin4/issues/9736>`_ - Fix an issue where the AI Assistant was not retaining conversation context between messages, with chat history compaction to manage token budgets.
|
||||
| `Issue #9740 <https://github.com/pgadmin-org/pgadmin4/issues/9740>`_ - Fixed an issue where the AI Assistant input textbox sometimes swallows the first character of input.
|
||||
| `Issue #9758 <https://github.com/pgadmin-org/pgadmin4/issues/9758>`_ - Clarify where the LLM API key files should be.
|
||||
| `Issue #9789 <https://github.com/pgadmin-org/pgadmin4/issues/9789>`_ - Fixed an issue where the Query tool kept prompting for a password when using a shared server.
|
||||
| `Issue #9795 <https://github.com/pgadmin-org/pgadmin4/issues/9795>`_ - Support /v1/responses for OpenAI models.
|
||||
@@ -0,0 +1,113 @@
|
||||
************
|
||||
Version 9.15
|
||||
************
|
||||
|
||||
Release date: 2026-05-11
|
||||
|
||||
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.14.
|
||||
|
||||
Supported Database Servers
|
||||
**************************
|
||||
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
|
||||
|
||||
**EDB Advanced Server**: 13, 14, 15, 16, 17 and 18
|
||||
|
||||
Bundled PostgreSQL Utilities
|
||||
****************************
|
||||
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.2
|
||||
|
||||
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #9657 <https://github.com/pgadmin-org/pgadmin4/issues/9657>`_ - Allow the container image to run as a non-default user via the PUID and PGID environment variables.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
| `Issue #9764 <https://github.com/pgadmin-org/pgadmin4/issues/9764>`_ - Update the Swedish translation.
|
||||
| `Issue #9827 <https://github.com/pgadmin-org/pgadmin4/issues/9827>`_ - Bump Python and JavaScript dependencies.
|
||||
| `Issue #9832 <https://github.com/pgadmin-org/pgadmin4/issues/9832>`_ - Fix the Czech translation for 'Refresh'.
|
||||
| `Issue #9834 <https://github.com/pgadmin-org/pgadmin4/issues/9834>`_ - Bump runtime dependencies and upgrade ESLint to v10.
|
||||
| `Issue #9839 <https://github.com/pgadmin-org/pgadmin4/issues/9839>`_ - Update the Russian translation.
|
||||
| `Issue #9870 <https://github.com/pgadmin-org/pgadmin4/issues/9870>`_ - Bump runtime and development dependencies.
|
||||
| `Issue #9873 <https://github.com/pgadmin-org/pgadmin4/issues/9873>`_ - Use an ``<OWNER>`` placeholder in resql tests instead of a hardcoded 'postgres' role to support non-default superuser names.
|
||||
| `Issue #9893 <https://github.com/pgadmin-org/pgadmin4/issues/9893>`_ - Update the Spanish translation.
|
||||
| `Issue #9906 <https://github.com/pgadmin-org/pgadmin4/issues/9906>`_ - Update the Italian translation.
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Issue #9656 <https://github.com/pgadmin-org/pgadmin4/issues/9656>`_ - Use absolute paths for ``a2enmod`` and ``a2enconf`` in the Debian setup script so it works when ``/usr/sbin`` is not on PATH.
|
||||
| `Issue #9830 <https://github.com/pgadmin-org/pgadmin4/issues/9830>`_ - Fix cross-user data access and shared-server privilege escalation in server mode (CVE-2026-7813). Also applies the ``@with_object_filters`` access-control decorator to ``ServerNode.list``.
|
||||
| `Issue #9835 <https://github.com/pgadmin-org/pgadmin4/issues/9835>`_ - Tighten Shared Server feature parity, owner-only field handling, and write guards as a follow-up to the data-isolation hardening.
|
||||
| `Issue #9865 <https://github.com/pgadmin-org/pgadmin4/issues/9865>`_ - Fix stored cross-site scripting (XSS) via crafted PostgreSQL object names rendered in the Browser Tree and Explain Visualizer (CVE-2026-7814). Reported by Fahar Abbas.
|
||||
| `Issue #9898 <https://github.com/pgadmin-org/pgadmin4/issues/9898>`_ - Fix SQL injection in Maintenance tool option values (CVE-2026-7815). Reported by j3seer.
|
||||
| `Issue #9899 <https://github.com/pgadmin-org/pgadmin4/issues/9899>`_ - Fix OS command injection in Import/Export query export (CVE-2026-7816). Reported by Chung Kim (chungkn), OneMount Group.
|
||||
| `Issue #9900 <https://github.com/pgadmin-org/pgadmin4/issues/9900>`_ - Fix local-file inclusion and server-side request forgery in LLM API configuration endpoints (CVE-2026-7817). Reported by j3seer.
|
||||
| `Issue #9901 <https://github.com/pgadmin-org/pgadmin4/issues/9901>`_ - Fix unsafe deserialization in the session manager that could lead to remote code execution (CVE-2026-7818). Also encrypts session files at rest using Fernet, restricts session-file permissions to 0o600, switches the session-digest default from SHA-1 to SHA-256, drops several non-roundtrippable live objects from the session (``AuthSourceManager`` and the Azure, RDS, Google Cloud, and BigAnimal cloud-provider instances), tightens DATA_DIR file and directory permissions at creation, creates ``pgadmin4.log`` with mode 0o600, hardens ``EnhancedRotatingFileHandler._open`` against rotation failures, and bounds the ``user_info_server`` prompt retry loop so a non-interactive caller cannot spin forever. Reported by Fernando Bortotti.
|
||||
| `Issue #9902 <https://github.com/pgadmin-org/pgadmin4/issues/9902>`_ - Fix symlink-based path traversal in the file manager (CVE-2026-7819). Reported by Fernando Bortotti.
|
||||
| `Issue #9904 <https://github.com/pgadmin-org/pgadmin4/issues/9904>`_ - Fix account-lockout bypass on Flask-Security's default ``/login`` view by overriding ``User.is_active`` and ``User.is_locked()`` so the ``locked`` field is honored on every authentication path (CVE-2026-7820). Reported by Fernando Bortotti.
|
||||
|
||||
Additional changes (no associated issue)
|
||||
****************************************
|
||||
|
||||
The commits below did not have a dedicated GitHub issue. They are listed here for transparency.
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
| ``1518b0828`` - Restore the SERVER_MODE python-test path and fix two endpoint regressions surfaced by it.
|
||||
| ``d57acce35`` - Harden validation, preference, and connection-params paths against pre-existing edge cases.
|
||||
|
||||
Test-suite stability
|
||||
--------------------
|
||||
|
||||
| ``a11d289bd`` - Harden ``click_modal`` backdrop wait and ``open_query_tool`` stale-element retry in feature tests.
|
||||
| ``a50a553b0`` - Feature tests use ``sys.executable``; sync ``yarn.lock`` to ``package.json``.
|
||||
| ``0fad04de8`` - PSQL socket tests use the authenticated tester; the role-dependencies test skips cleanly on auth failure.
|
||||
| ``1f7194924`` - Harden six regression tests against environmental drift.
|
||||
| ``dc61039e9`` - Quote the username in the views/mview test helper for dotted local roles.
|
||||
| ``9b29bc203`` - Quote the username in the types/compound-triggers test helpers for dotted local roles.
|
||||
| ``504775de8`` - Quote the username in the user-mappings test helper for dotted local roles.
|
||||
| ``208541cc4`` - ``ImportExportServersTestCase`` uses ``sys.executable`` instead of a bare ``python`` and surfaces subprocess errors instead of swallowing them as a misleading JSON-parse failure.
|
||||
|
||||
Refactoring
|
||||
-----------
|
||||
|
||||
| ``6f4f28def`` - Factor the WTForms-error-to-JSON conversion into a helper and drop a dead import.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
| ``9923eefca`` - Clarify in ``login.rst`` and ``ldap.rst`` that ``MAX_LOGIN_ATTEMPTS`` applies only to the ``INTERNAL`` authentication source. Operators using LDAP, OAuth2, Kerberos, or Webserver auth should rely on the upstream identity provider's lockout policy and reverse-proxy request rate-limiting.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
Non-breaking ``dependabot`` updates aggregated for v9.15.
|
||||
|
||||
Python:
|
||||
|
||||
| ``boto3`` 1.42 -> 1.43 (``python_version > '3.9'``; Python 3.9 stays on 1.42)
|
||||
| ``cryptography`` 46.0 -> 47.0
|
||||
| ``psycopg`` 3.3.3 -> 3.3.4 (``python_version >= '3.10'``)
|
||||
| ``pycodestyle`` >=2.5.0 -> >=2.14.0
|
||||
| ``requests`` >=2.21.0 -> >=2.33.1
|
||||
| ``safety`` >=1.9.0 -> >=3.7.0
|
||||
| ``testtools`` 2.8.7 -> 2.9.1
|
||||
| ``typer`` 0.24 -> 0.25 (``python > '3.9'``)
|
||||
|
||||
JavaScript (``web/``):
|
||||
|
||||
| ``@tanstack/react-query`` 5.90 -> 5.100.5
|
||||
| ``axios`` 1.15.2 -> 1.16.0
|
||||
| ``moment-timezone`` 0.6.0 -> 0.6.2
|
||||
| ``postcss`` 8.5.6 -> 8.5.12
|
||||
|
||||
JavaScript (``runtime/``):
|
||||
|
||||
| ``axios`` 1.15.2 -> 1.16.0
|
||||
| ``electron`` 41.3.0 -> 41.5.0
|
||||
| ``eslint`` 10.2.1 -> 10.3.0
|
||||
| ``globals`` 17.5.0 -> 17.6.0
|
||||
@@ -270,6 +270,50 @@ username/email address.
|
||||
/path/to/python /path/to/setup.py get-users --username user1@gmail.com
|
||||
|
||||
|
||||
Load Users
|
||||
**********
|
||||
|
||||
To bulk import users from a JSON file, invoke ``setup.py`` with ``load-users`` command line option,
|
||||
followed by the path to the JSON file.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
/path/to/python /path/to/setup.py load-users /path/to/users.json
|
||||
|
||||
**JSON File Format**
|
||||
|
||||
The input JSON file must contain a ``users`` array with user objects:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"username": "admin@example.com",
|
||||
"email": "admin@example.com",
|
||||
"password": "securepassword",
|
||||
"role": "Administrator",
|
||||
"active": true,
|
||||
"auth_source": "internal"
|
||||
},
|
||||
{
|
||||
"username": "ldap_user",
|
||||
"email": "ldap_user@example.com",
|
||||
"role": "User",
|
||||
"active": true,
|
||||
"auth_source": "ldap"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The command handles errors gracefully:
|
||||
|
||||
* Users that already exist are skipped
|
||||
* Invalid roles are reported and skipped
|
||||
* Missing passwords for internal auth are reported and skipped
|
||||
* Passwords shorter than 6 characters are reported and skipped
|
||||
|
||||
|
||||
Output
|
||||
******
|
||||
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
# Proposal: Eliminate Insecure Deserialization RCE and Symlink-based Path Traversal
|
||||
|
||||
**Status:** Draft (v5 — addresses review findings of 2026-04-30, including end-to-end coherence pass)
|
||||
|
||||
> **Note on line numbers:** Citations in this proposal are accurate as of `HEAD` of
|
||||
> `fix/pickle-rce` at v5 publication time. If upstream changes land in `session.py` or
|
||||
> `file_manager/__init__.py` before PR 1 merges, the implementing PR will recompute line
|
||||
> references against the rebase base.
|
||||
**Date:** 2026-04-30
|
||||
**Author:** Ashesh Vashi
|
||||
**Tracking branch:** `fix/pickle-rce`
|
||||
**Related:** Independent security report dated 2026-04-29; CVE-2024-2044 (prior RCE in
|
||||
SERVER_MODE=False)
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
A security report dated 2026-04-29 identified two complementary vulnerabilities in pgAdmin 4
|
||||
(confirmed on v9.14) that, when combined, allow an authenticated user to achieve OS-level Remote
|
||||
Code Execution on the pgAdmin host or container.
|
||||
|
||||
### 1.1 Vulnerability 1 — Insecure Deserialization Before HMAC Verification (CWE-502)
|
||||
|
||||
**Location:** `web/pgadmin/utils/session.py:240-267` — `FileBackedSessionManager.get()`
|
||||
|
||||
The session manager deserializes the session file with `pickle.load()` **before** verifying the
|
||||
HMAC integrity signature stored alongside it. Any file an attacker can place in the sessions
|
||||
directory is therefore executed unconditionally — `pickle.load()` fires arbitrary
|
||||
`__reduce__` hooks during reads, before any of the integrity checks run.
|
||||
|
||||
The vulnerable sequence:
|
||||
|
||||
1. `open(fname, 'rb')` and read all bytes.
|
||||
2. `pickle.load(f)` — RCE here.
|
||||
3. Compare the in-body HMAC against the cookie HMAC — too late.
|
||||
|
||||
### 1.2 Vulnerability 2 — Symlink Escape in the File Manager (CWE-61 / CWE-22)
|
||||
|
||||
**Location:** `web/pgadmin/misc/file_manager/__init__.py` — `check_access_permission()` at
|
||||
line 894 and the upload handler `add()` at line 1062.
|
||||
|
||||
Both code paths use `os.path.abspath()`, which resolves `.` and `..` but does **not** resolve
|
||||
symbolic links. The subsequent `open(path, 'wb')` is performed by the OS kernel, which **does**
|
||||
follow symlinks. If a symlink exists inside the user's storage directory pointing outside the
|
||||
sandbox, the access check (path-string semantics) passes while the kernel write
|
||||
(filesystem semantics) follows the symlink.
|
||||
|
||||
The same path-string-vs-kernel mismatch applies to `rename()` (line 990), `delete()` (line
|
||||
1037), and `download()` (line 1263), which all call `check_access_permission()` and then
|
||||
operate on the literal path via syscall.
|
||||
|
||||
A separate timing bug in `add()`: the inline access check runs *before* `open()` (good) but
|
||||
`Filemanager.check_access_permission()` is *also* called *after* the file write at line 1104 —
|
||||
pointless, because the file is already on disk by then.
|
||||
|
||||
### 1.3 Threat Model
|
||||
|
||||
- Attacker has a valid pgAdmin login (any role suffices — no admin required).
|
||||
- Attacker can reach the file manager and session cookie surfaces.
|
||||
- Goal: OS-level command execution as the user running gunicorn/pgAdmin (root in the official
|
||||
Docker image).
|
||||
- Out of scope: attackers who already have host shell or Docker volume write access; supply-chain
|
||||
compromise.
|
||||
|
||||
### 1.4 Severity
|
||||
|
||||
CVSS v3.1 base score (per the report): **8.8 (High)**.
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| Attack vector | Network |
|
||||
| Privileges required | Low (any authenticated user) |
|
||||
| User interaction | None |
|
||||
| Confidentiality / Integrity / Availability | High / High / High |
|
||||
|
||||
### 1.5 Attack chain feasibility — what the report does and does not prove
|
||||
|
||||
The report demonstrates two scenarios. Both rely on a primitive the report **does not prove
|
||||
exists inside pgAdmin**:
|
||||
|
||||
- **Scenario A (SERVER_MODE=False):** in this mode `check_access_permission()` returns
|
||||
immediately without any check (see line 895). An authenticated user can directly upload a
|
||||
payload to the sessions directory via the file manager. No symlink needed. **This is a
|
||||
complete in-pgAdmin attack chain when `SERVER_MODE=False`** — and it's the likely vector for
|
||||
the prior CVE-2024-2044.
|
||||
- **Scenario B (SERVER_MODE=True):** requires a symlink to be planted inside the user's
|
||||
storage directory. The report says "create a symlink" but does not describe how to do this
|
||||
through pgAdmin alone.
|
||||
|
||||
A grep over the **entire `web/pgadmin/` tree** (not just file_manager) for symlink-creating
|
||||
primitives returns zero matches across all of:
|
||||
|
||||
| Audit pattern | Matches in `web/pgadmin/` |
|
||||
|---|---|
|
||||
| `os.symlink` / `os.link(` / `symbolic_link` / `symlink_to` / `Path.symlink` / `.symlink(` | 0 |
|
||||
| `tarfile` / `zipfile` / `unpack_archive` / `extractall` / `extract_archive` / `shutil.copytree` (archive extraction can preserve symlinks from a malicious archive) | 0 |
|
||||
| `subprocess` / `os.popen` / shell-invocation primitives in `file_manager/` | 0 |
|
||||
|
||||
The download endpoint serves files; the upload endpoint writes file *bodies*; no
|
||||
rename/move/extract handler creates links. **Therefore Scenario B is only exploitable when the
|
||||
attacker has an out-of-band way to plant a symlink** — host shell, Docker volume mount with
|
||||
shared storage, container escape, or a future pgAdmin feature that introduces a symlink
|
||||
primitive (e.g., archive extraction). This audit excludes the bundled
|
||||
`docker-entrypoint.sh`/runtime startup scripts, which run before user authentication and are
|
||||
not part of the user-facing attack surface.
|
||||
|
||||
We close Vulnerability 2 anyway, as defense in depth: (a) prevents future regressions if such a
|
||||
primitive is introduced, (b) closes the path for deployments where the storage directory is
|
||||
shared with another writer, (c) the change is small and low-risk. But the spec does **not**
|
||||
claim Vulnerability 2 alone is a current authenticated-user RCE primitive in
|
||||
`SERVER_MODE=True`.
|
||||
|
||||
### 1.6 Known residual issues not introduced or fixed by this proposal
|
||||
|
||||
- **Session-file replay after logout.** A user who saves a copy of their valid, HMAC'd session
|
||||
file and the matching cookie can `cp` the file back into the sessions directory after
|
||||
logout-driven deletion, regaining their session. The current code has this property; the
|
||||
Phase 1 design preserves it. Closing it requires a per-session-revision nonce that's
|
||||
invalidated server-side on logout — out of scope for this proposal, tracked as a follow-up.
|
||||
- **Read-side information disclosure via symlink.** `download()` at line 1263 calls
|
||||
`check_access_permission()` and `send_from_directory()`. Once `check_access_permission()`
|
||||
uses `realpath()`, symlink-based reads are also blocked at the access-check layer. The
|
||||
underlying TOCTOU caveat (§4.2.3) applies symmetrically to read.
|
||||
- **CSRF-token theft via session-file read (Scenario B step 3 of the report).** The report's
|
||||
Scenario B requires the attacker to read the victim's session file from disk to extract the
|
||||
raw `csrf_token`. This requires file-read access to the sessions directory — either via the
|
||||
same out-of-band primitive needed to plant a symlink (host shell, Docker volume mount), or
|
||||
via the file manager's `download()` endpoint pointed at the sessions directory through a
|
||||
symlink. The realpath fix in §4.2.1 closes the symlink-based read path. Out-of-band
|
||||
file-read access remains out of scope (§1.3 threat model). After this proposal lands, the
|
||||
attacker's only remaining path to a session file's contents is host-level access, which
|
||||
also yields easier RCE primitives directly.
|
||||
- **Residual `pickle.loads` callsites that operate on session-stored bytes (Phase 2 scope).**
|
||||
After PR 1's HMAC-header gate on the session file, no attacker-controlled bytes can reach
|
||||
`pickle.loads` via the session-on-disk path. However, several modules unpickle values that
|
||||
were themselves placed inside `flask.session` by pgAdmin's own write paths. These are safe
|
||||
*as long as the write paths cannot be influenced by attackers*, but they remain
|
||||
insecure-deserialization vectors that should be eliminated in Phase 2 / PR 7. Enumerated
|
||||
here so future reviewers see the surface:
|
||||
- `web/pgadmin/tools/sqleditor/__init__.py`: `import pickle` at line 12; `pickle.loads`
|
||||
at lines 320, 795, 854, 1857, 1935; `pickle.dumps` at lines 334, 628, 962, 1178, 1235,
|
||||
1348, 1642, 1698, 1739, 1783, 1992 (the `command_obj` round-trip).
|
||||
- `web/pgadmin/tools/sqleditor/utils/start_running_query.py`: lines 12, 53, 207.
|
||||
- `web/pgadmin/tools/sqleditor/utils/query_tool_connection_check.py`: lines 11, 33.
|
||||
- `web/pgadmin/tools/sqleditor/utils/filter_dialog.py`: lines 11, 97.
|
||||
- `web/pgadmin/tools/schema_diff/__init__.py`: `import pickle` at line 12; `pickle.loads`
|
||||
at line 184; `pickle.dumps` at lines 197, 225 (the `diff_model_obj` round-trip).
|
||||
- `web/pgadmin/misc/bgprocess/processes.py`: `from pickle import dumps, loads` at line 20;
|
||||
used at lines 162, 164, 251, 704, 706 to round-trip the BatchProcess descriptor stored
|
||||
in the SQLite `process` table. This one is interesting because it deserializes from the
|
||||
database, not the session — same risk class but a different threat surface (host-local
|
||||
DB write access).
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. **Close both vulnerabilities** in shippable, independently reviewable units of work.
|
||||
2. **Eliminate `pickle` from the pgAdmin web codebase entirely** as a long-term defence-in-depth
|
||||
measure, not just from `session.py`. The same module is also used inside the cloud provider
|
||||
handlers and the SQL editor's grid state.
|
||||
3. **No regressions** in: login (internal, OAuth2, LDAP, Kerberos, Webserver, MFA), cloud
|
||||
deployment wizards (RDS, Azure, Google, BigAnimal), Query Tool, Schema Diff, Debugger.
|
||||
4. **No silent data loss** for active sessions on upgrade where avoidable. Sessions written by
|
||||
pre-fix code will be invalidated (one-time re-login). Operators are alerted via a single
|
||||
warning log line per rejected sid (see §4.3).
|
||||
|
||||
## 3. Non-goals
|
||||
|
||||
- Changing the cookie format or HMAC algorithm of the cookie.
|
||||
- Changing the session storage backend (still file-backed with HMAC).
|
||||
- Refactoring the `Filemanager` class beyond what's needed for the symlink fix.
|
||||
- Deprecating `SERVER_MODE=False` (orthogonal concern).
|
||||
- Closing session-file replay-after-logout (§1.6).
|
||||
|
||||
---
|
||||
|
||||
## 4. Design
|
||||
|
||||
### 4.1 Vulnerability 1 — Two-phase fix
|
||||
|
||||
#### Phase 1 (urgent): HMAC-first verification with a plaintext header
|
||||
|
||||
`pickle` stays as the on-disk format **temporarily**, but is gated behind an HMAC verified from
|
||||
a plaintext header that lives outside the serialized blob.
|
||||
|
||||
**File format (new):**
|
||||
|
||||
```
|
||||
+----------------------------------------------------+
|
||||
| H bytes : hex HMAC over the body, ASCII-encoded |
|
||||
+----------------------------------------------------+
|
||||
| N bytes : pickle body — (randval, digest, data) |
|
||||
+----------------------------------------------------+
|
||||
```
|
||||
|
||||
`H` is determined by the chosen digest algorithm: `len(hmac.new(...).hexdigest()) ==
|
||||
2 * digest_size`. For SHA-256, `H = 64`. We use **SHA-256** for the file HMAC regardless of
|
||||
`config.SESSION_DIGEST_METHOD` (which defaults to SHA-1 — see §8 Open Question 1). The HMAC is
|
||||
computed with `app.config['SECRET_KEY']` over the body bytes.
|
||||
|
||||
**Read path (new):**
|
||||
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
import pickle
|
||||
import logging
|
||||
|
||||
_FILE_HMAC = hashlib.sha256
|
||||
_HMAC_HEX_LEN = _FILE_HMAC().digest_size * 2 # 64 for SHA-256
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _compute_file_hmac(secret, body: bytes) -> bytes:
|
||||
# Flask's SECRET_KEY may be str or bytes; normalize before HMAC.
|
||||
key = secret.encode() if isinstance(secret, str) else secret
|
||||
return hmac.new(key, body, _FILE_HMAC).hexdigest().encode()
|
||||
|
||||
def get(self, sid, digest):
|
||||
fname = safe_join(self.path, sid)
|
||||
if fname is None or not os.path.exists(fname):
|
||||
return self.new_session()
|
||||
try:
|
||||
with open(fname, 'rb') as f:
|
||||
header = f.read(_HMAC_HEX_LEN)
|
||||
body = f.read()
|
||||
if len(header) != _HMAC_HEX_LEN or len(body) == 0:
|
||||
# 0-byte file from new_session(), or pre-fix session file. Silently
|
||||
# discard; Phase 1 deployment expects one-time re-login churn.
|
||||
return self.new_session()
|
||||
expected = _compute_file_hmac(self.secret, body)
|
||||
if not hmac.compare_digest(header, expected):
|
||||
logger.warning(
|
||||
"session file rejected: bad file-HMAC for sid=%s", sid[:8]
|
||||
)
|
||||
return self.new_session()
|
||||
randval, hmac_digest, data = pickle.loads(body) # safe: body is verified
|
||||
except (pickle.UnpicklingError, EOFError, OSError, MemoryError, ValueError, TypeError):
|
||||
# Narrow catch: silently invalidate corrupted/unreadable session files.
|
||||
# Programming errors (AttributeError, NameError, etc.) propagate intentionally
|
||||
# so they show up in tests and logs rather than being masked as new_session().
|
||||
return self.new_session()
|
||||
if hmac_digest != digest:
|
||||
return self.new_session()
|
||||
return ManagedSession(data, sid=sid, randval=randval, hmac_digest=hmac_digest)
|
||||
```
|
||||
|
||||
**Write path (new):**
|
||||
|
||||
```python
|
||||
def put(self, session):
|
||||
...
|
||||
# The existing disk_write_delay and skip_paths skip logic at lines 271-285
|
||||
# is preserved verbatim — only the format step below changes.
|
||||
body = pickle.dumps(
|
||||
(session.randval, session.hmac_digest, dict(session)), -1
|
||||
)
|
||||
header = _compute_file_hmac(self.secret, body)
|
||||
fname = safe_join(self.path, session.sid)
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(header)
|
||||
f.write(body)
|
||||
```
|
||||
|
||||
**Defensive precondition (`__init__`):** raise on empty `SECRET_KEY`:
|
||||
|
||||
```python
|
||||
if not self.secret:
|
||||
raise RuntimeError("SECRET_KEY must be non-empty")
|
||||
```
|
||||
|
||||
`assert` is **not** acceptable here — Python's `-O` flag strips assertions, which would silently
|
||||
disable the security-critical check in optimized production builds. Pre-fix code shares the
|
||||
implicit non-empty assumption, but an empty `SECRET_KEY` reduces the file-HMAC to a known-key
|
||||
MAC, and a security-driven redesign is the right time to fail fast.
|
||||
|
||||
**Performance.** HMAC-SHA256 over typical session bodies (<10 KB) costs <100 µs per request on
|
||||
modern x86-64. Sessions exceeding 1 MB (rare; large `gridData` with cloud-handler bytes blobs)
|
||||
will see sub-millisecond overhead. No measurable production impact expected. Confirmed by
|
||||
inspection — the read path was already `f.read()`-bound on full file bytes, so the additional
|
||||
work is one `hmac.new(...).hexdigest()` per request.
|
||||
|
||||
**Important note on the report's Fix 1.** The report's suggested "HMAC-first" code calls
|
||||
`pickle.loads(raw)` once just to read the stored HMAC — which is the exact line that fires
|
||||
the RCE. The plaintext-header design above avoids that: the HMAC is verified against raw bytes
|
||||
before any deserialization.
|
||||
|
||||
**Migration:** files written by the old code (no header) fail the header check and are silently
|
||||
discarded — affected sessions get a fresh `new_session()`. One-time re-login on upgrade.
|
||||
Operators see a transient burst of `session file rejected` warnings during rollout, then they
|
||||
quiesce.
|
||||
|
||||
**Empty-file behavior.** `new_session()` at line 218-238 creates a 0-byte placeholder. The next
|
||||
`get()` reads short (< 64 bytes) header + empty body, falls through to `new_session()`.
|
||||
Subsequent `put()` writes the full header + body. This matches the existing behavior of the old
|
||||
code (where `pickle.load` on 0 bytes raised, was caught, and fell through to `new_session()`).
|
||||
No regression.
|
||||
|
||||
**Concurrent `put()` from multiple workers.** Two workers writing the same sid race on the
|
||||
file. `open(fname, 'wb')` truncates atomically; the worst case is interleaved writes producing
|
||||
a header/body mismatch. The next `get()` recomputes the file-HMAC, fails verification, logs
|
||||
the warning, and returns `new_session()`. **Race-tolerant by design** — partial writes can
|
||||
never produce an executable payload because they cannot satisfy the HMAC. The pre-fix code
|
||||
shared the same race shape but with `pickle.load` instead of HMAC validation; not a
|
||||
regression.
|
||||
|
||||
**Caching layer.** `CachingSessionManager` (session.py:104-196) wraps
|
||||
`FileBackedSessionManager`. Cache hits use the cached `ManagedSession.hmac_digest` for the
|
||||
cookie compare; cache misses go through `parent.get()`, which performs the new file-HMAC
|
||||
check. The cache is only populated from a verified disk read (or from `new_session()`), so the
|
||||
file-HMAC check is enforced **at every cache miss** — including LRU evictions (`_normalize`
|
||||
trims to 80% capacity once size exceeds `num_to_store=1000`) and writes (`put()` invalidates
|
||||
the cached entry). No additional change needed here.
|
||||
|
||||
#### Phase 2 (defence in depth): switch to plain JSON
|
||||
|
||||
After Phase 1 ships, refactor every site that stashes a Python object or `pickle.dumps(...)`
|
||||
bytes inside `flask.session` so that the session contains only data primitives. Once the
|
||||
session contents are pure JSON-safe types, the on-disk format switches from pickle to plain
|
||||
JSON, and `pickle` is removed from the web codebase entirely.
|
||||
|
||||
The session's purpose is to hold **identity and state pointers**, not live SDK clients. The
|
||||
existing `__pgsql_server_managers` pattern (`web/pgadmin/utils/driver/psycopg3/server_manager.py:519-530` —
|
||||
stores `as_dict()` in session, keeps live `ServerManager` instances in module-global state) is
|
||||
the template.
|
||||
|
||||
**Per-site refactor (verified by grep):**
|
||||
|
||||
| Site (file:line) | Current | Replacement |
|
||||
|---|---|---|
|
||||
| `web/pgadmin/authenticate/__init__.py:113` | `session['auth_obj'] = AuthSourceManager(...)` (live object) | Store source name; reconstruct `AuthSourceManager` in OAuth2 callback (`oauth2.py:55`) |
|
||||
| `web/pgadmin/misc/cloud/azure/__init__.py:89` | `session['azure']['azure_obj'] = Azure(...)` (live object) | Store `authentication_record_json` + tenant/auth_type/cache_name; rebuild `Azure` per request via process-local cache keyed by sid. **See §5.4 for an unverified-claims caveat.** |
|
||||
| `web/pgadmin/misc/cloud/rds/__init__.py:73` | `session['aws']['aws_rds_obj'] = pickle.dumps(_rds, -1)` | Store credentials dict; rebuild `RDS` from process-local cache. 1 write, 2 reads (lines 73, 100, 129). |
|
||||
| `web/pgadmin/misc/cloud/google/__init__.py:110` | `session['google']['google_obj'] = pickle.dumps(...)` (4 write sites: 110, 129, 145, 181) | Store credentials/state dict; rebuild `Google` from process-local cache. |
|
||||
| `web/pgadmin/misc/cloud/biganimal/__init__.py:73` | `session['biganimal']['provider_obj'] = pickle.dumps(...)` (4 write sites: 60, 73, 95, 106) | Same pattern. |
|
||||
| `web/pgadmin/tools/sqleditor/__init__.py` (lines 338, 632, 699, 740 write `session['gridData']`; the per-trans `command_obj` field holds `pickle.dumps(transaction, -1)` produced in `tools/sqleditor/utils/start_running_query.py:204-208`, where the local `session` parameter is later persisted via `update_session_grid_transaction`) | serialized `Command` bytes inside a session-resident dict | Store command kind + identifying inputs; rebuild `Command` via `from_state()`; cache PG metadata in process-local TTL cache. **High risk — see §5.4.** |
|
||||
|
||||
**Process-local cache pattern (illustrative):**
|
||||
|
||||
```python
|
||||
# Module-level
|
||||
_azure_cache: dict[str, tuple[float, Azure]] = {}
|
||||
_AZURE_CACHE_TTL = 600 # seconds
|
||||
|
||||
def get_azure_for_session(sid: str) -> Azure:
|
||||
now = time.monotonic()
|
||||
cached = _azure_cache.get(sid)
|
||||
if cached and (now - cached[0]) < _AZURE_CACHE_TTL:
|
||||
return cached[1]
|
||||
state = session['azure'] # JSON-safe dict only
|
||||
obj = Azure.from_state(state) # new constructor
|
||||
_azure_cache[sid] = (now, obj)
|
||||
return obj
|
||||
```
|
||||
|
||||
Eviction: lazy on read (TTL check) and on logout (`_azure_cache.pop(sid, None)`).
|
||||
|
||||
**Worker-restart UX trade-off (Azure).** Today's behavior pickles the live `Azure` (with its
|
||||
populated SDK client cache) into the session file, so a worker restart preserves Azure auth
|
||||
state. After Phase 2, the in-memory client cache is gone on restart. Whether the user is
|
||||
re-prompted for device-code on the next request depends on whether
|
||||
`authentication_record_json` is sufficient to silently rebuild the credential. **This is
|
||||
unverified** (see §5.4) and must be confirmed during the Azure refactor PR before merge. If
|
||||
re-prompting is unavoidable, that is a UX regression — Phase 1 ships independently and is not
|
||||
gated on resolving this.
|
||||
|
||||
#### Final state
|
||||
|
||||
- `web/pgadmin/utils/session.py`: `from json import dumps, loads` replaces `from pickle import
|
||||
dump, load`. HMAC header retained.
|
||||
- All `import pickle` and `from pickle import ...` lines in the web tree removed (except in
|
||||
test fixtures, by deliberate exception).
|
||||
- CI guard (proposed): `grep -rEn '^\s*(import pickle\b|from pickle\b)' web/pgadmin/ | grep -v
|
||||
'/tests/'` must return empty.
|
||||
|
||||
### 4.2 Vulnerability 2 — `realpath`, syscall hardening, and ordering fix
|
||||
|
||||
#### 4.2.1 `check_access_permission` — realpath both sides
|
||||
|
||||
At `web/pgadmin/misc/file_manager/__init__.py:913-940` (definition starts at line 913 in the
|
||||
implemented branch), replace `os.path.abspath()` with `os.path.realpath()` for both
|
||||
`orig_path` and `in_dir`. The `in_dir` argument must also be
|
||||
resolved through `realpath()` — otherwise a symlinked storage root would compare unequal to a
|
||||
resolved target path, breaking legitimate access.
|
||||
|
||||
Python 3.9+ semantics (pgAdmin's documented floor — see `web/pgAdmin4.wsgi:14` and
|
||||
`README.md:23`): `os.path.realpath(path)` resolves symlinks for components that exist and
|
||||
leaves not-yet-existing tail components as literal text. This is the correct semantics for
|
||||
upload paths.
|
||||
|
||||
**Windows behavior is believed correct in Python 3.9+ but unverified by this proposal.**
|
||||
CPython release notes indicate `ntpath.realpath` resolves NTFS junctions and symbolic links via
|
||||
`GetFinalPathNameByHandle`, with several bug fixes landed across 3.10/3.11. Verification on
|
||||
Windows is a **PR-1 merge gate**: a smoke test creates a junction, calls `realpath`, and asserts
|
||||
the resolved path matches the target. This test must pass on the lowest supported Python
|
||||
(3.9.x) before PR 1 merges.
|
||||
|
||||
#### 4.2.2 Upload handler `add()` — realpath, ordering, and `O_NOFOLLOW`
|
||||
|
||||
Three changes at `web/pgadmin/misc/file_manager/__init__.py:1087-1132` (definition at line
|
||||
1087 in the implemented branch):
|
||||
|
||||
1. Replace `os.path.abspath` with `os.path.realpath` in the inline check (line 1112), and
|
||||
resolve `the_dir` through `realpath` too (line 1114).
|
||||
2. Delete the dead post-write `Filemanager.check_access_permission(the_dir, path)` call.
|
||||
The inline check above already covers it, the file is on disk by the time it
|
||||
runs, and replicating the same check at two granularities is bug-prone.
|
||||
3. **Replace `open(new_name, 'wb')` with `os.open(...)` + `O_NOFOLLOW`** (now extracted to
|
||||
the module-level helper `_open_upload_target` at line 52) to close the leaf-component
|
||||
TOCTOU gap between the access check and the syscall:
|
||||
|
||||
```python
|
||||
fd = os.open(
|
||||
new_name,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
| getattr(os, 'O_NOFOLLOW', 0),
|
||||
0o600,
|
||||
)
|
||||
with os.fdopen(fd, 'wb') as f:
|
||||
while True:
|
||||
data = file_obj.read(4194304)
|
||||
if not data:
|
||||
break
|
||||
f.write(data)
|
||||
```
|
||||
|
||||
`O_NOFOLLOW` causes the kernel to refuse to open the path if its **final** component is a
|
||||
symlink. Combined with `realpath`-based directory resolution at check time, this closes the
|
||||
**leaf-component race** where another worker plants a symlink at the leaf between check and
|
||||
open. **Intermediate-component races remain unmitigated** and rely on §1.5 reasoning (no
|
||||
in-pgAdmin primitive can plant the directory symlinks needed to exploit them).
|
||||
|
||||
Windows: `os.O_NOFOLLOW` is not defined on Windows. The `getattr(..., 0)` fallback above makes
|
||||
the flag a no-op on Windows. This leaves a small residual leaf-component TOCTOU on Windows,
|
||||
mitigated by the absence of the symlink-creation primitive.
|
||||
|
||||
**Intentional mode change (0o644 → 0o600).** The pseudocode passes mode `0o600` to `os.open`,
|
||||
restricting uploaded files to owner-only. The current code uses `open(name, 'wb')` which
|
||||
applies the umask-default (typically `0o644`, world-readable). The change is intentional
|
||||
hardening — uploaded files are user-private session artifacts and have no need for
|
||||
world-read access — but it **is** a behavioral change. Release notes must call this out so
|
||||
operators relying on cross-user readability of uploaded files (uncommon) can adjust.
|
||||
|
||||
#### 4.2.3 Cover `rename`, `delete`, `download`
|
||||
|
||||
These all call `check_access_permission()` and then operate on the literal path via
|
||||
`os.rename`, `os.rmdir`/`os.remove`, and `send_from_directory` respectively. Once
|
||||
`check_access_permission()` is realpath-based:
|
||||
|
||||
- **`rename` (definition at line 1015 in the implemented branch):** `os.rename` does not follow symlinks at the leaf for either source
|
||||
or destination — it renames the link itself, not the target. Intermediate-component symlinks
|
||||
in the destination path *would* route through the link at the kernel level (same shape as
|
||||
upload TOCTOU). Python's stdlib does not expose `renameat2` flags, so there is no clean
|
||||
`O_NOFOLLOW` analog for rename. **Residual TOCTOU on intermediate components is accepted**
|
||||
under §1.5 reasoning (no in-pgAdmin symlink primitive). Realpath-based access check at T1 is
|
||||
the only layer of defense.
|
||||
- **`delete` (definition at line 1062):** `os.rmdir` and `os.remove` operate on the leaf,
|
||||
not the target. No additional change required.
|
||||
- **`download` (definition at line 1287):** Werkzeug 3.1.* (`requirements.txt:65`) provides
|
||||
`send_from_directory`, which uses `werkzeug.security.safe_join` for `..`-traversal
|
||||
protection. `safe_join` does NOT call `realpath` and does NOT protect against symlinks —
|
||||
verified by reading the werkzeug 3.x source. Our `check_access_permission()` (post-§4.2.1)
|
||||
is therefore the **sole** symlink defense for downloads. TOCTOU between check and
|
||||
`send_from_directory`'s file open is symmetric to upload (§4.2.2) and not closeable from
|
||||
Python without bypassing `send_from_directory`. Residual TOCTOU accepted under §1.5.
|
||||
|
||||
A separate latent bug — `Filemanager.check_access_permission(the_dir,
|
||||
"{}{}".format(path, path))` in `download` — concatenates `path` with itself. This is
|
||||
unrelated to the security fix and is tracked as a separate cleanup ticket.
|
||||
|
||||
#### 4.2.4 `addfolder` (mkdir endpoint)
|
||||
|
||||
`addfolder` (definition at line 1256 in the implemented branch) calls
|
||||
`Filemanager.check_access_permission(user_dir, path+name)` before `os.mkdir(create_path)`. Once §4.2.1 fixes `check_access_permission`, intermediate-
|
||||
symlink resolution is caught at the access-check layer. `os.mkdir` itself does not create
|
||||
symlinks — it creates a real directory at the literal path — so even a TOCTOU race cannot
|
||||
escalate to symlink creation. No additional change required for this endpoint.
|
||||
|
||||
#### 4.2.5 Audit — sites covered by the Vulnerability 2 fix
|
||||
|
||||
This table enumerates the sites involved in the symlink-escape fix, not every path-handling
|
||||
site in `file_manager/__init__.py`. Other `open()` / `os.path.*` calls in the file (e.g., file
|
||||
encoding detection at line 438, `read_file_generator` at line 108) are not security-relevant
|
||||
to Vuln 2 and are out of scope.
|
||||
|
||||
Line numbers below are as of the implemented branch (`fix/pickle-rce`).
|
||||
|
||||
| Line | Function / Site | Resolution |
|
||||
|---|---|---|
|
||||
| 924 | `check_access_permission` — was `os.path.abspath`, now `os.path.realpath` | §4.2.1 |
|
||||
| 936 | `check_access_permission` — `in_dir` realpath | §4.2.1 |
|
||||
| 1112 | `add` (inline upload check) — was `os.path.abspath`, now `os.path.realpath` | §4.2.2 |
|
||||
| 1119 | `add` — was `open(new_name, 'wb')`, now `_open_upload_target(new_name)` | §4.2.2 |
|
||||
| 52 | new module-level `_open_upload_target` helper — `os.open` + `O_NOFOLLOW` + 0o600 | §4.2.2 |
|
||||
| (in `rename`, line 1015) | `os.rename(oldpath_sys, newpath_sys)` | Covered by §4.2.1 realpath; intermediate-component TOCTOU accepted (§4.2.3) |
|
||||
| (in `delete`, line 1062) | `os.rmdir` / `os.remove` | Leaf-only operation, no additional change (§4.2.3) |
|
||||
| (in `download`, line 1287) | `send_from_directory` | Covered by §4.2.1 realpath at access check (§4.2.3) |
|
||||
| (in `addfolder`, line 1256) | `os.mkdir` | Covered by §4.2.1 realpath (§4.2.4) |
|
||||
|
||||
All identified sites are covered. **Out-of-scope for this proposal:** `os.path.abspath`
|
||||
callsites elsewhere in `web/pgadmin/` have not been exhaustively audited. A follow-up
|
||||
security review is in scope for tracking, but is not gated on this proposal.
|
||||
|
||||
### 4.3 Tests
|
||||
|
||||
#### 4.3.1 New regression tests
|
||||
|
||||
1. **Unsafe-payload session rejection (Vuln 1):** drop a hand-crafted file in the sessions
|
||||
directory containing a `pickle` payload whose `__reduce__` writes a sentinel file. Issue a
|
||||
request with that sid in the cookie. Assert (a) the sentinel is **not** created, (b) the
|
||||
request gets a fresh session, (c) a single `session file rejected: bad file-HMAC`
|
||||
warning is logged.
|
||||
2. **Header-tampered session rejection:** write a session with a corrupted HMAC header (flip
|
||||
one byte). Assert `get()` returns a fresh session, no exception leaks, warning logged.
|
||||
3. **Empty-file passthrough:** create a 0-byte file at `sessions/<sid>` (the post-`new_session()`
|
||||
state). Assert `get()` returns a fresh session, **no warning** (this case is expected).
|
||||
4. **Symlink escape rejection (Vuln 2, SERVER_MODE=True):** create a symlink inside the user's
|
||||
storage dir pointing to `/tmp` (or another out-of-sandbox path). Attempt an upload. Assert
|
||||
the upload is denied **and** no file is created at the symlink target.
|
||||
5. **`O_NOFOLLOW` leaf-symlink rejection (Linux/macOS):** pre-plant a symlink at the upload
|
||||
target leaf path (no race needed — test the *property*, not the timing). Attempt upload.
|
||||
Assert `OSError` is raised with `errno.ELOOP` (Linux) or `errno.EMLINK` (macOS) and no file
|
||||
exists at the symlink target afterward. *Skip on Windows* (no `O_NOFOLLOW`).
|
||||
6. **Upload happy path:** confirm legitimate uploads still succeed in both `SERVER_MODE=False`
|
||||
and `SERVER_MODE=True`.
|
||||
7. **MFA login flow regression:** cover at least one full MFA email and authenticator login
|
||||
round-trip. The MFA fields are JSON-safe but the test suite should exercise them.
|
||||
8. **Cookie-HMAC mismatch with valid file-HMAC:** write a session file with a valid file-HMAC
|
||||
header but a `hmac_digest` inside the body that does NOT match the cookie-supplied digest.
|
||||
Assert `get()` returns a fresh session, **and** assert the `session file rejected:
|
||||
bad file-HMAC` warning is **not** logged (this is a cookie-binding mismatch, not a
|
||||
file-tamper event; current proposal does not log cookie mismatches to avoid noise from
|
||||
normal cookie-tampering attempts).
|
||||
9. **Round-trip:** construct a `ManagedSession`, call `session.sign(secret)` to populate
|
||||
`randval` and `hmac_digest` (a `put()` precondition; `put()` itself calls `sign()` on
|
||||
first write but a deterministic test should pre-sign), call `put()`, then `get()` with the
|
||||
matching cookie. Assert the returned `ManagedSession` has the same `data`, `randval`,
|
||||
`hmac_digest` as the input. Without this, a `put()` regression silently breaks every
|
||||
session save.
|
||||
10. **SERVER_MODE=False direct upload to sessions dir:** unit test against
|
||||
`FileBackedSessionManager.get()` directly (preferred over a full integration test to
|
||||
avoid Flask/CSRF setup). Write a malicious pickle file to the sessions path that
|
||||
`safe_join(self.path, sid)` would resolve to. Call `get(sid, fake_digest)`. Assert
|
||||
(a) `pickle.loads` does not execute the payload (sentinel not created), (b) `get()`
|
||||
returns a fresh session, (c) the file-HMAC warning is logged. This validates the
|
||||
Scenario A chain from §1.5 is closed at the session-read layer regardless of whether
|
||||
`SERVER_MODE` permits the upload.
|
||||
|
||||
#### 4.3.2 Existing tests to update
|
||||
|
||||
- `web/pgadmin/browser/tests/test_oauth2_with_mocking.py` — uses `session['oauth2_token']`
|
||||
(already a dict); verify it continues to pass.
|
||||
- Cloud-wizard tests using legacy serialized fixtures — update to dict fixtures during the
|
||||
per-provider PR.
|
||||
|
||||
#### 4.3.3 Manual verification
|
||||
|
||||
- All four cloud deployment wizards end-to-end (RDS, Azure, Google, BigAnimal).
|
||||
- OAuth2 login round-trip with a real provider.
|
||||
- Query Tool: execute, page, edit cell, transactions, savepoint, BYTEA result.
|
||||
- Debugger: at least one direct-debug session.
|
||||
|
||||
---
|
||||
|
||||
## 5. Blast Radius
|
||||
|
||||
### 5.1 Phase 1 (PR 1)
|
||||
|
||||
| Aspect | Estimate |
|
||||
|---|---|
|
||||
| Source files modified | 2 (`web/pgadmin/utils/session.py`, `web/pgadmin/misc/file_manager/__init__.py`) |
|
||||
| Test files added/modified | ~2 |
|
||||
| LOC changed | Source: ~80–110 (session HMAC logic ~50–80, file_manager realpath/O_NOFOLLOW/coverage ~30). Tests: ~250–400 (10 regression tests in §4.3.1, ~25–40 LOC each plus shared fixtures). **Total ~350–500.** |
|
||||
| Behavioural change | Sessions written by old code become unreadable → users re-login once on upgrade. One transient warning logged per rejected sid. |
|
||||
| Risk | **Low.** Self-contained, well-bounded, easy to review. |
|
||||
|
||||
### 5.2 Phase 2 — verified site counts
|
||||
|
||||
Per-feature breakdown (all numbers verified by grep):
|
||||
|
||||
| Feature | Files | `pickle.dumps` writes | `pickle.loads` reads | Risk | Rationale |
|
||||
|---|---|---|---|---|---|
|
||||
| `auth_obj` (OAuth2 login) | `authenticate/__init__.py`, `authenticate/oauth2.py` | 0 (live object stored directly) | 0 (live object read directly) | Medium | Login path — regression locks users out. Test against a real OAuth2 provider before merge. |
|
||||
| Azure cloud | `misc/cloud/azure/__init__.py` | 0 (live object stored directly at line 89) | 0 (read directly at lines 117, 138, 152, 164, 175, 189, 202, 216, 228, 692, …) | **High (verification gated)** | See §5.4. |
|
||||
| AWS RDS cloud | `misc/cloud/rds/__init__.py` | 1 (line 73) | 2 (lines 100, 129) | Medium | Few sites; boto3 client recreation should be cheap (assumed — verify per §5.4). |
|
||||
| Google cloud | `misc/cloud/google/__init__.py` | 4 (lines 110, 129, 145, 181) | 10 (lines 95, 127, 143, 161, 179, 199, 220, 241, 307, …) | Medium | Refactor: 1 `from_state()` constructor + ~10 callsites converted from `pickle.loads(...)` to `get_google_for_session(sid)`. |
|
||||
| BigAnimal cloud | `misc/cloud/biganimal/__init__.py` | 4 (lines 60, 73, 95, 106) | 11 (lines 57, 83, 93, 104, 115, 125, 137, 150, 162, 528, …) | Medium | Same shape as Google. |
|
||||
| SQL editor — `command_obj` producer | `tools/sqleditor/utils/start_running_query.py:204-208` | 1 (`pickle.dumps` at line 207, into a per-trans dict later persisted via `update_session_grid_transaction`) | 0 | **High (verification gated)** | See §5.4 item 4. |
|
||||
| SQL editor — Flask-session reads/writes | `tools/sqleditor/__init__.py` | gridData writes at lines 338, 632, 699, 740 | 1 (`pickle.loads` at line 1935) | **High (verification gated)** | The per-trans dict containing `command_obj` lives inside `session['gridData']`. Refactor: store command kind + identifying inputs; rebuild `Command` via `from_state()`; cache PG metadata in process-local TTL cache. |
|
||||
| Final swap (`session.py` to JSON) | `utils/session.py` | n/a | n/a | Low | Trivial once Phase 2 lands. |
|
||||
|
||||
**Total source files touched (Phase 1 + Phase 2):** 8 (files), 2 of which are big files
|
||||
(`misc/cloud/google/__init__.py`, `misc/cloud/biganimal/__init__.py`).
|
||||
**Total LOC changed (rough):** 600–900 across Phase 2.
|
||||
**PR count target:** 6–7 (one per feature plus the final swap).
|
||||
|
||||
> **Note on the two SQL-editor rows above:** they describe the same logical refactor but split
|
||||
> by file. The producer row covers where `pickle.dumps` is called; the Flask-session row
|
||||
> covers how that pickled blob enters and exits the `flask.session['gridData']` dict. Both
|
||||
> rows are addressed by a single PR (PR 7).
|
||||
|
||||
### 5.3 Failure modes considered
|
||||
|
||||
- **Existing session files lost on upgrade:** users re-login once. Expected; called out in
|
||||
release notes.
|
||||
- **Process restart drops in-memory caches (cloud SDKs):** users may need to re-authenticate
|
||||
against the cloud provider on the next request after a worker recycle. **Differs from
|
||||
today's behavior for Azure** (see §5.4 and §4.1 Phase 2).
|
||||
- **`session['azure']` grows because state replaces object:** dict holds ~5 primitive fields,
|
||||
far below the file-write size threshold.
|
||||
|
||||
### 5.4 Unverified claims that gate merge of specific PRs
|
||||
|
||||
These claims appear in the spec or its rationale and are **not yet evidence-backed**. They must
|
||||
be verified during the relevant PR before merge:
|
||||
|
||||
1. **Azure: `authentication_record_json` is sufficient to silently rebuild the credential
|
||||
without re-prompting the user for device code.** The class clearly persists the auth record
|
||||
and supports rebuilding the credential from it (`misc/cloud/azure/__init__.py:298-322`),
|
||||
*but* whether **all** ~12 Azure endpoints work after a per-request rebuild has not been
|
||||
end-to-end tested. Verification: run the Azure cloud wizard end-to-end after Phase 2 with
|
||||
the in-memory cache disabled; confirm no re-prompt.
|
||||
2. **boto3 RDS clients are cheap to recreate from credentials dict.** Generally true; SSO/STS
|
||||
token-cache state may complicate it. Verification: run the RDS wizard end-to-end across a
|
||||
simulated worker restart.
|
||||
3. **Google Cloud SDK clients are reconstructable from refresh-token credentials.**
|
||||
Refresh-token flows typically work; OAuth-installed-app flows with one-time auth codes do
|
||||
not. Verification: run the Google wizard end-to-end across a simulated worker restart.
|
||||
4. **The `Command` subclass tree in sqleditor is amenable to a `from_state()` constructor.**
|
||||
The proposal author has not yet read the Command hierarchy. The refactor PR (PR 7) starts
|
||||
with an audit that may reveal additional design work; the spec marks this PR as **High
|
||||
risk, scope-revisit on audit**.
|
||||
|
||||
**Fallback if PR 7 is intractable:** If the audit concludes the refactor is more than ~2
|
||||
weeks of work (e.g., the `Command` tree has deep coupling to live cursor state),
|
||||
the spec resolves the conflict as follows:
|
||||
- **Option A (preferred):** ship Phase 2 without sqleditor. The CI guard at PR 8 carves
|
||||
out `web/pgadmin/tools/sqleditor/` as a documented exception, with a TODO and tracking
|
||||
issue. Concrete encoding: sqleditor stores the pickled `Command` bytes
|
||||
**base64-encoded into a JSON string field** inside `session['gridData'][trans_id]`.
|
||||
The outer JSON session body is HMAC-protected by Phase 1's file-HMAC, so the encoded
|
||||
bytes cannot be tampered with from the disk side. **Caveat:** `pickle.loads` still runs
|
||||
on those bytes inside sqleditor on every read. If any other code path can write to
|
||||
`gridData` with attacker-controlled content (an unrelated bug), the RCE primitive
|
||||
returns. This is a strictly weaker guarantee than full pickle removal — Option A is a
|
||||
scope-management choice, not a security equivalence.
|
||||
- **Option B (rejected unless A is infeasible):** keep pickle in `session.py` itself, do
|
||||
not migrate to JSON, accept the ongoing footgun across the entire session surface.
|
||||
|
||||
PR 7 author chooses A or B based on the audit; spec is amended in the PR description with
|
||||
the rationale.
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Plan
|
||||
|
||||
### 6.1 PR sequencing
|
||||
|
||||
1. **PR 1 — Close the CVEs.** HMAC-header redesign in `session.py`; in `file_manager`:
|
||||
`realpath` (§4.2.1), `O_NOFOLLOW` and dead-check removal in upload (§4.2.2),
|
||||
access-check coverage for `rename`/`delete`/`download` (§4.2.3) and `addfolder` (§4.2.4).
|
||||
Tests: **all 10 items** in §4.3.1. *Ships first, separately, with a security advisory.*
|
||||
2. **PR 2 — `auth_obj` data-only refactor.** Removes the live `AuthSourceManager` from the
|
||||
session.
|
||||
3. **PR 3 — Azure cloud refactor.** Includes verification step §5.4 item 1.
|
||||
4. **PR 4 — AWS RDS cloud refactor.** Includes verification §5.4 item 2.
|
||||
5. **PR 5 — Google cloud refactor.** Includes verification §5.4 item 3.
|
||||
6. **PR 6 — BigAnimal cloud refactor.**
|
||||
7. **PR 7 — SQL editor `command_obj` refactor.** Begins with a Command-tree audit
|
||||
(verification §5.4 item 4); scope may grow.
|
||||
8. **PR 8 — Final swap: `session.py` to plain JSON, delete `pickle` imports, add CI guard.**
|
||||
|
||||
PRs 2–7 are independent and can land in any order. **PR 8 has a hard dependency on PRs 2–7
|
||||
all having merged** — until every `pickle` site is removed, the JSON swap will fail to
|
||||
serialize live objects or pickled-bytes blobs.
|
||||
|
||||
### 6.2 Rollback strategy
|
||||
|
||||
Each PR is reverted independently if a regression is discovered post-merge. PR 1 has no
|
||||
in-codebase compatibility shim — a revert of PR 1 restores the pre-fix vulnerable behavior,
|
||||
which is unacceptable. **The team commits to fix-forward only for PR 1: any post-merge
|
||||
regression is resolved by a same-day hotfix, never by a revert.** This commitment requires
|
||||
on-call availability during the release window; the security advisory cannot publish without
|
||||
this commitment in place.
|
||||
|
||||
PRs 2–7 each remove a `pickle` site without changing the on-disk format; reverting any of them
|
||||
is safe and does not re-open RCE (since PR 1 already gates the on-disk read).
|
||||
|
||||
PR 8 (the JSON swap) carries upgrade risk for sessions written by PRs 1-7's pickle-with-header
|
||||
format. Format-detection on read after PR 8: the file-HMAC header is still validated first
|
||||
(same algorithm), then `json.loads(body)` is attempted. A pre-PR-8 pickle body fails JSON parse
|
||||
with `JSONDecodeError`, falls through to `new_session()`, and a warning is logged. **No
|
||||
execution path through `pickle.loads` remains in `session.py` post-PR-8** — a malicious or
|
||||
legitimate pickle file is rejected at JSON parse, never deserialized. One-time re-login at PR 8
|
||||
release; warning logged per dropped sid.
|
||||
|
||||
### 6.3 Multi-instance / HA note
|
||||
|
||||
pgAdmin is typically deployed single-instance. If two replicas share a sessions directory, both
|
||||
must upgrade together: in **both directions**, the receiver fails to parse the format and
|
||||
silently invalidates the session (old code reading a header'd file fails on `pickle.load` of
|
||||
ASCII hex; new code reading a header-less file fails the HMAC length check). No data loss
|
||||
beyond a one-time re-login per affected user, but operators must coordinate the upgrade window.
|
||||
Document in release notes.
|
||||
|
||||
**Mixed-version Python workers (rare):** `pickle.dumps(..., -1)` selects `HIGHEST_PROTOCOL`
|
||||
which varies by Python version (3.9 → protocol 5; 3.13 → protocol 5; 3.14 → 5). A protocol
|
||||
written by a newer worker and read by an older worker may fail to deserialize. Pre-existing
|
||||
assumption that all workers in a deployment run the same Python; not introduced by this
|
||||
proposal. After PR 8 (JSON swap), this concern disappears entirely.
|
||||
|
||||
### 6.4 Acceptance criteria
|
||||
|
||||
**PR 1 (security fix):**
|
||||
- [ ] All 10 regression tests in §4.3.1 added and passing.
|
||||
- [ ] Defensive non-empty-`SECRET_KEY` check added in `FileBackedSessionManager.__init__`
|
||||
(raise, not assert).
|
||||
- [ ] `_compute_file_hmac` accepts both `str` and `bytes` for `secret`.
|
||||
- [ ] Round-trip test (§4.3.1 test 9) passes — confirms `put()` produces a file readable by
|
||||
`get()`.
|
||||
- [ ] Windows realpath smoke test (§4.2.1) passes on Python 3.9.x.
|
||||
- [ ] CVE assigned and security advisory drafted; advisory publishes on the PR-1 release.
|
||||
- [ ] On-call coverage confirmed for PR-1 release window (per §6.2 fix-forward commitment).
|
||||
|
||||
**Each of PR 2-7 (per-feature refactor):**
|
||||
- [ ] Corresponding §5.4 verification item passes (where applicable: Azure → item 1, RDS →
|
||||
item 2, Google → item 3, sqleditor → item 4; auth_obj and BigAnimal have no §5.4 entry).
|
||||
- [ ] No regression in the relevant feature's existing test suite.
|
||||
- [ ] Manual verification per §4.3.3 for the affected feature (cloud wizard end-to-end,
|
||||
OAuth2 round-trip, or Query Tool sweep).
|
||||
- [ ] No new `pickle` site introduced in any other file (regression check).
|
||||
|
||||
**PR 8 (final pickle removal):**
|
||||
- [ ] `grep -rEn '^\s*(import pickle\b|from pickle\b)' web/pgadmin/ | grep -v '/tests/'`
|
||||
returns empty (or returns only the documented sqleditor exception per §5.4 item 4
|
||||
Option A).
|
||||
- [ ] All four cloud wizards verified end-to-end in `SERVER_MODE=True` and `SERVER_MODE=False`.
|
||||
- [ ] OAuth2 round-trip verified against a real provider.
|
||||
- [ ] Query Tool regression sweep passed (pagination, edit, transactions, BYTEA).
|
||||
- [ ] Audit table in §9 re-run on the merge commit and the result recorded in the PR
|
||||
description; any drift from the table in this proposal explained.
|
||||
|
||||
---
|
||||
|
||||
## 7. Alternatives Considered
|
||||
|
||||
### 7.1 Big-bang refactor in one PR
|
||||
|
||||
Rejected. Combines an urgent security fix with a multi-feature refactor; a regression in any
|
||||
cloud wizard would block the security fix from shipping. Review surface is too large for
|
||||
adequate scrutiny.
|
||||
|
||||
### 7.2 Keep pickle, only fix HMAC ordering
|
||||
|
||||
Rejected as the long-term answer. Closes the immediate CVE but leaves `pickle` as the de-facto
|
||||
session format and inside cloud handlers. Any future bug that lets an attacker influence
|
||||
session contents re-opens RCE. Adopted as **Phase 1** only — buys time for Phase 2.
|
||||
|
||||
### 7.3 Use `flask.json.tag.TaggedJSONSerializer`
|
||||
|
||||
Rejected for the on-disk session format. `TaggedJSONSerializer` handles `bytes`, `datetime`,
|
||||
`UUID`, `tuple`, `set` — which would let the in-session serialized blobs (the cloud-handler
|
||||
`pickle.dumps` outputs) silently survive the JSON switch. We want those *gone*, not preserved.
|
||||
Plain JSON forces the refactor.
|
||||
|
||||
### 7.4 Switch to `itsdangerous.URLSafeTimedSerializer` only (no file backend)
|
||||
|
||||
Rejected. Cookie size limits make this impractical for pgAdmin's session contents (especially
|
||||
`gridData` and server manager state). File-backed sessions stay.
|
||||
|
||||
### 7.5 chroot or seccomp-style sandboxing of the worker process
|
||||
|
||||
Out of scope. Requires deployment-level changes that vary by environment
|
||||
(Docker, Kubernetes, Snap, AppImage, systemd, native pip, conda, etc.) and that operators
|
||||
must opt into per-deployment. Worth tracking separately as a defense-in-depth follow-up.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open Questions (resolved before approval)
|
||||
|
||||
1. **HMAC algorithm for the file header: SHA-256 vs `config.SESSION_DIGEST_METHOD` (default
|
||||
SHA-1)?**
|
||||
*Resolution:* hard-code SHA-256 in the file header. The primary rationale is **header-length
|
||||
predictability**: the file format needs a fixed prefix length decoupled from operator config,
|
||||
so that file parsing is deterministic. Secondary rationale: SHA-256 is the modern default
|
||||
and avoids a future deprecation step. The cookie HMAC remains tied to
|
||||
`SESSION_DIGEST_METHOD` for backward compatibility — that's an unrelated mechanism.
|
||||
2. **TTL for the process-local SDK client cache?**
|
||||
*Resolution:* match `permanent_session_lifetime` (Flask config), with passive eviction on
|
||||
each access.
|
||||
3. **Migration strategy for in-flight sessions:** silent drop vs. warning per dropped sid.
|
||||
*Resolution:* warning logged per rejected sid (sid prefix only, to avoid log-leaking
|
||||
active sids). Provides operator visibility without information disclosure.
|
||||
4. **CI guard for `pickle`:** add a lint check that fails CI if `import pickle` reappears
|
||||
outside `web/pgadmin/tests/`?
|
||||
*Resolution:* yes. Implementation: a single grep-based check in the existing test
|
||||
makefile, gated on PR 8.
|
||||
|
||||
## 9. Audit summary (referenced from §1.5 and §4.2)
|
||||
|
||||
This proposal relies on the following grep audits run against the v9.14 branch
|
||||
(commit-of-record: head of `fix/pickle-rce` at proposal time). Patterns are listed as
|
||||
ripgrep-compatible regex.
|
||||
|
||||
**Reproducer:** `rg --type py 'PATTERN' PATH` (or equivalently
|
||||
`grep -rEn --include='*.py' 'PATTERN' PATH`). Re-running these audits on the merge commit is
|
||||
a PR-8 acceptance criterion (§6.4).
|
||||
|
||||
**Audit 1 — symlink-creating primitives:**
|
||||
|
||||
```regex
|
||||
os\.symlink|os\.link\(|symbolic_link|symlink_to|Path\.symlink|\.symlink\(
|
||||
```
|
||||
|
||||
Path: `web/pgadmin/`. Result: **0 matches.**
|
||||
|
||||
**Audit 2 — archive extraction (could preserve symlinks from a malicious archive):**
|
||||
|
||||
```regex
|
||||
tarfile\.|zipfile\.|shutil\.unpack_archive|shutil\.copytree|extractall|extract_archive
|
||||
```
|
||||
|
||||
Path: `web/pgadmin/`. Result: **0 matches.**
|
||||
|
||||
**Audit 3 — shell-invocation primitives in file_manager:**
|
||||
|
||||
```regex
|
||||
subprocess|os\.popen|os\.system|shell=True
|
||||
```
|
||||
|
||||
Path: `web/pgadmin/misc/file_manager/`. Result: **0 matches.**
|
||||
|
||||
**Audit 4 — `os.path.abspath` callers in file_manager:**
|
||||
|
||||
```regex
|
||||
os\.path\.abspath
|
||||
```
|
||||
|
||||
Path: `web/pgadmin/misc/file_manager/__init__.py`. Result: **2 matches** at lines 903 and 1086.
|
||||
|
||||
**Audit 5 — directory-creation handlers in file_manager:**
|
||||
|
||||
```regex
|
||||
def (addfolder|add_folder|new_folder|mkdir|create_directory|createfolder)
|
||||
```
|
||||
|
||||
Path: `web/pgadmin/misc/file_manager/`. Result: **1 match** — `addfolder` at line 1232.
|
||||
|
||||
**Audit 6 — `os.mkdir` / `os.makedirs` in file_manager:**
|
||||
|
||||
```regex
|
||||
os\.mkdir|os\.makedirs
|
||||
```
|
||||
|
||||
Path: `web/pgadmin/misc/file_manager/`. Result: **1 match** at line 1250 (inside `addfolder`).
|
||||
|
||||
These results justify the §1.5 conclusion (no in-pgAdmin symlink primitive) and the §4.2.5
|
||||
conclusion (exhaustive audit of file_manager path-handling sites).
|
||||
|
||||
## 10. References
|
||||
|
||||
- Security report: 2026-04-29 (private)
|
||||
- CVE-2024-2044 — pgAdmin RCE in `SERVER_MODE=False`, earlier versions
|
||||
- CWE-502 — Deserialization of Untrusted Data
|
||||
- CWE-61 — UNIX Symbolic Link (Symlink) Following
|
||||
- CWE-22 — Improper Limitation of a Pathname to a Restricted Directory
|
||||
- OWASP Top 10 — A08:2021 Software and Data Integrity Failures
|
||||
- Existing pattern: `web/pgadmin/utils/driver/psycopg3/server_manager.py:519-530` —
|
||||
`as_dict()` + module-global live state
|
||||
- Linux `open(2)` — `O_NOFOLLOW`: refuse to follow symbolic links at the leaf component
|
||||
@@ -86,7 +86,11 @@ EOF
|
||||
#!/bin/sh
|
||||
|
||||
echo "Load apparmor pgAdmin profile..."
|
||||
apparmor_parser -r /etc/apparmor.d/pgadmin4
|
||||
if command -v apparmor_parser >/dev/null 2>&1; then
|
||||
apparmor_parser -r /etc/apparmor.d/pgadmin4
|
||||
else
|
||||
echo "Warning: apparmor_parser not found, skipping profile load. pgAdmin desktop may not work on Ubuntu 24+ with userns restrictions."
|
||||
fi
|
||||
EOF
|
||||
chmod 755 "${DESKTOPROOT}/DEBIAN/postinst"
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Fixup the passwd file, in case we're on OpenShift
|
||||
if ! whoami > /dev/null 2>&1; then
|
||||
if [ "$(id -u)" -ne 5050 ]; then
|
||||
if [ -w /etc/passwd ]; then
|
||||
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
|
||||
########################################################################
|
||||
# PUID/PGID support
|
||||
#
|
||||
# When the container runs as root (e.g. --user root), the pgadmin user
|
||||
# is reassigned to the requested UID/GID and all initialization +
|
||||
# gunicorn run via su-exec as that user.
|
||||
#
|
||||
# When the container runs as non-root (default USER 5050, or OpenShift
|
||||
# random UID), PUID/PGID are ignored and everything runs as the
|
||||
# current user.
|
||||
########################################################################
|
||||
|
||||
PUID=${PUID:-5050}
|
||||
PGID=${PGID:-0}
|
||||
|
||||
# Validate PUID/PGID are numeric and in acceptable range
|
||||
if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
|
||||
echo "ERROR: PUID must be a numeric value, got '$PUID'"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
|
||||
echo "ERROR: PGID must be a numeric value, got '$PGID'"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$PUID" -eq 0 ]; then
|
||||
echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
# Ensure a group with the target GID exists
|
||||
if ! getent group "$PGID" > /dev/null 2>&1; then
|
||||
if ! addgroup -g "$PGID" pggroup; then
|
||||
echo "ERROR: Failed to create group with GID=$PGID"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reassign the pgadmin user to the desired UID/GID
|
||||
if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then
|
||||
echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fix ownership of runtime directories BEFORE any initialization
|
||||
for dir in /run/pgadmin /var/lib/pgadmin; do
|
||||
if [ -d "$dir" ]; then
|
||||
chown -R "$PUID:$PGID" "$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# Fix ownership of individual files (no -R needed)
|
||||
if [ -e /pgadmin4/config_distro.py ]; then
|
||||
chown "$PUID:$PGID" /pgadmin4/config_distro.py
|
||||
fi
|
||||
|
||||
if [ -d /certs ]; then
|
||||
chown -R "$PUID:$PGID" /certs
|
||||
fi
|
||||
|
||||
SU_EXEC="su-exec $PUID:$PGID"
|
||||
echo "pgAdmin will run as UID=$PUID, GID=$PGID"
|
||||
else
|
||||
SU_EXEC=""
|
||||
|
||||
# Fixup the passwd file, in case we're on OpenShift
|
||||
if ! whoami > /dev/null 2>&1; then
|
||||
if [ "$(id -u)" -ne 5050 ]; then
|
||||
if [ -w /etc/passwd ]; then
|
||||
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
|
||||
@@ -23,6 +89,10 @@ function file_env() {
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ] && [ ! -r "${!fileVar}" ]; then
|
||||
printf >&2 'error: %s is set to "%s" but the file does not exist or is not readable\n' \
|
||||
"$fileVar" "${!fileVar}"
|
||||
exit 1
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
@@ -70,12 +140,17 @@ EOF
|
||||
esac
|
||||
echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}"
|
||||
done
|
||||
|
||||
# If running as root with custom config distro path, fix ownership
|
||||
if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then
|
||||
chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check whether the external configuration database exists if it is being used.
|
||||
external_config_db_exists="False"
|
||||
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then
|
||||
external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db("${PGADMIN_CONFIG_CONFIG_DATABASE_URI}"); print(val)")
|
||||
external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db(\"${PGADMIN_CONFIG_CONFIG_DATABASE_URI}\"); print(val)")
|
||||
fi
|
||||
|
||||
# DRY of the code to load the PGADMIN_SERVER_JSON_FILE
|
||||
@@ -92,9 +167,9 @@ function load_server_json_file() {
|
||||
# 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}" ${EXTRA_ARGS}
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
|
||||
else
|
||||
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -120,7 +195,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
|
||||
fi
|
||||
email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}"
|
||||
echo "email config is ${email_config}"
|
||||
is_valid_email=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
|
||||
is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
|
||||
if echo "${is_valid_email}" | grep "False" > /dev/null; then
|
||||
echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
|
||||
echo "Validation output: ${is_valid_email}"
|
||||
@@ -136,7 +211,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
|
||||
|
||||
# Initialize DB before starting Gunicorn
|
||||
# Importing pgadmin4 (from this script) is enough
|
||||
/venv/bin/python3 run_pgadmin.py
|
||||
$SU_EXEC /venv/bin/python3 run_pgadmin.py
|
||||
|
||||
export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}"
|
||||
|
||||
@@ -146,22 +221,30 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
|
||||
# Pre-load any required preferences
|
||||
if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then
|
||||
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
|
||||
DESKTOP_USER=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
|
||||
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
|
||||
DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
|
||||
else
|
||||
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
|
||||
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
|
||||
fi
|
||||
fi
|
||||
# Copy the pgpass file passed using secrets
|
||||
if [ -f "${PGPASS_FILE}" ]; then
|
||||
if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then
|
||||
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
|
||||
cp ${PGPASS_FILE} /var/lib/pgadmin/.pgpass
|
||||
cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass
|
||||
chmod 600 /var/lib/pgadmin/.pgpass
|
||||
# Fix ownership when running as root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
|
||||
fi
|
||||
else
|
||||
PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g')
|
||||
mkdir -p /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}
|
||||
cp ${PGPASS_FILE} /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
|
||||
chmod 600 /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
|
||||
mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
|
||||
cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
|
||||
chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
|
||||
# Fix ownership when running as root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
|
||||
@@ -176,7 +259,7 @@ fi
|
||||
|
||||
# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
|
||||
# to define the Gunicorn worker timeout
|
||||
TIMEOUT=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
|
||||
TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
|
||||
|
||||
# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
|
||||
# Using --threads to have multi-threaded single-process worker
|
||||
@@ -192,7 +275,7 @@ else
|
||||
fi
|
||||
|
||||
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
|
||||
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
|
||||
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
|
||||
else
|
||||
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
|
||||
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: pgadmin4-helm
|
||||
version: 9.12.0
|
||||
appVersion: 9.12.0
|
||||
version: 9.15.0
|
||||
appVersion: 9.15.0
|
||||
description: pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.
|
||||
home: https://pgadmin.org
|
||||
sources: ["https://github.com/pgadmin-org/pgadmin4/tree/master/helm"]
|
||||
|
||||
@@ -140,7 +140,7 @@ spec:
|
||||
mountPath: /pgadmin4/config_distro.py
|
||||
subPath: config_distro.py
|
||||
- name: empty-dir
|
||||
mountPath: /usr/bin/python3
|
||||
mountPath: /usr/local/bin/python3
|
||||
subPath: python3
|
||||
- name: empty-dir
|
||||
mountPath: /tmp
|
||||
@@ -214,7 +214,7 @@ spec:
|
||||
image: {{ template "pgadmin4.image" . }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command: ["sh", "-x", "-c"]
|
||||
args: ['ls /usr/bin/python3.* | sort -V -r | head -n 1 | xargs -i cp {} python3']
|
||||
args: ['ls /venv/bin/python3.* | sort -V -r | head -n 1 | xargs -i cp {} python3']
|
||||
workingDir: /emptyDir
|
||||
volumeMounts:
|
||||
- name: empty-dir
|
||||
|
||||
@@ -112,8 +112,8 @@ if [ ${IS_DEBIAN} == 1 ]; then
|
||||
|
||||
case ${RESPONSE} in
|
||||
y|Y )
|
||||
a2enmod wsgi 1> /dev/null
|
||||
a2enconf pgadmin4 1> /dev/null
|
||||
/usr/sbin/a2enmod wsgi 1> /dev/null
|
||||
/usr/sbin/a2enconf pgadmin4 1> /dev/null
|
||||
;;
|
||||
* )
|
||||
exit 1;;
|
||||
|
||||
@@ -12,15 +12,17 @@
|
||||
# relevant packages.
|
||||
###############################################################################
|
||||
|
||||
Authlib==1.6.6
|
||||
azure-identity==1.25.1
|
||||
Authlib==1.6.*; python_version <= '3.9'
|
||||
Authlib==1.7.*; python_version > '3.9'
|
||||
azure-identity==1.25.3
|
||||
azure-mgmt-rdbms==10.1.1
|
||||
azure-mgmt-resource==24.0.0
|
||||
azure-mgmt-resource==25.0.0
|
||||
azure-mgmt-subscription==3.1.1
|
||||
bcrypt==5.0.*
|
||||
boto3==1.42.*
|
||||
certifi==2026.1.4
|
||||
cryptography==46.0.*
|
||||
boto3==1.42.*; python_version <= '3.9'
|
||||
boto3==1.43.*; python_version > '3.9'
|
||||
certifi==2026.4.22
|
||||
cryptography==47.0.*
|
||||
Flask-Babel==4.0.*
|
||||
Flask-Compress==1.*
|
||||
Flask-Login==0.*
|
||||
@@ -28,14 +30,16 @@ Flask-Mail==0.*
|
||||
Flask-Migrate==4.*
|
||||
Flask-Paranoid==0.*
|
||||
Flask-Security-Too==5.4.*; python_version <= '3.9'
|
||||
Flask-Security-Too==5.7.*; python_version > '3.9'
|
||||
Flask-Security-Too==5.8.*; python_version > '3.9'
|
||||
Flask-SocketIO==5.6.*
|
||||
Flask-SQLAlchemy==3.1.*
|
||||
Flask-WTF==1.2.*
|
||||
Flask-WTF==1.2.*; python_version <= '3.9'
|
||||
Flask-WTF==1.3.*; python_version > '3.9'
|
||||
Flask==3.1.*
|
||||
google-api-python-client==2.*
|
||||
google-auth-oauthlib==1.2.4
|
||||
gssapi==1.10.*
|
||||
google-auth-oauthlib==1.3.1; python_version <= '3.9'
|
||||
google-auth-oauthlib==1.4.0; python_version > '3.9'
|
||||
gssapi==1.11.*
|
||||
jsonformatter~=0.3.4
|
||||
keyring==25.*
|
||||
ldap3==2.*
|
||||
@@ -44,21 +48,22 @@ paramiko==3.5.1
|
||||
passlib==1.*
|
||||
psutil==7.2.*
|
||||
psycopg[c]==3.2.13; python_version < '3.10'
|
||||
psycopg[c]==3.3.2; python_version >= '3.10'
|
||||
psycopg[c]==3.3.4; python_version >= '3.10'
|
||||
pyotp==2.*
|
||||
python-dateutil==2.*
|
||||
pytz==2025.*
|
||||
pytz==2026.*
|
||||
# pinned for https://github.com/andfoy/pywinpty/issues/545
|
||||
pywinpty==2.0.*; sys_platform=="win32"
|
||||
qrcode[pil]==8.*
|
||||
setuptools==80.*
|
||||
setuptools==82.*
|
||||
SQLAlchemy==2.*
|
||||
sqlparse==0.*
|
||||
sshtunnel==0.*
|
||||
typer[all]==0.21.*
|
||||
typer[all]==0.23.*; python_version <= '3.9'
|
||||
typer==0.25.*; python_version > '3.9'
|
||||
urllib3==1.26.*; python_version <= '3.9'
|
||||
urllib3==2.6.*; python_version > '3.9'
|
||||
user-agents==2.2.0
|
||||
Werkzeug==3.1.*
|
||||
WTForms==3.1.*; python_version <= '3.9'
|
||||
WTForms==3.2.*; python_version > '3.9'
|
||||
WTForms==3.2.*; python_version > '3.9'
|
||||
@@ -12,13 +12,15 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"devDependencies": {
|
||||
"electron": "40.0.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-unused-imports": "^4.2.0"
|
||||
"@eslint/js": "^10.0.1",
|
||||
"electron": "^41.5.0",
|
||||
"eslint": "^10.4.0",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"globals": "^17.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.3",
|
||||
"electron-context-menu": "^4.1.0",
|
||||
"axios": "^1.16.1",
|
||||
"electron-context-menu": "^4.1.2",
|
||||
"electron-store": "^11.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { autoUpdater, ipcMain } from 'electron';
|
||||
import { refreshMenus } from './menu.js';
|
||||
import * as misc from './misc.js';
|
||||
@@ -76,7 +85,7 @@ function handleSendDataForAppUpdate({
|
||||
data.product_name
|
||||
) {
|
||||
const ftpUrl = encodeURIComponent(
|
||||
`${data.auto_update_url}/pgadmin4-${data.upgrade_version}-${process.arch}.zip`
|
||||
`${data.auto_update_url}/pgadmin4-${data.upgrade_version}-${process.arch}.zip`,
|
||||
);
|
||||
let serverUrl = `${baseUrl}/misc/auto_update/${data.current_version_int}/${data.upgrade_version}/${data.upgrade_version_int}/${data.product_name}/${ftpUrl}/?key=${UUID}`;
|
||||
|
||||
@@ -122,7 +131,7 @@ export function setupAutoUpdater({
|
||||
baseUrl,
|
||||
UUID,
|
||||
forceQuitAndInstallUpdate,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ function updateProgress(callerWindow) {
|
||||
return;
|
||||
}
|
||||
setBadge(Object.keys(downloadQueue).length);
|
||||
let progress = 0;
|
||||
let progress;
|
||||
if(Object.values(downloadQueue).some((item) => item.total === null)) {
|
||||
// If any of the items in the queue does not have a total, we cannot calculate progress
|
||||
// so we return 2 to indicate that the progress is indeterminate.
|
||||
|
||||
@@ -135,11 +135,11 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, configStore, callbacks) {
|
||||
{ type: 'separator' },
|
||||
...(isMac
|
||||
? [
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
]
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
]
|
||||
: []),
|
||||
{ role: 'quit' },
|
||||
],
|
||||
@@ -172,7 +172,7 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, configStore, callbacks) {
|
||||
{ type: 'separator' },
|
||||
].concat(isLinux ? [] : [{ role: 'togglefullscreen' }]),
|
||||
},
|
||||
{ role: 'windowMenu' }
|
||||
{ role: 'windowMenu' },
|
||||
);
|
||||
|
||||
template.push(pgadminMenus[pgadminMenus.length - 1]);
|
||||
|
||||
@@ -100,17 +100,13 @@ const serverLogFile = path.join(getLocalAppDataPath(), 'pgadmin4.' + (new Date()
|
||||
|
||||
// This function is used to read the file and return the content
|
||||
export const readServerLog = () => {
|
||||
let data = null;
|
||||
|
||||
if (fs.existsSync(serverLogFile)) {
|
||||
data = fs.readFileSync(serverLogFile, 'utf8');
|
||||
} else {
|
||||
let errMsg = 'Unable to read file ' + serverLogFile + '.';
|
||||
console.warn(errMsg);
|
||||
return errMsg;
|
||||
return fs.readFileSync(serverLogFile, 'utf8');
|
||||
}
|
||||
|
||||
return data;
|
||||
let errMsg = 'Unable to read file ' + serverLogFile + '.';
|
||||
console.warn(errMsg);
|
||||
return errMsg;
|
||||
};
|
||||
|
||||
// This function is used to write the data into the log file
|
||||
|
||||
@@ -372,7 +372,7 @@ function launchPgAdminWindow() {
|
||||
|
||||
setupMenu(pgAdminMainScreen, configStore, menuCallbacks);
|
||||
|
||||
setupDownloader()
|
||||
setupDownloader();
|
||||
|
||||
pgAdminMainScreen.loadURL(startPageUrl);
|
||||
|
||||
@@ -452,7 +452,7 @@ function notifyUpdateInstalled() {
|
||||
}, 10000);
|
||||
} else {
|
||||
// If main screen not ready, wait and send after it's created
|
||||
app.once('browser-window-created', (event, window) => {
|
||||
app.once('browser-window-created', (_event, _window) => {
|
||||
misc.writeServerLog('[Auto-Updater]: Update installed successfully.');
|
||||
setTimeout(() => {
|
||||
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_installed: true});
|
||||
|
||||
@@ -35,80 +35,68 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint-community/regexpp@npm:^4.12.1":
|
||||
"@eslint-community/regexpp@npm:^4.12.2":
|
||||
version: 4.12.2
|
||||
resolution: "@eslint-community/regexpp@npm:4.12.2"
|
||||
checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/config-array@npm:^0.21.1":
|
||||
version: 0.21.1
|
||||
resolution: "@eslint/config-array@npm:0.21.1"
|
||||
"@eslint/config-array@npm:^0.23.5":
|
||||
version: 0.23.5
|
||||
resolution: "@eslint/config-array@npm:0.23.5"
|
||||
dependencies:
|
||||
"@eslint/object-schema": "npm:^2.1.7"
|
||||
"@eslint/object-schema": "npm:^3.0.5"
|
||||
debug: "npm:^4.3.1"
|
||||
minimatch: "npm:^3.1.2"
|
||||
checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c
|
||||
minimatch: "npm:^10.2.4"
|
||||
checksum: 10c0/b24833c4c76e78ee075d306cd3f095db46b2db0f90cc13a6ee6e4275f9889731c05bf5403ab5fefb79c756e07ac9184ed0e04570341382f9eccbccc80e6d1a0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/config-helpers@npm:^0.4.2":
|
||||
version: 0.4.2
|
||||
resolution: "@eslint/config-helpers@npm:0.4.2"
|
||||
"@eslint/config-helpers@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "@eslint/config-helpers@npm:0.6.0"
|
||||
dependencies:
|
||||
"@eslint/core": "npm:^0.17.0"
|
||||
checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4
|
||||
"@eslint/core": "npm:^1.2.1"
|
||||
checksum: 10c0/f9af20e8b60b0ba27edb74b8eb40c0c5d51a9bf9baf9e053bb57833a87cb0a1c49b4dfaad88fc24d49c907ad1324c8a0b668684fa9c321351dac4bc9155ec10a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/core@npm:^0.17.0":
|
||||
version: 0.17.0
|
||||
resolution: "@eslint/core@npm:0.17.0"
|
||||
"@eslint/core@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@eslint/core@npm:1.2.1"
|
||||
dependencies:
|
||||
"@types/json-schema": "npm:^7.0.15"
|
||||
checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e
|
||||
checksum: 10c0/10979b40588ecfef771fcb5013a542a35fb30692cc95a65f3481b0b36fbd89f5679efeb30d57f4eed35203d859aabace2a620177d6c536f71b299a1af2f3398f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/eslintrc@npm:^3.3.1":
|
||||
version: 3.3.3
|
||||
resolution: "@eslint/eslintrc@npm:3.3.3"
|
||||
"@eslint/js@npm:^10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "@eslint/js@npm:10.0.1"
|
||||
peerDependencies:
|
||||
eslint: ^10.0.0
|
||||
peerDependenciesMeta:
|
||||
eslint:
|
||||
optional: true
|
||||
checksum: 10c0/9f3fcaf71ba7fdf65d82e8faad6ecfe97e11801cc3c362b306a88ea1ed1344ae0d35330dddb0e8ad18f010f6687a70b75491b9e01c8af57acd7987cee6b3ec6c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/object-schema@npm:^3.0.5":
|
||||
version: 3.0.5
|
||||
resolution: "@eslint/object-schema@npm:3.0.5"
|
||||
checksum: 10c0/1db337431f520b99e9edda64ef5fafd7ec6a029843eeb608753025125b6649d861d843cffafafd3c4e37926d7d5f9ec0c6a8e3665c13c3da2144e8132892e92e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/plugin-kit@npm:^0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "@eslint/plugin-kit@npm:0.7.1"
|
||||
dependencies:
|
||||
ajv: "npm:^6.12.4"
|
||||
debug: "npm:^4.3.2"
|
||||
espree: "npm:^10.0.1"
|
||||
globals: "npm:^14.0.0"
|
||||
ignore: "npm:^5.2.0"
|
||||
import-fresh: "npm:^3.2.1"
|
||||
js-yaml: "npm:^4.1.1"
|
||||
minimatch: "npm:^3.1.2"
|
||||
strip-json-comments: "npm:^3.1.1"
|
||||
checksum: 10c0/532c7acc7ddd042724c28b1f020bd7bf148fcd4653bb44c8314168b5f772508c842ce4ee070299cac51c5c5757d2124bdcfcef5551c8c58ff9986e3e17f2260d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.39.2":
|
||||
version: 9.39.2
|
||||
resolution: "@eslint/js@npm:9.39.2"
|
||||
checksum: 10c0/00f51c52b04ac79faebfaa65a9652b2093b9c924e945479f1f3945473f78aee83cbc76c8d70bbffbf06f7024626575b16d97b66eab16182e1d0d39daff2f26f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/object-schema@npm:^2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "@eslint/object-schema@npm:2.1.7"
|
||||
checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/plugin-kit@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "@eslint/plugin-kit@npm:0.4.1"
|
||||
dependencies:
|
||||
"@eslint/core": "npm:^0.17.0"
|
||||
"@eslint/core": "npm:^1.2.1"
|
||||
levn: "npm:^0.4.1"
|
||||
checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b
|
||||
checksum: 10c0/335b0c1c46fd906cb50bd5ce442b9cee18dc44342ce35c718ba4a63d1aa51d2797f16a517b2f4fe371ccd777b6862fafb2dc8195e00e69197ef4cb17ab32c01b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -171,7 +159,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/estree@npm:^1.0.6":
|
||||
"@types/esrecurse@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "@types/esrecurse@npm:4.3.1"
|
||||
checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "@types/estree@npm:1.0.8"
|
||||
checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
|
||||
@@ -179,9 +174,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/http-cache-semantics@npm:*":
|
||||
version: 4.0.4
|
||||
resolution: "@types/http-cache-semantics@npm:4.0.4"
|
||||
checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6
|
||||
version: 4.2.0
|
||||
resolution: "@types/http-cache-semantics@npm:4.2.0"
|
||||
checksum: 10c0/82dd33cbe7d4843f1e884a251c6a12d385b62274353b9db167462e7fbffdbb3a83606f9952203017c5b8cabbd7b9eef0cf240a3a9dedd20f69875c9701939415
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -202,20 +197,20 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*":
|
||||
version: 25.0.10
|
||||
resolution: "@types/node@npm:25.0.10"
|
||||
version: 25.5.0
|
||||
resolution: "@types/node@npm:25.5.0"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/9edc3c812b487c32c76eebac7c87acae1f69515a0bc3f6b545806d513eb9e918c3217bf751dc93da39f60e06bf1b0caa92258ef3a6dd6457124b2e761e54f61f
|
||||
undici-types: "npm:~7.18.0"
|
||||
checksum: 10c0/70c508165b6758c4f88d4f91abca526c3985eee1985503d4c2bd994dbaf588e52ac57e571160f18f117d76e963570ac82bd20e743c18987e82564312b3b62119
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^24.9.0":
|
||||
version: 24.10.9
|
||||
resolution: "@types/node@npm:24.10.9"
|
||||
version: 24.12.0
|
||||
resolution: "@types/node@npm:24.12.0"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/e9e436fcd2136bddb1bbe3271a89f4653910bcf6ee8047c4117f544c7905a106c039e2720ee48f28505ef2560e22fb9ead719f28bf5e075fdde0c1120e38e3b2
|
||||
checksum: 10c0/8b31c0af5b5474f13048a4e77c57f22cd4f8fe6e58c4b6fde9456b0c13f46a5bfaf5744ff88fd089581de9f0d6e99c584e022681de7acb26a58d258c654c4843
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -246,12 +241,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.15.0":
|
||||
version: 8.15.0
|
||||
resolution: "acorn@npm:8.15.0"
|
||||
"acorn@npm:^8.16.0":
|
||||
version: 8.16.0
|
||||
resolution: "acorn@npm:8.16.0"
|
||||
bin:
|
||||
acorn: bin/acorn
|
||||
checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec
|
||||
checksum: 10c0/c9c52697227661b68d0debaf972222d4f622aa06b185824164e153438afa7b08273432ca43ea792cadb24dada1d46f6f6bb1ef8de9956979288cc1b96bf9914e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agent-base@npm:6":
|
||||
version: 6.0.2
|
||||
resolution: "agent-base@npm:6.0.2"
|
||||
dependencies:
|
||||
debug: "npm:4"
|
||||
checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -269,46 +273,37 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv@npm:^6.12.4":
|
||||
version: 6.12.6
|
||||
resolution: "ajv@npm:6.12.6"
|
||||
"ajv@npm:^6.14.0":
|
||||
version: 6.14.0
|
||||
resolution: "ajv@npm:6.14.0"
|
||||
dependencies:
|
||||
fast-deep-equal: "npm:^3.1.1"
|
||||
fast-json-stable-stringify: "npm:^2.0.0"
|
||||
json-schema-traverse: "npm:^0.4.1"
|
||||
uri-js: "npm:^4.2.2"
|
||||
checksum: 10c0/41e23642cbe545889245b9d2a45854ebba51cda6c778ebced9649420d9205f2efb39cb43dbc41e358409223b1ea43303ae4839db682c848b891e4811da1a5a71
|
||||
checksum: 10c0/a2bc39b0555dc9802c899f86990eb8eed6e366cddbf65be43d5aa7e4f3c4e1a199d5460fd7ca4fb3d864000dbbc049253b72faa83b3b30e641ca52cb29a68c22
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv@npm:^8.0.0, ajv@npm:^8.17.1":
|
||||
version: 8.17.1
|
||||
resolution: "ajv@npm:8.17.1"
|
||||
version: 8.18.0
|
||||
resolution: "ajv@npm:8.18.0"
|
||||
dependencies:
|
||||
fast-deep-equal: "npm:^3.1.3"
|
||||
fast-uri: "npm:^3.0.1"
|
||||
json-schema-traverse: "npm:^1.0.0"
|
||||
require-from-string: "npm:^2.0.2"
|
||||
checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35
|
||||
checksum: 10c0/e7517c426173513a07391be951879932bdf3348feaebd2199f5b901c20f99d60db8cd1591502d4d551dc82f594e82a05c4fe1c70139b15b8937f7afeaed9532f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-regex@npm:^6.0.1":
|
||||
"ansi-regex@npm:^6.2.2":
|
||||
version: 6.2.2
|
||||
resolution: "ansi-regex@npm:6.2.2"
|
||||
checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-styles@npm:^4.1.0":
|
||||
version: 4.3.0
|
||||
resolution: "ansi-styles@npm:4.3.0"
|
||||
dependencies:
|
||||
color-convert: "npm:^2.0.1"
|
||||
checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-styles@npm:^6.0.0":
|
||||
version: 6.2.3
|
||||
resolution: "ansi-styles@npm:6.2.3"
|
||||
@@ -316,13 +311,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"argparse@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "argparse@npm:2.0.1"
|
||||
checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-function@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-function@npm:1.0.0"
|
||||
@@ -345,30 +333,31 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"atomically@npm:^2.0.3":
|
||||
version: 2.1.0
|
||||
resolution: "atomically@npm:2.1.0"
|
||||
version: 2.1.1
|
||||
resolution: "atomically@npm:2.1.1"
|
||||
dependencies:
|
||||
stubborn-fs: "npm:^2.0.0"
|
||||
when-exit: "npm:^2.1.4"
|
||||
checksum: 10c0/c352ce2e247e4f9aec4e5f46b9720a3598a8241ba4ac7d067060cfd418051268976fec5c958b9d13cc1139b9f11076a8cbdb7837c2e15ca34d3dcceb9c1000b5
|
||||
checksum: 10c0/8813decdea834eab9b95c63ae3762355e9182e718b49be50153539bb52f727851f5096ef180f84901572dac31c51cb113a3bf3dda12fa633a16bc58f49ba003d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.13.3":
|
||||
version: 1.13.3
|
||||
resolution: "axios@npm:1.13.3"
|
||||
"axios@npm:^1.16.1":
|
||||
version: 1.16.1
|
||||
resolution: "axios@npm:1.16.1"
|
||||
dependencies:
|
||||
follow-redirects: "npm:^1.15.6"
|
||||
form-data: "npm:^4.0.4"
|
||||
proxy-from-env: "npm:^1.1.0"
|
||||
checksum: 10c0/86f0770624d9f14a3f8f8738c8b8f7f7fbb7b0d4ad38757db1de2d71007a0311bc597661c5ff4b4a9ee6350c6956a7282e3a281fcdf7b5b32054e35a8801e2ce
|
||||
follow-redirects: "npm:^1.16.0"
|
||||
form-data: "npm:^4.0.5"
|
||||
https-proxy-agent: "npm:^5.0.1"
|
||||
proxy-from-env: "npm:^2.1.0"
|
||||
checksum: 10c0/2f77e37e6552bbff8a772d058fb09500198e9188c6b20dc799d82dbe12a8cb506f6eed4e4e62a9ba612a35cbab496faa26d68f9bff14a53af6d15c3e136391a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"balanced-match@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "balanced-match@npm:1.0.2"
|
||||
checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee
|
||||
"balanced-match@npm:^4.0.2":
|
||||
version: 4.0.4
|
||||
resolution: "balanced-match@npm:4.0.4"
|
||||
checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -379,13 +368,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.12
|
||||
resolution: "brace-expansion@npm:1.1.12"
|
||||
"brace-expansion@npm:^5.0.5":
|
||||
version: 5.0.5
|
||||
resolution: "brace-expansion@npm:5.0.5"
|
||||
dependencies:
|
||||
balanced-match: "npm:^1.0.0"
|
||||
concat-map: "npm:0.0.1"
|
||||
checksum: 10c0/975fecac2bb7758c062c20d0b3b6288c7cc895219ee25f0a64a9de662dbac981ff0b6e89909c3897c1f84fa353113a721923afdec5f8b2350255b097f12b1f73
|
||||
balanced-match: "npm:^4.0.2"
|
||||
checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -428,23 +416,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"callsites@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "callsites@npm:3.1.0"
|
||||
checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^4.0.0":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^4.1.0"
|
||||
supports-color: "npm:^7.1.0"
|
||||
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-truncate@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "cli-truncate@npm:4.0.0"
|
||||
@@ -464,22 +435,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color-convert@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "color-convert@npm:2.0.1"
|
||||
dependencies:
|
||||
color-name: "npm:~1.1.4"
|
||||
checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color-name@npm:~1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "color-name@npm:1.1.4"
|
||||
checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"combined-stream@npm:^1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "combined-stream@npm:1.0.8"
|
||||
@@ -489,16 +444,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concat-map@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "concat-map@npm:0.0.1"
|
||||
checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"conf@npm:^15.0.2":
|
||||
version: 15.0.2
|
||||
resolution: "conf@npm:15.0.2"
|
||||
version: 15.1.0
|
||||
resolution: "conf@npm:15.1.0"
|
||||
dependencies:
|
||||
ajv: "npm:^8.17.1"
|
||||
ajv-formats: "npm:^3.0.1"
|
||||
@@ -509,7 +457,7 @@ __metadata:
|
||||
json-schema-typed: "npm:^8.0.1"
|
||||
semver: "npm:^7.7.2"
|
||||
uint8array-extras: "npm:^1.5.0"
|
||||
checksum: 10c0/77713ffe61e1ec26713fba972b6a4f589a3c45872cf07f0e6499a3e54fe855d2c88bf80e148346b28abef0174e4bffcb98cc0a58dd42d451c936a045bd319113
|
||||
checksum: 10c0/ba3c74891f48232a7d32e473508c147a431a7bfdd266ebdeaf4e737227568345ea632a42d02e35f13740cb9a54076d79572d31591d6debae3b6bee089fa91c8a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -533,7 +481,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2":
|
||||
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2":
|
||||
version: 4.4.3
|
||||
resolution: "debug@npm:4.4.3"
|
||||
dependencies:
|
||||
@@ -624,14 +572,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-context-menu@npm:^4.1.0":
|
||||
version: 4.1.1
|
||||
resolution: "electron-context-menu@npm:4.1.1"
|
||||
"electron-context-menu@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "electron-context-menu@npm:4.1.2"
|
||||
dependencies:
|
||||
cli-truncate: "npm:^4.0.0"
|
||||
electron-dl: "npm:^4.0.0"
|
||||
electron-is-dev: "npm:^3.0.1"
|
||||
checksum: 10c0/3e2eb5298a311f320734427fadb1cf22ef6685c8d86387d1a2d558b698b079de8eabb4d7a8790cab100cf5eba3607df887846067f5a805d1c011016b9eccdf70
|
||||
checksum: 10c0/30fc6f1370dd24b1f089911f40895d13fbfc683f534e79a2c17ded832b76a6554180bfa2f165528c5ffea19dcbda6c4ff77ccd63c5ddef382cbaa83fd47f153f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -663,16 +611,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron@npm:40.0.0":
|
||||
version: 40.0.0
|
||||
resolution: "electron@npm:40.0.0"
|
||||
"electron@npm:^41.5.0":
|
||||
version: 41.5.0
|
||||
resolution: "electron@npm:41.5.0"
|
||||
dependencies:
|
||||
"@electron/get": "npm:^2.0.0"
|
||||
"@types/node": "npm:^24.9.0"
|
||||
extract-zip: "npm:^2.0.1"
|
||||
bin:
|
||||
electron: cli.js
|
||||
checksum: 10c0/acd42835a64f007817ba43969d7bdd1281f109f26fbf4c6639208a8262b80ee1b89dfb844a30de694baa152ff4afc1be1d1e09b0a69629a57be03f9bbf3f9416
|
||||
checksum: 10c0/5d311e7d411cc5372a6f1eed644776353a781c63122bfc8e70ef00bf5aaf78293e4bca8ccbbacbd7217c7ce72b35e25e4f9f97ced974d97e24e9912e570ee7c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -769,26 +717,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-unused-imports@npm:^4.2.0":
|
||||
version: 4.3.0
|
||||
resolution: "eslint-plugin-unused-imports@npm:4.3.0"
|
||||
"eslint-plugin-unused-imports@npm:^4.4.1":
|
||||
version: 4.4.1
|
||||
resolution: "eslint-plugin-unused-imports@npm:4.4.1"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/eslint-plugin": ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
|
||||
eslint: ^9.0.0 || ^8.0.0
|
||||
eslint: ^10.0.0 || ^9.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
"@typescript-eslint/eslint-plugin":
|
||||
optional: true
|
||||
checksum: 10c0/aa4d8ebd8bea5fc0ac8d3fdeb0d0ed2dd0fccb96a1f5569bca772f71b4c45edd1b3c32d9754fb175c45ccf4d123725f333bf99380b9896bb678a8d599e290aa3
|
||||
checksum: 10c0/bef630eedc3c239ca1c0a11c6af60485310e3934bd0819d3eb51e0acabdafc722c97d35457750a957541f5cc6a99aa78abb359eb3837d3702d836b6d24cbd573
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-scope@npm:^8.4.0":
|
||||
version: 8.4.0
|
||||
resolution: "eslint-scope@npm:8.4.0"
|
||||
"eslint-scope@npm:^9.1.2":
|
||||
version: 9.1.2
|
||||
resolution: "eslint-scope@npm:9.1.2"
|
||||
dependencies:
|
||||
"@types/esrecurse": "npm:^4.3.1"
|
||||
"@types/estree": "npm:^1.0.8"
|
||||
esrecurse: "npm:^4.3.0"
|
||||
estraverse: "npm:^5.2.0"
|
||||
checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0
|
||||
checksum: 10c0/9fb8bca5a73e5741efb6cec84467027b6cb6f4203ff9b43a938e272c5cd30800bde46a5c20dfd1609f840225f0b62b7673be391b20acadf8658ca9fa4729b3dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -799,38 +749,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-visitor-keys@npm:^4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "eslint-visitor-keys@npm:4.2.1"
|
||||
checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43
|
||||
"eslint-visitor-keys@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "eslint-visitor-keys@npm:5.0.1"
|
||||
checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:^9.39.2":
|
||||
version: 9.39.2
|
||||
resolution: "eslint@npm:9.39.2"
|
||||
"eslint@npm:^10.4.0":
|
||||
version: 10.4.0
|
||||
resolution: "eslint@npm:10.4.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.1"
|
||||
"@eslint/config-array": "npm:^0.21.1"
|
||||
"@eslint/config-helpers": "npm:^0.4.2"
|
||||
"@eslint/core": "npm:^0.17.0"
|
||||
"@eslint/eslintrc": "npm:^3.3.1"
|
||||
"@eslint/js": "npm:9.39.2"
|
||||
"@eslint/plugin-kit": "npm:^0.4.1"
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@eslint/config-array": "npm:^0.23.5"
|
||||
"@eslint/config-helpers": "npm:^0.6.0"
|
||||
"@eslint/core": "npm:^1.2.1"
|
||||
"@eslint/plugin-kit": "npm:^0.7.1"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
"@humanwhocodes/retry": "npm:^0.4.2"
|
||||
"@types/estree": "npm:^1.0.6"
|
||||
ajv: "npm:^6.12.4"
|
||||
chalk: "npm:^4.0.0"
|
||||
ajv: "npm:^6.14.0"
|
||||
cross-spawn: "npm:^7.0.6"
|
||||
debug: "npm:^4.3.2"
|
||||
escape-string-regexp: "npm:^4.0.0"
|
||||
eslint-scope: "npm:^8.4.0"
|
||||
eslint-visitor-keys: "npm:^4.2.1"
|
||||
espree: "npm:^10.4.0"
|
||||
esquery: "npm:^1.5.0"
|
||||
eslint-scope: "npm:^9.1.2"
|
||||
eslint-visitor-keys: "npm:^5.0.1"
|
||||
espree: "npm:^11.2.0"
|
||||
esquery: "npm:^1.7.0"
|
||||
esutils: "npm:^2.0.2"
|
||||
fast-deep-equal: "npm:^3.1.3"
|
||||
file-entry-cache: "npm:^8.0.0"
|
||||
@@ -840,8 +787,7 @@ __metadata:
|
||||
imurmurhash: "npm:^0.1.4"
|
||||
is-glob: "npm:^4.0.0"
|
||||
json-stable-stringify-without-jsonify: "npm:^1.0.1"
|
||||
lodash.merge: "npm:^4.6.2"
|
||||
minimatch: "npm:^3.1.2"
|
||||
minimatch: "npm:^10.2.4"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
optionator: "npm:^0.9.3"
|
||||
peerDependencies:
|
||||
@@ -851,22 +797,22 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10c0/bb88ca8fd16bb7e1ac3e13804c54d41c583214460c0faa7b3e7c574e69c5600c7122295500fb4b0c06067831111db740931e98da1340329527658e1cf80073d3
|
||||
checksum: 10c0/6bf644dc08fa5a6b23157d23a4a4638d45823d03a67da1daac8dc1085b03934fa98013efd2eac2cd6ec90fe88d36b336bdf38d5f000325f22d823a15f2031426
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"espree@npm:^10.0.1, espree@npm:^10.4.0":
|
||||
version: 10.4.0
|
||||
resolution: "espree@npm:10.4.0"
|
||||
"espree@npm:^11.2.0":
|
||||
version: 11.2.0
|
||||
resolution: "espree@npm:11.2.0"
|
||||
dependencies:
|
||||
acorn: "npm:^8.15.0"
|
||||
acorn: "npm:^8.16.0"
|
||||
acorn-jsx: "npm:^5.3.2"
|
||||
eslint-visitor-keys: "npm:^4.2.1"
|
||||
checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b
|
||||
eslint-visitor-keys: "npm:^5.0.1"
|
||||
checksum: 10c0/cf87e18ffd9dc113eb8d16588e7757701bc10c9934a71cce8b89c2611d51672681a918307bd6b19ac3ccd0e7ba1cbccc2f815b36b52fa7e73097b251014c3d81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esquery@npm:^1.5.0":
|
||||
"esquery@npm:^1.7.0":
|
||||
version: 1.7.0
|
||||
resolution: "esquery@npm:1.7.0"
|
||||
dependencies:
|
||||
@@ -1001,23 +947,23 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"flatted@npm:^3.2.9":
|
||||
version: 3.3.3
|
||||
resolution: "flatted@npm:3.3.3"
|
||||
checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538
|
||||
version: 3.4.1
|
||||
resolution: "flatted@npm:3.4.1"
|
||||
checksum: 10c0/3987a7f1e39bc7215cece001354313b462cdb4fb2dde0df4f7acd9e5016fbae56ee6fb3f0870b2150145033be8bda4f01af6f87a00946049651131bbfca7dfa6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"follow-redirects@npm:^1.15.6":
|
||||
version: 1.15.11
|
||||
resolution: "follow-redirects@npm:1.15.11"
|
||||
"follow-redirects@npm:^1.16.0":
|
||||
version: 1.16.0
|
||||
resolution: "follow-redirects@npm:1.16.0"
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343
|
||||
checksum: 10c0/a1e2900163e6f1b4d1ed5c221b607f41decbab65534c63fe7e287e40a5d552a6496e7d9d7d976fa4ba77b4c51c11e5e9f683f10b43011ea11e442ff128d0e181
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data@npm:^4.0.4":
|
||||
"form-data@npm:^4.0.5":
|
||||
version: 4.0.5
|
||||
resolution: "form-data@npm:4.0.5"
|
||||
dependencies:
|
||||
@@ -1056,9 +1002,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"get-east-asian-width@npm:^1.0.0":
|
||||
version: 1.4.0
|
||||
resolution: "get-east-asian-width@npm:1.4.0"
|
||||
checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83
|
||||
version: 1.5.0
|
||||
resolution: "get-east-asian-width@npm:1.5.0"
|
||||
checksum: 10c0/bff8bbc8d81790b9477f7aa55b1806b9f082a8dc1359fff7bd8b96939622c86b729685afc2bfeb22def1fc6ef1e5228e4d87dd4e6da60bc43a5edfb03c4ee167
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1125,10 +1071,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globals@npm:^14.0.0":
|
||||
version: 14.0.0
|
||||
resolution: "globals@npm:14.0.0"
|
||||
checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d
|
||||
"globals@npm:^17.6.0":
|
||||
version: 17.6.0
|
||||
resolution: "globals@npm:17.6.0"
|
||||
checksum: 10c0/cf94fb4329cc5c68cf81018fd68324f413181ee169f0235b0b33b82bc93fe7825a21beea951f83a80e8e4bbdad9c0c80515a145b5fd4b5cb52f2a80db899a93f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1175,13 +1121,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-flag@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "has-flag@npm:4.0.0"
|
||||
checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-property-descriptors@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "has-property-descriptors@npm:1.0.2"
|
||||
@@ -1233,6 +1172,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "https-proxy-agent@npm:5.0.1"
|
||||
dependencies:
|
||||
agent-base: "npm:6"
|
||||
debug: "npm:4"
|
||||
checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ignore@npm:^5.2.0":
|
||||
version: 5.3.2
|
||||
resolution: "ignore@npm:5.3.2"
|
||||
@@ -1240,16 +1189,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"import-fresh@npm:^3.2.1":
|
||||
version: 3.3.1
|
||||
resolution: "import-fresh@npm:3.3.1"
|
||||
dependencies:
|
||||
parent-module: "npm:^1.0.0"
|
||||
resolve-from: "npm:^4.0.0"
|
||||
checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"imurmurhash@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "imurmurhash@npm:0.1.4"
|
||||
@@ -1294,17 +1233,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "js-yaml@npm:4.1.1"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.js
|
||||
checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-buffer@npm:3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "json-buffer@npm:3.0.1"
|
||||
@@ -1387,13 +1315,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.merge@npm:^4.6.2":
|
||||
version: 4.6.2
|
||||
resolution: "lodash.merge@npm:4.6.2"
|
||||
checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lowercase-keys@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "lowercase-keys@npm:2.0.0"
|
||||
@@ -1461,12 +1382,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "minimatch@npm:3.1.2"
|
||||
"minimatch@npm:^10.2.4":
|
||||
version: 10.2.5
|
||||
resolution: "minimatch@npm:10.2.5"
|
||||
dependencies:
|
||||
brace-expansion: "npm:^1.1.7"
|
||||
checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311
|
||||
brace-expansion: "npm:^5.0.5"
|
||||
checksum: 10c0/6bb058bd6324104b9ec2f763476a35386d05079c1f5fe4fbf1f324a25237cd4534d6813ecd71f48208f4e635c1221899bef94c3c89f7df55698fe373aaae20fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1546,15 +1467,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parent-module@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "parent-module@npm:1.0.1"
|
||||
dependencies:
|
||||
callsites: "npm:^3.0.0"
|
||||
checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-exists@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "path-exists@npm:4.0.0"
|
||||
@@ -1587,12 +1499,14 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "pgadmin4@workspace:."
|
||||
dependencies:
|
||||
axios: "npm:^1.13.3"
|
||||
electron: "npm:40.0.0"
|
||||
electron-context-menu: "npm:^4.1.0"
|
||||
"@eslint/js": "npm:^10.0.1"
|
||||
axios: "npm:^1.16.1"
|
||||
electron: "npm:^41.5.0"
|
||||
electron-context-menu: "npm:^4.1.2"
|
||||
electron-store: "npm:^11.0.2"
|
||||
eslint: "npm:^9.39.2"
|
||||
eslint-plugin-unused-imports: "npm:^4.2.0"
|
||||
eslint: "npm:^10.4.0"
|
||||
eslint-plugin-unused-imports: "npm:^4.4.1"
|
||||
globals: "npm:^17.6.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -1610,20 +1524,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proxy-from-env@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "proxy-from-env@npm:1.1.0"
|
||||
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
|
||||
"proxy-from-env@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "proxy-from-env@npm:2.1.0"
|
||||
checksum: 10c0/ed01729fd4d094eab619cd7e17ce3698b3413b31eb102c4904f9875e677cd207392795d5b4adee9cec359dfd31c44d5ad7595a3a3ad51c40250e141512281c58
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pump@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "pump@npm:3.0.3"
|
||||
version: 3.0.4
|
||||
resolution: "pump@npm:3.0.4"
|
||||
dependencies:
|
||||
end-of-stream: "npm:^1.1.0"
|
||||
once: "npm:^1.3.1"
|
||||
checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9
|
||||
checksum: 10c0/2780e66b5471c19e3e3e1063b84f3f6a3a08367f24c5ed552f98cd5901e6ada27c7ad6495d4244f553fd03b01884a4561933064f053f47c8994d84fd352768ea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1664,13 +1578,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"resolve-from@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "resolve-from@npm:4.0.0"
|
||||
checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"responselike@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "responselike@npm:2.0.1"
|
||||
@@ -1711,11 +1618,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.3.2, semver@npm:^7.7.2":
|
||||
version: 7.7.3
|
||||
resolution: "semver@npm:7.7.3"
|
||||
version: 7.7.4
|
||||
resolution: "semver@npm:7.7.4"
|
||||
bin:
|
||||
semver: bin/semver.js
|
||||
checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e
|
||||
checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1791,18 +1698,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"strip-ansi@npm:^7.1.0":
|
||||
version: 7.1.2
|
||||
resolution: "strip-ansi@npm:7.1.2"
|
||||
version: 7.2.0
|
||||
resolution: "strip-ansi@npm:7.2.0"
|
||||
dependencies:
|
||||
ansi-regex: "npm:^6.0.1"
|
||||
checksum: 10c0/0d6d7a023de33368fd042aab0bf48f4f4077abdfd60e5393e73c7c411e85e1b3a83507c11af2e656188511475776215df9ca589b4da2295c9455cc399ce1858b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-json-comments@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "strip-json-comments@npm:3.1.1"
|
||||
checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd
|
||||
ansi-regex: "npm:^6.2.2"
|
||||
checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1831,15 +1731,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:^7.1.0":
|
||||
version: 7.2.0
|
||||
resolution: "supports-color@npm:7.2.0"
|
||||
dependencies:
|
||||
has-flag: "npm:^4.0.0"
|
||||
checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tagged-tag@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "tagged-tag@npm:1.0.0"
|
||||
@@ -1864,11 +1755,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^5.0.0, type-fest@npm:^5.0.1":
|
||||
version: 5.4.1
|
||||
resolution: "type-fest@npm:5.4.1"
|
||||
version: 5.4.4
|
||||
resolution: "type-fest@npm:5.4.4"
|
||||
dependencies:
|
||||
tagged-tag: "npm:^1.0.0"
|
||||
checksum: 10c0/500386d690e634499e6fb765a1e33909a75fea0258acdd56a2d1c9565d6810e222f6d95bd80daa5498377c7ea976af46f518185b5dbd67ff8454562544808aa1
|
||||
checksum: 10c0/bf9c6d7df5383fd720aac71da8ce8690ff1c554459d19cf3c72d61eac98255dba57abe20c628f91f4116f66211791462fdafa90b2be2d7405a5a4c295e4d849d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1886,6 +1777,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~7.18.0":
|
||||
version: 7.18.2
|
||||
resolution: "undici-types@npm:7.18.2"
|
||||
checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^0.1.0":
|
||||
version: 0.1.2
|
||||
resolution: "universalify@npm:0.1.2"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
requests>=2.21.0
|
||||
requests[security]>=2.21.0
|
||||
safety>=1.9.0
|
||||
requests>=2.32.5,<2.33; python_version <= '3.9'
|
||||
requests>=2.34.2; python_version > '3.9'
|
||||
requests[security]>=2.32.5,<2.33; python_version <= '3.9'
|
||||
requests[security]>=2.34.2; python_version > '3.9'
|
||||
safety>=3.7.0
|
||||
Sphinx==7.4.7
|
||||
sphinxcontrib-youtube==1.4.1
|
||||
sphinxcontrib-youtube==1.5.0
|
||||
@@ -42,7 +42,7 @@ module.exports = [
|
||||
'babelOptions': {
|
||||
'plugins': [
|
||||
'@babel/plugin-syntax-jsx',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-transform-class-properties',
|
||||
],
|
||||
},
|
||||
...reactjs.configs.recommended.parserOptions,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
approvedGitRepositories:
|
||||
- "**"
|
||||
|
||||
checksumBehavior: update
|
||||
|
||||
enableScripts: true
|
||||
|
||||
logFilters:
|
||||
- code: YN0013
|
||||
level: discard
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"presets": [["@babel/preset-env", {"modules": "commonjs", "useBuiltIns": "usage", "corejs": 3}], ["@babel/preset-react", {
|
||||
"runtime": "automatic"
|
||||
}], "@babel/preset-typescript"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/proposal-object-rest-spread"]
|
||||
"plugins": ["@babel/plugin-transform-class-properties", "@babel/plugin-transform-object-rest-spread"]
|
||||
}
|
||||
@@ -376,8 +376,16 @@ SESSION_DB_PATH = os.path.join(DATA_DIR, 'sessions')
|
||||
|
||||
SESSION_COOKIE_NAME = 'pga4_session'
|
||||
|
||||
# Session digest method
|
||||
SESSION_DIGEST_METHOD = 'hashlib.sha1'
|
||||
# Session digest method.
|
||||
# Used as the HMAC algorithm for the session cookie (signing the
|
||||
# (sid, randval) pair). HMAC-SHA1 is still cryptographically acceptable
|
||||
# for authentication, but SHA-256 is the modern default and aligns with
|
||||
# the file-HMAC header introduced for session-on-disk integrity.
|
||||
# Operators on existing deployments will see all sessions invalidated on
|
||||
# upgrade regardless (the file-HMAC header is new), so we flip this
|
||||
# default at the same time rather than leaving SHA-1 as a long-term
|
||||
# liability.
|
||||
SESSION_DIGEST_METHOD = 'hashlib.sha256'
|
||||
|
||||
##########################################################################
|
||||
# Mail server settings
|
||||
@@ -970,6 +978,99 @@ MAX_SERVER_TAGS_ALLOWED = 5
|
||||
|
||||
ON_DEMAND_LOG_COUNT = 10000
|
||||
|
||||
##########################################################################
|
||||
# AI/LLM Settings
|
||||
##########################################################################
|
||||
|
||||
# Master switch to enable/disable LLM features entirely.
|
||||
# When False, all AI/LLM features are disabled and cannot be enabled
|
||||
# by users through preferences. When True, users can configure their
|
||||
# preferred LLM provider in preferences.
|
||||
LLM_ENABLED = True
|
||||
|
||||
# Default LLM Provider
|
||||
# Specifies which LLM provider to use by default when LLM_ENABLED is True.
|
||||
# Users can override this in their preferences.
|
||||
# Valid values: 'anthropic', 'openai', 'ollama', 'docker', or '' (disabled)
|
||||
DEFAULT_LLM_PROVIDER = ''
|
||||
|
||||
# Anthropic Configuration
|
||||
# URL for the Anthropic API endpoint. Leave empty to use the default
|
||||
# (https://api.anthropic.com/v1). Set a custom URL to use an
|
||||
# Anthropic-compatible API provider.
|
||||
ANTHROPIC_API_URL = ''
|
||||
|
||||
# Path to a file containing the Anthropic API key. The file should contain
|
||||
# only the API key with no additional whitespace or formatting.
|
||||
# Default: ~/.anthropic-api-key
|
||||
# Note: The API key may be optional when using a custom API URL with a
|
||||
# provider that does not require authentication.
|
||||
ANTHROPIC_API_KEY_FILE = '~/.anthropic-api-key'
|
||||
|
||||
# The Anthropic model to use for AI features.
|
||||
# Examples: claude-sonnet-4-20250514, claude-3-5-haiku-20241022
|
||||
ANTHROPIC_API_MODEL = ''
|
||||
|
||||
# OpenAI Configuration
|
||||
# URL for the OpenAI API endpoint. Leave empty to use the default
|
||||
# (https://api.openai.com/v1). Set a custom URL to use any
|
||||
# OpenAI-compatible API provider (e.g., LiteLLM, LM Studio, EXO).
|
||||
# Include the /v1 path prefix if required by your provider
|
||||
# (e.g., http://localhost:1234/v1).
|
||||
OPENAI_API_URL = ''
|
||||
|
||||
# Path to a file containing the OpenAI API key. The file should contain
|
||||
# only the API key with no additional whitespace or formatting.
|
||||
# Default: ~/.openai-api-key
|
||||
# Note: The API key may be optional when using a custom API URL with a
|
||||
# provider that does not require authentication.
|
||||
OPENAI_API_KEY_FILE = '~/.openai-api-key'
|
||||
|
||||
# The OpenAI model to use for AI features.
|
||||
# Examples: gpt-4o, gpt-4o-mini, gpt-4-turbo
|
||||
OPENAI_API_MODEL = ''
|
||||
|
||||
# Ollama Configuration
|
||||
# URL for the Ollama API endpoint. Leave empty to disable Ollama.
|
||||
# Typical value: http://localhost:11434
|
||||
OLLAMA_API_URL = ''
|
||||
|
||||
# The Ollama model to use for AI features.
|
||||
# Examples: llama3.2, codellama, mistral
|
||||
OLLAMA_API_MODEL = ''
|
||||
|
||||
# Docker Model Runner Configuration
|
||||
# Docker Desktop 4.40+ includes a built-in model runner with an
|
||||
# OpenAI-compatible API. No API key is required.
|
||||
# URL for the Docker Model Runner API endpoint. Leave empty to disable.
|
||||
# Typical value: http://localhost:12434
|
||||
# Tip: You can also use the OpenAI provider with a custom API URL for any
|
||||
# OpenAI-compatible endpoint, including Docker Model Runner.
|
||||
DOCKER_API_URL = ''
|
||||
|
||||
# The Docker Model Runner model to use for AI features.
|
||||
# Examples: ai/qwen3-coder, ai/llama3.2
|
||||
DOCKER_API_MODEL = ''
|
||||
|
||||
# Allowed LLM API URLs
|
||||
# A list of scheme://host:port entries that LLM API requests are allowed
|
||||
# to connect to. Only URLs matching an entry in this list will be permitted.
|
||||
# This prevents SSRF attacks via user-controlled API URL fields.
|
||||
# Set to an empty list to disable URL restriction (not recommended).
|
||||
# Add entries for custom providers (LiteLLM, LM Studio, corporate proxies).
|
||||
ALLOWED_LLM_API_URLS = [
|
||||
'https://api.anthropic.com:443',
|
||||
'https://api.openai.com:443',
|
||||
'http://localhost:11434', # Ollama default
|
||||
'http://localhost:12434', # Docker Model Runner default
|
||||
]
|
||||
|
||||
# Maximum Tool Iterations
|
||||
# The maximum number of tool call iterations allowed during an AI conversation.
|
||||
# This prevents runaway conversations that could consume excessive resources.
|
||||
# Users can override this in their preferences.
|
||||
MAX_LLM_TOOL_ITERATIONS = 20
|
||||
|
||||
#############################################################################
|
||||
# Patch the default config with custom config and other manipulations
|
||||
#############################################################################
|
||||
|
||||
@@ -52,7 +52,7 @@ module.exports = {
|
||||
],
|
||||
'testEnvironment': 'jsdom',
|
||||
'transformIgnorePatterns': [
|
||||
'[/\\\\]node_modules[/\\\\](?!react-dnd|dnd-core|@react-dnd|react-resize-detector|react-data-grid).+\\.(js|jsx|mjs|cjs|ts|tsx)$',
|
||||
'[/\\\\]node_modules[/\\\\](?!react-dnd|dnd-core|@react-dnd|react-resize-detector|react-data-grid|marked).+\\.(js|jsx|mjs|cjs|ts|tsx)$',
|
||||
'^.+\\.module\\.(css|sass|scss)$'
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Add tools_ai permission to existing roles
|
||||
|
||||
Revision ID: add_tools_ai_perm
|
||||
Revises: 018e16dad6aa
|
||||
Create Date: 2025-12-01
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_tools_ai_perm'
|
||||
down_revision = '018e16dad6aa'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Get metadata from current connection
|
||||
meta = sa.MetaData()
|
||||
meta.reflect(op.get_bind(), only=('role',))
|
||||
role_table = sa.Table('role', meta)
|
||||
|
||||
# Get all roles with permissions
|
||||
conn = op.get_bind()
|
||||
result = conn.execute(
|
||||
sa.select(role_table.c.id, role_table.c.permissions)
|
||||
.where(role_table.c.permissions.isnot(None))
|
||||
)
|
||||
|
||||
# Add tools_ai permission to each role that has permissions
|
||||
for row in result:
|
||||
role_id = row[0]
|
||||
permissions = row[1]
|
||||
if permissions:
|
||||
perms_list = permissions.split(',')
|
||||
if 'tools_ai' not in perms_list:
|
||||
perms_list.append('tools_ai')
|
||||
new_permissions = ','.join(perms_list)
|
||||
conn.execute(
|
||||
role_table.update()
|
||||
.where(role_table.c.id == role_id)
|
||||
.values(permissions=new_permissions)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
||||
@@ -0,0 +1,149 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Add user_id to debugger_function_arguments and indexes for data isolation
|
||||
|
||||
Revision ID: add_user_id_dbg_args
|
||||
Revises: add_tools_ai_perm
|
||||
Create Date: 2026-04-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_user_id_dbg_args'
|
||||
down_revision = 'add_tools_ai_perm'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
dialect = conn.dialect.name
|
||||
|
||||
# --- DebuggerFunctionArguments: add user_id to composite PK ---
|
||||
if dialect == 'sqlite':
|
||||
# SQLite cannot ALTER composite PKs. Recreate the table.
|
||||
# Existing debugger argument data is ephemeral (cached function
|
||||
# args) so dropping is acceptable.
|
||||
op.execute(
|
||||
'DROP TABLE IF EXISTS debugger_function_arguments'
|
||||
)
|
||||
op.create_table(
|
||||
'debugger_function_arguments',
|
||||
sa.Column('user_id', sa.Integer(),
|
||||
sa.ForeignKey('user.id'), nullable=False),
|
||||
sa.Column('server_id', sa.Integer(), nullable=False),
|
||||
sa.Column('database_id', sa.Integer(), nullable=False),
|
||||
sa.Column('schema_id', sa.Integer(), nullable=False),
|
||||
sa.Column('function_id', sa.Integer(), nullable=False),
|
||||
sa.Column('arg_id', sa.Integer(), nullable=False),
|
||||
sa.Column('is_null', sa.Integer(), nullable=False),
|
||||
sa.Column('is_expression', sa.Integer(), nullable=False),
|
||||
sa.Column('use_default', sa.Integer(), nullable=False),
|
||||
sa.Column('value', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint(
|
||||
'user_id', 'server_id', 'database_id',
|
||||
'schema_id', 'function_id', 'arg_id'
|
||||
),
|
||||
sa.CheckConstraint('is_null >= 0 AND is_null <= 1'),
|
||||
sa.CheckConstraint(
|
||||
'is_expression >= 0 AND is_expression <= 1'),
|
||||
sa.CheckConstraint(
|
||||
'use_default >= 0 AND use_default <= 1'),
|
||||
)
|
||||
else:
|
||||
# PostgreSQL: add column, backfill from server owner, recreate
|
||||
# PK using batch_alter_table for portability.
|
||||
op.add_column(
|
||||
'debugger_function_arguments',
|
||||
sa.Column('user_id', sa.Integer(),
|
||||
sa.ForeignKey('user.id'), nullable=True)
|
||||
)
|
||||
# Backfill: assign user_id from the server's owner
|
||||
op.execute(
|
||||
'UPDATE debugger_function_arguments '
|
||||
'SET user_id = s.user_id '
|
||||
'FROM server s '
|
||||
'WHERE debugger_function_arguments.server_id = s.id'
|
||||
)
|
||||
# Delete orphans (rows with no matching server)
|
||||
op.execute(
|
||||
'DELETE FROM debugger_function_arguments '
|
||||
'WHERE user_id IS NULL'
|
||||
)
|
||||
op.alter_column(
|
||||
'debugger_function_arguments', 'user_id', nullable=False
|
||||
)
|
||||
# Recreate PK with user_id using batch_alter_table
|
||||
with op.batch_alter_table(
|
||||
'debugger_function_arguments'
|
||||
) as batch:
|
||||
batch.drop_constraint(
|
||||
'debugger_function_arguments_pkey', type_='primary'
|
||||
)
|
||||
batch.create_primary_key(
|
||||
'debugger_function_arguments_pkey',
|
||||
['user_id', 'server_id', 'database_id',
|
||||
'schema_id', 'function_id', 'arg_id']
|
||||
)
|
||||
|
||||
# --- Indexes for data isolation query performance ---
|
||||
# Only create indexes on tables that exist (sharedserver may be
|
||||
# absent in older schemas that haven't run all prior migrations).
|
||||
inspector = sa.inspect(conn)
|
||||
index_stmts = [
|
||||
('server',
|
||||
'CREATE INDEX IF NOT EXISTS ix_server_user_id '
|
||||
'ON server (user_id)'),
|
||||
('server',
|
||||
'CREATE INDEX IF NOT EXISTS ix_server_servergroup_id '
|
||||
'ON server (servergroup_id)'),
|
||||
('sharedserver',
|
||||
'CREATE INDEX IF NOT EXISTS ix_sharedserver_user_id '
|
||||
'ON sharedserver (user_id)'),
|
||||
('sharedserver',
|
||||
'CREATE INDEX IF NOT EXISTS ix_sharedserver_osid '
|
||||
'ON sharedserver (osid)'),
|
||||
('servergroup',
|
||||
'CREATE INDEX IF NOT EXISTS ix_servergroup_user_id '
|
||||
'ON servergroup (user_id)'),
|
||||
]
|
||||
for table_name, stmt in index_stmts:
|
||||
if inspector.has_table(table_name):
|
||||
op.execute(stmt)
|
||||
|
||||
# --- Unique constraint on SharedServer(osid, user_id) ---
|
||||
# Prevents duplicate SharedServer records from TOCTOU race.
|
||||
# First remove duplicates (keep lowest id per osid+user_id).
|
||||
if inspector.has_table('sharedserver'):
|
||||
if dialect == 'sqlite':
|
||||
op.execute(
|
||||
'DELETE FROM sharedserver WHERE id NOT IN '
|
||||
'(SELECT MIN(id) FROM sharedserver '
|
||||
'GROUP BY osid, user_id)'
|
||||
)
|
||||
else:
|
||||
op.execute(
|
||||
'DELETE FROM sharedserver s1 USING '
|
||||
'sharedserver s2 WHERE s1.osid = s2.osid '
|
||||
'AND s1.user_id = s2.user_id '
|
||||
'AND s1.id > s2.id'
|
||||
)
|
||||
with op.batch_alter_table('sharedserver') as batch:
|
||||
batch.create_unique_constraint(
|
||||
'uq_sharedserver_osid_user',
|
||||
['osid', 'user_id']
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
||||
@@ -15,8 +15,6 @@ Create Date: 2018-08-29 15:33:57.855491
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy.orm.session import Session
|
||||
from pgadmin.model import DebuggerFunctionArguments
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ca00ec32581b'
|
||||
@@ -26,11 +24,10 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
session = Session(bind=op.get_bind())
|
||||
|
||||
debugger_records = session.query(DebuggerFunctionArguments).all()
|
||||
if debugger_records:
|
||||
session.delete(debugger_records)
|
||||
# Use raw SQL instead of importing the model class, because
|
||||
# model changes in later migrations (e.g. adding user_id) would
|
||||
# cause this migration to fail on fresh databases.
|
||||
op.execute('DELETE FROM debugger_function_arguments')
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""Normalize User.locked column data from TEXT to integer 0/1 (SQLite only)
|
||||
|
||||
Migration 6650c52670c2 added the locked column with
|
||||
``server_default='false'``. SQLite has no native BOOLEAN affinity, so the
|
||||
literal 'false' was stored as TEXT both in the column DEFAULT and on
|
||||
existing rows backfilled by the ALTER TABLE. SQLAlchemy's Boolean
|
||||
processor then reads that TEXT back as Python ``True`` (because
|
||||
``bool('false')`` is ``True`` for any non-empty string), which means any
|
||||
code that branches on ``User.locked`` sees a locked-out account where
|
||||
the data should be unlocked.
|
||||
|
||||
This was harmless for years because the only writer (the lockout block in
|
||||
``/authenticate/login``) writes Python ``True``/``False`` (stored as
|
||||
integer 1/0), and no reader branched on the field. The ``User.is_active``
|
||||
and ``User.is_locked`` overrides added for the lockout-bypass fix are
|
||||
the first readers, so they trip over every legacy row.
|
||||
|
||||
The data corruption is SQLite-specific. PostgreSQL accepts the string
|
||||
``'false'`` for a BOOLEAN column and coerces it on insert, so existing
|
||||
rows on a PostgreSQL config DB already hold proper boolean values and
|
||||
need no fix. Restrict the data normalization to SQLite — the integer
|
||||
literals below would also fail on PostgreSQL because the locked column
|
||||
is BOOLEAN, not INTEGER.
|
||||
|
||||
Revision ID: normalize_locked_text_default
|
||||
Revises: sharedserver_feature_parity
|
||||
Create Date: 2026-05-05
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'normalize_locked_text_default'
|
||||
down_revision = 'sharedserver_feature_parity'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != 'sqlite':
|
||||
# On PostgreSQL the locked column was always stored as a proper
|
||||
# BOOLEAN, so there is nothing to normalize. Skip — UPDATEs with
|
||||
# integer literals against a BOOLEAN column would otherwise raise
|
||||
# "column locked is of type boolean but expression is of type
|
||||
# integer" on PostgreSQL.
|
||||
return
|
||||
|
||||
op.execute(
|
||||
"UPDATE \"user\" SET locked = 1 "
|
||||
"WHERE LOWER(CAST(locked AS TEXT)) IN ('true', 't', 'yes', 'y', '1')"
|
||||
)
|
||||
op.execute(
|
||||
"UPDATE \"user\" SET locked = 0 "
|
||||
"WHERE locked IS NULL OR LOWER(CAST(locked AS TEXT)) NOT IN "
|
||||
"('true', 't', 'yes', 'y', '1')"
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
||||
@@ -0,0 +1,87 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
"""SharedServer feature parity columns, orphan cleanup, and db_res removal
|
||||
|
||||
Adds passexec_cmd, passexec_expiration, kerberos_conn, tags, and
|
||||
post_connection_sql to the sharedserver table so non-owners get their
|
||||
own per-user values instead of inheriting the owner's. Drops db_res
|
||||
which was never overlaid or writable by non-owners. Also cleans up
|
||||
orphaned records whose parent server was deleted.
|
||||
|
||||
Revision ID: sharedserver_feature_parity
|
||||
Revises: add_user_id_dbg_args
|
||||
Create Date: 2026-04-13
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'sharedserver_feature_parity'
|
||||
down_revision = 'add_user_id_dbg_args'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = sa.inspect(conn)
|
||||
|
||||
if not inspector.has_table('sharedserver'):
|
||||
return
|
||||
|
||||
# Clean up orphaned SharedServer records whose osid
|
||||
# references a Server that no longer exists.
|
||||
op.execute(
|
||||
"DELETE FROM sharedserver WHERE osid NOT IN "
|
||||
"(SELECT id FROM server)"
|
||||
)
|
||||
|
||||
# Add missing columns (idempotent — guard against re-runs).
|
||||
existing_cols = {
|
||||
c['name'] for c in inspector.get_columns('sharedserver')
|
||||
}
|
||||
new_columns = [
|
||||
('passexec_cmd',
|
||||
sa.Column('passexec_cmd', sa.Text(),
|
||||
nullable=True)),
|
||||
('passexec_expiration',
|
||||
sa.Column('passexec_expiration', sa.Integer(),
|
||||
nullable=True)),
|
||||
('kerberos_conn',
|
||||
sa.Column('kerberos_conn', sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.false())),
|
||||
('tags',
|
||||
sa.Column('tags', sa.JSON(), nullable=True)),
|
||||
('post_connection_sql',
|
||||
sa.Column('post_connection_sql', sa.String(),
|
||||
nullable=True)),
|
||||
]
|
||||
cols_to_add = [
|
||||
col for name, col in new_columns
|
||||
if name not in existing_cols
|
||||
]
|
||||
if cols_to_add:
|
||||
with op.batch_alter_table('sharedserver') as batch_op:
|
||||
for col in cols_to_add:
|
||||
batch_op.add_column(col)
|
||||
|
||||
# Drop db_res — database restrictions are an owner-level
|
||||
# concept; the column was never overlaid or writable by
|
||||
# non-owners and its presence invites accidental leakage.
|
||||
if 'db_res' in existing_cols:
|
||||
with op.batch_alter_table('sharedserver') as batch_op:
|
||||
batch_op.drop_column('db_res')
|
||||
|
||||
|
||||
def downgrade():
|
||||
# pgAdmin only upgrades, downgrade not implemented.
|
||||
pass
|
||||
@@ -7,62 +7,64 @@
|
||||
],
|
||||
"license": "PostgreSQL",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/eslint-parser": "^7.26.5",
|
||||
"@babel/eslint-plugin": "^7.26.10",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.10.1",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.0",
|
||||
"@babel/plugin-transform-runtime": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/eslint-parser": "^7.28.6",
|
||||
"@babel/eslint-plugin": "^7.27.1",
|
||||
"@babel/plugin-syntax-jsx": "^7.28.6",
|
||||
"@babel/plugin-transform-class-properties": "^7.28.6",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.28.6",
|
||||
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@emotion/utils": "^1.0.0",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"browserify": "^17.0.0",
|
||||
"babel-loader": "^10.1.1",
|
||||
"browserify": "^17.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-jest": "^29.9.0",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
"css-minimizer-webpack-plugin": "^8.0.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-jest": "^29.15.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"exports-loader": "^5.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"html-react-parser": "^5.2.12",
|
||||
"globals": "^17.5.0",
|
||||
"html-react-parser": "^5.2.17",
|
||||
"image-minimizer-webpack-plugin": "^4.1.4",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "^2.10.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"jest": "^30.3.0",
|
||||
"jest-environment-jsdom": "^30.3.0",
|
||||
"loader-utils": "^3.3.1",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"postcss-loader": "^8.2.1",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.7.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"shim-loader": "^1.0.1",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylis": "^4.3.4",
|
||||
"svgo": "^4.0.0",
|
||||
"stylis": "^4.4.0",
|
||||
"svgo": "^4.0.1",
|
||||
"svgo-loader": "^4.0.0",
|
||||
"terser-webpack-plugin": "^5.3.12",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"terser-webpack-plugin": "^5.5.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"webfonts-loader": "^8.0.1",
|
||||
"webpack": "^5.103.0",
|
||||
"webpack-bundle-analyzer": "^5.2.0",
|
||||
"webfonts-loader": "^8.1.1",
|
||||
"webpack": "^5.106.2",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"yarn-audit-html": "7.3.2"
|
||||
},
|
||||
@@ -70,90 +72,89 @@
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@date-io/core": "^3.0.0",
|
||||
"@date-io/core": "^3.2.0",
|
||||
"@date-io/date-fns": "3.x",
|
||||
"@emotion/sheet": "^1.0.1",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
"@fluentui/keyboard-key": "^0.4.23",
|
||||
"@fortawesome/fontawesome-free": "latest",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/material": "^7.3.7",
|
||||
"@mui/x-date-pickers": "^8.26.0",
|
||||
"@mui/icons-material": "^9.0.0",
|
||||
"@mui/material": "^9.0.0",
|
||||
"@mui/x-date-pickers": "^9.0.2",
|
||||
"@nozbe/microfuzz": "^1.0.0",
|
||||
"@projectstorm/react-diagrams": "^7.0.4",
|
||||
"@simonwep/pickr": "^1.5.1",
|
||||
"@simonwep/pickr": "^1.9.1",
|
||||
"@szhsin/react-menu": "^4.5.1",
|
||||
"@tanstack/react-query": "^5.90.19",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"@tanstack/react-virtual": "^3.13.13",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.24",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"ajv": "^8.8.2",
|
||||
"anti-trojan-source": "^1.4.0",
|
||||
"aspen-decorations": "^1.0.2",
|
||||
"axios": "^1.13.3",
|
||||
"ajv": "^8.18.0",
|
||||
"anti-trojan-source": "^1.8.1",
|
||||
"aspen-decorations": "^1.1.1",
|
||||
"axios": "^1.16.0",
|
||||
"babelify": "~10.0.0",
|
||||
"bignumber.js": "^9.2.0",
|
||||
"bignumber.js": "^10.0.2",
|
||||
"brace": "^0.11.1",
|
||||
"chart.js": "^4.4.7",
|
||||
"chartjs-plugin-zoom": "^2.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"convert-units": "^2.3.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"diff-arrays-of-objects": "^1.1.8",
|
||||
"hotkeys-js": "^4.0.0",
|
||||
"diff-arrays-of-objects": "^1.1.10",
|
||||
"dompurify": "^3.4.1",
|
||||
"hotkeys-js": "^4.0.3",
|
||||
"html-to-image": "1.11.11",
|
||||
"immutability-helper": "^3.0.0",
|
||||
"insert-if": "^1.1.0",
|
||||
"ip-address": "^10.1.0",
|
||||
"json-bignumber": "^1.0.1",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"insert-if": "^1.2.0",
|
||||
"ip-address": "^10.1.1",
|
||||
"json-bignumber": "^1.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "4.*",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"marked": "^18.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.2",
|
||||
"notificar": "^1.0.1",
|
||||
"notistack": "^3.0.1",
|
||||
"papaparse": "^5.5.2",
|
||||
"path-fx": "^2.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"notistack": "^3.0.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"path-fx": "^2.1.2",
|
||||
"postcss": "^8.5.14",
|
||||
"rc-dock": "^4.0.0-alpha.2",
|
||||
"react": "^19.2.3",
|
||||
"react-arborist": "^3.2.0",
|
||||
"react-aspen": "^1.1.0",
|
||||
"react-checkbox-tree": "^1.7.2",
|
||||
"react": "^19.2.5",
|
||||
"react-arborist": "^3.5.0",
|
||||
"react-aspen": "^1.2.0",
|
||||
"react-checkbox-tree": "^2.0.1",
|
||||
"react-data-grid": "https://github.com/pgadmin-org/react-data-grid.git#8ddaa7bed99c8329e39b8ff37ba156f7fc8e85d4",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-dropzone": "^14.2.1",
|
||||
"react-frame-component": "^5.2.6",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"react-frame-component": "~5.2.6",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-new-window": "^1.0.1",
|
||||
"react-resize-detector": "^12.3.0",
|
||||
"react-rnd": "^10.5.1",
|
||||
"react-select": "^5.7.2",
|
||||
"react-timer-hook": "^4.0.1",
|
||||
"react-virtualized-auto-sizer": "^2.0.2",
|
||||
"react-rnd": "^10.5.3",
|
||||
"react-select": "^5.10.2",
|
||||
"react-timer-hook": "^4.0.5",
|
||||
"react-virtualized-auto-sizer": "^2.0.3",
|
||||
"snapsvg-cjs": "^0.0.6",
|
||||
"socket.io-client": "^4.5.0",
|
||||
"split.js": "^1.5.10",
|
||||
"sql-formatter": "^15.6.10",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"split.js": "^1.6.5",
|
||||
"sql-formatter": "^15.7.3",
|
||||
"uplot": "^1.6.32",
|
||||
"uplot-react": "^1.1.4",
|
||||
"uplot-react": "^1.2.4",
|
||||
"use-resize-observer": "^9.1.0",
|
||||
"valid-filename": "^4.0.0",
|
||||
"vanilla-jsoneditor": "^3.3.1",
|
||||
"vanilla-jsoneditor": "^3.12.0",
|
||||
"wkx": "^0.5.0",
|
||||
"zustand": "^5.0.9"
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"resolutions": {
|
||||
"rc-resize-observer": "1.4.0"
|
||||
@@ -179,7 +180,7 @@
|
||||
"auditpy": "safety check --full-report -i 51668 -i 52495",
|
||||
"audit-all": "yarn run auditjs && yarn run auditpy"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"packageManager": "yarn@4.15.0",
|
||||
"browserslist": [
|
||||
"defaults and fully supports es6-module"
|
||||
]
|
||||
|
||||
@@ -403,50 +403,67 @@ def create_app(app_name=None):
|
||||
backup_db_file()
|
||||
|
||||
def run_migration_for_sqlite():
|
||||
# Run migration for the first time i.e. create database
|
||||
# If version not available, user must have aborted. Tables are not
|
||||
# created and so its an empty db
|
||||
if not os.path.exists(SQLITE_PATH) or get_version() == -1:
|
||||
# If running in cli mode then don't try to upgrade, just raise
|
||||
# the exception
|
||||
if not cli_mode:
|
||||
upgrade_db()
|
||||
# Tighten the process umask while we may be creating the SQLite
|
||||
# DB file. The post-hoc chmod below sets 0o600, but only AFTER
|
||||
# SQLAlchemy/SQLite has already created the file with the
|
||||
# umask-default mode (0o644 on a typical 0o022 umask). That's a
|
||||
# TOCTOU window — small in practice because the parent dir is
|
||||
# 0o700, but easy to close. With umask 0o077 in effect during
|
||||
# creation, the file is born 0o600.
|
||||
_saved_umask = os.umask(0o077)
|
||||
try:
|
||||
# Run migration for the first time i.e. create database
|
||||
# If version not available, user must have aborted. Tables
|
||||
# are not created and so its an empty db
|
||||
if not os.path.exists(SQLITE_PATH) or get_version() == -1:
|
||||
# If running in cli mode then don't try to upgrade, just
|
||||
# raise the exception
|
||||
if not cli_mode:
|
||||
upgrade_db()
|
||||
else:
|
||||
if not os.path.exists(SQLITE_PATH):
|
||||
raise FileNotFoundError(
|
||||
'SQLite database file "' + SQLITE_PATH +
|
||||
'" does not exists.')
|
||||
raise RuntimeError(
|
||||
'The configuration database file is not valid.')
|
||||
else:
|
||||
if not os.path.exists(SQLITE_PATH):
|
||||
raise FileNotFoundError(
|
||||
'SQLite database file "' + SQLITE_PATH +
|
||||
'" does not exists.')
|
||||
raise RuntimeError(
|
||||
'The configuration database file is not valid.')
|
||||
else:
|
||||
schema_version = get_version()
|
||||
schema_version = get_version()
|
||||
|
||||
# Run migration if current schema version is greater than the
|
||||
# schema version stored in version table
|
||||
if CURRENT_SCHEMA_VERSION > schema_version:
|
||||
# Take a backup of the old database file.
|
||||
try:
|
||||
prev_database_file_name = \
|
||||
"{0}.prev.bak".format(SQLITE_PATH)
|
||||
shutil.copyfile(SQLITE_PATH, prev_database_file_name)
|
||||
except Exception as e:
|
||||
app.logger.error(e)
|
||||
# Run migration if current schema version is greater than
|
||||
# the schema version stored in version table
|
||||
if CURRENT_SCHEMA_VERSION > schema_version:
|
||||
# Take a backup of the old database file.
|
||||
try:
|
||||
prev_database_file_name = \
|
||||
"{0}.prev.bak".format(SQLITE_PATH)
|
||||
shutil.copyfile(
|
||||
SQLITE_PATH, prev_database_file_name)
|
||||
except Exception as e:
|
||||
app.logger.error(e)
|
||||
|
||||
upgrade_db()
|
||||
else:
|
||||
# check all tables are present in the db.
|
||||
is_db_error, invalid_tb_names = check_db_tables()
|
||||
if is_db_error:
|
||||
app.logger.error(
|
||||
'Table(s) {0} are missing in the'
|
||||
' database'.format(invalid_tb_names))
|
||||
backup_db_file()
|
||||
upgrade_db()
|
||||
else:
|
||||
# check all tables are present in the db.
|
||||
is_db_error, invalid_tb_names = check_db_tables()
|
||||
if is_db_error:
|
||||
app.logger.error(
|
||||
'Table(s) {0} are missing in the'
|
||||
' database'.format(invalid_tb_names))
|
||||
backup_db_file()
|
||||
|
||||
# Update schema version to the latest
|
||||
if CURRENT_SCHEMA_VERSION > schema_version:
|
||||
set_version(CURRENT_SCHEMA_VERSION)
|
||||
db.session.commit()
|
||||
# Update schema version to the latest
|
||||
if CURRENT_SCHEMA_VERSION > schema_version:
|
||||
set_version(CURRENT_SCHEMA_VERSION)
|
||||
db.session.commit()
|
||||
finally:
|
||||
# Always restore the prior umask — including the exception
|
||||
# paths above, so a migration failure doesn't leave the
|
||||
# whole process running with 0o077.
|
||||
os.umask(_saved_umask)
|
||||
|
||||
# Belt-and-suspenders: covers the case where the file already
|
||||
# existed at a wider mode from an older install.
|
||||
if os.name != 'nt':
|
||||
os.chmod(config.SQLITE_PATH, 0o600)
|
||||
|
||||
|
||||
@@ -122,11 +122,22 @@ export default function AboutComponent() {
|
||||
</Grid>
|
||||
}
|
||||
{ aboutData.settings &&
|
||||
<Box flexGrow="1" display="flex" flexDirection="column" minHeight="0">
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: '1',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '0'
|
||||
}}>
|
||||
<Box>
|
||||
<InputLabel style={{fontWeight: 'bold'}}>{gettext('pgAdmin Server Configuration')}</InputLabel>
|
||||
</Box>
|
||||
<Box flexGrow="1" paddingTop="1px" minHeight="0">
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: '1',
|
||||
paddingTop: '1px',
|
||||
minHeight: '0'
|
||||
}}>
|
||||
<InputSQL value={aboutData.settings}
|
||||
controlProps={{
|
||||
readonly: true,
|
||||
|
||||
@@ -110,7 +110,10 @@ def _login():
|
||||
# Sending empty form as oauth2 does not require form attribute
|
||||
auth_obj = AuthSourceManager({}, copy.deepcopy(
|
||||
config.AUTHENTICATION_SOURCES))
|
||||
session['auth_obj'] = auth_obj
|
||||
# Persist only the OAuth2 provider selection across the redirect.
|
||||
# The auth-source instance lives on current_app's registry so we
|
||||
# re-look-up rather than persist a live class instance in session.
|
||||
session['oauth2_current_client'] = request.form.get('oauth2_button')
|
||||
else:
|
||||
auth_obj = AuthSourceManager(form, copy.deepcopy(
|
||||
config.AUTHENTICATION_SOURCES))
|
||||
@@ -183,16 +186,14 @@ def _login():
|
||||
user.login_attempts = 0
|
||||
db.session.commit()
|
||||
|
||||
if 'auth_obj' in session:
|
||||
session.pop('auth_obj')
|
||||
session.pop('oauth2_current_client', None)
|
||||
return redirect(pga_utils.get_safe_post_login_redirect())
|
||||
|
||||
elif isinstance(msg, Response):
|
||||
return msg
|
||||
elif 'oauth2_button' in request.form and not isinstance(msg, str):
|
||||
return msg
|
||||
if 'auth_obj' in session:
|
||||
session.pop('auth_obj')
|
||||
session.pop('oauth2_current_client', None)
|
||||
flash(msg, MessageType.ERROR)
|
||||
form_class = _security.forms.get('login_form').cls
|
||||
form = form_class()
|
||||
|
||||
@@ -52,16 +52,37 @@ def init_app(app):
|
||||
methods=['GET', 'POST'])
|
||||
@pgCSRFProtect.exempt
|
||||
def oauth_authorize():
|
||||
auth_obj = session['auth_obj']
|
||||
auth_obj.set_current_source(auth_obj.source.get_source_name())
|
||||
# Reconstruct the minimal AuthSourceManager state from the session.
|
||||
# The previous design stored a live AuthSourceManager instance,
|
||||
# which required serializable-anything session storage and
|
||||
# presented a deserialization vector. Now we persist only the
|
||||
# OAuth2 provider name and re-look-up the auth-source from
|
||||
# current_app's registry.
|
||||
from pgadmin.authenticate import AuthSourceManager, get_auth_sources
|
||||
oauth2_client = session.get('oauth2_current_client')
|
||||
if not oauth2_client:
|
||||
current_app.logger.warning(
|
||||
"OAuth2 callback received without provider context; "
|
||||
"session may have expired between login and callback")
|
||||
flash(gettext("Login session expired. Please try again."),
|
||||
MessageType.ERROR)
|
||||
return redirect(get_safe_post_logout_redirect())
|
||||
|
||||
oauth2_source = get_auth_sources(OAUTH2)
|
||||
# Restore the provider selection onto the (process-global) source
|
||||
# instance so login() picks the right OAuth2 client.
|
||||
oauth2_source.oauth2_current_client = oauth2_client
|
||||
|
||||
auth_obj = AuthSourceManager({}, [OAUTH2])
|
||||
auth_obj.set_source(oauth2_source)
|
||||
auth_obj.set_current_source(oauth2_source.get_source_name())
|
||||
|
||||
status, msg = auth_obj.login()
|
||||
if status:
|
||||
session['auth_source_manager'] = auth_obj.as_dict()
|
||||
if 'auth_obj' in session:
|
||||
session.pop('auth_obj')
|
||||
session.pop('oauth2_current_client', None)
|
||||
return redirect(get_safe_post_login_redirect())
|
||||
if 'auth_obj' in session:
|
||||
session.pop('auth_obj')
|
||||
session.pop('oauth2_current_client', None)
|
||||
logout_user()
|
||||
flash(msg, MessageType.ERROR)
|
||||
return redirect(get_safe_post_login_redirect())
|
||||
@@ -437,9 +458,9 @@ class OAuth2Authentication(BaseAuthentication):
|
||||
|
||||
def login(self, form):
|
||||
if not self.oauth2_current_client:
|
||||
error_msg = 'No OAuth2 provider available.'
|
||||
error_msg = gettext('No OAuth2 provider available.')
|
||||
current_app.logger.error(error_msg)
|
||||
return False, gettext(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
profile = self.get_user_profile()
|
||||
profile_dict = self.get_profile_dict(profile)
|
||||
@@ -468,35 +489,35 @@ class OAuth2Authentication(BaseAuthentication):
|
||||
not id_token_claims and
|
||||
not profile_dict
|
||||
):
|
||||
error_msg = "No profile data found from OIDC provider."
|
||||
error_msg = gettext("No profile data found from OIDC provider.")
|
||||
current_app.logger.error(error_msg)
|
||||
return False, gettext(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
# For non-OIDC providers, profile is required
|
||||
if not self._is_oidc_provider() and not profile_dict:
|
||||
error_msg = "No profile data found."
|
||||
error_msg = gettext("No profile data found.")
|
||||
current_app.logger.error(error_msg)
|
||||
return False, gettext(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
# Resolve username using OIDC-aware logic
|
||||
username, email = self._resolve_username(id_token_claims, profile_dict)
|
||||
|
||||
if not username:
|
||||
if self._is_oidc_provider():
|
||||
error_msg = (
|
||||
error_msg = gettext(
|
||||
'Could not extract username from OIDC claims. '
|
||||
'Please ensure your OIDC provider returns standard '
|
||||
'claims (email, preferred_username, or sub).'
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
error_msg = gettext(
|
||||
'An email id or OAUTH2_USERNAME_CLAIM is required to '
|
||||
'login into pgAdmin. Please update your OAuth2 profile '
|
||||
'for email id or set OAUTH2_USERNAME_CLAIM config '
|
||||
'parameter.'
|
||||
)
|
||||
current_app.logger.error(error_msg)
|
||||
return False, gettext(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
additional_claims = None
|
||||
if 'OAUTH2_ADDITIONAL_CLAIMS' in self.oauth2_config[
|
||||
@@ -678,10 +699,12 @@ class OAuth2Authentication(BaseAuthentication):
|
||||
self.oauth2_current_client].authorize_redirect(redirect_url)
|
||||
|
||||
def __auto_create_user(self, username, email):
|
||||
if config.OAUTH2_AUTO_CREATE_USER:
|
||||
user = User.query.filter_by(username=username,
|
||||
auth_source=OAUTH2).first()
|
||||
if not user:
|
||||
user = User.query.filter_by(username=username,
|
||||
auth_source=OAUTH2).first()
|
||||
if user:
|
||||
return True, {'username': username}
|
||||
else:
|
||||
if config.OAUTH2_AUTO_CREATE_USER:
|
||||
create_msg = ("Creating user {0} with email {1} "
|
||||
"from auth source OAUTH2.")
|
||||
current_app.logger.info(create_msg.format(username,
|
||||
@@ -693,18 +716,20 @@ class OAuth2Authentication(BaseAuthentication):
|
||||
'active': True,
|
||||
'auth_source': OAUTH2
|
||||
})
|
||||
|
||||
return True, {'username': username}
|
||||
else:
|
||||
return False, gettext('No Email/Username found.'
|
||||
' Please contact your administrator.')
|
||||
|
||||
def __is_any_claim_valid(self, identity, additional_claims):
|
||||
if additional_claims is None:
|
||||
reason = "Additional claim config is None, no check to do."
|
||||
reason = gettext("Additional claim config is None,"
|
||||
" no check to do.")
|
||||
return (True, reason)
|
||||
if not isinstance(additional_claims, dict):
|
||||
reason = "Additional claim check config is not a dict."
|
||||
reason = gettext("Additional claim check config is not a dict.")
|
||||
return (False, reason)
|
||||
if additional_claims.keys() is None:
|
||||
reason = "Additional claim check config dict is empty."
|
||||
if len(additional_claims.keys()) == 0:
|
||||
reason = gettext("Additional claim check config dict is empty.")
|
||||
return (False, reason)
|
||||
for key in additional_claims.keys():
|
||||
claim = identity.get(key)
|
||||
@@ -716,7 +741,7 @@ class OAuth2Authentication(BaseAuthentication):
|
||||
if not isinstance(authorized_claims, list):
|
||||
authorized_claims = [authorized_claims]
|
||||
if any(item in authorized_claims for item in claim):
|
||||
reason = "Claim match found. Authorized access."
|
||||
reason = gettext("Claim match found. Authorized access.")
|
||||
return True, reason
|
||||
reason = "No match was found."
|
||||
reason = gettext("No match was found.")
|
||||
return False, reason
|
||||
@@ -68,11 +68,31 @@ from pgadmin.authenticate import AuthSourceManager
|
||||
from pgadmin.utils.exception import CryptKeyMissing
|
||||
|
||||
from pgadmin.user_login_check import pga_login_required
|
||||
from flask_security.views import default_render_json
|
||||
|
||||
MODULE_NAME = 'browser'
|
||||
BROWSER_STATIC = 'browser.static'
|
||||
BROWSER_INDEX = 'browser.index'
|
||||
|
||||
|
||||
def _first_form_error_message(form, default=None):
|
||||
"""Extract the first validator error message from a wtforms form,
|
||||
coerced to a plain str.
|
||||
|
||||
Returns `default` if there are no errors. Babel `LazyString`
|
||||
(`LazyProxy`) instances — emitted by wtforms validators with
|
||||
translatable messages — must be force-resolved before they reach
|
||||
`json.dumps`, which otherwise fails with "Circular reference
|
||||
detected" because `LazyProxy` reports itself as iterable. Use this
|
||||
helper any time you return a form-validation error in a JSON
|
||||
envelope.
|
||||
"""
|
||||
for errors in form.errors.values():
|
||||
for error in errors:
|
||||
if error:
|
||||
return str(error)
|
||||
return default
|
||||
|
||||
|
||||
PGADMIN_BROWSER = 'pgAdmin.Browser'
|
||||
PASS_ERROR_MSG = gettext('Your password has not been changed.')
|
||||
SMTP_SOCKET_ERROR = gettext(
|
||||
@@ -494,6 +514,9 @@ def utils():
|
||||
except Exception:
|
||||
pg_libpq_version = 0
|
||||
|
||||
# Check if LLM features are enabled (system-level AND provider configured)
|
||||
from pgadmin.llm.utils import is_llm_enabled
|
||||
|
||||
for submodule in current_blueprint.submodules:
|
||||
snippets.extend(submodule.jssnippets)
|
||||
|
||||
@@ -538,6 +561,7 @@ def utils():
|
||||
"Administrator") else restricted_shared_storage_list,
|
||||
enable_server_passexec_cmd=config.ENABLE_SERVER_PASS_EXEC_CMD,
|
||||
max_server_tags_allowed=config.MAX_SERVER_TAGS_ALLOWED,
|
||||
llm_enabled=is_llm_enabled(),
|
||||
), 200)
|
||||
response.headers['Content-Type'] = MIMETYPE_APP_JS
|
||||
response.headers['Cache-Control'] = NO_CACHE_CONTROL
|
||||
@@ -914,7 +938,8 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
|
||||
elif errormsg is not None:
|
||||
return internal_server_error(errormsg)
|
||||
else:
|
||||
return bad_request(list(form.errors.values())[0][0])
|
||||
return bad_request(_first_form_error_message(
|
||||
form, gettext('Invalid input.')))
|
||||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
@@ -1003,8 +1028,16 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
|
||||
do_flash(*get_message('PASSWORD_RESET_REQUEST',
|
||||
email=form.user.email))
|
||||
|
||||
if request.get_json(silent=True) and not has_error:
|
||||
return default_render_json(form, include_user=False)
|
||||
if request.get_json(silent=True) is not None:
|
||||
# Flask-Security's default_render_json signature changed
|
||||
# (no longer accepts `include_user`); return our standard
|
||||
# JSON envelope.
|
||||
if has_error or form.errors:
|
||||
return bad_request(_first_form_error_message(
|
||||
form, gettext('Password reset failed.')))
|
||||
return make_json_response(
|
||||
success=1,
|
||||
info=gettext('Password reset instructions sent.'))
|
||||
|
||||
for errors in form.errors.values():
|
||||
for error in errors:
|
||||
|
||||
@@ -445,7 +445,7 @@ def register_browser_preferences(self):
|
||||
'text', '%FUNCTION%(%ARGS%)',
|
||||
category_label=PREF_LABEL_DISPLAY,
|
||||
help_str=gettext(
|
||||
'Supported placeholders are %FUNCTION%, %ARGS%, %SCHEMA% and'
|
||||
'Supported placeholders are %FUNCTION%, %ARGS%, %SCHEMA%, and'
|
||||
' %DATABASE%. Users can provide any string with or '
|
||||
'without placeholders of their choice. A blank title will revert '
|
||||
'to the default.'
|
||||
|
||||
@@ -25,6 +25,8 @@ from sqlalchemy import exc
|
||||
from pgadmin.model import db, ServerGroup, Server
|
||||
import config
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from pgadmin.utils.server_access import get_server_group, \
|
||||
get_server_groups_for_user
|
||||
|
||||
|
||||
def get_icon_css_class(group_id, group_user_id,
|
||||
@@ -228,7 +230,7 @@ class ServerGroupView(NodeView):
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return make_json_response(
|
||||
status=410, success=0, errormsg=e.message
|
||||
status=410, success=0, errormsg=str(e)
|
||||
)
|
||||
|
||||
return make_json_response(result=request.form)
|
||||
@@ -265,7 +267,7 @@ class ServerGroupView(NodeView):
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return make_json_response(
|
||||
status=410, success=0, errormsg=e.message
|
||||
status=410, success=0, errormsg=str(e)
|
||||
)
|
||||
|
||||
icon_class, is_shared = get_icon_css_class(gid, servergroup.user_id)
|
||||
@@ -286,7 +288,7 @@ class ServerGroupView(NodeView):
|
||||
def properties(self, gid):
|
||||
"""Update the server-group properties"""
|
||||
|
||||
sg = ServerGroup.query.filter(ServerGroup.id == gid).first()
|
||||
sg = get_server_group(gid)
|
||||
|
||||
if sg is None:
|
||||
return make_json_response(
|
||||
@@ -296,7 +298,8 @@ class ServerGroupView(NodeView):
|
||||
)
|
||||
else:
|
||||
return ajax_response(
|
||||
response={'id': sg.id, 'name': sg.name, 'user_id': sg.user_id},
|
||||
response={'id': sg.id, 'name': sg.name,
|
||||
'user_id': sg.user_id},
|
||||
status=200
|
||||
)
|
||||
|
||||
@@ -342,7 +345,7 @@ class ServerGroupView(NodeView):
|
||||
return make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=e.message)
|
||||
errormsg=str(e))
|
||||
|
||||
else:
|
||||
return make_json_response(
|
||||
@@ -373,8 +376,9 @@ class ServerGroupView(NodeView):
|
||||
@staticmethod
|
||||
def get_all_server_groups():
|
||||
"""
|
||||
Returns the list of server groups to show in server mode and
|
||||
if there is any shared server in the group.
|
||||
Returns the list of server groups to show in server mode.
|
||||
Includes groups owned by the user and groups containing
|
||||
shared servers accessible to this user.
|
||||
:return: server groups
|
||||
"""
|
||||
|
||||
@@ -383,17 +387,18 @@ class ServerGroupView(NodeView):
|
||||
pref = Preferences.module('browser')
|
||||
hide_shared_server = pref.preference('hide_shared_server').get()
|
||||
|
||||
server_groups = ServerGroup.query.all()
|
||||
groups = []
|
||||
for group in server_groups:
|
||||
if hide_shared_server and \
|
||||
ServerGroupModule.has_shared_server(group.id) and \
|
||||
group.user_id != current_user.id:
|
||||
continue
|
||||
if group.user_id == current_user.id or \
|
||||
ServerGroupModule.has_shared_server(group.id):
|
||||
server_groups = get_server_groups_for_user()
|
||||
|
||||
if hide_shared_server:
|
||||
groups = []
|
||||
for group in server_groups:
|
||||
if group.user_id != current_user.id and \
|
||||
ServerGroupModule.has_shared_server(group.id):
|
||||
continue
|
||||
groups.append(group)
|
||||
return groups
|
||||
return groups
|
||||
|
||||
return server_groups
|
||||
|
||||
@pga_login_required
|
||||
def nodes(self, gid=None):
|
||||
@@ -421,7 +426,7 @@ class ServerGroupView(NodeView):
|
||||
)
|
||||
)
|
||||
else:
|
||||
group = ServerGroup.query.filter(ServerGroup.id == gid).first()
|
||||
group = get_server_group(gid)
|
||||
|
||||
if not group:
|
||||
return gone(
|
||||
|
||||
@@ -39,11 +39,30 @@ from pgadmin.browser.server_groups.servers.utils import \
|
||||
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
|
||||
SERVER_CONNECTION_CLOSED, RESTRICTION_TYPE_SQL
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import object_session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from .... import socketio as sio
|
||||
from pgadmin.utils import get_complete_file_path
|
||||
from pgadmin.settings.utils import with_object_filters
|
||||
from pgadmin.utils.server_access import get_server, \
|
||||
get_user_server_query, get_server_group
|
||||
|
||||
|
||||
# File-path keys in connection_params that are per-user and must
|
||||
# not be copied from the owner to a new SharedServer or leaked
|
||||
# through the property merge.
|
||||
SENSITIVE_CONN_KEYS = frozenset({
|
||||
'passfile', 'sslcert', 'sslkey',
|
||||
'sslrootcert', 'sslcrl', 'sslcrldir',
|
||||
})
|
||||
|
||||
|
||||
def _is_non_owner(server):
|
||||
"""True if the server is shared and the current user is not
|
||||
the owner. Centralises the check used in 15+ places."""
|
||||
return server.shared and server.user_id != current_user.id
|
||||
|
||||
|
||||
def has_any(data, keys):
|
||||
@@ -150,15 +169,31 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
@staticmethod
|
||||
def get_shared_server_properties(server, sharedserver):
|
||||
"""
|
||||
Return shared server properties
|
||||
Return shared server properties.
|
||||
|
||||
Overlays per-user SharedServer values onto the owner's Server
|
||||
object so each non-owner sees their own customizations.
|
||||
|
||||
The server is expunged from the SQLAlchemy session before
|
||||
mutation so that the owner's record is never dirtied.
|
||||
:param server:
|
||||
:param sharedserver:
|
||||
:return: shared server
|
||||
:return: shared server (detached)
|
||||
"""
|
||||
if sharedserver is None:
|
||||
return server
|
||||
|
||||
# Detach from session so in-place mutations are never
|
||||
# flushed back to the owner's Server row.
|
||||
sess = object_session(server)
|
||||
if sess is not None:
|
||||
sess.expunge(server)
|
||||
|
||||
server.bgcolor = sharedserver.bgcolor
|
||||
server.fgcolor = sharedserver.fgcolor
|
||||
server.name = sharedserver.name
|
||||
server.role = sharedserver.role
|
||||
server.service = sharedserver.service
|
||||
server.use_ssh_tunnel = sharedserver.use_ssh_tunnel
|
||||
server.tunnel_host = sharedserver.tunnel_host
|
||||
server.tunnel_port = sharedserver.tunnel_port
|
||||
@@ -168,23 +203,33 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
server.save_password = sharedserver.save_password
|
||||
server.tunnel_identity_file = sharedserver.tunnel_identity_file
|
||||
server.tunnel_prompt_password = sharedserver.tunnel_prompt_password
|
||||
if hasattr(server, 'connection_params') and \
|
||||
hasattr(sharedserver, 'connection_params') and \
|
||||
'passfile' in server.connection_params and \
|
||||
'passfile' in sharedserver.connection_params:
|
||||
server.connection_params['passfile'] = \
|
||||
sharedserver.connection_params['passfile']
|
||||
|
||||
# Override per-user connection_params keys. Use the
|
||||
# SharedServer value whenever it is present, regardless of
|
||||
# whether the owner's Server has the same key.
|
||||
s_conn = getattr(server, 'connection_params', None) \
|
||||
or {}
|
||||
ss_conn = getattr(sharedserver, 'connection_params',
|
||||
None) or {}
|
||||
for key in SENSITIVE_CONN_KEYS:
|
||||
if key in ss_conn:
|
||||
s_conn[key] = ss_conn[key]
|
||||
elif key in s_conn:
|
||||
# Owner has this key but non-owner doesn't —
|
||||
# remove it so the owner's path doesn't leak.
|
||||
del s_conn[key]
|
||||
server.connection_params = s_conn
|
||||
|
||||
server.servergroup_id = sharedserver.servergroup_id
|
||||
if hasattr(server, 'connection_params') and \
|
||||
hasattr(sharedserver, 'connection_params') and \
|
||||
'sslcert' in server.connection_params and \
|
||||
'sslcert' in sharedserver.connection_params:
|
||||
server.connection_params['sslcert'] = \
|
||||
sharedserver.connection_params['sslcert']
|
||||
server.username = sharedserver.username
|
||||
server.server_owner = sharedserver.server_owner
|
||||
server.password = sharedserver.password
|
||||
server.prepare_threshold = sharedserver.prepare_threshold
|
||||
server.passexec_cmd = sharedserver.passexec_cmd
|
||||
server.passexec_expiration = sharedserver.passexec_expiration
|
||||
server.kerberos_conn = sharedserver.kerberos_conn
|
||||
server.tags = sharedserver.tags
|
||||
server.post_connection_sql = sharedserver.post_connection_sql
|
||||
|
||||
return server
|
||||
|
||||
@@ -202,12 +247,13 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
if server.discovery_id and \
|
||||
not server.shared and \
|
||||
config.SERVER_MODE and \
|
||||
len(SharedServer.query.filter_by(
|
||||
SharedServer.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
name=server.name).all()) > 0 and not hide_shared_server:
|
||||
osid=server.id).first() is not None \
|
||||
and not hide_shared_server:
|
||||
continue
|
||||
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
|
||||
shared_server = self.get_shared_server(server, gid)
|
||||
|
||||
@@ -244,8 +290,7 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
"""Return a JSON document listing the server groups for the user"""
|
||||
|
||||
hide_shared_server = get_preferences()
|
||||
servers = Server.query.filter(
|
||||
or_(Server.user_id == current_user.id, Server.shared),
|
||||
servers = get_user_server_query().filter(
|
||||
Server.servergroup_id == gid, Server.is_adhoc == 0)
|
||||
|
||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||
@@ -391,6 +436,18 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
try:
|
||||
db.session.rollback()
|
||||
user = User.query.filter_by(id=data.user_id).first()
|
||||
|
||||
# Strip owner's sensitive file paths from
|
||||
# connection_params — each user should configure
|
||||
# their own SSL/passfile paths.
|
||||
safe_conn_params = {}
|
||||
if data.connection_params:
|
||||
safe_conn_params = {
|
||||
k: v for k, v in
|
||||
data.connection_params.items()
|
||||
if k not in SENSITIVE_CONN_KEYS
|
||||
}
|
||||
|
||||
shared_server = SharedServer(
|
||||
osid=data.id,
|
||||
user_id=current_user.id,
|
||||
@@ -409,43 +466,62 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
service=data.service if data.service else None,
|
||||
use_ssh_tunnel=data.use_ssh_tunnel,
|
||||
tunnel_host=data.tunnel_host,
|
||||
tunnel_port=22,
|
||||
tunnel_port=data.tunnel_port
|
||||
if data.tunnel_port else 22,
|
||||
tunnel_username=None,
|
||||
tunnel_authentication=0,
|
||||
tunnel_identity_file=None,
|
||||
tunnel_keep_alive=0,
|
||||
tunnel_keep_alive=data.tunnel_keep_alive
|
||||
if data.tunnel_keep_alive else 0,
|
||||
tunnel_prompt_password=0,
|
||||
shared=True,
|
||||
connection_params=data.connection_params,
|
||||
prepare_threshold=data.prepare_threshold
|
||||
connection_params=safe_conn_params,
|
||||
prepare_threshold=data.prepare_threshold,
|
||||
passexec_cmd=None,
|
||||
passexec_expiration=None,
|
||||
kerberos_conn=False,
|
||||
tags=None,
|
||||
post_connection_sql=None
|
||||
)
|
||||
db.session.add(shared_server)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
if shared_server:
|
||||
db.session.delete(shared_server)
|
||||
db.session.commit()
|
||||
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_shared_server(server, gid):
|
||||
"""
|
||||
return the shared server
|
||||
Return the SharedServer record for the current user,
|
||||
creating one lazily if it doesn't exist. The unique
|
||||
constraint on (osid, user_id) prevents duplicates from
|
||||
concurrent requests.
|
||||
:param server:
|
||||
:param gid:
|
||||
:return: shared_server
|
||||
:return: shared_server (never None)
|
||||
:raises: Exception if SharedServer cannot be created
|
||||
"""
|
||||
shared_server = SharedServer.query.filter_by(
|
||||
name=server.name, user_id=current_user.id,
|
||||
servergroup_id=int(gid), osid=server.id).first()
|
||||
user_id=current_user.id,
|
||||
osid=server.id).first()
|
||||
|
||||
if shared_server is None:
|
||||
ServerModule.create_shared_server(server, int(gid))
|
||||
try:
|
||||
ServerModule.create_shared_server(
|
||||
server, int(gid))
|
||||
except IntegrityError:
|
||||
# Unique constraint violation from a concurrent
|
||||
# request — the record now exists.
|
||||
db.session.rollback()
|
||||
|
||||
shared_server = SharedServer.query.filter_by(
|
||||
name=server.name, user_id=current_user.id,
|
||||
servergroup_id=int(gid), osid=server.id).first()
|
||||
user_id=current_user.id,
|
||||
osid=server.id).first()
|
||||
|
||||
if shared_server is None:
|
||||
raise Exception(
|
||||
"Failed to create shared server record "
|
||||
"for server {0}".format(server.id))
|
||||
|
||||
return shared_server
|
||||
|
||||
@@ -494,17 +570,28 @@ class ServerNode(PGChildNodeView):
|
||||
'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}],
|
||||
})
|
||||
|
||||
def update_connection_parameter(self, data, server):
|
||||
def update_connection_parameter(self, data, server, sharedserver=None):
|
||||
"""
|
||||
This function is used to update the connection parameters.
|
||||
"""
|
||||
if 'connection_params' in data and \
|
||||
hasattr(server, 'connection_params'):
|
||||
existing_conn_params = getattr(server, 'connection_params')
|
||||
# For shared servers accessed by non-owners, apply changes
|
||||
# to the SharedServer's connection_params (a copy) so we
|
||||
# don't mutate the owner's Server record in-place.
|
||||
if sharedserver is not None and \
|
||||
server.shared and \
|
||||
server.user_id != current_user.id:
|
||||
existing_conn_params = dict(
|
||||
sharedserver.connection_params or {})
|
||||
else:
|
||||
existing_conn_params = getattr(
|
||||
server, 'connection_params')
|
||||
new_conn_params = data['connection_params']
|
||||
if 'deleted' in new_conn_params:
|
||||
for item in new_conn_params['deleted']:
|
||||
del existing_conn_params[item['name']]
|
||||
if item['name'] in existing_conn_params:
|
||||
del existing_conn_params[item['name']]
|
||||
if 'added' in new_conn_params:
|
||||
for item in new_conn_params['added']:
|
||||
existing_conn_params[item['name']] = item['value']
|
||||
@@ -559,15 +646,13 @@ class ServerNode(PGChildNodeView):
|
||||
Return a JSON document listing the servers under this server group
|
||||
for the user.
|
||||
"""
|
||||
servers = Server.query.filter(
|
||||
or_(Server.user_id == current_user.id,
|
||||
Server.shared),
|
||||
servers = get_user_server_query().filter(
|
||||
Server.servergroup_id == gid, Server.is_adhoc == 0)
|
||||
|
||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in servers:
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(server, gid)
|
||||
server = \
|
||||
ServerModule.get_shared_server_properties(server,
|
||||
@@ -626,24 +711,22 @@ class ServerNode(PGChildNodeView):
|
||||
@pga_login_required
|
||||
def node(self, gid, sid):
|
||||
"""Return a JSON document listing the server groups for the user"""
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
shared_server = ServerModule.get_shared_server(server, gid)
|
||||
server = ServerModule.get_shared_server_properties(server,
|
||||
shared_server)
|
||||
server = get_server(sid)
|
||||
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
gettext(
|
||||
"Could not find the server with id# {0}."
|
||||
).format(sid)
|
||||
)
|
||||
"Could not find the server with id# {0}."
|
||||
).format(sid)
|
||||
)
|
||||
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(server, gid)
|
||||
server = ServerModule.get_shared_server_properties(server,
|
||||
shared_server)
|
||||
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(server.id)
|
||||
conn = manager.connection()
|
||||
connected = conn.connected()
|
||||
@@ -692,16 +775,20 @@ class ServerNode(PGChildNodeView):
|
||||
),
|
||||
)
|
||||
|
||||
def delete_shared_server(self, server_name, gid, osid):
|
||||
def delete_shared_server(self, gid, osid, user_id=None):
|
||||
"""
|
||||
Delete the shared server
|
||||
:param server_name:
|
||||
:return:
|
||||
Delete SharedServer records for a given original server.
|
||||
:param gid: Server group ID
|
||||
:param osid: Original server ID
|
||||
:param user_id: If set, only delete for this user.
|
||||
If None, delete for ALL users (owner unshare/delete).
|
||||
"""
|
||||
try:
|
||||
shared_server = SharedServer.query.filter_by(name=server_name,
|
||||
servergroup_id=gid,
|
||||
osid=osid)
|
||||
filters = dict(servergroup_id=gid, osid=osid)
|
||||
if user_id is not None:
|
||||
filters['user_id'] = user_id
|
||||
shared_server = SharedServer.query.filter_by(
|
||||
**filters)
|
||||
for s in shared_server:
|
||||
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
|
||||
db.session.delete(s)
|
||||
@@ -711,7 +798,7 @@ class ServerNode(PGChildNodeView):
|
||||
current_app.logger.exception(e)
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=e.message)
|
||||
errormsg=str(e))
|
||||
|
||||
@pga_login_required
|
||||
def delete(self, gid, sid):
|
||||
@@ -737,14 +824,14 @@ class ServerNode(PGChildNodeView):
|
||||
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
|
||||
db.session.delete(s)
|
||||
db.session.commit()
|
||||
self.delete_shared_server(server_name, gid, sid)
|
||||
self.delete_shared_server(gid, sid)
|
||||
QueryHistory.clear_history(current_user.id, sid)
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=e.message)
|
||||
errormsg=str(e))
|
||||
|
||||
return make_json_response(success=1,
|
||||
info=gettext("Server deleted"))
|
||||
@@ -753,7 +840,7 @@ class ServerNode(PGChildNodeView):
|
||||
@pga_login_required
|
||||
def update(self, gid, sid):
|
||||
"""Update the server settings"""
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
server = get_server(sid)
|
||||
sharedserver = None
|
||||
|
||||
if server is None:
|
||||
@@ -820,8 +907,8 @@ class ServerNode(PGChildNodeView):
|
||||
data['db_res'] = ','.join(data['db_res'])
|
||||
|
||||
# Update connection parameter if any.
|
||||
self.update_connection_parameter(data, server)
|
||||
self.update_tags(data, server)
|
||||
self.update_connection_parameter(data, server, sharedserver)
|
||||
self.update_tags(data, sharedserver or server)
|
||||
|
||||
if 'connection_params' in data and \
|
||||
'hostaddr' in data['connection_params'] and \
|
||||
@@ -854,7 +941,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
# tags is JSON type, sqlalchemy sometimes will not detect change
|
||||
if 'tags' in data:
|
||||
flag_modified(server, 'tags')
|
||||
flag_modified(sharedserver or server, 'tags')
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
@@ -862,7 +949,7 @@ class ServerNode(PGChildNodeView):
|
||||
current_app.logger.exception(e)
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=e.message
|
||||
errormsg=str(e)
|
||||
)
|
||||
|
||||
# When server is connected, we don't require to update the connection
|
||||
@@ -870,6 +957,10 @@ class ServerNode(PGChildNodeView):
|
||||
# which will affect the connections.
|
||||
if not conn.connected():
|
||||
manager.update(server)
|
||||
# Suppress passexec for non-owners so the manager
|
||||
# never holds the owner's password-exec command.
|
||||
if _is_non_owner(server):
|
||||
manager.passexec = None
|
||||
|
||||
return jsonify(
|
||||
node=self.blueprint.generate_browser_node(
|
||||
@@ -877,7 +968,7 @@ class ServerNode(PGChildNodeView):
|
||||
server.name,
|
||||
server_icon_and_background(
|
||||
connected, manager, sharedserver)
|
||||
if server.shared and server.user_id != current_user.id
|
||||
if _is_non_owner(server)
|
||||
else server_icon_and_background(
|
||||
connected, manager, server),
|
||||
True,
|
||||
@@ -891,7 +982,7 @@ class ServerNode(PGChildNodeView):
|
||||
role=server.role,
|
||||
is_password_saved=bool(server.save_password),
|
||||
description=server.comment,
|
||||
tags=server.tags
|
||||
tags=(sharedserver or server).tags
|
||||
)
|
||||
)
|
||||
|
||||
@@ -901,7 +992,7 @@ class ServerNode(PGChildNodeView):
|
||||
if value == '':
|
||||
value = None
|
||||
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
setattr(sharedserver, config_param_map[arg], value)
|
||||
else:
|
||||
setattr(server, config_param_map[arg], value)
|
||||
@@ -915,22 +1006,37 @@ class ServerNode(PGChildNodeView):
|
||||
if not crypt_key_present:
|
||||
raise CryptKeyMissing
|
||||
|
||||
# Fields that non-owners must never set on their
|
||||
# SharedServer — they enable command/SQL execution
|
||||
# or are owner-level concepts not on SharedServer.
|
||||
_owner_only_fields = frozenset({
|
||||
'passexec_cmd', 'passexec_expiration',
|
||||
'db_res', 'db_res_type',
|
||||
})
|
||||
|
||||
for arg in config_param_map:
|
||||
if arg in data:
|
||||
# Non-owners cannot set dangerous fields.
|
||||
if _is_non_owner(server) and \
|
||||
arg in _owner_only_fields:
|
||||
continue
|
||||
value = data[arg]
|
||||
if arg == 'password':
|
||||
value = encrypt(data[arg], crypt_key)
|
||||
# sqlite3 do not have boolean type so we need to convert
|
||||
# it manually to integer
|
||||
if 'shared' in data and not data['shared']:
|
||||
# Delete the shared server from DB if server
|
||||
# owner uncheck shared property
|
||||
self.delete_shared_server(server.name, gid, server.id)
|
||||
# sqlite3 do not have boolean type so we need to
|
||||
# convert it manually to integer.
|
||||
# Only the owner may unshare — this deletes ALL
|
||||
# users' SharedServer records.
|
||||
if 'shared' in data and not data['shared'] \
|
||||
and not _is_non_owner(server):
|
||||
self.delete_shared_server(gid, server.id)
|
||||
if arg in ('sslcompression', 'use_ssh_tunnel',
|
||||
'tunnel_authentication', 'kerberos_conn', 'shared'):
|
||||
'tunnel_authentication',
|
||||
'kerberos_conn', 'shared'):
|
||||
value = 1 if value else 0
|
||||
self._update_server_details(server, sharedserver,
|
||||
config_param_map, arg, value)
|
||||
config_param_map, arg,
|
||||
value)
|
||||
idx += 1
|
||||
|
||||
return idx
|
||||
@@ -951,23 +1057,21 @@ class ServerNode(PGChildNodeView):
|
||||
)
|
||||
|
||||
@pga_login_required
|
||||
@with_object_filters
|
||||
def list(self, gid, object_filters):
|
||||
"""
|
||||
Return list of attributes of all servers.
|
||||
"""
|
||||
servers = Server.query.filter(
|
||||
or_(Server.user_id == current_user.id, Server.shared),
|
||||
servers = get_user_server_query().filter(
|
||||
Server.servergroup_id == gid,
|
||||
Server.is_adhoc == 0).order_by(Server.name)
|
||||
sg = ServerGroup.query.filter_by(
|
||||
id=gid
|
||||
).first()
|
||||
sg = get_server_group(gid)
|
||||
res = []
|
||||
|
||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in servers:
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(server, gid)
|
||||
server = \
|
||||
ServerModule.get_shared_server_properties(server,
|
||||
@@ -1001,8 +1105,7 @@ class ServerNode(PGChildNodeView):
|
||||
def properties(self, gid, sid):
|
||||
"""Return list of attributes of a server"""
|
||||
|
||||
server = Server.query.filter_by(
|
||||
id=sid).first()
|
||||
server = get_server(sid)
|
||||
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
@@ -1025,7 +1128,7 @@ class ServerNode(PGChildNodeView):
|
||||
# port and user when server is connected
|
||||
display_connection_str = self.update_connection_string(manager, server)
|
||||
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(server, gid)
|
||||
server = ServerModule.get_shared_server_properties(server,
|
||||
shared_server)
|
||||
@@ -1077,11 +1180,8 @@ class ServerNode(PGChildNodeView):
|
||||
'fgcolor': server.fgcolor,
|
||||
'db_res': get_db_restriction(server.db_res_type, server.db_res),
|
||||
'db_res_type': server.db_res_type,
|
||||
'passexec_cmd':
|
||||
server.passexec_cmd if server.passexec_cmd else None,
|
||||
'passexec_expiration':
|
||||
server.passexec_expiration if server.passexec_expiration
|
||||
else None,
|
||||
'passexec_cmd': server.passexec_cmd,
|
||||
'passexec_expiration': server.passexec_expiration,
|
||||
'service': server.service if server.service else None,
|
||||
'use_ssh_tunnel': use_ssh_tunnel,
|
||||
'tunnel_host': tunnel_host,
|
||||
@@ -1167,8 +1267,17 @@ class ServerNode(PGChildNodeView):
|
||||
).format(arg)
|
||||
)
|
||||
|
||||
connection_params = convert_connection_parameter(
|
||||
data.get('connection_params', []))
|
||||
# convert_connection_parameter() is bidirectional: list->dict for
|
||||
# save (frontend shape), dict->list for display (DB shape). Here
|
||||
# we want the storage shape (dict). If the input is already a
|
||||
# dict (e.g. from internal callers / tests that mimic stored
|
||||
# form), keep it as-is; otherwise assume frontend list shape and
|
||||
# convert.
|
||||
raw_params = data.get('connection_params', [])
|
||||
if isinstance(raw_params, dict):
|
||||
connection_params = raw_params
|
||||
else:
|
||||
connection_params = convert_connection_parameter(raw_params)
|
||||
|
||||
if 'hostaddr' in connection_params and \
|
||||
not is_valid_ipaddress(connection_params['hostaddr']):
|
||||
@@ -1394,7 +1503,12 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
def connect_status(self, gid, sid):
|
||||
"""Check and return the connection status."""
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
server = get_server(sid)
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
status=410, success=0,
|
||||
errormsg=self.not_found_error_msg()
|
||||
)
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
conn = manager.connection()
|
||||
connected = conn.connected()
|
||||
@@ -1463,15 +1577,16 @@ class ServerNode(PGChildNodeView):
|
||||
# function in that case no need to fetch the server detail based on
|
||||
# sid.
|
||||
if server is None:
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
server = get_server(sid)
|
||||
|
||||
if server is None:
|
||||
return bad_request(self.not_found_error_msg())
|
||||
|
||||
shared_server = None
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(server, gid)
|
||||
server = ServerModule.get_shared_server_properties(server,
|
||||
shared_server)
|
||||
if server is None:
|
||||
return bad_request(self.not_found_error_msg())
|
||||
|
||||
# Return if username is blank and the server is shared
|
||||
if server.username is None and not server.service and \
|
||||
@@ -1513,6 +1628,12 @@ class ServerNode(PGChildNodeView):
|
||||
# the API call is not made from SQL Editor or View/Edit Data tool
|
||||
if not manager.connection().connected() and not is_qt:
|
||||
manager.update(server)
|
||||
# Re-suppress passexec after update() which rebuilds
|
||||
# from the (overlaid) server object. Belt-and-suspenders:
|
||||
# the overlay already defaults passexec to None, but this
|
||||
# guards against direct DB edits.
|
||||
if _is_non_owner(server):
|
||||
manager.passexec = None
|
||||
conn = manager.connection()
|
||||
|
||||
# Get enc key
|
||||
@@ -1613,12 +1734,8 @@ class ServerNode(PGChildNodeView):
|
||||
else:
|
||||
if save_password and config.ALLOW_SAVE_PASSWORD:
|
||||
try:
|
||||
# If DB server is running in trust mode then password may
|
||||
# not be available but we don't need to ask password
|
||||
# every time user try to connect
|
||||
# 1 is True in SQLite as no boolean type
|
||||
setattr(server, 'save_password', 1)
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
setattr(shared_server, 'save_password', 1)
|
||||
else:
|
||||
setattr(server, 'save_password', 1)
|
||||
@@ -1626,7 +1743,7 @@ class ServerNode(PGChildNodeView):
|
||||
# Save the encrypted password using the user's login
|
||||
# password key, if there is any password to save
|
||||
if password:
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
setattr(shared_server, 'password', password)
|
||||
else:
|
||||
setattr(server, 'password', password)
|
||||
@@ -1637,12 +1754,16 @@ class ServerNode(PGChildNodeView):
|
||||
manager.release(database=server.maintenance_db)
|
||||
conn = None
|
||||
|
||||
return internal_server_error(errormsg=e.message)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD:
|
||||
try:
|
||||
# Save the encrypted tunnel password.
|
||||
setattr(server, 'tunnel_password', tunnel_password)
|
||||
if _is_non_owner(server):
|
||||
setattr(shared_server, 'tunnel_password',
|
||||
tunnel_password)
|
||||
else:
|
||||
setattr(server, 'tunnel_password', tunnel_password)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
# Release Connection
|
||||
@@ -1650,7 +1771,7 @@ class ServerNode(PGChildNodeView):
|
||||
manager.release(database=server.maintenance_db)
|
||||
conn = None
|
||||
|
||||
return internal_server_error(errormsg=e.message)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
current_app.logger.info('Connection Established for server: \
|
||||
%s - %s' % (server.id, server.name))
|
||||
@@ -1689,7 +1810,7 @@ class ServerNode(PGChildNodeView):
|
||||
def disconnect(self, gid, sid):
|
||||
"""Disconnect the Server."""
|
||||
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
server = get_server(sid)
|
||||
if server is None:
|
||||
return bad_request(self.not_found_error_msg())
|
||||
|
||||
@@ -1814,7 +1935,7 @@ class ServerNode(PGChildNodeView):
|
||||
raise CryptKeyMissing
|
||||
|
||||
# Fetch Server Details
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
server = get_server(sid, only_owned=False)
|
||||
if server is None:
|
||||
return bad_request(self.not_found_error_msg())
|
||||
|
||||
@@ -1901,11 +2022,24 @@ class ServerNode(PGChildNodeView):
|
||||
# Store password in sqlite only if no pgpass file
|
||||
if not is_passfile:
|
||||
password = encrypt(data['newPassword'], crypt_key)
|
||||
# Check if old password was stored in pgadmin4 sqlite database.
|
||||
# If yes then update that password.
|
||||
if server.password is not None and config.ALLOW_SAVE_PASSWORD:
|
||||
setattr(server, 'password', password)
|
||||
db.session.commit()
|
||||
# Check if old password was stored in pgadmin4
|
||||
# sqlite database. If yes then update that password.
|
||||
# For non-owners of shared servers, check the
|
||||
# SharedServer record (not the owner's Server).
|
||||
if config.ALLOW_SAVE_PASSWORD:
|
||||
if server.shared and \
|
||||
server.user_id != current_user.id:
|
||||
shared_server = \
|
||||
ServerModule.get_shared_server(
|
||||
server, gid)
|
||||
if shared_server and \
|
||||
shared_server.password is not None:
|
||||
setattr(shared_server, 'password',
|
||||
password)
|
||||
db.session.commit()
|
||||
elif server.password is not None:
|
||||
setattr(server, 'password', password)
|
||||
db.session.commit()
|
||||
# Also update password in connection manager.
|
||||
manager.password = password
|
||||
manager.update_session()
|
||||
@@ -1925,9 +2059,7 @@ class ServerNode(PGChildNodeView):
|
||||
"""
|
||||
Utility function for wal_replay for resume/pause.
|
||||
"""
|
||||
server = Server.query.filter_by(
|
||||
user_id=current_user.id, id=sid
|
||||
).first()
|
||||
server = get_server(sid)
|
||||
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
@@ -2011,9 +2143,7 @@ class ServerNode(PGChildNodeView):
|
||||
sid: Server id
|
||||
"""
|
||||
is_pgpass = False
|
||||
server = Server.query.filter_by(
|
||||
user_id=current_user.id, id=sid
|
||||
).first()
|
||||
server = get_server(sid)
|
||||
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
@@ -2104,38 +2234,22 @@ class ServerNode(PGChildNodeView):
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
shared_server = None
|
||||
server = get_server(sid, only_owned=False)
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
info=self.not_found_error_msg()
|
||||
)
|
||||
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
shared_server = SharedServer.query.filter_by(
|
||||
name=server.name, user_id=current_user.id,
|
||||
servergroup_id=gid, osid=server.id).first()
|
||||
|
||||
if shared_server is None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
info=gettext("Could not find the required server.")
|
||||
)
|
||||
server = ServerModule. \
|
||||
get_shared_server_properties(server, shared_server)
|
||||
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(
|
||||
server, gid)
|
||||
setattr(shared_server, 'password', None)
|
||||
if shared_server.save_password:
|
||||
setattr(shared_server, 'save_password', 0)
|
||||
else:
|
||||
setattr(server, 'password', None)
|
||||
|
||||
# If password was saved then clear the flag also
|
||||
# 0 is False in SQLite db
|
||||
if server.save_password:
|
||||
if server.shared and server.user_id != current_user.id:
|
||||
setattr(shared_server, 'save_password', 0)
|
||||
else:
|
||||
if server.save_password:
|
||||
setattr(server, 'save_password', 0)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
@@ -2161,13 +2275,19 @@ class ServerNode(PGChildNodeView):
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
server = get_server(sid, only_owned=False)
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
info=self.not_found_error_msg()
|
||||
)
|
||||
setattr(server, 'tunnel_password', None)
|
||||
|
||||
if _is_non_owner(server):
|
||||
shared_server = ServerModule.get_shared_server(
|
||||
server, gid)
|
||||
setattr(shared_server, 'tunnel_password', None)
|
||||
else:
|
||||
setattr(server, 'tunnel_password', None)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
|
||||
@@ -34,6 +34,7 @@ from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
||||
|
||||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
||||
from pgadmin.model import db, Server, Database
|
||||
from pgadmin.utils.server_access import get_server
|
||||
from pgadmin.browser.utils import underscore_escape
|
||||
from pgadmin.utils.constants import TWO_PARAM_STRING
|
||||
|
||||
@@ -579,7 +580,9 @@ class DatabaseView(PGChildNodeView):
|
||||
'already_connected': already_connected,
|
||||
'connected': True,
|
||||
'info_prefix': TWO_PARAM_STRING.
|
||||
format(Server.query.filter_by(id=sid)[0].name, conn.db)
|
||||
format(getattr(
|
||||
get_server(sid), 'name', None) or
|
||||
_('Unknown'), conn.db)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -602,7 +605,9 @@ class DatabaseView(PGChildNodeView):
|
||||
'icon': 'icon-database-not-connected',
|
||||
'connected': False,
|
||||
'info_prefix': TWO_PARAM_STRING.
|
||||
format(Server.query.filter_by(id=sid)[0].name, conn.db)
|
||||
format(getattr(
|
||||
get_server(sid), 'name', None) or
|
||||
_('Unknown'), conn.db)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -866,7 +871,7 @@ class DatabaseView(PGChildNodeView):
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return True, e.message, False
|
||||
return True, str(e), False
|
||||
return False, '', can_drop
|
||||
|
||||
def _get_data_from_request(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
FOREIGN DATA WRAPPER test_fdw_for_fs;
|
||||
|
||||
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OWNER TO postgres;
|
||||
OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'Test comment';
|
||||
@@ -8,11 +8,11 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OPTIONS (host '192.168.1.1', dbname 'edb', port '5450');
|
||||
|
||||
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OWNER TO postgres;
|
||||
OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'Test comment';
|
||||
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO postgres;
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO <OWNER>;
|
||||
|
||||
@@ -7,9 +7,9 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
FOREIGN DATA WRAPPER test_fdw_for_fs;
|
||||
|
||||
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OWNER TO postgres;
|
||||
OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'Test comment';
|
||||
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO postgres;
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO <OWNER>;
|
||||
@@ -9,10 +9,10 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OPTIONS (host '192.168.1.1', dbname 'edb', port '5450');
|
||||
|
||||
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OWNER TO postgres;
|
||||
OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'Test comment';
|
||||
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO postgres;
|
||||
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO <OWNER>;
|
||||
@@ -5,7 +5,7 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OPTIONS (host '192.168.1.1', dbname 'edb', port '5450');
|
||||
|
||||
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
OWNER TO postgres;
|
||||
OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'Test comment';
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
"fsrvoption":"port",
|
||||
"fsrvvalue":"5450"
|
||||
}],
|
||||
"fsrvowner":"postgres",
|
||||
"fsrvowner":"<OWNER>",
|
||||
"description":"Test comment",
|
||||
"fsrvacl":[{
|
||||
"grantee":"PUBLIC",
|
||||
"grantor":"postgres",
|
||||
"grantor":"<OWNER>",
|
||||
"privileges":[{
|
||||
"privilege_type":"U",
|
||||
"privilege":true,
|
||||
@@ -60,7 +60,7 @@
|
||||
"sql_endpoint": "NODE-foreign_server.sql_id",
|
||||
"data": {
|
||||
"name":"FS_$%{}[]()&*^!@\"'`\\/#",
|
||||
"fsrvowner":"postgres"
|
||||
"fsrvowner":"<OWNER>"
|
||||
}
|
||||
}, {
|
||||
"type": "alter",
|
||||
@@ -96,7 +96,7 @@
|
||||
"fsrvacl":{
|
||||
"added": [{
|
||||
"grantee":"PUBLIC",
|
||||
"grantor":"postgres",
|
||||
"grantor":"<OWNER>",
|
||||
"privileges":[{
|
||||
"privilege_type":"U",
|
||||
"privilege":true,
|
||||
@@ -129,7 +129,7 @@
|
||||
"fsrvacl":{
|
||||
"deleted": [{
|
||||
"grantee":"PUBLIC",
|
||||
"grantor":"postgres",
|
||||
"grantor":"<OWNER>",
|
||||
"privileges":[{
|
||||
"privilege_type":"U",
|
||||
"privilege":true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- User Mapping : postgres
|
||||
-- User Mapping : <OWNER>
|
||||
|
||||
-- DROP USER MAPPING IF EXISTS FOR postgres SERVER test_fs_for_user_mapping
|
||||
-- DROP USER MAPPING IF EXISTS FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
|
||||
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
|
||||
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
OPTIONS ("user" 'test_user12', password 'secret123');
|
||||
@@ -1,5 +1,5 @@
|
||||
-- User Mapping : postgres
|
||||
-- User Mapping : <OWNER>
|
||||
|
||||
-- DROP USER MAPPING IF EXISTS FOR postgres SERVER test_fs_for_user_mapping
|
||||
-- DROP USER MAPPING IF EXISTS FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
|
||||
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping;
|
||||
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping;
|
||||
@@ -1,2 +1,2 @@
|
||||
ALTER USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
|
||||
ALTER USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
OPTIONS (DROP "user", DROP password);
|
||||
@@ -1,2 +1,2 @@
|
||||
ALTER USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
|
||||
ALTER USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
OPTIONS (SET "user" 'test_user12', SET password 'secret123');
|
||||
@@ -1,6 +1,6 @@
|
||||
-- User Mapping : postgres
|
||||
-- User Mapping : <OWNER>
|
||||
|
||||
-- DROP USER MAPPING IF EXISTS FOR postgres SERVER test_fs_for_user_mapping
|
||||
-- DROP USER MAPPING IF EXISTS FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
|
||||
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
|
||||
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
OPTIONS ("user" 'test_user', password 'secret');
|
||||
@@ -1,2 +1,2 @@
|
||||
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
|
||||
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
|
||||
OPTIONS ("user" 'test_user', password 'secret');
|
||||
@@ -27,7 +27,7 @@
|
||||
"sql_endpoint": "NODE-user_mapping.sql_id",
|
||||
"msql_endpoint": "NODE-user_mapping.msql",
|
||||
"data": {
|
||||
"name": "postgres",
|
||||
"name": "<OWNER>",
|
||||
"umoptions":[{
|
||||
"umoption":"user",
|
||||
"umvalue":"test_user"
|
||||
|
||||
@@ -13,6 +13,7 @@ import sys
|
||||
import traceback
|
||||
import json
|
||||
|
||||
from pgadmin.utils.driver.psycopg3 import Driver
|
||||
from regression.python_test_utils.test_utils import get_db_connection,\
|
||||
set_isolation_level
|
||||
|
||||
@@ -61,8 +62,12 @@ def create_user_mapping(server, db_name, fsrv_name):
|
||||
old_isolation_level = connection.isolation_level
|
||||
set_isolation_level(connection, 0)
|
||||
pg_cursor = connection.cursor()
|
||||
# Quote the username as an SQL identifier so usernames containing
|
||||
# special characters (e.g. dots like 'ashesh.vashi') do not parse
|
||||
# as schema-qualified names.
|
||||
quoted_username = Driver.qtIdent(None, server['username'])
|
||||
query = "CREATE USER MAPPING FOR %s SERVER %s OPTIONS" \
|
||||
" (user '%s', password '%s')" % (server['username'],
|
||||
" (user '%s', password '%s')" % (quoted_username,
|
||||
fsrv_name,
|
||||
server['username'],
|
||||
server['db_password']
|
||||
|
||||
@@ -13,4 +13,4 @@ COMMENT ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'a comment';
|
||||
|
||||
GRANT USAGE ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
|
||||
GRANT USAGE ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#" TO postgres WITH GRANT OPTION;
|
||||
GRANT USAGE ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#" TO <OWNER> WITH GRANT OPTION;
|
||||
@@ -103,7 +103,7 @@
|
||||
"fdwacl": {
|
||||
"added":[{
|
||||
"grantee": "PUBLIC",
|
||||
"grantor": "postgres",
|
||||
"grantor": "<OWNER>",
|
||||
"privileges":[
|
||||
{
|
||||
"privilege_type": "U",
|
||||
@@ -111,8 +111,8 @@
|
||||
"with_grant": false
|
||||
}]
|
||||
},{
|
||||
"grantee": "postgres",
|
||||
"grantor": "postgres",
|
||||
"grantee": "<OWNER>",
|
||||
"grantor": "<OWNER>",
|
||||
"privileges":[{
|
||||
"privilege_type": "U",
|
||||
"privilege": true,
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
"lanproc": "plpgsql_call_handler",
|
||||
"lanval": null,
|
||||
"description": null,
|
||||
"lanowner": "postgres",
|
||||
"lanowner": "<OWNER>",
|
||||
"laninl": null,
|
||||
"seclabels": null
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ COMMENT ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#"
|
||||
|
||||
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
|
||||
|
||||
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO postgres WITH GRANT OPTION;
|
||||
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO <OWNER> WITH GRANT OPTION;
|
||||
@@ -3,4 +3,4 @@ COMMENT ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#"
|
||||
|
||||
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
|
||||
|
||||
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO postgres WITH GRANT OPTION;
|
||||
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO <OWNER> WITH GRANT OPTION;
|
||||
@@ -1,2 +1,2 @@
|
||||
REVOKE ALL ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" FROM PUBLIC;
|
||||
REVOKE ALL ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" FROM postgres;
|
||||
REVOKE ALL ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" FROM <OWNER>;
|
||||
@@ -40,7 +40,7 @@
|
||||
"lanacl": {
|
||||
"added":[{
|
||||
"grantee": "PUBLIC",
|
||||
"grantor": "postgres",
|
||||
"grantor": "<OWNER>",
|
||||
"privileges":[
|
||||
{
|
||||
"privilege_type": "U",
|
||||
@@ -48,8 +48,8 @@
|
||||
"with_grant": false
|
||||
}]
|
||||
},{
|
||||
"grantee": "postgres",
|
||||
"grantor": "postgres",
|
||||
"grantee": "<OWNER>",
|
||||
"grantor": "<OWNER>",
|
||||
"privileges":[{
|
||||
"privilege_type": "U",
|
||||
"privilege": true,
|
||||
@@ -70,7 +70,7 @@
|
||||
"lanacl": {
|
||||
"deleted":[{
|
||||
"grantee": "PUBLIC",
|
||||
"grantor": "postgres",
|
||||
"grantor": "<OWNER>",
|
||||
"privileges":[
|
||||
{
|
||||
"privilege_type": "U",
|
||||
@@ -78,8 +78,8 @@
|
||||
"with_grant": false
|
||||
}]
|
||||
},{
|
||||
"grantee": "postgres",
|
||||
"grantor": "postgres",
|
||||
"grantee": "<OWNER>",
|
||||
"grantor": "<OWNER>",
|
||||
"privileges":[{
|
||||
"privilege_type": "U",
|
||||
"privilege": true,
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": false,
|
||||
"evnt_truncate": false,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": true,
|
||||
"pubtable": ""
|
||||
@@ -116,11 +116,13 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": ["public.test_table_publication"]
|
||||
"pubtable": [
|
||||
"public.test_table_publication"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables.sql",
|
||||
"expected_msql_file": "create_publication_few_tables_msql.sql"
|
||||
@@ -132,7 +134,10 @@
|
||||
"sql_endpoint": "NODE-publication.sql_id",
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": ["public.test_table_publication", "public.test_table_publication_2"]
|
||||
"pubtable": [
|
||||
"public.test_table_publication",
|
||||
"public.test_table_publication_2"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_tables.sql",
|
||||
"expected_msql_file": "alter_publication_add_tables_msql.sql"
|
||||
@@ -144,7 +149,9 @@
|
||||
"sql_endpoint": "NODE-publication.sql_id",
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": ["public.test_table_publication_2"]
|
||||
"pubtable": [
|
||||
"public.test_table_publication_2"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "alter_publication_drop_tables.sql",
|
||||
"expected_msql_file": "alter_publication_drop_tables_msql.sql"
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": false,
|
||||
"evnt_truncate": false,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": true,
|
||||
"only_table": false,
|
||||
@@ -139,13 +139,15 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication"
|
||||
}],
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication"
|
||||
}
|
||||
],
|
||||
"pubschema": ""
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables.sql",
|
||||
@@ -159,9 +161,11 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"added": [{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}]
|
||||
"added": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_tables.sql",
|
||||
@@ -175,9 +179,11 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"deleted": [{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}]
|
||||
"deleted": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_drop_tables.sql",
|
||||
@@ -191,10 +197,14 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"added": [{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": ["dept_id"]
|
||||
}]
|
||||
"added": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": [
|
||||
"dept_id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_tables_columns.sql",
|
||||
@@ -208,10 +218,14 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"changed": [{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": ["dept_name"]
|
||||
}]
|
||||
"changed": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": [
|
||||
"dept_name"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_set_tables_columns.sql",
|
||||
@@ -225,9 +239,11 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"deleted": [{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}]
|
||||
"deleted": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_drop_tables.sql",
|
||||
@@ -241,10 +257,12 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"added": [{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"where": "dept_id=2"
|
||||
}]
|
||||
"added": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"where": "dept_id=2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_tables_where.sql",
|
||||
@@ -258,10 +276,12 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"changed": [{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"where": "dept_name='test'"
|
||||
}]
|
||||
"changed": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"where": "dept_name='test'"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_set_tables_where.sql",
|
||||
@@ -275,9 +295,11 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"deleted": [{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}]
|
||||
"deleted": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_drop_tables.sql",
|
||||
@@ -291,11 +313,15 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"added": [{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": ["dept_id"],
|
||||
"where": "dept_id=2"
|
||||
}]
|
||||
"added": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": [
|
||||
"dept_id"
|
||||
],
|
||||
"where": "dept_id=2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_tables_columns_where.sql",
|
||||
@@ -309,11 +335,15 @@
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": {
|
||||
"changed": [{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": ["dept_name"],
|
||||
"where": "dept_name='test'"
|
||||
}]
|
||||
"changed": [
|
||||
{
|
||||
"table_name": "public.test_table_publication_2",
|
||||
"columns": [
|
||||
"dept_name"
|
||||
],
|
||||
"where": "dept_name='test'"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_publication_set_tables_columns_where.sql",
|
||||
@@ -339,12 +369,14 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": "",
|
||||
"pubschema": ["test_schema_publication"]
|
||||
"pubschema": [
|
||||
"test_schema_publication"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_schemas.sql",
|
||||
"expected_msql_file": "create_publication_few_schemas_msql.sql"
|
||||
@@ -356,7 +388,10 @@
|
||||
"sql_endpoint": "NODE-publication.sql_id",
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubschema": ["test_schema_publication","test_schema_publication_2"]
|
||||
"pubschema": [
|
||||
"test_schema_publication",
|
||||
"test_schema_publication_2"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_schemas.sql",
|
||||
"expected_msql_file": "alter_publication_add_schemas_msql.sql"
|
||||
@@ -368,7 +403,9 @@
|
||||
"sql_endpoint": "NODE-publication.sql_id",
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubschema": ["test_schema_publication_2"]
|
||||
"pubschema": [
|
||||
"test_schema_publication_2"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "alter_publication_drop_schemas.sql",
|
||||
"expected_msql_file": "alter_publication_drop_schemas_msql.sql"
|
||||
@@ -393,13 +430,15 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": true,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication"
|
||||
}],
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication"
|
||||
}
|
||||
],
|
||||
"pubschema": ""
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables_only.sql",
|
||||
@@ -425,14 +464,18 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication"
|
||||
}],
|
||||
"pubschema": ["test_schema_publication"]
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication"
|
||||
}
|
||||
],
|
||||
"pubschema": [
|
||||
"test_schema_publication"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables_schemas.sql",
|
||||
"expected_msql_file": "create_publication_few_tables_schemas_msql.sql"
|
||||
@@ -457,14 +500,19 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication",
|
||||
"columns": ["emp_id", "name"]
|
||||
}],
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication",
|
||||
"columns": [
|
||||
"emp_id",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
],
|
||||
"pubschema": ""
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables_columns.sql",
|
||||
@@ -490,14 +538,16 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication",
|
||||
"where": "emp_id=2 and name='test'"
|
||||
}],
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication",
|
||||
"where": "emp_id=2 and name='test'"
|
||||
}
|
||||
],
|
||||
"pubschema": ""
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables_where.sql",
|
||||
@@ -523,15 +573,20 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication",
|
||||
"columns": ["emp_id", "name"],
|
||||
"where": "emp_id=2 and name='test'"
|
||||
}],
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication",
|
||||
"columns": [
|
||||
"emp_id",
|
||||
"name"
|
||||
],
|
||||
"where": "emp_id=2 and name='test'"
|
||||
}
|
||||
],
|
||||
"pubschema": ""
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables_columns_where.sql",
|
||||
@@ -557,15 +612,19 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"publish_via_partition_root": false,
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": [{
|
||||
"table_name": "public.test_table_publication",
|
||||
"where": "emp_id=2 and name='test'"
|
||||
}],
|
||||
"pubschema": ["test_schema_publication"]
|
||||
"pubtable": [
|
||||
{
|
||||
"table_name": "public.test_table_publication",
|
||||
"where": "emp_id=2 and name='test'"
|
||||
}
|
||||
],
|
||||
"pubschema": [
|
||||
"test_schema_publication"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables_where_schemas.sql",
|
||||
"expected_msql_file": "create_publication_few_tables_where_schemas_msql.sql"
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": false,
|
||||
"evnt_truncate": false,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"all_table": true,
|
||||
"pubtable": ""
|
||||
},
|
||||
@@ -115,10 +115,12 @@
|
||||
"evnt_update": true,
|
||||
"evnt_delete": true,
|
||||
"evnt_truncate": true,
|
||||
"pubowner": "postgres",
|
||||
"pubowner": "<OWNER>",
|
||||
"all_table": false,
|
||||
"only_table": false,
|
||||
"pubtable": ["public.test_table_publication"]
|
||||
"pubtable": [
|
||||
"public.test_table_publication"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "create_publication_few_tables.sql",
|
||||
"expected_msql_file": "create_publication_few_tables_msql.sql"
|
||||
@@ -130,7 +132,10 @@
|
||||
"sql_endpoint": "NODE-publication.sql_id",
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": ["public.test_table_publication", "public.test_table_publication_2"]
|
||||
"pubtable": [
|
||||
"public.test_table_publication",
|
||||
"public.test_table_publication_2"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "alter_publication_add_tables.sql",
|
||||
"expected_msql_file": "alter_publication_add_tables_msql.sql"
|
||||
@@ -142,7 +147,9 @@
|
||||
"sql_endpoint": "NODE-publication.sql_id",
|
||||
"msql_endpoint": "NODE-publication.msql_id",
|
||||
"data": {
|
||||
"pubtable": ["public.test_table_publication_2"]
|
||||
"pubtable": [
|
||||
"public.test_table_publication_2"
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "alter_publication_drop_tables.sql",
|
||||
"expected_msql_file": "alter_publication_drop_tables_msql.sql"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"data": {
|
||||
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
|
||||
"schema": "testschema",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"description": "Description for extra params",
|
||||
"locale": "locale",
|
||||
"provider": "icu",
|
||||
@@ -70,7 +70,7 @@
|
||||
"data": {
|
||||
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
|
||||
"schema": "testschema",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"description": "libc",
|
||||
"locale": "C",
|
||||
"provider": "libc",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"data": {
|
||||
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
|
||||
"schema": "testschema",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"description": "Description for extra params",
|
||||
"locale": "locale",
|
||||
"provider": "icu",
|
||||
@@ -70,7 +70,7 @@
|
||||
"data": {
|
||||
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
|
||||
"schema": "testschema",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"description": "libc",
|
||||
"locale": "C",
|
||||
"provider": "libc",
|
||||
@@ -95,7 +95,7 @@
|
||||
"data": {
|
||||
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
|
||||
"schema": "testschema",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"description": "builtin",
|
||||
"locale": "C",
|
||||
"provider": "builtin",
|
||||
|
||||
@@ -8,7 +8,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
DEFAULT 5
|
||||
NOT NULL;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
|
||||
|
||||
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
COLLATE pg_catalog."C"
|
||||
DEFAULT 3;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
|
||||
|
||||
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#"
|
||||
COLLATE pg_catalog."C"
|
||||
DEFAULT 3;
|
||||
|
||||
ALTER DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
ALTER DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#"
|
||||
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
|
||||
|
||||
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
COLLATE pg_catalog."C"
|
||||
DEFAULT 5;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
|
||||
|
||||
@@ -8,7 +8,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
DEFAULT 5
|
||||
NOT NULL;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
|
||||
|
||||
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
DEFAULT 3
|
||||
NOT NULL;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'test_comment';
|
||||
@@ -4,7 +4,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
DEFAULT 5
|
||||
NOT NULL;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
|
||||
|
||||
@@ -3,7 +3,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
DEFAULT 3
|
||||
NOT NULL;
|
||||
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
|
||||
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
|
||||
|
||||
COMMENT ON DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
|
||||
IS 'test_comment';
|
||||
@@ -9,7 +9,7 @@
|
||||
"name": "Dom1_$%{}[]()&*^!@\"'`\\/#",
|
||||
"schema": "public",
|
||||
"schema_id": "<SCHEMA_ID>",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"basensp": "public",
|
||||
"description": "test_comment",
|
||||
"basetype": "text",
|
||||
@@ -90,7 +90,7 @@
|
||||
"name": "Dom1_$%{}[]()&*^!@\"'`\\/#",
|
||||
"schema": "public",
|
||||
"schema_id": "<SCHEMA_ID>",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"basensp": "public",
|
||||
"description": "test_comment",
|
||||
"basetype": "numeric",
|
||||
|
||||
@@ -101,13 +101,16 @@ class DomainReverseEngineeredSQLTestCase(BaseTestGenerator):
|
||||
|
||||
# Replace multiple spaces with one space and check the expected sql
|
||||
sql = re.sub(r'\s+', ' ', orig_sql).strip()
|
||||
from pgadmin.utils.driver.psycopg3 import Driver
|
||||
expected_sql = '-- DOMAIN: {0}.{1} -- DROP DOMAIN IF EXISTS ' \
|
||||
'{0}.{1}; CREATE DOMAIN {0}.{1} {2} ' \
|
||||
'ALTER DOMAIN {0}.{1} OWNER' \
|
||||
' TO {3};'.format(self.schema_name,
|
||||
self.test_data['domain_name'],
|
||||
self.test_data['domain_sql'],
|
||||
self.server["username"])
|
||||
Driver.qtIdent(
|
||||
None,
|
||||
self.server["username"]))
|
||||
|
||||
self.assertEqual(sql, expected_sql)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"endpoint": "NODE-foreign_server.obj",
|
||||
"sql_endpoint": "NODE-foreign_server.sql_id",
|
||||
"data": {
|
||||
"name":"test_fs_for_foreign_table_column"
|
||||
"name": "test_fs_for_foreign_table_column"
|
||||
},
|
||||
"store_object_id": "True"
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
"schema": "public",
|
||||
"basensp": "public",
|
||||
"ftsrvname": "test_fs_for_foreign_table_column",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"ftoptions": [],
|
||||
"inherits": [],
|
||||
"relacl": [],
|
||||
@@ -51,9 +51,9 @@
|
||||
"name": "col_1_$%{}[]()&*^!@\"'`\\/#",
|
||||
"description": "Comment for create",
|
||||
"cltype": "numeric",
|
||||
"attlen":"10",
|
||||
"attprecision":"5",
|
||||
"colconstype":"n",
|
||||
"attlen": "10",
|
||||
"attprecision": "5",
|
||||
"colconstype": "n",
|
||||
"attnotnull": true,
|
||||
"attoptions": [],
|
||||
"seclabels": [],
|
||||
@@ -73,7 +73,14 @@
|
||||
"name": "new_col_1_$%{}[]()&*^!@\"'`\\/#",
|
||||
"description": "Comment for alter",
|
||||
"cltype": "real",
|
||||
"coloptions":{"added":[{"option":"column_name","value":"test"}]}
|
||||
"coloptions": {
|
||||
"added": [
|
||||
{
|
||||
"option": "column_name",
|
||||
"value": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_column_numeric.sql",
|
||||
"expected_msql_file": "alter_column_numeric.msql"
|
||||
@@ -153,7 +160,7 @@
|
||||
"data": {
|
||||
"attlen": "10",
|
||||
"attnotnull": false,
|
||||
"attnum":3,
|
||||
"attnum": 3,
|
||||
"description": "test comment modification"
|
||||
},
|
||||
"expected_sql_file": "alter_column_timestamp_with_default_value_using_function.sql",
|
||||
@@ -174,16 +181,28 @@
|
||||
"endpoint": "NODE-foreign_table_column.obj",
|
||||
"sql_endpoint": "NODE-foreign_table_column.sql_id",
|
||||
"msql_endpoint": "NODE-foreign_table_column.msql",
|
||||
"data": {"name":"col_4_$%{}[]()&*^!@\"'`\\/#",
|
||||
"description":"test comment",
|
||||
"cltype":"bigint",
|
||||
"attnotnull":true,
|
||||
"coloptions":[{"option":"column_name","value":"test_options"}],
|
||||
"colconstype":"g",
|
||||
"is_tlength":false,
|
||||
"is_precision":false,
|
||||
"genexpr":"1000+1",
|
||||
"attoptions":[{"name":"n_distinct","value":"1"}]},
|
||||
"data": {
|
||||
"name": "col_4_$%{}[]()&*^!@\"'`\\/#",
|
||||
"description": "test comment",
|
||||
"cltype": "bigint",
|
||||
"attnotnull": true,
|
||||
"coloptions": [
|
||||
{
|
||||
"option": "column_name",
|
||||
"value": "test_options"
|
||||
}
|
||||
],
|
||||
"colconstype": "g",
|
||||
"is_tlength": false,
|
||||
"is_precision": false,
|
||||
"genexpr": "1000+1",
|
||||
"attoptions": [
|
||||
{
|
||||
"name": "n_distinct",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected_sql_file": "create_column_with_integer_generated_always_column_option_variables.sql",
|
||||
"expected_msql_file": "create_column_with_integer_generated_always_column_option_variables.msql"
|
||||
},
|
||||
@@ -193,11 +212,25 @@
|
||||
"endpoint": "NODE-foreign_table_column.obj_id",
|
||||
"sql_endpoint": "NODE-foreign_table_column.sql_id",
|
||||
"msql_endpoint": "NODE-foreign_table_column.msql_id",
|
||||
"data":{
|
||||
"attoptions":{
|
||||
"changed":[{"name":"n_distinct","value":"111"}]},
|
||||
"coloptions":{"changed":[{"option":"column_name","value":"test_options_update"}]},
|
||||
"attnum":4},
|
||||
"data": {
|
||||
"attoptions": {
|
||||
"changed": [
|
||||
{
|
||||
"name": "n_distinct",
|
||||
"value": "111"
|
||||
}
|
||||
]
|
||||
},
|
||||
"coloptions": {
|
||||
"changed": [
|
||||
{
|
||||
"option": "column_name",
|
||||
"value": "test_options_update"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attnum": 4
|
||||
},
|
||||
"expected_sql_file": "alter_column_with_integer_generated_always_column_option_variables.sql",
|
||||
"expected_msql_file": "alter_column_with_integer_generated_always_column_option_variables.msql"
|
||||
},
|
||||
@@ -207,7 +240,8 @@
|
||||
"endpoint": "NODE-foreign_table_column.obj_id",
|
||||
"sql_endpoint": "NODE-foreign_table_columnn.sql_id",
|
||||
"data": {
|
||||
"name":"col_4_$%{}[]()&*^!@\"'`\\/#"
|
||||
"name": "col_4_$%{}[]()&*^!@\"'`\\/#"
|
||||
}
|
||||
}
|
||||
]}
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"endpoint": "NODE-foreign_server.obj",
|
||||
"sql_endpoint": "NODE-foreign_server.sql_id",
|
||||
"data": {
|
||||
"name":"test_fs_for_foreign_table_column"
|
||||
"name": "test_fs_for_foreign_table_column"
|
||||
},
|
||||
"store_object_id": "True"
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
"schema": "public",
|
||||
"basensp": "public",
|
||||
"ftsrvname": "test_fs_for_foreign_table_column",
|
||||
"owner": "postgres",
|
||||
"owner": "<OWNER>",
|
||||
"ftoptions": [],
|
||||
"inherits": [],
|
||||
"relacl": [],
|
||||
@@ -51,10 +51,10 @@
|
||||
"name": "col_1_$%{}[]()&*^!@\"'`\\/#",
|
||||
"description": "Comment for create",
|
||||
"cltype": "numeric",
|
||||
"attlen":"10",
|
||||
"attprecision":"5",
|
||||
"coloptions":[],
|
||||
"colconstype":"n",
|
||||
"attlen": "10",
|
||||
"attprecision": "5",
|
||||
"coloptions": [],
|
||||
"colconstype": "n",
|
||||
"attnotnull": true,
|
||||
"attoptions": [],
|
||||
"seclabels": [],
|
||||
@@ -74,7 +74,14 @@
|
||||
"name": "new_col_1_$%{}[]()&*^!@\"'`\\/#",
|
||||
"description": "Comment for alter",
|
||||
"cltype": "real",
|
||||
"coloptions":{"added":[{"option":"column_name","value":"test"}]}
|
||||
"coloptions": {
|
||||
"added": [
|
||||
{
|
||||
"option": "column_name",
|
||||
"value": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected_sql_file": "alter_column_numeric.sql",
|
||||
"expected_msql_file": "alter_column_numeric.msql"
|
||||
@@ -127,4 +134,5 @@
|
||||
"name": "col_2_$%{}[]()&*^!@\"'`\\/#"
|
||||
}
|
||||
}
|
||||
]}
|
||||
]
|
||||
}
|
||||