Skip to content

Preserve literal passwords in PostgreSQL service files - #1631

Open
jackwalkerlabs wants to merge 3 commits into
dbcli:mainfrom
jackwalkerlabs:fix/service-password-literals-1512
Open

jackwalkerlabs wants to merge 3 commits into
dbcli:mainfrom
jackwalkerlabs:fix/service-password-literals-1512

Conversation

@jackwalkerlabs

@jackwalkerlabs jackwalkerlabs commented Sep 8, 2026

Copy link
Copy Markdown

Service-file passwords containing hashes, percent signs, commas, or quote characters now reach PostgreSQL unchanged, matching libpq and psql. The parser preserves leading comments and original error line numbers, and unknown service names retain the existing error-handling behavior.

Fixes #1512. This is a breaking change for values previously quoted only to escape the old parser: those quotes must be removed. The changelog now states that explicitly. The parser comment and AUTHORS entry follow the maintainer's requested wording changes. Main configuration parsing is handled separately in #1635.

Validation of current head 7726fceee3b25b4bcd65a5ec1f44fe7779d9829c: independent Factory source/UX review approved; isolated offline Python 3.11.2 verification passed the configuration/prompt and selected startup tests, eight service-file test cases, 16 configuration compatibility checks, and Ruff lint/format. Full live-PostgreSQL integration and the supported-Python matrix were not rerun for this revision.

Factory run: FC-20260913-EEB554. The original PR and branch were retained, with a descendant commit and current upstream changes merged in.

AI disclosure: The original implementation used Codex. This revision was implemented, independently reviewed, and verified by hosted FactoryChief using Codex under the account owner's authorization; no human code review is claimed.

Checklist

  • Added changelog entry.
  • Added submitting account to AUTHORS.
  • Installed pre-commit hooks for the original contribution; current revision passed Ruff lint and formatting directly.
  • Verified behavior and ran the checks listed above.
  • Please squash merge.

Fixes dbcli#1512. Implemented and validated with Codex.
Comment thread pgcli/main.py Outdated
err.line_number += skipped_lines
raise err
# libpq treats values literally: hashes, commas, quotes and percent
# signs are part of the value, not ConfigObj comments or syntax.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense in the diff, but it looks odd to reference ConfigObj in a comment here while the module makes not reference to it at all. I'd remove that part.

Suggested change
# signs are part of the value, not ConfigObj comments or syntax.
# signs are part of the value.

Comment thread AUTHORS Outdated
* Chris (ChrisJr404)
* Pieter Ouwerkerk (pouwerkerk)

* jackwalkerlabs (Codex-assisted)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the empty line above this line. Also, the "Codex-assisted" comment is not warranted. I appreciate that you mentioned it in the pull request description. But here in the changelog it's... you: I suppose that you do have an existence outside Codex. And who knows, maybe you could contribute without Codex, someday! ;)

(I suppose that Codex wrote that comment. That makes me a bit sad, actually.)

Comment thread changelog.rst Outdated
Comment on lines +25 to +27
* Preserve hashes, percent signs, commas and quotes in service-file passwords,
matching libpq. Quotes around service-file values are now literal; remove
quotes previously added solely to escape ConfigObj syntax.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make it clearer that it's a breaking change. Also, I would not reference ConfigObj, which is an implementation detail. Suggestion:

Suggested change
* Preserve hashes, percent signs, commas and quotes in service-file passwords,
matching libpq. Quotes around service-file values are now literal; remove
quotes previously added solely to escape ConfigObj syntax.
* [breaking change] Password in service files (usually ``~/.pg_service.conf``) are now read as they appear, as `libpq` and `psql` do. Previously, hashes, percent signs and other values had to be quoted. They must not be quoted anymore.

@j-bennet: it's a (small) breaking change, I think it would warrant a new minor version (from 4.6.0 to 4.7.0). Do you agree?

Comment thread pgcli/main.py
Comment thread pgcli/main.py
Comment thread pgcli/main.py
@@ -1,5 +1,5 @@
from zoneinfo import ZoneInfoNotFoundError
from configobj import ConfigObj, ParseError
from configparser import ConfigParser

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to drop the configobj dependency. pgcli still uses it to parse its main configuration file, but we could probably replace that by configparser as well. There may be incompatibilities/differences between the two, they would have to be listed first (maybe we can ignore them, maybe we cant'). Are you willing to give it a try? (That should be in a separate pull request.)

DiegoDAF added a commit to DiegoDAF/pgcli.daf that referenced this pull request Sep 8, 2026
dbcli#1631 fixes a real bug we share: ConfigObj strips everything after a #
in a service-file password. dbcli#1633 applies too: we rewrite the keyring
password on every successful connection, including one just read from it.
@DiegoDAF

DiegoDAF commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

I hit this same divergence in my fork and had already measured it against libpq, so here is the data in case it is useful for this PR.

Method: a throwaway cluster, and dbname as the probe rather than password, because the server echoes it back verbatim in database "..." does not exist. That reads out exactly what libpq parsed, with no guessing.

in the service file libpq parses ConfigObj parsed this PR
abc#def abc#def abc correct
#abc #abc `` (empty) correct
abc #def abc #def abc correct
a,b a,b ["a", "b"] (a list) correct
'quoted' 'quoted' quoted correct
"quoted" "quoted" quoted correct
a b / a%b / a=b unchanged unchanged correct
abc abc abc correct

So the change matches libpq on all ten cases, including the list one, which I had not expected: a value with a comma became a Python list, not a string.

Two notes from having done the same rewrite:

On itertools.repeat("\n", skipped_lines): it is load-bearing, it keeps the reported line numbers aligned with the real file. Without it, a bad line at file line 6 is reported as line 3. The old code did the same thing differently, with err.line_number += skipped_lines. With the blank lines fed in, ConfigParser produces:

Source contains parsing errors: '/home/daf/.pg_service.conf'
	[line  6]: 'not a valid entry\n'

which is the correct line.

On the unknown-service concern: I could not reproduce a KeyError. The if service not in service_file_config: return None, service_file guard is still above it, and ConfigParser.__contains__ returns False for an unknown section, so parse_service_info("nope") still returns (None, path). I ran the exact code from this PR to check. Worth a regression test either way, since nothing currently pins that contract.

One real behaviour change that has not been mentioned: a malformed file now raises configparser.ParsingError where it used to raise configobj.ParseError. Nothing in pgcli catches either, so it only changes the traceback, but it is worth deciding deliberately rather than by accident.

Happy to contribute the parametrized version of that table as a test if you want it, it maps one-to-one onto the rows above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pgcli couldn't parse password that contained character '#'

3 participants