PostgreSQL触发器使用实践——数据审计

1、说明

PostgreSQL中审计可以通过自带的审计日志功能来实现,但是不足指出在于只能实现语句级别,数据库级别,用户级别的审计,审计的颗粒度太大。
因此,我们可以使用触发器来实现粒度更细的审计,例如:级别,行级别(带条件的),用户级别的数据库操作。但是不建议在数据库中大量使用触发器来审计,因为此方法开销较大。

2、使用场景

2.1、用户信息审计
我们使用触发器来记录:谁改变了数据、数据什么使用被改、什么操作改变了数据、被改变前后的数据分别是什么。
首先,创建表audit_log来记录相关的信息:

1
2
3
4
5
6
7
8
9
bill=# CREATE TABLE audit_log
bill-# (username text, -- who did the change
bill(# event_time_utc timestamp, -- when the event was recorded
bill(# table_name text, -- contains schema-qualified table name
bill(# operation text, -- INSERT, UPDATE, DELETE or TRUNCATE
bill(# before_value json, -- the OLD tuple value
bill(# after_value json -- the NEW tuple value
bill(# );
CREATE TABLE

简单说明下:

  • username即记录哪个用户修改了数据,可以通过session_user来获取;
  • event_time_utc用来记录数据被修改的时间;
  • table_name即被修改的表名;
  • operation表示数据被什么操作修改的;
  • before_value和after_value分别表示被修改前后的数据。

接着我们要创建触发器函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bill=# CREATE OR REPLACE FUNCTION audit_trigger() RETURNS trigger AS $$
bill$#  DECLARE
bill$#   old_row json := NULL;
bill$#   new_row json := NULL;
bill$#  BEGIN
bill$#   IF TG_OP IN ('UPDATE','DELETE')
bill$#   THEN
bill$#     old_row = row_to_json(OLD);
bill$#   END IF;
bill$#   IF TG_OP IN ('INSERT','UPDATE')
bill$#   THEN
bill$#     new_row = row_to_json(NEW);
bill$#     END IF;
bill$#   INSERT INTO audit_log(username, event_time_utc, table_name, operation, before_value, after_value )
bill$#   VALUES (session_user, current_timestamp AT TIME ZONE 'UTC', TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, TG_OP, old_row, new_row );
bill$#   RETURN NEW;
bill$#   END;
bill$#    $$ LANGUAGE plpgsql;
CREATE FUNCTION

最后我们再创建对应的表和触发器即可:

1
2
3
4
5
6
7
8
9
bill=# create table notify_test(c1 int);
CREATE TABLE

bill=# create trigger audit_log
bill-#   after insert or update or delete
bill-#   on notify_test
bill-#   for each row
bill-#   execute procedure audit_trigger();
CREATE TRIGGER

验证:
在audit_log表中记录了相关的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
bill=# insert into notify_test values(1);
INSERT 0 1
bill=# update notify_test set c1=2;
UPDATE 1
bill=# delete from notify_test ;
DELETE 1
bill=# select * from audit_log ;
 username |       event_time_utc       |     table_name     | operation | before_value | after_value
----------+----------------------------+--------------------+-----------+--------------+-------------
 bill     | 2020-05-11 02:42:01.500445 | public.notify_test | INSERT    |              | {"c1":1}
 bill     | 2020-05-11 02:42:25.211593 | public.notify_test | UPDATE    | {"c1":1}     | {"c1":2}
 bill     | 2020-05-11 02:42:31.0065   | public.notify_test | DELETE    | {"c1":2}     |
(3 rows)

2.2、禁止delete
某些重要的表中,我们可能只会允许用户去插入和更新数据,不允许用户删除数据,那么我们也可以使用触发器来实现。
和上面的例子类似,首先我们要创建一个触发器函数:

1
2
3
4
5
6
7
8
9
bill=# CREATE OR REPLACE FUNCTION cancel_op() RETURNS TRIGGER AS $$
bill$# BEGIN
bill$#     IF TG_WHEN = 'AFTER' THEN
bill$#     RAISE EXCEPTION 'YOU ARE NOT ALLOWED TO % ROWS IN %.%', TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME;
bill$#     END IF; RAISE NOTICE '% ON ROWS IN %.% WON"T HAPPEN', TG_OP, TG _ABLE_SCHEMA, TG_TABLE_NAME;
bill$#     RETURN NULL;
bill$#     END;
bill$#     $$ LANGUAGE plpgsql;
CREATE FUNCTION

接着创建对应的触发器:

1
2
3
bill=# create TRIGGER disallow_delete AFTER delete on notify_test
bill-#   for each row execute procedure cancel_op();
CREATE TRIGGER

然后我们可以验证了:

1
2
3
4
5
6
7
8
bill=# delete from notify_test where c1 = 1;
psql: ERROR:  YOU ARE NOT ALLOWED TO DELETE ROWS IN public.notify_test
CONTEXT:  PL/pgSQL function cancel_op() line 4 at RAISE
bill=# select * from notify_test ;          
 c1
----
  1
(1 row)

2.3、禁止truncate
前面的例子中我们实现了禁止从某张表中delete数据,但是实际上我们可以直接truncate表来删除数据,所以我们想要禁止删除某张表的数据,还得加上禁止truncate的触发器才行。

1
2
3
bill=# create TRIGGER disallow_truncate AFTER truncate on notify_test
bill-#   for each statement execute procedure cancel_op();
CREATE TRIGGER

需要注意的是,因为truncate是针对整张表的,所以不能使用for each row。
验证:

1
2
3
bill=# truncate notify_test ;
psql: ERROR:  YOU ARE NOT ALLOWED TO TRUNCATE ROWS IN public.notify_test
CONTEXT:  PL/pgSQL function cancel_op() line 4 at RAISE

2.4、带有条件的触发器
某些情况下,我们的审计策略可能是这样的:对于某张表,我们在某一时间段不允许进行更新或者删除操作。那么这种情况我们需要创建的触发器必须得指定相应的条件。
例如前面禁止delte的例子,我们现在要求是不允许在周一上午delte这张表的数据,其它时间段可以。
首先我们还是得创建触发器函数,同上即可。
接下来关键就是我们要创建的带有条件的触发器:

1
2
3
4
5
6
7
8
bill=# drop trigger no_delete_on_monday_morning ON t1;
DROP TRIGGER
bill=# create TRIGGER no_delete_on_monday_morning
bill-#   after delete on t1
bill-#   for each row
bill-#   when (current_time < '12:00' and extract(DOW from current_timestamp) = 1)
bill-#   execute procedure cancel_op();
CREATE TRIGGER

查看当前时间:现在是周一上午,满足触发器的条件。

1
2
3
4
5
bill=# select current_timestamp;
       current_timestamp      
-------------------------------
 2020-05-11 11:21:10.339308+08
(1 row)

我们来验证下:无法删除数据。

1
2
3
bill=# delete from t1 where id=1;
psql: ERROR:  YOU ARE NOT ALLOWED TO DELETE ROWS IN public.t1
CONTEXT:  PL/pgSQL function cancel_op() line 4 at RAISE

3、总结

对于数据库的审计,我们可以使用触发器来实现更细的粒度。但是我们实际应用中还是应该避免大量使用触发器来实现类似的功能,因为开销较大,并且可能会导致某些难以调试的问题。