module

Krikri::PluginHelpers::PostgresqlAcl

PostgresqlAcl - pure logic for parsing a PostgreSQL ACL array (the relacl/nspacl/datacl columns of pg_class/pg_namespace/ pg_database, cast to ::text in the query so the driver hands back a plain string rather than needing a native aclitem[] codec) and mapping privilege names to their single-letter codes. No I/O - postgresql_privs.cr does the actual GRANT/REVOKE and ACL lookups.

Format verified against a real PostgreSQL 17 server, not assumed from docs: {postgres=arwdDxtm/postgres,bob=rw/postgres,alice=r*/postgres}

  • comma-separated grantee=privs/grantor entries inside {}; an empty grantee (=r/postgres) means the PUBLIC pseudo-role; a * immediately following a privilege letter means that specific privilege carries WITH GRANT OPTION (not a whole-entry flag - two privileges on the same entry can differ, e.g. r*w is SELECT WITH GRANT OPTION plus a plain UPDATE).

Constants

ALL_PRIVS = {"table" => ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"] of ::String, "sequence" => ["SELECT", "UPDATE", "USAGE"] of ::String, "schema" => ["CREATE", "USAGE"] of ::String, "database" => ["CREATE", "CONNECT", "TEMPORARY"] of ::String, "language" => ["USAGE"] of ::String, "tablespace" => ["CREATE"] of ::String, "type" => ["USAGE"] of ::String, "foreign_data_wrapper" => ["USAGE"] of ::String, "foreign_server" => ["USAGE"] of ::String, "function" => ["EXECUTE"] of ::String, "procedure" => ["EXECUTE"] of ::String, "parameter" => ["SET", "ALTER_SYSTEM"] of ::String}

"ALL"/"ALL PRIVILEGES" expands to every privilege real PostgreSQL grants under GRANT ALL ON <type> ... for that object type - deliberately excludes MAINTAIN for table/sequence even though it's a real privilege letter above, matching real PostgreSQL's own GRANT ALL behavior verified against a real server (ALL does not imply MAINTAIN pre-17, and even on 17 the module's own real-Ansible behavior this codebase matches doesn't special-case it in ALL's expansion either). A lookup table rather than a case/when chain, same reasoning as PostgresqlPrivsPlugin::OBJECT_KINDS - keeps this well under ameba's cyclomatic-complexity budget regardless of how many object types get added.

PRIV_LETTERS = {"table" => {"SELECT" => 'r', "INSERT" => 'a', "UPDATE" => 'w', "DELETE" => 'd', "TRUNCATE" => 'D', "REFERENCES" => 'x', "TRIGGER" => 't', "MAINTAIN" => 'm'}, "sequence" => {"SELECT" => 'r', "UPDATE" => 'w', "USAGE" => 'U'}, "schema" => {"CREATE" => 'C', "USAGE" => 'U'}, "database" => {"CREATE" => 'C', "CONNECT" => 'c', "TEMPORARY" => 'T', "TEMP" => 'T'}, "language" => {"USAGE" => 'U'}, "tablespace" => {"CREATE" => 'C'}, "type" => {"USAGE" => 'U'}, "foreign_data_wrapper" => {"USAGE" => 'U'}, "foreign_server" => {"USAGE" => 'U'}, "function" => {"EXECUTE" => 'X'}, "procedure" => {"EXECUTE" => 'X'}, "parameter" => {"SET" => 's', "ALTER_SYSTEM" => 'A'}}

Privilege name -> single-letter ACL code, per object type. Real PostgreSQL's own privilege letters (see the GRANT/\dp docs), verified against actual relacl/nspacl/datacl output rather than assumed. MAINTAIN (PostgreSQL 17+) is included for table/sequence since it's harmless to recognize even on an older server (a request for it there would just fail with the server's own "unrecognized privilege" error, same as any other server-version-specific privilege).

Class methods

all_privs(type : String) : Array(String)
Source
has_grant_option?(parsed : Hash(String, Hash(Char, Bool)), grantee : String, letter : Char) : Bool
Source
has_privilege?(parsed : Hash(String, Hash(Char, Bool)), grantee : String, letter : Char) : Bool
Source
letter_for(type : String, priv : String) : Char
Source
parse(acl_text : String | Nil) : Hash(String, Hash(Char, Bool))

Parses a relacl/nspacl/datacl ::text value into {grantee_name => {letter => grant_option}}. PUBLIC's entry (an empty grantee before the =) is keyed under the literal string "PUBLIC" for callers' convenience, not "". nil/empty input (a SQL NULL ACL column, meaning no explicit grants exist yet beyond the object's implicit owner/PUBLIC defaults) parses to an empty hash - every privilege then reads as "not granted", which is the correct default for GRANT idempotency purposes here even though it doesn't reflect PostgreSQL's own implicit-default grants (documented simplification, matching how postgresql_user.cr already doesn't compare against inherited role membership either).

Source
resolve_privs(type : String, privs : String) : Array(String)

Resolves "ALL"/"ALL PRIVILEGES" or a comma-separated privs: list into individual privilege names, validating each against the known set for type.

Source

Nested types