@@ -220,3 +220,164 @@ BEGIN
220220END;
221221$$ LANGUAGE plpgsql;
222222
223+ /*
224+ * The `pg_hba_checksum` table, functions, and view are taken from
225+ * https://github.com/CrunchyData/pgmonitor/blob/development/postgres_exporter/common
226+ *
227+ * The goal of these table, functions, and view is to monitor changes
228+ * to the pg_hba_file_rules system catalog.
229+ *
230+ * This material is used in the metric `ccp_pg_hba_checksum`.
231+ */
232+
233+ /*
234+ * `monitor.pg_hba_checksum` table is used to store
235+ * - the pg_hba settings as string (for reference)
236+ * - the pg_hba settings as hash (for quick comparison)
237+ * - the `hba_hash_known_provided` (for overide hash manually given to the `monitor.pg_hba_checksum` function)
238+ * - the `valid` field to signal whether the pg_hba settings have not changed since they were accepted as valid
239+ *
240+ * We create an index on `created_at` in order to pull the most recent entry for
241+ * comparison in the `monitor.pg_hba_checksum` function
242+ */
243+ DROP TABLE IF EXISTS monitor .pg_hba_checksum ;
244+ CREATE TABLE monitor .pg_hba_checksum (
245+ hba_hash_generated text NOT NULL
246+ , hba_hash_known_provided text
247+ , hba_string text NOT NULL
248+ , created_at timestamptz DEFAULT now() NOT NULL
249+ , valid smallint NOT NULL );
250+ COMMENT ON COLUMN monitor.pg_hba_checksum.valid IS ' Set this column to zero if this group of settings is a valid change' ;
251+ CREATE INDEX ON monitor .pg_hba_checksum (created_at);
252+
253+ /*
254+ * `monitor.pg_hba_checksum(text)` is used to compare the previous pg_hba hash
255+ * with a hash made of the current pg_hba hash, derived from the `monitor.pg_hba_hash` view below.
256+ *
257+ * This function returns
258+ * - 0, indicating NO settings have changed
259+ * - 1, indicating something has changed since last known valid state
260+ *
261+ * `monitor.pg_hba_checksum` can take a hash to be used as an override.
262+ * This may be useful when you have a standby with different pg_hba rules;
263+ * since it will have different rules (and therefore a different hash), you
264+ * could alter the metric function to pass the actual hash, which would be
265+ * used in lieu of this table's value (derived from the primary cluster's rules).
266+ */
267+ DROP FUNCTION IF EXISTS monitor .pg_hba_checksum (text );
268+ CREATE FUNCTION monitor .pg_hba_checksum(p_known_hba_hash text DEFAULT NULL )
269+ RETURNS smallint
270+ LANGUAGE plpgsql SECURITY DEFINER
271+ SET search_path TO pg_catalog, pg_temp
272+ AS $function$
273+ DECLARE
274+
275+ v_hba_hash text ;
276+ v_hba_hash_old text ;
277+ v_hba_string text ;
278+ v_is_in_recovery boolean ;
279+ v_valid smallint ;
280+
281+ BEGIN
282+
283+ -- Retrieve the current settings from the `monitor.pg_hba_hash` view below
284+ IF current_setting(' server_version_num' )::int >= 100000 THEN
285+ SELECT sha256_hash, hba_string
286+ INTO v_hba_hash, v_hba_string
287+ FROM monitor .pg_hba_hash ;
288+ ELSE
289+ RAISE EXCEPTION ' pg_hba change monitoring unsupported in versions older than PostgreSQL 10' ;
290+ END IF;
291+
292+ -- Retrieve the last previous hash from the table
293+ SELECT hba_hash_generated, valid
294+ INTO v_hba_hash_old, v_valid
295+ FROM monitor .pg_hba_checksum
296+ ORDER BY created_at DESC LIMIT 1 ;
297+
298+ -- If an manual/override hash has been given, we will use that:
299+ -- Do not base validity on the stored value if manual hash is given.
300+ IF p_known_hba_hash IS NOT NULL THEN
301+ v_hba_hash_old := p_known_hba_hash;
302+ v_valid := 0 ;
303+ END IF;
304+
305+ /* If the table is not empty or a manual hash was given,
306+ * then we want to compare the old hash (from the table)
307+ * with the new hash: if those differ, then we set the validity to 1;
308+ * if they are the same, then we honor what the validity was
309+ * in the table (which would be 1).
310+ */
311+ IF (v_hba_hash_old IS NOT NULL ) THEN
312+ IF (v_hba_hash != v_hba_hash_old) THEN
313+ v_valid := 1 ;
314+ END IF;
315+ ELSE
316+ v_valid := 0 ;
317+ END IF;
318+
319+ /*
320+ * We only want to insert into the table if we're on a primary and
321+ * - the table/manually entered hash is empty, e.g., we've just started the cluster; or
322+ * - the hashes don't match
323+ *
324+ * There's no value added by inserting into the table when no change was detected.
325+ */
326+ IF (v_hba_hash_old IS NULL ) OR (v_hba_hash != v_hba_hash_old) THEN
327+ SELECT pg_is_in_recovery() INTO v_is_in_recovery;
328+ IF v_is_in_recovery = false THEN
329+ INSERT INTO monitor .pg_hba_checksum (
330+ hba_hash_generated
331+ , hba_hash_known_provided
332+ , hba_string
333+ , valid)
334+ VALUES (
335+ v_hba_hash
336+ , p_known_hba_hash
337+ , v_hba_string
338+ , v_valid);
339+ END IF;
340+ END IF;
341+
342+ RETURN v_valid;
343+
344+ END
345+ $function$;
346+
347+ /*
348+ * The `monitor.pg_hba_hash` view return both a hash and a string aggregate of the
349+ * pg_catalog.pg_hba_file_rules.
350+ * Note: We use `sha256` to hash to allow this to run on FIPS environments.
351+ */
352+ DROP VIEW IF EXISTS monitor .pg_hba_hash ;
353+ CREATE VIEW monitor .pg_hba_hash AS
354+ -- Order by line number so it's caught if no content is changed but the order of entries is changed
355+ WITH hba_ordered_list AS (
356+ SELECT COALESCE(type, ' <<NULL>>' ) AS type
357+ , array_to_string(COALESCE(database, ARRAY[' <<NULL>>' ]), ' ,' ) AS database
358+ , array_to_string(COALESCE(user_name, ARRAY[' <<NULL>>' ]), ' ,' ) AS user_name
359+ , COALESCE(address, ' <<NULL>>' ) AS address
360+ , COALESCE(netmask, ' <<NULL>>' ) AS netmask
361+ , COALESCE(auth_method, ' <<NULL>>' ) AS auth_method
362+ , array_to_string(COALESCE(options, ARRAY[' <<NULL>>' ]), ' ,' ) AS options
363+ FROM pg_catalog .pg_hba_file_rules
364+ ORDER BY line_number)
365+ SELECT sha256((string_agg(type|| database|| user_name|| address|| netmask|| auth_method|| options, ' ,' ))::bytea ) AS sha256_hash
366+ , string_agg(type|| database|| user_name|| address|| netmask|| auth_method|| options, ' ,' ) AS hba_string
367+ FROM hba_ordered_list;
368+
369+ /*
370+ * The `monitor.pg_hba_checksum_set_valid` function provides an interface for resetting the
371+ * checksum monitor.
372+ * Note: configuration history will be cleared.
373+ */
374+ DROP FUNCTION IF EXISTS monitor .pg_hba_checksum_set_valid ();
375+ CREATE FUNCTION monitor .pg_hba_checksum_set_valid() RETURNS smallint
376+ LANGUAGE sql
377+ AS $function$
378+
379+ TRUNCATE monitor .pg_hba_checksum ;
380+
381+ SELECT monitor .pg_hba_checksum ();
382+
383+ $function$;
0 commit comments