IAM appliance - iam-lib-pgconnect

Python library for connecting to PostgreSQL databases in the IAM Appliance without hunting for credentials by hand. Instead of

docker exec -it -u postgres czechidm-db psql -d czechidm

you write, in Python:

import iam_lib_pgconnect

with iam_lib_pgconnect.connect() as db:
    print(db.query("SELECT count(*) FROM idm_identity"))

The library on its own:

  • resolves the container’s IP through the Docker SDK,

  • reads the password from a pwfile on the host,

  • opens a psycopg2 connection.

Connections are read-only by default. The common case is reading data, and the default user is the postgres superuser - so a mistyped UPDATE or DELETE would otherwise write straight into the IdM database with full rights. To modify data you opt in explicitly with readonly=False (see below).

Installation

This library is mainly a dependency of other iam-* utlities so you probably won’t need to explicitly install it. You can of course do that, in case you want to do some DIY scripting, though.

dnf install iam-lib-pgconnect

Library dependencies (python3-psycopg2, python3-docker) are pulled in automatically by dnf.

Required privileges

The library reads two privileged resources, so it only works under root or under an account that has been explicitly granted access to both:

  • Password files on the host at /data/volumes/{db}-db/secrets/*.pwfile, typically owned by root with mode 0600.

  • The Docker API via the local socket /var/run/docker.sock, used to resolve the target container’s IP. Reachable by root or members of the docker group.

Running under any other account fails fast: a PermissionError when reading the pwfile, or a Docker permission error when opening the socket. The library does not attempt to escalate or fall back.

Quick start

Connection to the czechidm DB as user postgres:

import iam_lib_pgconnect

with iam_lib_pgconnect.connect() as db:
    rows = db.query("SELECT id, username FROM idm_identity LIMIT 5")
    for row in rows:
        print(row["id"], row["username"])

rows is a list of dicts, keys are column names.

How to use the library

Reading data (SELECT)

import iam_lib_pgconnect

with iam_lib_pgconnect.connect() as db:
    # No parameters:
    rows = db.query("SELECT count(*) AS total FROM idm_identity")
    print(rows[0]["total"])

    # With parameters (always via %s, never via string interpolation - SQL injection):
    rows = db.query(
        "SELECT id, username FROM idm_identity WHERE username = %s",
        ["admin"],
    )
    print(rows)

Modifying data (INSERT / UPDATE / DELETE)

Connections are read-only by default, so writes require readonly=False:

import iam_lib_pgconnect

with iam_lib_pgconnect.connect(user="czechidm", readonly=False) as db:
    affected = db.execute(
        "UPDATE idm_identity SET modified = now() WHERE username = %s",
        ["admin"],
    )
    print("rows affected:", affected)
# When the `with` block exits without an exception, commit is called automatically.
# If the block raises, a rollback is performed. The connection is always closed.

Without readonly=False, the write fails with psycopg2’s ReadOnlySqlTransaction error before anything is committed.

Multiple statements in one transaction

Same as a single statement - as long as you stay inside with, no commit happens:

import iam_lib_pgconnect

with iam_lib_pgconnect.connect(user="czechidm", readonly=False) as db:
    db.execute("INSERT INTO idm_audit_log (msg) VALUES (%s)", ["start"])
    db.execute("UPDATE idm_identity SET disabled = true WHERE id = %s", [user_id])
    db.execute("INSERT INTO idm_audit_log (msg) VALUES (%s)", ["done"])
    # Commit happens only here, after the block finishes successfully.

If you want to drive commit/rollback manually (e.g. several commits in one connection), call them yourself and don’t use with:

db = iam_lib_pgconnect.connect(user="czechidm", readonly=False)
try:
    db.execute("INSERT ...", [...])
    db.commit()
    db.execute("UPDATE ...", [...])
    db.commit()
finally:
    db.close()

A DB instance other than czechidm

The db parameter drives everything (container {db}-db, database name, pwfile paths):

import iam_lib_pgconnect

with iam_lib_pgconnect.connect(db="bridge") as conn:
    rows = conn.query("SELECT current_database()")
    print(rows)

with iam_lib_pgconnect.connect(db="bridge", user="bridge") as conn:
    rows = conn.query("SELECT current_user")
    print(rows)

Combinations of (db, user) that make sense on a typical appliance:

db user=postgres user=<application name>

czechidm

superuser, full rights

czechidm (restricted)

bridge

superuser, full rights

bridge (restricted)

If the {db}-db container is not running or the pwfile is missing, connect() raises RuntimeError straight away.

API overview

iam_lib_pgconnect.connect(…​)

connect(db='czechidm', user='postgres',
        password_provider=None, host=None, port=5432, dbname=None, readonly=True)
parameter purpose

db

instance identifier, e.g. 'czechidm' or 'bridge'. Default 'czechidm'.

user

DB user, e.g. 'postgres' or 'czechidm'. Default 'postgres'.

password_provider

custom password source; if None, auto-detection is used.

host

DB address; if None, the IP of the {db}-db container is resolved.

port

DB port. Default 5432.

dbname

DB name; if None, the value of db is used.

readonly

open the session read-only. Default True; pass False to allow writes.

Returns IamDbConnection.

IamDbConnection

method what it does

query(sql, params=None)

SELECT - returns list[dict], key = column name.

execute(sql, params=None)

INSERT / UPDATE / DELETE - returns the number of affected rows.

commit()

commits the transaction.

rollback()

rolls the transaction back.

close()

closes the connection.

Context manager (with)

  • No exception in the block → commit() + close().

  • Exception in the block → rollback() + close().

Note: this differs from psycopg2’s behaviour, whose with block does not close the connection.

Password sources - outlook

The way passwords are stored may change over time. The library is built for that: a new source is added as an internal backend and the auto-detection in connect() picks the one available in the current environment. The call itself does not change.

state backend

today

pwfile on the host (/data/volumes/{db}-db/secrets/*.pwfile)

planned

key=value secret storage (e.g. Vault) - once it’s available