-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcyanaudit.sql
1635 lines (1384 loc) · 47.8 KB
/
cyanaudit.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
BEGIN;
SET client_min_messages TO warning;
----- INITIAL SETUP -----
do language plpgsql
$$
declare
my_value varchar;
my_version integer[];
my_command varchar;
begin
my_version := regexp_matches(version(), 'PostgreSQL (\d)+\.(\d+)\.(\d+)');
-- Verify minimum version
if my_version < array[9,6,0]::integer[] then
raise exception 'Cyan Audit requires PostgreSQL 9.6.0 or above';
end if;
end;
$$;
set local search_path to '';
CREATE SCHEMA IF NOT EXISTS cyanaudit;
------------------
----- TABLES -----
------------------
-- tb_audit_field
CREATE SEQUENCE IF NOT EXISTS cyanaudit.sq_pk_audit_field;
CREATE TABLE IF NOT EXISTS cyanaudit.tb_audit_field
(
audit_field integer primary key default nextval('cyanaudit.sq_pk_audit_field'),
table_schema varchar not null default 'public',
table_name varchar not null,
column_name varchar not null,
enabled boolean not null,
loggable boolean not null,
CONSTRAINT tb_audit_field_table_column_key
UNIQUE( table_schema, table_name, column_name ),
CONSTRAINT tb_audit_field_tb_audit_event_not_allowed
CHECK( table_schema != 'cyanaudit' )
);
COMMENT ON TABLE cyanaudit.tb_audit_field
IS 'Each row is a column in your database. Toggle logging with the enabled flag.';
ALTER SEQUENCE cyanaudit.sq_pk_audit_field
owned by cyanaudit.tb_audit_field.audit_field;
-- tb_audit_transaction_type
CREATE SEQUENCE IF NOT EXISTS cyanaudit.sq_pk_audit_transaction_type;
CREATE TABLE IF NOT EXISTS cyanaudit.tb_audit_transaction_type
(
audit_transaction_type integer primary key
default nextval('cyanaudit.sq_pk_audit_transaction_type'),
label varchar unique
);
COMMENT ON TABLE cyanaudit.tb_audit_transaction_type
IS 'A label assigned to one or more entries in the audit log.';
ALTER SEQUENCE cyanaudit.sq_pk_audit_transaction_type
owned by cyanaudit.tb_audit_transaction_type.audit_transaction_type;
-- tb_config
CREATE TABLE IF NOT EXISTS cyanaudit.tb_config
(
name varchar(100) primary key,
value text
);
COMMENT ON TABLE cyanaudit.tb_config
IS 'Cyan Audit configuration settings. Do not modify version setting';
-- tb_audit_event
CREATE TABLE IF NOT EXISTS cyanaudit.tb_audit_event
(
audit_field integer not null references cyanaudit.tb_audit_field,
pk_vals text[] not null,
recorded timestamp not null default clock_timestamp(),
uid integer not null,
row_op char(1) not null,
txid bigint not null default txid_current(),
audit_transaction_type integer references cyanaudit.tb_audit_transaction_type,
old_value text,
new_value text,
CONSTRAINT tb_audit_event_consistency_chk
CHECK( case row_op when 'I' then old_value is null
when 'D' then new_value is null
when 'U' then old_value is distinct from new_value
else false end )
);
COMMENT ON TABLE cyanaudit.tb_audit_event
IS 'Parent table for sharded audit logs. Only child tables contain data.';
------------------------
------ FUNCTIONS ------
------------------------
----- User/Application Functions ----
-- fn_set_current_uid
CREATE OR REPLACE FUNCTION cyanaudit.fn_set_current_uid
(
in_uid integer
)
returns integer
language sql strict
as $_$
select set_config( 'cyanaudit.uid', in_uid::varchar, false )::integer;
$_$;
COMMENT ON FUNCTION cyanaudit.fn_set_current_uid( integer )
IS 'Sets the uid to which future operations in this session will be attributed.';
-- fn_get_current_uid
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_current_uid()
returns integer
language plpgsql stable
as $_$
declare
my_uid integer;
begin
my_uid := nullif( current_setting( 'cyanaudit.uid', true ), '' );
if my_uid is null or my_uid < 0 then
select cyanaudit.fn_get_uid_by_username( current_user::varchar )
into my_uid;
end if;
return cyanaudit.fn_set_current_uid( coalesce( my_uid, 0 ) );
end
$_$;
COMMENT ON FUNCTION cyanaudit.fn_get_current_uid()
IS 'Returns the uid set by fn_set_current_uid(), or 0 if unset..';
-- fn_get_last_txid
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_last_txid()
returns bigint
language sql stable
as $_$
select case when current_setting( 'cyanaudit.last_txid', true ) ~ '^\d+$'
then current_setting( 'cyanaudit.last_txid' )
else null
end::bigint;
$_$;
-- fn_get_or_create_audit_transaction_type
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_or_create_audit_transaction_type
(
in_label varchar
)
returns integer
language plpgsql strict
as $_$
declare
my_audit_transaction_type integer;
begin
select audit_transaction_type
into my_audit_transaction_type
from cyanaudit.tb_audit_transaction_type
where label = in_label;
if not found then
insert into cyanaudit.tb_audit_transaction_type
(
label
)
values
(
in_label
)
returning audit_transaction_type
into my_audit_transaction_type;
end if;
return my_audit_transaction_type;
end
$_$;
-- fn_label_transaction
CREATE OR REPLACE FUNCTION cyanaudit.fn_label_transaction
(
in_label varchar,
in_txid bigint default txid_current()
)
returns void
as $_$
update cyanaudit.tb_audit_event
set audit_transaction_type = cyanaudit.fn_get_or_create_audit_transaction_type( in_label )
where txid = in_txid
and audit_transaction_type is null;
$_$
language sql;
COMMENT ON FUNCTION cyanaudit.fn_label_transaction( varchar, bigint )
IS 'Applies description to completed operations in the given transaction.';
-- fn_label_last_transaction
CREATE OR REPLACE FUNCTION cyanaudit.fn_label_last_transaction
(
in_label varchar
)
returns void as
$_$
select cyanaudit.fn_label_transaction
(
in_label,
cyanaudit.fn_get_last_txid()
);
$_$
language sql strict;
COMMENT ON FUNCTION cyanaudit.fn_label_last_transaction( varchar )
IS 'Shorthand for: fn_label_transaction( in_label, cyanaudit.fn_get_last_txid() )';
CREATE OR REPLACE FUNCTION cyanaudit.fn_set_transaction_label
(
in_label varchar
)
returns integer as
$_$
SELECT set_config( 'cyanaudit.audit_transaction_type',
cyanaudit.fn_get_or_create_audit_transaction_type( in_label )::text,
true )::integer;
$_$
language sql strict;
-- fn_undo_transaction
CREATE OR REPLACE FUNCTION cyanaudit.fn_undo_transaction
(
in_txid bigint
)
returns setof varchar
as $_$
declare
my_statement varchar;
begin
for my_statement in
select query
from cyanaudit.vw_undo_statement
where txid = in_txid
loop
execute my_statement;
return next my_statement;
end loop;
perform cyanaudit.fn_label_transaction('Undo transaction');
return;
end
$_$
language plpgsql strict;
-- fn_undo_last_transaction
CREATE OR REPLACE FUNCTION cyanaudit.fn_undo_last_transaction()
returns setof varchar as
$_$
select cyanaudit.fn_undo_transaction(cyanaudit.fn_get_last_txid());
$_$
language 'sql';
COMMENT ON FUNCTION cyanaudit.fn_undo_last_transaction()
IS 'Shorthand for: cyanaudit.fn_undo_transaction( cyanaudit.fn_get_last_txid() )';
-- fn_update_audit_fields
-- Create or update audit_fields for all columns in the passed-in schema.
-- If passed-in schema is null, create or update for all already-known schemas.
CREATE OR REPLACE FUNCTION cyanaudit.fn_update_audit_fields
(
in_schema varchar default null
)
returns void as
$_$
declare
my_schemas varchar[];
begin
if pg_trigger_depth() > 0 then
return;
end if;
-- Add only those tables in the passed-in schemas.
-- If no schemas are passed in, use only those we already know about.
-- This way, we will never log any schema that has not been explicitly
-- requested to be logged.
select case when in_schema is not null
then ARRAY[ in_schema ]
else array_agg( distinct table_schema )
end
into my_schemas
from cyanaudit.tb_audit_field;
with tt_audit_fields as
(
select coalesce
(
af.audit_field,
cyanaudit.fn_get_or_create_audit_field
(
n.nspname::varchar,
c.relname::varchar,
a.attname::varchar
)
) as audit_field,
(a.attnum is null and af.loggable) as stale
from (
pg_class c
join pg_attribute a
on a.attrelid = c.oid
and a.attnum > 0
and a.attisdropped is false
join pg_namespace n
on c.relnamespace = n.oid
and n.nspname::varchar = any( my_schemas )
join pg_constraint cn
on conrelid = c.oid
and cn.contype = 'p'
)
full join cyanaudit.tb_audit_field af
on af.table_schema = n.nspname::varchar
and af.table_name = c.relname::varchar
and af.column_name = a.attname::varchar
and af.loggable is true
)
update cyanaudit.tb_audit_field af
set loggable = false -- trigger will update to actual value
from tt_audit_fields ttaf
where af.audit_field = ttaf.audit_field
and af.loggable
and ttaf.stale;
return;
end;
$_$
language 'plpgsql';
COMMENT ON FUNCTION cyanaudit.fn_update_audit_fields( varchar )
IS 'Updates tb_audit_field to reflect any new or dropped columns in known schemas.';
---- INTERNAL UTILITY FUNCTIONS ----
-- fn_get_email_by_uid
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_email_by_uid
(
in_uid integer
)
returns varchar
language plpgsql stable strict
as $_$
declare
my_email varchar;
my_query varchar;
my_user_table_uid_col varchar;
my_user_table varchar;
my_user_table_email_col varchar;
begin
select value
into my_user_table
from cyanaudit.tb_config
where name = 'user_table';
select value
into my_user_table_uid_col
from cyanaudit.tb_config
where name = 'user_table_uid_col';
select value
into my_user_table_email_col
from cyanaudit.tb_config
where name = 'user_table_email_col';
if my_user_table IS NULL OR
my_user_table_uid_col IS NULL OR
my_user_table_email_col IS NULL
then
return null;
end if;
my_query := 'SELECT ' || quote_ident(my_user_table_email_col)
|| ' FROM ' || quote_ident(my_user_table)
|| ' WHERE ' || quote_ident(my_user_table_uid_col)
|| ' = ' || quote_nullable(in_uid);
execute my_query
into my_email;
return my_email;
exception
when undefined_table then
raise notice 'cyanaudit: Invalid user_table setting: ''%''', my_user_table;
return null;
when undefined_column then
raise notice 'cyanaudit: Invalid user_table_uid_col (''%'') or user_table_email_col (''%'')',
my_user_table_uid_col, my_user_table_email_col;
return null;
end
$_$;
-- fn_get_uid_by_username
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_uid_by_username
(
in_username varchar
)
returns integer
language plpgsql stable strict
as $_$
declare
my_uid varchar;
my_query varchar;
my_user_table_uid_col varchar;
my_user_table varchar;
my_user_table_username_col varchar;
begin
select value
into my_user_table
from cyanaudit.tb_config
where name = 'user_table';
select value
into my_user_table_uid_col
from cyanaudit.tb_config
where name = 'user_table_uid_col';
select value
into my_user_table_username_col
from cyanaudit.tb_config
where name = 'user_table_username_col';
if my_user_table IS NULL OR
my_user_table_uid_col IS NULL OR
my_user_table_username_col IS NULL
then
return null;
end if;
my_query := 'select ' || quote_ident(my_user_table_uid_col)
|| ' from ' || quote_ident(my_user_table)
|| ' where ' || quote_ident(my_user_table_username_col)
|| ' = ' || quote_nullable(in_username);
execute my_query
into my_uid;
return my_uid;
exception
when undefined_table then
raise notice 'cyanaudit: Invalid user_table setting: ''%''', my_user_table;
return null;
when undefined_column then
raise notice 'cyanaudit: Invalid user_table_uid_col (''%'') or user_table_username_col (''%'')',
my_user_table_uid_col, my_user_table_username_col;
return null;
end
$_$;
-- fn_get_table_pk_cols
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_table_pk_cols
(
in_table_name varchar,
in_table_schema varchar default 'public'
)
returns varchar[]
language sql stable strict
as $_$
with tt_conkey as
(
SELECT cn.conkey,
c.oid as relid
from pg_class c
join pg_namespace n
on c.relnamespace = n.oid
join pg_constraint cn
on c.oid = cn.conrelid
where cn.contype = 'p'
and c.relname::varchar = in_table_name
and n.nspname::varchar = in_table_schema
),
tt_subscripts as
(
select generate_subscripts( conkey, 1 ) as i
from tt_conkey
)
select array_agg( a.attname order by s.i )::varchar[]
from tt_subscripts s
cross join tt_conkey c
join pg_attribute a
on c.conkey[s.i] = a.attnum
and c.relid = a.attrelid
$_$;
-- fn_get_where_string
-- Takes in array of pk_cols and array of values
-- Returns a correctly quoted string 'pkcol1 = val1 AND pkcol2 = val2' etc.
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_where_string
(
in_pk_cols varchar[],
in_values varchar[]
)
returns text as
$_$
with tt_names_values as
(
select unnest( in_pk_cols ) as colname,
unnest( in_values ) as value
)
select string_agg( format( '%I = %L', colname, value ), ' AND ' )
from tt_names_values;
$_$
language sql strict immutable;
-- fn_get_or_create_audit_field
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_or_create_audit_field
(
in_table_schema varchar,
in_table_name varchar,
in_column_name varchar
)
returns integer as
$_$
declare
my_audit_field integer;
my_loggable boolean;
begin
select audit_field,
loggable
into my_audit_field,
my_loggable
from cyanaudit.tb_audit_field
where table_schema = in_table_schema
and table_name = in_table_name
and column_name = in_column_name;
if not found then
insert into cyanaudit.tb_audit_field
(
table_schema,
table_name,
column_name
)
values
(
in_table_schema,
in_table_name,
in_column_name
)
returning audit_field
into my_audit_field;
elsif my_loggable is false then
update cyanaudit.tb_audit_field
set loggable = loggable -- trigger will set correct value
where audit_field = my_audit_field;
end if;
return my_audit_field;
end
$_$
language 'plpgsql';
-----------------------
------ TRIGGERS -------
-----------------------
-- fn_log_audit_event (MAIN LOGGING TRIGGER FUNCTION)
CREATE OR REPLACE FUNCTION cyanaudit.fn_log_audit_event()
RETURNS trigger
LANGUAGE plpgsql
AS $_$
declare
my_audit_fields varchar[];
my_audit_field integer;
my_column_names varchar[];
my_column_name varchar;
my_new_row record;
my_old_row record;
my_pk_cols varchar;
my_pk_vals_constructor varchar;
my_pk_vals varchar[];
my_old_value text;
my_new_value text;
my_clock_timestamp timestamp;
my_enabled text;
my_exception_text text;
begin
my_exception_text := 'cyanaudit: Operation not logged';
if( TG_OP = 'INSERT' ) then
my_new_row := NEW;
my_old_row := NEW;
elsif( TG_OP = 'UPDATE' ) then
my_new_row := NEW;
my_old_row := OLD;
elsif( TG_OP = 'DELETE' ) then
my_new_row := OLD;
my_old_row := OLD;
end if;
my_enabled := current_setting( 'cyanaudit.enabled', true );
if my_enabled = '0' or my_enabled = 'false' or my_enabled = 'f' then
return my_new_row;
end if;
my_pk_cols := TG_ARGV[0]::varchar[];
my_audit_fields := TG_ARGV[1]::varchar[];
my_column_names := TG_ARGV[2]::varchar[];
my_clock_timestamp := clock_timestamp(); -- same for all entries from this invocation
-- Bookmark this txid in cyanaudit.last_txid
perform (set_config('cyanaudit.last_txid', txid_current()::text, false))::bigint;
-- Given: my_pk_cols::varchar[] = ARRAY[ 'column foo',bar ]
-- Result: my_pk_vals_constructor::varchar = 'select ARRAY[ $1."column foo", $1.bar ]::varchar[]'
select 'SELECT ARRAY[' || string_agg( '$1.' || quote_ident(pk_col), ',' ) || ']::varchar[]'
into my_pk_vals_constructor
from ( select unnest(my_pk_cols::varchar[]) as pk_col ) x;
-- Execute the result using my_new_row in $1 to produce the following result:
-- my_pk_vals::varchar[] = ARRAY[ 'val1', 'val2' ]
execute my_pk_vals_constructor
into my_pk_vals
using my_new_row; -- To allow undoing updates to pk columns, logged pk_vals are post-update.
for my_column_name, my_audit_field in
select unnest( my_column_names::varchar[] ),
unnest( my_audit_fields::varchar[] )
loop
if TG_OP = 'INSERT' THEN
EXECUTE format('select null::text, $1.%I::text', my_column_name)
INTO my_old_value, my_new_value
USING my_new_row;
CONTINUE when my_new_value is null;
elsif TG_OP = 'UPDATE' THEN
EXECUTE format( 'select $1.%1$I::text, $2.%1$I::text', my_column_name)
INTO my_old_value, my_new_value
USING my_old_row, my_new_row;
CONTINUE when my_old_value is not distinct from my_new_value;
elsif TG_OP = 'DELETE' THEN
EXECUTE format('select $1.%I::text, null::text', my_column_name)
INTO my_old_value, my_new_value
USING my_old_row;
CONTINUE when my_old_value is null;
end if;
execute format( 'INSERT INTO cyanaudit.tb_audit_event '
|| '( audit_field, recorded, pk_vals, uid, row_op, audit_transaction_type, old_value, new_value ) '
|| 'VALUES( $1, $2, $3, $4, $5::char(1), $6, $7, $8 ) ',
my_column_name
)
using my_audit_field,
my_clock_timestamp,
my_pk_vals,
cyanaudit.fn_get_current_uid(),
TG_OP,
nullif( current_setting( 'cyanaudit.audit_transaction_type', true ), '' )::integer,
my_old_value,
my_new_value;
end loop;
return new;
exception
when foreign_key_violation OR undefined_column then
raise notice '%: %: %: Please run fn_update_audit_fields().',
my_exception_text, SQLSTATE, SQLERRM;
return my_new_row;
when undefined_function OR undefined_table OR insufficient_privilege then
raise notice '%: %: %: Please reinstall cyanaudit.',
my_exception_text, SQLSTATE, SQLERRM;
return my_new_row;
when invalid_text_representation then
raise notice '%: %: %: GUC ''cyanaudit.audit_transaction_type'' has non-integer value ''%''. Set with fn_set_transaction_label() or leave unset.',
my_exception_text, SQLSTATE, SQLERRM,
current_setting( 'cyanaudit.audit_transaction_type', true );
when others then
raise notice '%: %: %: Please report error.',
my_exception_text, SQLSTATE, SQLERRM;
return my_new_row;
end
$_$;
COMMENT ON FUNCTION cyanaudit.fn_log_audit_event()
IS 'Trigger function installed on all tables logged by Cyan Audit.';
-- fn_before_audit_field_change
CREATE OR REPLACE FUNCTION cyanaudit.fn_before_audit_field_change()
returns trigger as
$_$
declare
my_pk_colname varchar;
begin
IF TG_OP = 'INSERT' THEN
if NEW.table_schema = 'cyanaudit' then return NULL; end if;
ELSIF TG_OP = 'DELETE' then
raise exception 'cyanaudit: Deletion from this table is not allowed.';
ELSIf TG_OP = 'UPDATE' then
if NEW.table_schema != OLD.table_schema OR
NEW.table_name != OLD.table_name OR
NEW.column_name != OLD.column_name
then
raise exception 'Updating table_schema, table_name or column_name not allowed.';
end if;
end if;
perform *
from pg_attribute a
join pg_class c
on a.attrelid = c.oid
join pg_namespace n
on c.relnamespace = n.oid
join pg_constraint cn
on conrelid = c.oid
where n.nspname::varchar = NEW.table_schema
and c.relname::varchar = NEW.table_name
and a.attname::varchar = NEW.column_name
and cn.contype = 'p'
and a.attnum > 0
and a.attisdropped is false;
NEW.loggable := found;
-- Got to double check our value if it's true
if NEW.enabled is null then
-- Sensible default value for "enabled" is important to avoid freaking people out:
-- If any column on same table is enabled, then true.
-- Else If we know of fields on this table but all are inactive, then false.
-- Else If we know of no fields in this table, then:
-- If any field in same schema is enabled, then true.
-- Else If we know of fields in this schema but all are inactive, then false.
-- Else If we know of no columns in this schema, then:
-- If any column in the database is enabled, then true.
-- Else If we know of fields in this database but all are inactive, then false.
-- Else, true:
select enabled
into NEW.enabled
from cyanaudit.tb_audit_field
order by (table_name = NEW.table_name) desc, -- Sort enabled fields over table to top of that
(table_schema = NEW.table_schema) desc, -- Sort enabled fields within schema to top of that
enabled desc; -- Sort any remaining enabled fields to the top
-- If we got here, we found no fields in the db. Activate logging by default.
if NEW.enabled is null then
NEW.enabled = true;
end if;
end if;
return NEW;
end
$_$
language plpgsql;
DROP TRIGGER IF EXISTS tr_before_audit_field_change on cyanaudit.tb_audit_field;
CREATE TRIGGER tr_before_audit_field_change
BEFORE INSERT OR UPDATE ON cyanaudit.tb_audit_field
FOR EACH ROW EXECUTE PROCEDURE cyanaudit.fn_before_audit_field_change();
-- fn_after_audit_field_change
CREATE OR REPLACE FUNCTION cyanaudit.fn_after_audit_field_change()
returns trigger
language plpgsql
as $_$
declare
my_pk_colnames varchar[];
my_function_name varchar;
my_audit_fields varchar[];
my_column_names varchar[];
begin
if TG_OP = 'UPDATE' and OLD.enabled = NEW.enabled and OLD.loggable = NEW.loggable THEN
return NEW;
end if;
perform cyanaudit.fn_verify_active_partition();
-- See if a logging trigger is already on the table
perform *
from pg_trigger t
join pg_class c
on t.tgrelid = c.oid
join pg_namespace n
on c.relnamespace = n.oid
where n.nspname::varchar = NEW.table_schema
and c.relname::varchar = NEW.table_name
and tgname = 'tr_log_audit_event';
-- If so, remove it so we can update it.
IF FOUND THEN
execute format( 'DROP TRIGGER tr_log_audit_event ON %I.%I',
NEW.table_schema, NEW.table_name );
END IF;
-- Get a list of audit fields and column names for this table
select array_agg(audit_field),
array_agg(column_name)
into my_audit_fields,
my_column_names
from cyanaudit.tb_audit_field
where enabled
and loggable
and table_schema = NEW.table_schema
and table_name = NEW.table_name;
IF array_length(my_audit_fields, 1) > 0 THEN
my_pk_colnames := cyanaudit.fn_get_table_pk_cols( NEW.table_name, NEW.table_schema );
-- Create the table trigger (if it doesn't exist) to call the function
execute format( 'CREATE TRIGGER tr_log_audit_event '
|| 'AFTER INSERT OR UPDATE OR DELETE ON %I.%I FOR EACH ROW '
|| 'EXECUTE PROCEDURE cyanaudit.fn_log_audit_event(%L,%L,%L)',
NEW.table_schema,
NEW.table_name,
my_pk_colnames,
my_audit_fields,
my_column_names
);
END IF;
return NEW;
end
$_$;
DROP TRIGGER IF EXISTS tr_after_audit_field_change on cyanaudit.tb_audit_field;
CREATE TRIGGER tr_after_audit_field_change
AFTER INSERT OR UPDATE on cyanaudit.tb_audit_field
FOR EACH ROW EXECUTE PROCEDURE cyanaudit.fn_after_audit_field_change();
-- EVENT TRIGGER
-- fn_update_audit_fields_event_trigger()
CREATE OR REPLACE FUNCTION cyanaudit.fn_update_audit_fields_event_trigger()
returns event_trigger
language plpgsql as
$function$
begin
-- Avoid creating logging triggers during database restore,
-- since the triggers will be restored without our help.
perform *
from pg_stat_activity
where application_name = 'pg_restore'
and datname = current_database()
and state = 'active';
if not found then
perform cyanaudit.fn_update_audit_fields();
end if;
exception
when insufficient_privilege
then return;
end
$function$;
-- fn_create_event_trigger
-- Function to install the event trigger explicitly after pg_restore completes,
-- because we don't want it firing during pg_restore.
CREATE OR REPLACE FUNCTION cyanaudit.fn_create_event_trigger()
RETURNS void
LANGUAGE plpgsql
AS $_$
begin
PERFORM *
from pg_event_trigger
where evtname = 'tr_update_audit_fields';
if not found then
CREATE EVENT TRIGGER tr_update_audit_fields ON ddl_command_end
WHEN TAG IN ('ALTER TABLE', 'CREATE TABLE', 'DROP TABLE')
EXECUTE PROCEDURE cyanaudit.fn_update_audit_fields_event_trigger();
end if;
end;
$_$;
COMMENT ON FUNCTION cyanaudit.fn_create_event_trigger()
IS 'Installs event trigger to run fn_update_audit_fields() on any schema change.';
--------- Partitioning -----------
-- fn_redirect_audit_events
-- This trigger function is installed onto tb_audit_event
CREATE OR REPLACE FUNCTION cyanaudit.fn_redirect_audit_events()
returns trigger as
$_$
declare
my_table_name varchar;
begin
my_table_name := TG_ARGV[0];
execute format( 'insert into cyanaudit.%I select $1.*', my_table_name )
using NEW;
return null;
end
$_$
language 'plpgsql';
-- fn_parse_tgargs
-- This is a handy utility function to get the TGARGS values passed into a
-- trigger function. Used for validating the active partition name, below
CREATE OR REPLACE FUNCTION cyanaudit.fn_parse_tgargs
(
in_tgargs BYTEA
)
RETURNS VARCHAR[] AS
$_$
SELECT string_to_array(
regexp_replace(
encode(
in_tgargs,
'escape'
)::VARCHAR,
'\\000$',
''
),
'\000'
)::VARCHAR[];
$_$
LANGUAGE SQL IMMUTABLE;
-- fn_get_active_partition_name
CREATE OR REPLACE FUNCTION cyanaudit.fn_get_active_partition_name()
returns varchar as
$_$
select (cyanaudit.fn_parse_tgargs( tgargs ))[1]
from pg_trigger t
join pg_class c
on t.tgrelid = c.oid
join pg_namespace n
on c.relnamespace = n.oid
where n.nspname = 'cyanaudit'
and c.relname = 'tb_audit_event'
and t.tgname = 'tr_redirect_audit_events';
$_$
language sql;
-- fn_create_new_partition
CREATE OR REPLACE FUNCTION cyanaudit.fn_create_new_partition
(
in_new_table_name varchar default 'tb_audit_event_' || to_char(now(), 'YYYYMMDD_HH24MI')
)
returns varchar as