File Coverage

blib/lib/auto/share/dist/App-Dochazka-REST/sql/audit_Config.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 8 8 100.0


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32             #
33             # sql/audit_Config.pm
34             #
35             # - audit-related SQL statements
36             # - code to initialize audit schema and related triggers
37              
38             #
39             # the audit trigger expects to be able to do "SELECT current_setting('dochazka.eid')"
40             # to get the EID of the current user. Therefore, when the connection is established
41             # we need to always do "SET dochazka.eid = $EID"
42             set( 'SQL_SET_DOCHAZKA_EID_GUC', 'SET LOCAL dochazka.eid = ?' );
43              
44             #
45             set( 'DBINIT_AUDIT', [
46              
47             q#
48             -- An audit history is important on most tables. Provide an audit trigger that logs to
49             -- a dedicated audit table for the major relations.
50             --
51             -- This file should be generic and not depend on application roles or structures,
52             -- as it's being listed here:
53             --
54             -- https://wiki.postgresql.org/wiki/Audit_trigger_91plus
55             --
56             -- This trigger was originally based on
57             -- http://wiki.postgresql.org/wiki/Audit_trigger
58             -- but has been completely rewritten.
59             --
60             #,
61              
62             q#
63             CREATE SCHEMA audit
64             #,
65              
66             q#
67             REVOKE ALL ON SCHEMA audit FROM public
68             #,
69              
70             q#
71             COMMENT ON SCHEMA audit IS 'Out-of-table audit/history logging tables and trigger functions'
72             #,
73              
74             q#
75             --
76             -- Audited data. Lots of information is available, it's just a matter of how much
77             -- you really want to record. See:
78             --
79             -- http://www.postgresql.org/docs/9.1/static/functions-info.html
80             --
81             -- Remember, every column you add takes up more audit table space and slows audit
82             -- inserts.
83             --
84             -- Every index you add has a big impact too, so avoid adding indexes to the
85             -- audit table unless you REALLY need them. The hstore GIST indexes are
86             -- particularly expensive.
87             --
88             -- It is sometimes worth copying the audit table, or a coarse subset of it that
89             -- you're interested in, into a temporary table where you CREATE any useful
90             -- indexes and do your analysis.
91             --
92             CREATE TABLE audit.logged_actions (
93             event_id bigserial primary key,
94             schema_name text not null,
95             table_name text not null,
96             relid oid not null,
97             session_user_name text,
98             action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL,
99             action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL,
100             action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL,
101             transaction_id bigint,
102             dochazka_eid integer,
103             client_addr inet,
104             client_port integer,
105             client_query text,
106             action TEXT NOT NULL CHECK (action IN ('I','D','U', 'T')),
107             row_data hstore,
108             changed_fields hstore,
109             statement_only boolean not null
110             )
111             #,
112              
113             q#REVOKE ALL ON audit.logged_actions FROM public#,
114              
115             q#COMMENT ON TABLE audit.logged_actions IS 'History of auditable actions on audited tables, from audit.if_modified_func()'#,
116              
117             q#COMMENT ON COLUMN audit.logged_actions.event_id IS 'Unique identifier for each auditable event'#,
118              
119             q#COMMENT ON COLUMN audit.logged_actions.schema_name IS 'Database schema audited table for this event is in'#,
120              
121             q#COMMENT ON COLUMN audit.logged_actions.table_name IS 'Non-schema-qualified table name of table event occured in'#,
122              
123             q#COMMENT ON COLUMN audit.logged_actions.relid IS 'Table OID. Changes with drop/create. Get with ''tablename''::regclass'#,
124              
125             q#COMMENT ON COLUMN audit.logged_actions.session_user_name IS 'Login / session user whose statement caused the audited event'#,
126              
127             q#COMMENT ON COLUMN audit.logged_actions.action_tstamp_tx IS 'Transaction start timestamp for tx in which audited event occurred'#,
128              
129             q#COMMENT ON COLUMN audit.logged_actions.action_tstamp_stm IS 'Statement start timestamp for tx in which audited event occurred'#,
130              
131             q#COMMENT ON COLUMN audit.logged_actions.action_tstamp_clk IS 'Wall clock time at which audited event''s trigger call occurred'#,
132              
133             q#COMMENT ON COLUMN audit.logged_actions.transaction_id IS 'Identifier of transaction that made the change. May wrap, but unique paired with action_tstamp_tx.'#,
134              
135             q#COMMENT ON COLUMN audit.logged_actions.client_addr IS 'IP address of client that issued query. Null for unix domain socket.'#,
136              
137             q#COMMENT ON COLUMN audit.logged_actions.client_port IS 'Remote peer IP port address of client that issued query. Undefined for unix socket.'#,
138              
139             q#COMMENT ON COLUMN audit.logged_actions.client_query IS 'Top-level query that caused this auditable event. May be more than one statement.'#,
140              
141             q#COMMENT ON COLUMN audit.logged_actions.dochazka_eid IS 'Dochazka EID of the employee who conducted the database operation.'#,
142              
143             q#COMMENT ON COLUMN audit.logged_actions.action IS 'Action type; I = insert, D = delete, U = update, T = truncate'#,
144              
145             q#COMMENT ON COLUMN audit.logged_actions.row_data IS 'Record value. Null for statement-level trigger. For INSERT this is the new tuple. For DELETE and UPDATE it is the old tuple.'#,
146              
147             q#COMMENT ON COLUMN audit.logged_actions.changed_fields IS 'New values of fields changed by UPDATE. Null except for row-level UPDATE events.'#,
148              
149             q#COMMENT ON COLUMN audit.logged_actions.statement_only IS '''t'' if audit event is from an FOR EACH STATEMENT trigger, ''f'' for FOR EACH ROW'#,
150              
151             q#CREATE INDEX logged_actions_relid_idx ON audit.logged_actions(relid)#,
152              
153             q#CREATE INDEX logged_actions_action_tstamp_tx_stm_idx ON audit.logged_actions(action_tstamp_stm)#,
154              
155             q#CREATE INDEX logged_actions_action_idx ON audit.logged_actions(action)#,
156              
157             q#CREATE OR REPLACE FUNCTION audit.get_dochazka_eid() RETURNS INTEGER AS $body$
158             BEGIN
159             RETURN current_setting('dochazka.eid');
160             EXCEPTION
161             WHEN OTHERS THEN
162             RETURN -1;
163             END;
164             $body$
165             LANGUAGE plpgsql
166             #,
167              
168             q#CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS TRIGGER AS $body$
169             DECLARE
170             audit_row audit.logged_actions;
171             include_values boolean;
172             log_diffs boolean;
173             h_old hstore;
174             h_new hstore;
175             excluded_cols text[] = ARRAY[]::text[];
176             BEGIN
177             IF TG_WHEN <> 'AFTER' THEN
178             RAISE EXCEPTION 'audit.if_modified_func() may only run as an AFTER trigger';
179             END IF;
180              
181             audit_row = ROW(
182             nextval('audit.logged_actions_event_id_seq'), -- event_id
183             TG_TABLE_SCHEMA::text, -- schema_name
184             TG_TABLE_NAME::text, -- table_name
185             TG_RELID, -- relation OID for much quicker searches
186             session_user::text, -- session_user_name
187             current_timestamp, -- action_tstamp_tx
188             statement_timestamp(), -- action_tstamp_stm
189             clock_timestamp(), -- action_tstamp_clk
190             txid_current(), -- transaction ID
191             audit.get_dochazka_eid(), -- client application
192             inet_client_addr(), -- client_addr
193             inet_client_port(), -- client_port
194             current_query(), -- top-level query or queries (if multistatement) from client
195             substring(TG_OP,1,1), -- action
196             NULL, NULL, -- row_data, changed_fields
197             'f' -- statement_only
198             );
199              
200             IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN
201             audit_row.client_query = NULL;
202             END IF;
203              
204             IF TG_ARGV[1] IS NOT NULL THEN
205             excluded_cols = TG_ARGV[1]::text[];
206             END IF;
207            
208             IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
209             audit_row.row_data = hstore(OLD.*);
210             audit_row.changed_fields = (hstore(NEW.*) - audit_row.row_data) - excluded_cols;
211             IF audit_row.changed_fields = hstore('') THEN
212             -- All changed fields are ignored. Skip this update.
213             RETURN NULL;
214             END IF;
215             ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
216             audit_row.row_data = hstore(OLD.*) - excluded_cols;
217             ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
218             audit_row.row_data = hstore(NEW.*) - excluded_cols;
219             ELSIF (TG_LEVEL = 'STATEMENT' AND TG_OP IN ('INSERT','UPDATE','DELETE','TRUNCATE')) THEN
220             audit_row.statement_only = 't';
221             ELSE
222             RAISE EXCEPTION '[audit.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
223             RETURN NULL;
224             END IF;
225             INSERT INTO audit.logged_actions VALUES (audit_row.*);
226             RETURN NULL;
227             END;
228             $body$
229             LANGUAGE plpgsql
230             SECURITY DEFINER
231             SET search_path = pg_catalog, public
232             #,
233              
234             q#COMMENT ON FUNCTION audit.if_modified_func() IS $body$
235             Track changes to a table at the statement and/or row level.
236              
237             Optional parameters to trigger in CREATE TRIGGER call:
238              
239             param 0: boolean, whether to log the query text. Default 't'.
240              
241             param 1: text[], columns to ignore in updates. Default [].
242              
243             Updates to ignored cols are omitted from changed_fields.
244              
245             Updates with only ignored cols changed are not inserted
246             into the audit log.
247              
248             Almost all the processing work is still done for updates
249             that ignored. If you need to save the load, you need to use
250             WHEN clause on the trigger instead.
251              
252             No warning or error is issued if ignored_cols contains columns
253             that do not exist in the target table. This lets you specify
254             a standard set of ignored columns.
255              
256             There is no parameter to disable logging of values. Add this trigger as
257             a 'FOR EACH STATEMENT' rather than 'FOR EACH ROW' trigger if you do not
258             want to log row values.
259              
260             Note that the user name logged is the login role for the session. The audit trigger
261             cannot obtain the active role because it is reset by the SECURITY DEFINER invocation
262             of the audit trigger its self.
263             $body$;
264             #,
265              
266              
267             q#CREATE OR REPLACE FUNCTION audit.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean, ignored_cols text[]) RETURNS void AS $body$
268             DECLARE
269             stm_targets text = 'INSERT OR UPDATE OR DELETE OR TRUNCATE';
270             _q_txt text;
271             _ignored_cols_snip text = '';
272             BEGIN
273             EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_row ON ' || target_table;
274             EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_stm ON ' || target_table;
275              
276             IF audit_rows THEN
277             IF array_length(ignored_cols,1) > 0 THEN
278             _ignored_cols_snip = ', ' || quote_literal(ignored_cols);
279             END IF;
280             _q_txt = 'CREATE TRIGGER audit_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
281             target_table ||
282             ' FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func(' ||
283             quote_literal(audit_query_text) || _ignored_cols_snip || ');';
284             RAISE NOTICE '%',_q_txt;
285             EXECUTE _q_txt;
286             stm_targets = 'TRUNCATE';
287             ELSE
288             END IF;
289              
290             _q_txt = 'CREATE TRIGGER audit_trigger_stm AFTER ' || stm_targets || ' ON ' ||
291             target_table ||
292             ' FOR EACH STATEMENT EXECUTE PROCEDURE audit.if_modified_func('||
293             quote_literal(audit_query_text) || ');';
294             RAISE NOTICE '%',_q_txt;
295             EXECUTE _q_txt;
296              
297             END;
298             $body$
299             language 'plpgsql';
300             #,
301              
302             q#COMMENT ON FUNCTION audit.audit_table(regclass, boolean, boolean, text[]) IS $body$
303             Add auditing support to a table.
304              
305             Arguments:
306             target_table: Table name, schema qualified if not on search_path
307             audit_rows: Record each row change, or only audit at a statement level
308             audit_query_text: Record the text of the client query that triggered the audit event?
309             ignored_cols: Columns to exclude from update diffs, ignore updates that change only ignored cols.
310             $body$;
311              
312             -- Pg doesn't allow variadic calls with 0 params, so provide a wrapper
313             CREATE OR REPLACE FUNCTION audit.audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean) RETURNS void AS $body$
314             SELECT audit.audit_table($1, $2, $3, ARRAY[]::text[]);
315             $body$ LANGUAGE SQL;
316             #,
317              
318             q#
319             -- And provide a convenience call wrapper for the simplest case
320             -- of row-level logging with no excluded cols and query logging enabled.
321             --
322             CREATE OR REPLACE FUNCTION audit.audit_table(target_table regclass) RETURNS void AS $$
323             SELECT audit.audit_table($1, BOOLEAN 't', BOOLEAN 't');
324             $$ LANGUAGE 'sql';
325             #,
326              
327             q#
328             COMMENT ON FUNCTION audit.audit_table(regclass) IS $body$
329             Add auditing support to the given table. Row-level changes will be logged with full client query text. No cols are ignored.
330             $body$;
331             #,
332              
333             ] );
334              
335             #
336             set( 'DBINIT_CREATE_AUDIT_TRIGGERS',
337              
338             q#SELECT audit.audit_table(
339             '?', 'true', 'false', '{version_col, changed_by, changed_timestamp}'::text[]
340             )#,
341              
342             );
343              
344              
345             # -----------------------------------
346             # DO NOT EDIT ANYTHING BELOW THIS LINE
347             # -----------------------------------
348 41     41   32282 use strict;
  41         108  
  41         1140  
349 41     41   203 use warnings;
  41         87  
  41         1783  
350              
351             1;